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.
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.
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.
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.
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.