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

17 Commits

Author SHA1 Message Date
Ilia Denisov f05e6ca407 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
2026-06-18 00:11:59 +02:00
Ilia Denisov d97cec7d8a 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
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.
2026-06-18 00:09:39 +02:00
Ilia Denisov ed7c6ac56d 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
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.
2026-06-17 23:56:04 +02:00
Ilia Denisov 6d1d8030e3 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
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.
2026-06-17 23:44:53 +02:00
Ilia Denisov 91c4efc8a8 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
2026-06-17 22:58:17 +02:00
Ilia Denisov da491b6bde revert: game-over phrases 2026-06-17 22:54:28 +02:00
Ilia Denisov ffe9150f26 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
2026-06-17 21:54:22 +02:00
Ilia Denisov 071ff8ac37 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
2026-06-17 21:34:37 +02:00
Ilia Denisov 4e347702f3 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
2026-06-17 20:45:23 +02:00
Ilia Denisov 63669a3b25 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
2026-06-17 19:18:27 +02:00
Ilia Denisov 7832bf10f5 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
2026-06-17 18:58:25 +02:00
Ilia Denisov 1fe25a3a15 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
2026-06-17 18:04:48 +02:00
Ilia Denisov 8b7ddb9df0 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
2026-06-17 17:40:53 +02:00
Ilia Denisov 68972375f9 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
2026-06-17 17:37:07 +02:00
Ilia Denisov 5b0f608bb2 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
2026-06-17 17:18:09 +02:00
Ilia Denisov f9faebfa91 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
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.
2026-06-17 16:50:16 +02:00
Ilia Denisov cbb485ebd6 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
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.
2026-06-17 15:29:55 +02:00