fix(games): carry game kind on live events + fix the New Game lock funnel
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m56s

Address review of the active-game limit lock:

- Live-event GameViews (opponent_moved, match_found, game_started, ...) did
  not carry game_kind, so a lobby patch from an event zeroed a game's kind
  and the client's per-kind count under-counted — a capped kind read as
  free (a disabled start instead of the lock, and a wrong per-kind result).
  Thread Kind through notify.GameSummary → the event GameView.

- New Game screen refreshes the lobby games on mount so the per-kind count
  reflects the current set, not a stale cached snapshot.

- The guest funnel's login button routes to the profile screen (the account
  controls), not settings.

- Copy: the guest prompt is "sign in to use all the game's features"; the
  durable notice is "finish your active games to start a new one".

Regression tests: the notify opponent-moved payload carries kind; the client
lock locks only the reached kind (vs_ai at cap leaves random open).
This commit is contained in:
Ilia Denisov
2026-07-10 10:55:48 +02:00
parent 3306a016a0
commit e40adfb0c7
9 changed files with 36 additions and 10 deletions
+1
View File
@@ -52,6 +52,7 @@ func gameSummary(g Game, names []string) notify.GameSummary {
TurnTimeoutSecs: int(g.TurnTimeout.Seconds()),
MultipleWordsPerTurn: g.MultipleWordsPerTurn,
VsAI: g.VsAI,
Kind: int(g.Kind),
MoveCount: g.MoveCount,
EndReason: g.EndReason,
Seats: seats,
+1
View File
@@ -37,6 +37,7 @@ func toWireGame(g GameSummary) wire.GameView {
TurnTimeoutSecs: g.TurnTimeoutSecs,
MultipleWordsPerTurn: g.MultipleWordsPerTurn,
VsAI: g.VsAI,
Kind: g.Kind,
MoveCount: g.MoveCount,
EndReason: g.EndReason,
Seats: seats,
+2 -2
View File
@@ -115,7 +115,7 @@ func TestGameOverPayloadRoundTrips(t *testing.T) {
func TestOpponentMovedPayloadRoundTrips(t *testing.T) {
uid, gid := uuid.New(), uuid.New()
move := engine.MoveRecord{Player: 1, Action: engine.ActionPlay, Words: []string{"STOOL"}, Score: 24, Total: 130}
summary := notify.GameSummary{ID: gid.String(), MoveCount: 9, ToMove: 0, Seats: []notify.SeatStanding{{Seat: 1, Score: 130}}}
summary := notify.GameSummary{ID: gid.String(), MoveCount: 9, ToMove: 0, Kind: 2, Seats: []notify.SeatStanding{{Seat: 1, Score: 130}}}
in := notify.OpponentMoved(uid, gid, move, summary, 42)
if in.Kind != notify.KindOpponentMoved {
t.Fatalf("kind = %q", in.Kind)
@@ -132,7 +132,7 @@ func TestOpponentMovedPayloadRoundTrips(t *testing.T) {
if m == nil || m.Player() != 1 || string(m.Action()) != "play" || m.Total() != 130 {
t.Fatalf("move wrong: %+v", m)
}
if g := ev.Game(nil); g == nil || g.MoveCount() != 9 || g.ToMove() != 0 {
if g := ev.Game(nil); g == nil || g.MoveCount() != 9 || g.ToMove() != 0 || g.Kind() != 2 {
t.Fatalf("game summary wrong: %+v", ev.Game(nil))
}
}
+3
View File
@@ -35,6 +35,9 @@ type GameSummary struct {
EndReason string
Seats []SeatStanding
LastActivityUnix int64
// Kind is the game's origin for the active-game limits (0 unknown, 1 vs_ai, 2 random, 3 friends);
// it rides live events so a lobby patch keeps the per-kind count correct.
Kind int
}
// AlphabetLetter is one variant alphabet entry (a display-only row) embedded in an