feat(stats): show the best move word per game variant #81

Merged
developer merged 17 commits from feature/stats-best-move-word into development 2026-06-17 22:17:27 +00:00
Owner

Replaces the single "best move" number on the statistics screen with a full-width per-variant breakdown: the highest-scoring play in each variant the player has played, drawn as game tiles (a wildcard shows its letter but no value), with words and scores right-aligned to shared edges. Empty variants are omitted.

What changed

  • backend: new account_best_move table (PK account_id+variant) keeping the main word as JSON tiles {letter,value,blank}. Captured at game finish in buildStats (blank flags taken from every placed blank — equivalent to reading the final board, since a placed tile never moves), upserted in the finish transaction, replaced only by a strictly higher-scoring play. Guest/honest-AI games still record nothing. GetStats + statsDTO expose best_moves.
  • wire: StatsView gains best_moves:[BestMoveView{variant,score,word:[BestMoveTile]}] (trailing, backward-compatible); gateway encodeStats + UI codec updated; codegen regenerated.
  • ui: new WordTiles component (board tile look, fixed px size); Stats.svelte drops the single best-move card and adds the full-width per-variant card.
  • docs: ARCHITECTURE §9 + schema, FUNCTIONAL (+ru), UI_DESIGN.

Notes

  • The per-variant best move is filled forward only — plays finished before this table existed are not back-populated (the aggregate max_word_points still covers them numerically).
  • Per-tile values ride on the wire because the statistics screen has no cached alphabet.

Tests

mainWordTiles unit + buildStats end-to-end (inttest) + gateway and UI codec round-trips (incl. a blank tile) + e2e. Full local suite green: build/vet/gofmt, backend+gateway+pkg unit, integration, UI check/test/build/bundle-gate/e2e (144).

Replaces the single "best move" number on the statistics screen with a full-width per-variant breakdown: the highest-scoring play in each variant the player has played, drawn as game tiles (a wildcard shows its letter but no value), with words and scores right-aligned to shared edges. Empty variants are omitted. ## What changed - **backend**: new `account_best_move` table (PK `account_id`+`variant`) keeping the main word as JSON tiles `{letter,value,blank}`. Captured at game finish in `buildStats` (blank flags taken from every placed blank — equivalent to reading the final board, since a placed tile never moves), upserted in the finish transaction, replaced only by a strictly higher-scoring play. Guest/honest-AI games still record nothing. `GetStats` + `statsDTO` expose `best_moves`. - **wire**: `StatsView` gains `best_moves:[BestMoveView{variant,score,word:[BestMoveTile]}]` (trailing, backward-compatible); gateway `encodeStats` + UI codec updated; codegen regenerated. - **ui**: new `WordTiles` component (board tile look, fixed px size); `Stats.svelte` drops the single best-move card and adds the full-width per-variant card. - **docs**: ARCHITECTURE §9 + schema, FUNCTIONAL (+ru), UI_DESIGN. ## Notes - The per-variant best move is filled **forward only** — plays finished before this table existed are not back-populated (the aggregate `max_word_points` still covers them numerically). - Per-tile values ride on the wire because the statistics screen has no cached alphabet. ## Tests `mainWordTiles` unit + `buildStats` end-to-end (inttest) + gateway and UI codec round-trips (incl. a blank tile) + e2e. Full local suite green: build/vet/gofmt, backend+gateway+pkg unit, integration, UI check/test/build/bundle-gate/e2e (144).
developer added 1 commit 2026-06-17 13:30:15 +00:00
feat(stats): show the best move word per game variant
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m11s
cbb485ebd6
Replace the single "best move" number on the statistics screen with a
full-width per-variant breakdown: the highest-scoring play in each variant
the player has played, drawn as game tiles (a wildcard shows its letter but
no value), with the words and scores right-aligned to shared edges.

- backend: new account_best_move table (PK account_id+variant) keeping the
  main word as JSON tiles {letter,value,blank}; captured at game finish in
  buildStats (blank flags taken from every placed blank — equivalent to the
  final board), upserted in the finish transaction and replaced only by a
  strictly higher-scoring play. Guest/honest-AI games still record nothing.
  GetStats + statsDTO expose best_moves.
- wire: StatsView gains best_moves:[BestMoveView{variant,score,word:[BestMoveTile]}]
  (trailing, backward-compatible); gateway encodeStats + UI codec updated.
