feat(backend): per-tier, per-kind active-game limits with a guest funnel #234

Merged
developer merged 1 commits from feature/e8-guest-limits into development 2026-07-10 07:17:55 +00:00
Owner

What

Per-tier, per-kind caps on a player's simultaneous unfinished games, replacing the flat MaxActiveQuickGames=10 combined cap.

  • Config: a new single-row backend.config table holds {guest,durable}_{vs_ai,random,friends}_limit (smallint, -1 = unlimited), read through an in-memory cache (internal/gamelimits), loaded at boot and refreshed on an admin edit. Defaults: guest 1/1/0, durable 10/10/10.
  • Tagging: each game carries games.game_kind (0 = unknown/pre-existing, 1 = vs_ai, 2 = random, 3 = friends), set on creation and projected onto game.Game.
  • Enforcement: Server.ensureUnderGameLimit(kind) gates lobby/enqueue (random/vs_ai) with 409 game_limit_reached; the durable friends cap is enforced in CreateInvitation; game.Service.AtGameLimit resolves the tier and counts. Accepting an invitation stays exempt.
  • Guest gate: guests are refused friend requests, friend-code redemption, befriend-in-game and invitation creation outright (403 guest_forbidden) — previously UI-only.
  • Admin: a Kind column in both game lists (global + user card) and a config editor at /_gm/limits.

The at_game_limit lobby flag now reflects the random-kind cap.

Migration

00014_guest_limits.sql — additive (game_kind column + backend.config), applies forward with no data rewrite; an image rollback ignores the new column/table.

Tests

  • unit (gamelimits): Cap / LimitsFor tier + kind resolution.
  • integration (inttest/game_limit_test.go): game_kind persisted per path; guest refused friend/redeem/invitation (domain + HTTP 403); guest blocked from a 2nd vs_ai / 2nd random (+ at_game_limit); durable on the higher tier; the durable friends cap + config cache reflecting an admin edit; accept stays exempt.

Full local suite green (build, vet, gofmt, unit, integration).

Follow-up

The client-side lock badge + funnel/notice modals and the Profile.game_limits / GameView.kind wire land in a follow-up PR.

## What Per-tier, per-kind caps on a player's simultaneous unfinished games, replacing the flat `MaxActiveQuickGames=10` combined cap. - **Config**: a new single-row `backend.config` table holds `{guest,durable}_{vs_ai,random,friends}_limit` (smallint, `-1` = unlimited), read through an in-memory cache (`internal/gamelimits`), loaded at boot and refreshed on an admin edit. Defaults: guest **1/1/0**, durable **10/10/10**. - **Tagging**: each game carries `games.game_kind` (0 = unknown/pre-existing, 1 = vs_ai, 2 = random, 3 = friends), set on creation and projected onto `game.Game`. - **Enforcement**: `Server.ensureUnderGameLimit(kind)` gates `lobby/enqueue` (random/vs_ai) with **409 `game_limit_reached`**; the durable friends cap is enforced in `CreateInvitation`; `game.Service.AtGameLimit` resolves the tier and counts. Accepting an invitation stays exempt. - **Guest gate**: guests are refused friend requests, friend-code redemption, befriend-in-game and invitation creation outright (**403 `guest_forbidden`**) — previously UI-only. - **Admin**: a `Kind` column in both game lists (global + user card) and a config editor at `/_gm/limits`. The `at_game_limit` lobby flag now reflects the random-kind cap. ## Migration `00014_guest_limits.sql` — additive (`game_kind` column + `backend.config`), applies forward with no data rewrite; an image rollback ignores the new column/table. ## Tests - unit (`gamelimits`): `Cap` / `LimitsFor` tier + kind resolution. - integration (`inttest/game_limit_test.go`): `game_kind` persisted per path; guest refused friend/redeem/invitation (domain + HTTP 403); guest blocked from a 2nd vs_ai / 2nd random (+ `at_game_limit`); durable on the higher tier; the durable friends cap + config cache reflecting an admin edit; accept stays exempt. Full local suite green (build, vet, gofmt, unit, integration). ## Follow-up The client-side lock badge + funnel/notice modals and the `Profile.game_limits` / `GameView.kind` wire land in a follow-up PR.
developer added 1 commit 2026-07-10 07:04:27 +00:00
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
ed53e25e57
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.
owner approved these changes 2026-07-10 07:17:04 +00:00
developer merged commit e45167041f into development 2026-07-10 07:17:55 +00:00
developer deleted branch feature/e8-guest-limits 2026-07-10 07:17:55 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: developer/scrabble-game#234