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:
@@ -28,6 +28,7 @@ import (
|
||||
"scrabble/backend/internal/engine"
|
||||
"scrabble/backend/internal/feedback"
|
||||
"scrabble/backend/internal/game"
|
||||
"scrabble/backend/internal/gamelimits"
|
||||
"scrabble/backend/internal/link"
|
||||
"scrabble/backend/internal/lobby"
|
||||
"scrabble/backend/internal/notify"
|
||||
@@ -156,6 +157,17 @@ func run(ctx context.Context, cfg config.Config, logger *zap.Logger) error {
|
||||
logger.Info("active dictionary version", zap.String("version", games.ActiveVersion()))
|
||||
games.SetNotifier(hub)
|
||||
games.SetMetrics(tel.MeterProvider().Meter("scrabble/backend/game"))
|
||||
|
||||
// Active-game limit config: the per-tier, per-kind caps in backend.config, read once into an
|
||||
// in-memory cache at boot and refreshed when the admin edits them. A boot-time load fails fast if
|
||||
// the single config row is missing; the game domain reads the cache on every new-game gate.
|
||||
gameLimits := gamelimits.NewService(gamelimits.NewStore(db))
|
||||
if err := gameLimits.Load(ctx); err != nil {
|
||||
return fmt.Errorf("load game-limit config: %w", err)
|
||||
}
|
||||
games.SetGameLimits(gameLimits)
|
||||
logger.Info("game-limit config loaded")
|
||||
|
||||
go games.RunSweeper(ctx, cfg.Game.TimeoutSweepInterval)
|
||||
logger.Info("game turn-timeout sweeper started",
|
||||
zap.Duration("interval", cfg.Game.TimeoutSweepInterval))
|
||||
@@ -305,6 +317,7 @@ func run(ctx context.Context, cfg config.Config, logger *zap.Logger) error {
|
||||
BanView: banView,
|
||||
Ads: adsSvc,
|
||||
Payments: paymentsSvc,
|
||||
GameLimits: gameLimits,
|
||||
Notifier: hub,
|
||||
ExportSignKey: cfg.ExportSignKey,
|
||||
Renderer: renderer,
|
||||
|
||||
Reference in New Issue
Block a user