- ui: new WordTiles component (board's tile look, fixed px size); Stats.svelte
  drops the maxWord card and adds the full-width best-move card (catalogue
  order, empty variants omitted).
- docs: ARCHITECTURE §9 + schema, FUNCTIONAL (+ru), UI_DESIGN.

Tests: mainWordTiles unit + buildStats end-to-end (inttest) + gateway and UI
codec round-trips (incl. a blank tile) + e2e.
developer added 1 commit 2026-06-17 14:50:29 +00:00
fix(hint): stop the hint count going stale across games
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m20s
f9faebfa91
The in-game hint badge re-fetched on entry and, for a game where it was the
player's turn, showed a too-high count that "reset" (e.g. back to 11) — the
wallet hint spent in another game was not reflected.

Root cause: the server sends one hints_remaining = per-game allowance + global
wallet, and the client cached that combined number per game. The wallet is
global, so spending a wallet hint in one game left every other game's cached
count stale (a my-turn game holds the stalest value: the opponent-moved delta
preserves the old number, whereas a game you just moved in re-cached a fresh
one). The backend allowance-then-wallet spend order was already correct.

Fix: split the two. StateView/HintResult gain a trailing wallet_balance field
(the global wallet alone); the client derives the per-game allowance as
hints_remaining - wallet_balance (stable, cacheable) and reads the wallet live
from the profile, refreshing it from every state/hint response. The badge is
allowance + live wallet, so a wallet hint anywhere updates every game at once.

- wire: scrabble.fbs StateView/HintResult + pkg/wire.BuildStateView (the single
  encoder for both the gateway transcode and the backend's event StateView),
  gateway encode + resp structs, regen.
- backend: game StateView/HintResult + service (GameState/Hint) + eventwire +
  notify PlayerState/encode + server DTOs.
- ui: lib/hints.ts (pure hintsLeft), Game.svelte (badge + syncWallet on
  load/hint, carry wallet_balance through applyMoveResult), codec/model, mock.
- docs: ARCHITECTURE §Hint.

Tests: hints.ts unit (incl. the staleness case), TestHintPolicy extended
(wallet_balance + allowance-first), gateway state/hint round-trips.
developer added 1 commit 2026-06-17 15:18:13 +00:00
chore: stats tab label and ru-caption
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 50s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m9s
5b0f608bb2
developer added 1 commit 2026-06-17 15:37:12 +00:00
chore: chat.awaitingReply label
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m10s
68972375f9
developer added 1 commit 2026-06-17 15:40:57 +00:00
chore: new.searchHint label (ru)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 50s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m17s
8b7ddb9df0
developer added 1 commit 2026-06-17 16:04:53 +00:00
chore: game-over phrases
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Failing after 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 50s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
1fe25a3a15
developer added 1 commit 2026-06-17 16:58:29 +00:00
chore: chat.awaitingReply label
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Failing after 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
7832bf10f5
developer added 1 commit 2026-06-17 17:18:30 +00:00
fix: game-over unit test expecttion
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m16s
63669a3b25
developer added 1 commit 2026-06-17 18:45:29 +00:00
chore: lobby.new, lobby.stats label
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Failing after 4m45s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
4e347702f3
developer added 1 commit 2026-06-17 19:34:41 +00:00
fix: game.new button e2e test title
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Failing after 53s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
071ff8ac37
developer added 1 commit 2026-06-17 19:54:27 +00:00
fix: game.new button e2e test bundle
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m9s
ffe9150f26
developer added 2 commits 2026-06-17 20:58:21 +00:00
chore: drop the game.over label, show only the result
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m15s
91c4efc8a8
developer added 1 commit 2026-06-17 21:45:03 +00:00
feat(stats): per-game hints used + lifetime moves and hint share
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m24s
6d1d8030e3
Store the hints a player used in each game, and add two lifetime tiles —
Moves and Hint share — to the statistics screen.

- per-game: game_players.hints_used now counts EVERY hint (allowance + wallet),
  not just the free allowance, so it is the seat's true total hints used this
  game. The allowance decision (used < HintsPerPlayer) and the hint badge values
  (hints_remaining / wallet_balance) are unchanged — the lobby hint-count fix does
  NOT regress; only the admin "hints used" column, which silently under-counted
  wallet hints, becomes accurate.
- account_stats gains two summed counters: moves (the player's plays — passes and
  exchanges excluded) and hints_used (every hint). Computed at game finish in
  buildStats over the same non-guest, non-honest-AI games as the rest of the stats.
- wire: StatsView gains moves + hints_used (trailing); gateway + UI codec + model;
  regen.
- ui: two tiles (Moves, Hint share = hints_used/moves, one-decimal % in the active
  locale); card order games·wins·draws·losses·moves·hint-share·best game·win-rate.
- docs: ARCHITECTURE §9 + baseline comment, FUNCTIONAL (+ru), UI_DESIGN.

Tests: TestHintPolicy (hints_used counts the wallet hint), TestGameLifecycleAndStats
(moves>0, hints=0), gateway stats round-trip, UI hintShare unit + codec + e2e.
developer added 1 commit 2026-06-17 21:56:10 +00:00
fix(merge): carry moves/hints and per-variant best moves on account merge
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m18s
ed7c6ac56d
The account merge predated the recent statistics additions and silently dropped
them:
- account_stats.moves and hints_used were not summed into the primary (the
  secondary's row is read, then deleted), so the secondary's lifetime moves and
  hints were lost.
- account_best_move was not handled at all; the secondary is only tombstoned
  (not deleted), so its per-variant best moves lingered on the dead account and
  never reached the merged statistics screen.

mergeStats now also sums moves + hints_used; a new mergeBestMoves folds the
secondary's best moves into the primary keeping the higher-scoring play per
variant (the rule the per-game upsert uses), then deletes the secondary's rows.
Profile fields were already correct (hint wallet summed, paid_account ORed; no
new accounts columns since).

Tests: TestAccountMergeCore extended — moves/hints summed, best moves merged
(higher score per variant kept), secondary rows cleaned up. Docs: ARCHITECTURE §4.
developer added 1 commit 2026-06-17 22:09:58 +00:00
feat(admin): show moves and hints used on the user card
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m21s
d97cec7d8a
The admin user-detail stats panel listed wins/losses/draws/best game/best move
but not the two new lifetime counters. Surface them as raw operator-facing
numbers (Moves, Hints used) next to the rest.

Audit follow-up (no code needed elsewhere): the load-test harness uses the
shared pkg/fbs codec and only checks result codes / reads a StateView subset, so
the trailing StatsView/StateView/HintResult fields are backward-compatible there;
the game-detail per-seat "hints used" already reflects the true total via the
repurposed counter.
developer added 1 commit 2026-06-17 22:12:03 +00:00
chore: stats.played label
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m10s
f05e6ca407
owner approved these changes 2026-06-17 22:12:28 +00:00
developer merged commit 8793bd34f2 into development 2026-06-17 22:17:27 +00:00
developer deleted branch feature/stats-best-move-word 2026-06-17 22:17:27 +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#81