feat(backend): per-tier, per-kind active-game limits with a guest funnel
CI / changes (pull_request) Successful in 5s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m41s

Cap a player's simultaneous unfinished games per kind (vs_ai, random,
friends) with independent guest and durable-account tiers, held in a new
single-row backend.config table (-1 = unlimited) behind an in-memory cache
and editable live in the admin console (/_gm/limits). Each game is tagged
with games.game_kind on creation.

This replaces the earlier flat MaxActiveQuickGames=10 combined cap: the
per-tier/kind config is the single mechanism, enforced at the same handler
gate (ensureUnderGameLimit by kind on lobby/enqueue) plus the durable
friends cap in CreateInvitation. game.Service.AtGameLimit only resolves the
tier and counts; the limit policy stays at the request edge.

Guests are now refused friend requests, friend-code redemption,
befriend-in-game and invitation creation outright (403 guest_forbidden) --
previously only the UI hid these.

Admin: a kind column in both game lists and the config editor.

Defaults: guest 1 vs_ai / 1 random / 0 friends; durable 10 / 10 / 10.
This commit is contained in:
Ilia Denisov
2026-07-10 09:03:57 +02:00
parent 2e5136b22a
commit ed53e25e57
35 changed files with 897 additions and 223 deletions
+10 -5
View File
@@ -33,11 +33,16 @@ real game seating the caller with an **empty opponent seat** (status `open`) or,
another player already waits for the same variant and per-turn word rule, seats the
caller into that open game and starts it — and
friend-game invitations (invite → accept, starting a 24 player game once every
invitee accepts). A **simultaneous-game cap** (`game.MaxActiveQuickGames` = 10) limits a
player's active quick games — status `active`/`open`, excluding invitation-linked friend
games (`game.Service.CountActiveQuickGames`); the server refuses `lobby/enqueue` and
`invitations` creation with **409 `game_limit_reached`** at the cap (accepting an invitation
is exempt), and the `games.list` response carries an `at_game_limit` flag for the lobby.
invitee accepts). **Per-tier, per-kind active-game caps** limit a player's simultaneous
unfinished games by kind — `vs_ai`, `random` (quick auto-match), `friends` — with separate
guest and durable-account tiers held in the single-row `backend.config` table (a `-1` means
unlimited), read through an in-memory cache (`internal/gamelimits`) and tuned live in the admin
(`/_gm/limits`, no redeploy). Each game is tagged with its `games.game_kind` on creation;
`game.Service.AtGameLimit` counts the account's `active`/`open` games of a kind against the
tier's cap. The server refuses `lobby/enqueue` (random/vs_ai) with **409 `game_limit_reached`**
at the cap, and the `invitations` (friends) path enforces the durable friends cap and refuses a
**guest** outright (guests cannot use friends); accepting an invitation is exempt. The
`games.list` response carries an `at_game_limit` flag (the random-kind cap) for the lobby.
`internal/social` owns the friend graph (request/accept),
per-user blocks, and per-game chat with nudges folded in as a message kind; chat
messages are length-capped, content-filtered (no links/emails/phone numbers,