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)
|
||||
|
||||
|
||||
+6
-1
@@ -229,7 +229,12 @@ honest-AI practice game (a live game's export would leak the move journal; an AI
|
||||
game is throwaway). The client shares the `.gcg` file where the platform supports
|
||||
it, otherwise downloads it. Statistics (durable accounts only):
|
||||
wins, losses, draws, max points in a game, and max points for a single move (the
|
||||
best play, which already includes every word it formed plus the all-tiles bonus).
|
||||
best play, which already includes every word it formed plus the all-tiles bonus). It
|
||||
also shows the player's **move count** (their plays — passes and exchanges do not
|
||||
count) and their **hint share** (the percentage of those plays where a hint was taken).
|
||||
The best move is also broken down **per game variant**, showing the **word itself**
|
||||
drawn as game tiles (a wildcard shows its letter but no value) — one row per variant
|
||||
the player has played, omitting variants with no plays.
|
||||
A game that can no longer be continued — because a rule changed and an earlier
|
||||
move would now be illegal — is closed as a **draw** the moment someone opens it,
|
||||
never left stuck on an error: the move history shows an impersonal organizer note
|
||||
|
||||
@@ -233,7 +233,12 @@ UTC), суточного окна отсутствия (away; сетка по 10
|
||||
с ИИ одноразовая). Клиент делится файлом `.gcg` там, где платформа это поддерживает,
|
||||
иначе скачивает его. Статистика (только у постоянных аккаунтов):
|
||||
победы, поражения, ничьи, макс. очков за партию и макс. очков за один ход (лучший
|
||||
ход, уже включающий все образованные им слова и бонус за все фишки).
|
||||
ход, уже включающий все образованные им слова и бонус за все фишки). Также
|
||||
показываются **число ходов** игрока (его выкладки — пасы и обмены не считаются) и
|
||||
**доля подсказок** (процент таких ходов, на которых бралась подсказка).
|
||||
Лучший ход также даётся **с разбивкой по вариантам игры** — **само слово**,
|
||||
нарисованное игровыми фишками (wildcard показывает свою букву без очков), по одной
|
||||
строке на каждый сыгранный вариант; варианты без ходов не выводятся.
|
||||
Партия, которую больше нельзя продолжить — из-за изменения правил более ранний ход
|
||||
стал бы недопустимым, — закрывается **ничьёй** в момент открытия её игроком, а не
|
||||
остаётся висеть с ошибкой: в конце истории ходов показывается обезличенная заметка
|
||||
|
||||
+10
-3
@@ -279,9 +279,16 @@ enabled on the first, uncached load) and flip in place when an event refreshes t
|
||||
and the "searching" hint is hidden; the game starts already seated, the opponent shows as **🤖**
|
||||
everywhere (score card, lobby row, turn line), the add-friend 🤝 is never drawn, and the chat's
|
||||
send field and 🛎️ nudge stay **disabled** while the 🔎 dictionary word-check keeps working.
|
||||
- **Statistics** (`screens/Stats.svelte`, the lobby 📊 tab): a 2-column grid of stat
|
||||
cards (wins / losses / draws / games / win-rate / best game / best move) — pure
|
||||
numbers, no charts.
|
||||
- **Statistics** (`screens/Stats.svelte`, the lobby ✏️ tab): a 2-column grid of stat
|
||||
cards (games / wins / draws / losses / moves / hint-share / best game / win-rate) —
|
||||
pure numbers, no charts; hint-share is a one-decimal percentage in the active locale's
|
||||
notation (e.g. "4.8%" / "4,8%") — followed by a **full-width "best move" card** that
|
||||
breaks the best move
|
||||
down per variant: one row per played variant (catalogue order, empty variants
|
||||
omitted) with the variant name on the left, the **word drawn as game tiles**
|
||||
(`components/WordTiles.svelte`, the board's placed-tile look at a small fixed size;
|
||||
a wildcard shows its letter but no value) right-aligned to a shared edge, and the
|
||||
score right-aligned in its own column.
|
||||
- **Profile editing** (`screens/Profile.svelte`): an inline form — display name, a
|
||||
**UTC-offset** timezone dropdown (defaulting to the browser's offset), the away
|
||||
window as hour + 10-minute dropdowns (24-hour, ≤ 12 h), and block toggles — plus an
|
||||
|
||||
Reference in New Issue
Block a user