feat(stats): best-move word, moves & hint-share, and a hint-count fix (#81)
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 9s
CI / integration (push) Successful in 17s
CI / ui (push) Successful in 52s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m8s

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:
2026-06-17 22:17:27 +00:00
parent 5a3f0951ae
commit 8793bd34f2
71 changed files with 1789 additions and 132 deletions
+50 -2
View File
@@ -91,15 +91,63 @@ func encodeRedeemResult(r backendclient.RedeemResultResp) []byte {
return b.FinishedBytes()
}
// encodeStats builds a StatsView payload.
// buildBestMoveTile builds a BestMoveTile table and returns its offset.
func buildBestMoveTile(b *flatbuffers.Builder, t backendclient.BestMoveTileResp) flatbuffers.UOffsetT {
letter := b.CreateString(t.Letter)
fb.BestMoveTileStart(b)
fb.BestMoveTileAddLetter(b, letter)
fb.BestMoveTileAddValue(b, int32(t.Value))
fb.BestMoveTileAddBlank(b, t.Blank)
return fb.BestMoveTileEnd(b)
}
// buildBestMove builds a BestMoveView table — its word tile vector first, per the
// bottom-up rule — and returns its offset.
func buildBestMove(b *flatbuffers.Builder, m backendclient.BestMoveResp) flatbuffers.UOffsetT {
tiles := make([]flatbuffers.UOffsetT, len(m.Word))
for i, t := range m.Word {
tiles[i] = buildBestMoveTile(b, t)
}
fb.BestMoveViewStartWordVector(b, len(tiles))
for i := len(tiles) - 1; i >= 0; i-- {
b.PrependUOffsetT(tiles[i])
}
word := b.EndVector(len(tiles))
variant := b.CreateString(m.Variant)
fb.BestMoveViewStart(b)
fb.BestMoveViewAddVariant(b, variant)
fb.BestMoveViewAddScore(b, int32(m.Score))
fb.BestMoveViewAddWord(b, word)
return fb.BestMoveViewEnd(b)
}
// encodeStats builds a StatsView payload, embedding the per-variant best moves (each a
// BestMoveView with its word tiles) when present.
func encodeStats(r backendclient.StatsResp) []byte {
b := flatbuffers.NewBuilder(64)
b := flatbuffers.NewBuilder(256)
var bestMoves flatbuffers.UOffsetT
if len(r.BestMoves) > 0 {
offs := make([]flatbuffers.UOffsetT, len(r.BestMoves))
for i, m := range r.BestMoves {
offs[i] = buildBestMove(b, m)
}
fb.StatsViewStartBestMovesVector(b, len(offs))
for i := len(offs) - 1; i >= 0; i-- {
b.PrependUOffsetT(offs[i])
}
bestMoves = b.EndVector(len(offs))
}
fb.StatsViewStart(b)
fb.StatsViewAddWins(b, int32(r.Wins))
fb.StatsViewAddLosses(b, int32(r.Losses))
fb.StatsViewAddDraws(b, int32(r.Draws))
fb.StatsViewAddMaxGamePoints(b, int32(r.MaxGamePoints))
fb.StatsViewAddMaxWordPoints(b, int32(r.MaxWordPoints))
fb.StatsViewAddMoves(b, int32(r.Moves))
fb.StatsViewAddHintsUsed(b, int32(r.HintsUsed))
if len(r.BestMoves) > 0 {
fb.StatsViewAddBestMoves(b, bestMoves)
}
b.Finish(fb.StatsViewEnd(b))
return b.FinishedBytes()
}