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:
@@ -30,6 +30,34 @@ func setStats(t *testing.T, id uuid.UUID, w, l, d, mg, mw int) {
|
||||
}
|
||||
}
|
||||
|
||||
func setStatsCounts(t *testing.T, id uuid.UUID, moves, hints int) {
|
||||
t.Helper()
|
||||
if _, err := testDB.ExecContext(context.Background(),
|
||||
`UPDATE backend.account_stats SET moves=$2, hints_used=$3 WHERE account_id=$1`, id, moves, hints); err != nil {
|
||||
t.Fatalf("set stats counts: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func setBestMove(t *testing.T, id uuid.UUID, variant string, score int) {
|
||||
t.Helper()
|
||||
tiles := `[{"letter":"c","value":3,"blank":false}]`
|
||||
if _, err := testDB.ExecContext(context.Background(),
|
||||
`INSERT INTO backend.account_best_move (account_id, variant, score, tiles) VALUES ($1,$2,$3,$4)
|
||||
ON CONFLICT (account_id, variant) DO UPDATE SET score=$3, tiles=$4`, id, variant, score, tiles); err != nil {
|
||||
t.Fatalf("set best move: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func bestMoveCount(t *testing.T, id uuid.UUID) int {
|
||||
t.Helper()
|
||||
var n int
|
||||
if err := testDB.QueryRowContext(context.Background(),
|
||||
`SELECT count(*) FROM backend.account_best_move WHERE account_id=$1`, id).Scan(&n); err != nil {
|
||||
t.Fatalf("best move count: %v", err)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func setWallet(t *testing.T, id uuid.UUID, hints int, paid bool) {
|
||||
t.Helper()
|
||||
if _, err := testDB.ExecContext(context.Background(),
|
||||
@@ -110,8 +138,15 @@ func TestAccountMergeCore(t *testing.T) {
|
||||
|
||||
setStats(t, primary, 1, 0, 0, 100, 90)
|
||||
setStats(t, secondary, 3, 1, 2, 400, 80)
|
||||
setStatsCounts(t, primary, 100, 5)
|
||||
setStatsCounts(t, secondary, 50, 8)
|
||||
setWallet(t, primary, 2, false)
|
||||
setWallet(t, secondary, 5, true)
|
||||
// Best moves: secondary's scrabble_en (80) beats primary's (50) and is kept; secondary's
|
||||
// scrabble_ru (30) is new to primary and carried over.
|
||||
setBestMove(t, primary, "scrabble_en", 50)
|
||||
setBestMove(t, secondary, "scrabble_en", 80)
|
||||
setBestMove(t, secondary, "scrabble_ru", 30)
|
||||
|
||||
email := "merge-" + uuid.NewString() + "@example.com"
|
||||
bindEmailIdentity(t, secondary, email)
|
||||
@@ -153,6 +188,26 @@ func TestAccountMergeCore(t *testing.T) {
|
||||
if mergedInto(t, secondary) != primary {
|
||||
t.Errorf("secondary.merged_into = %s, want primary %s", mergedInto(t, secondary), primary)
|
||||
}
|
||||
|
||||
// The moves/hints counters sum, and the per-variant best moves merge (the higher score
|
||||
// per variant kept), with the secondary's best-move rows cleaned up.
|
||||
st, err := store.GetStats(ctx, primary)
|
||||
if err != nil {
|
||||
t.Fatalf("get merged stats: %v", err)
|
||||
}
|
||||
if st.Moves != 150 || st.HintsUsed != 13 {
|
||||
t.Errorf("primary moves/hints = %d/%d, want 150/13", st.Moves, st.HintsUsed)
|
||||
}
|
||||
best := map[string]int{}
|
||||
for _, b := range st.BestMoves {
|
||||
best[b.Variant] = b.Score
|
||||
}
|
||||
if len(st.BestMoves) != 2 || best["scrabble_en"] != 80 || best["scrabble_ru"] != 30 {
|
||||
t.Errorf("primary best moves = %+v, want scrabble_en:80 + scrabble_ru:30", st.BestMoves)
|
||||
}
|
||||
if bestMoveCount(t, secondary) != 0 {
|
||||
t.Error("secondary best-move rows should be deleted after merge")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAccountMergeActiveGameConflict refuses a merge when the two share an active
|
||||
|
||||
Reference in New Issue
Block a user