feat(stats): best-move word, moves & hint-share, and a hint-count fix (#81)
The statistics screen gains real depth, plus a hint-count bug fix found along the way. - Best move per variant: the screen shows the actual best-move word (drawn as game tiles; a wildcard shows its letter but no value), broken down by game variant, empty variants omitted. New account_best_move table, written at game finish. - Moves & hint share: two new lifetime tiles — the player's play count and the share of plays that used a hint — from summed account_stats counters (moves, hints_used). Honest-AI games are excluded, like the rest of the stats. - Hint-count fix: the in-game hint badge no longer goes stale across games. The global wallet now rides the wire apart from the per-game allowance (wallet_balance on StateView/HintResult/StatsView), so the client reads the live wallet rather than a per-game snapshot; game_players.hints_used now counts every hint (allowance + wallet), its true per-game total. - Account merge: sums the new moves/hints_used counters and merges the per-variant best moves (higher score kept), which it previously dropped. - Admin: the user card shows Moves and Hints used. - UI polish: tab/label wording, game-over text, and e2e selectors hardened against label changes. All wire additions are trailing (backward-compatible). Docs (ARCHITECTURE, FUNCTIONAL +ru, UISN_DESIGN) updated in step.
This commit was merged in pull request #81.
This commit is contained in:
+26
-5
@@ -203,7 +203,8 @@ arrive from a platform rather than completing a mandatory registration).
|
||||
guest is promoted to durable, clearing `is_guest`).
|
||||
- **Merge** retires the account that owns the linked identity into the **current**
|
||||
account, in a single transaction (`internal/accountmerge`): statistics summed
|
||||
(max points kept), the hint wallet summed, `paid_account` ORed, identities
|
||||
(counters incl. moves/hints added, max points kept, and the per-variant best moves
|
||||
merged keeping the higher-scoring play), the hint wallet summed, `paid_account` ORed, identities
|
||||
repointed, games / chat / complaints transferred, friends and blocks
|
||||
de-duplicated (friendships keep the strongest status accepted>pending>declined),
|
||||
pending invitations/codes dropped, and the secondary kept as an **audit
|
||||
@@ -347,7 +348,13 @@ Key points:
|
||||
placement and leaves the commit to the player. When the rack has no legal move the
|
||||
service spends **nothing** and returns `ErrNoHintAvailable` — surfaced as the distinct
|
||||
result code `no_hint_available` (separate from `hint_unavailable`) so the UI can say
|
||||
"no options" rather than "no hints left".
|
||||
"no options" rather than "no hints left". The hint count shown to the player is the
|
||||
per-game allowance remaining **plus** the global wallet; because the wallet is global,
|
||||
`game.state`/`game.hint` carry it as a separate `wallet_balance` field beside the combined
|
||||
`hints_remaining`, so the client derives the per-game allowance (`hints_remaining -
|
||||
wallet_balance`, which it may cache per game) and reads the wallet **live** from the
|
||||
profile — otherwise a wallet hint spent in one game would leave a stale, too-high count
|
||||
cached on every other game.
|
||||
- **Word-check tool**: unlimited dictionary lookups against the game's pinned
|
||||
dictionary; each result offers a **complaint** (complainant, game, variant,
|
||||
dict_version, word, the disputed result, an optional note) that lands in the admin
|
||||
@@ -568,7 +575,8 @@ disguised robot stays indistinguishable from a person.
|
||||
the `kind` admitting `robot`),
|
||||
`sessions` (revoke-only opaque-token hashes), the game tables
|
||||
`games` (carrying the `dropout_tiles` disposition column), `game_players`,
|
||||
`game_moves` (the move journal), `complaints` and `account_stats`, and the
|
||||
`game_moves` (the move journal), `complaints`, `account_stats` and
|
||||
`account_best_move`, and the
|
||||
social/lobby tables `friendships` (the request/accept graph, its status admitting
|
||||
`declined`), `blocks`
|
||||
(per-user blocks), `chat_messages` (per-game chat and nudges, carrying the per-message
|
||||
@@ -596,8 +604,21 @@ disguised robot stays indistinguishable from a person.
|
||||
non-guest accounts only — the finish-time recompute skips any `is_guest`
|
||||
seat): wins, losses, **draws**, max points in a game, and
|
||||
max points for a single **move** (which already folds in every word the move
|
||||
formed plus the all-tiles bonus). A tie increments draws only; a resignation or
|
||||
timeout is a loss for the acting player.
|
||||
formed plus the all-tiles bonus); plus two summed counters — `moves` (the player's
|
||||
plays, i.e. tile placements; passes and exchanges do not count) and `hints_used`
|
||||
(every hint taken, allowance + wallet) — from which the screen derives the **hint
|
||||
share** = hints_used / moves. A tie increments draws only; a resignation or
|
||||
timeout is a loss for the acting player. A companion table **`account_best_move`**
|
||||
(keyed by account **and variant**) keeps the highest-scoring single play **per
|
||||
variant** with the word itself: its main word as an ordered JSON array of tiles
|
||||
(letter, tile value, blank flag — value 0 for a blank), so the statistics screen
|
||||
renders it as game tiles without the variant's alphabet table. Blank flags are
|
||||
taken from every blank ever placed in the game (equivalent to reading the final
|
||||
board, since a placed tile never moves). It is replaced only by a strictly
|
||||
higher-scoring play, written in the same finish transaction, and skipped for
|
||||
guest/honest-AI games exactly like `account_stats`. It is filled forward only —
|
||||
plays finished before the table existed are not back-populated (the aggregate
|
||||
`max_word_points` still covers them numerically).
|
||||
|
||||
### 9.1 History invariant (must hold forever)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user