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
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:
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"scrabble/backend/internal/engine"
|
||||
"scrabble/backend/internal/game"
|
||||
"scrabble/backend/internal/gamelimits"
|
||||
)
|
||||
|
||||
// The handlers below cover the game and chat operations the UI needs. They follow
|
||||
@@ -46,10 +47,11 @@ type historyDTO struct {
|
||||
}
|
||||
|
||||
// gameListDTO is the caller's games (active and finished) for the lobby. AtGameLimit
|
||||
// reports whether the caller has reached the simultaneous quick-game cap
|
||||
// (game.MaxActiveQuickGames); while it is true the lobby disables "New Game" and shows a
|
||||
// notice. It rides the lobby response — which the lobby re-fetches on every game event —
|
||||
// instead of a separate request.
|
||||
// reports whether the caller has reached its tier's active-game cap for the random
|
||||
// (quick auto-match) kind (the per-tier, per-kind limits in backend.config); while
|
||||
// it is true the lobby disables "New Game" and shows a notice. It rides the lobby
|
||||
// response — which the lobby re-fetches on every game event — instead of a separate
|
||||
// request.
|
||||
type gameListDTO struct {
|
||||
Games []gameDTO `json:"games"`
|
||||
AtGameLimit bool `json:"at_game_limit"`
|
||||
@@ -395,23 +397,13 @@ func (s *Server) handleSaveDraft(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, okResponse{OK: true})
|
||||
}
|
||||
|
||||
// atGameLimit reports whether uid already holds the maximum number of simultaneous
|
||||
// quick games (game.MaxActiveQuickGames). It backs both the lobby's at_game_limit flag
|
||||
// and the new-game gate; friend games created by invitation are not counted.
|
||||
func (s *Server) atGameLimit(ctx context.Context, uid uuid.UUID) (bool, error) {
|
||||
n, err := s.games.CountActiveQuickGames(ctx, uid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return n >= game.MaxActiveQuickGames, nil
|
||||
}
|
||||
|
||||
// ensureUnderGameLimit aborts the request with 409 game_limit_reached when uid is at the
|
||||
// simultaneous quick-game cap, and reports whether the caller may proceed. It guards
|
||||
// every new-game entry point — quick auto-match/AI and invitation creation; accepting an
|
||||
// incoming invitation is deliberately exempt.
|
||||
func (s *Server) ensureUnderGameLimit(c *gin.Context, uid uuid.UUID) bool {
|
||||
atLimit, err := s.atGameLimit(c.Request.Context(), uid)
|
||||
// ensureUnderGameLimit aborts the request with 409 game_limit_reached when uid has reached its
|
||||
// tier's active-game cap for kind (the per-tier, per-kind limits in backend.config), and reports
|
||||
// whether the caller may proceed. It guards the quick new-game entry points — auto-match (random)
|
||||
// and AI (vs_ai). Friend-invitation limits are enforced in the lobby's CreateInvitation, and
|
||||
// accepting an incoming invitation is deliberately exempt.
|
||||
func (s *Server) ensureUnderGameLimit(c *gin.Context, uid uuid.UUID, kind gamelimits.Kind) bool {
|
||||
atLimit, err := s.games.AtGameLimit(c.Request.Context(), uid, kind)
|
||||
if err != nil {
|
||||
s.abortErr(c, err)
|
||||
return false
|
||||
@@ -437,7 +429,7 @@ func (s *Server) handleListGames(c *gin.Context) {
|
||||
s.abortErr(c, err)
|
||||
return
|
||||
}
|
||||
atLimit, err := s.atGameLimit(c.Request.Context(), uid)
|
||||
atLimit, err := s.games.AtGameLimit(c.Request.Context(), uid, gamelimits.KindRandom)
|
||||
if err != nil {
|
||||
s.abortErr(c, err)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user