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:
@@ -59,7 +59,7 @@ func TestGameStateRoundTripForwardsUserID(t *testing.T) {
|
||||
if r.URL.Path != "/api/v1/user/games/g-1/state" {
|
||||
t.Errorf("unexpected path %q", r.URL.Path)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"game":{"id":"g-1","variant":"scrabble_en","status":"active","players":2,"to_move":1,"seats":[{"seat":0,"account_id":"u-7","score":5}]},"seat":0,"rack":[0,1],"bag_len":80,"hints_remaining":1}`))
|
||||
_, _ = w.Write([]byte(`{"game":{"id":"g-1","variant":"scrabble_en","status":"active","players":2,"to_move":1,"seats":[{"seat":0,"account_id":"u-7","score":5}]},"seat":0,"rack":[0,1],"bag_len":80,"hints_remaining":4,"wallet_balance":3}`))
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
@@ -77,8 +77,8 @@ func TestGameStateRoundTripForwardsUserID(t *testing.T) {
|
||||
t.Fatalf("handler: %v", err)
|
||||
}
|
||||
st := fb.GetRootAsStateView(payload, 0)
|
||||
if st.BagLen() != 80 || st.RackLength() != 2 || st.HintsRemaining() != 1 {
|
||||
t.Fatalf("state decoded wrong: bag=%d rack=%d hints=%d", st.BagLen(), st.RackLength(), st.HintsRemaining())
|
||||
if st.BagLen() != 80 || st.RackLength() != 2 || st.HintsRemaining() != 4 || st.WalletBalance() != 3 {
|
||||
t.Fatalf("state decoded wrong: bag=%d rack=%d hints=%d wallet=%d", st.BagLen(), st.RackLength(), st.HintsRemaining(), st.WalletBalance())
|
||||
}
|
||||
game := st.Game(nil)
|
||||
if game == nil || string(game.Id()) != "g-1" || string(game.Variant()) != "scrabble_en" || game.ToMove() != 1 {
|
||||
@@ -317,7 +317,7 @@ func TestHintRoundTrip(t *testing.T) {
|
||||
if r.URL.Path != "/api/v1/user/games/g-3/hint" {
|
||||
t.Errorf("unexpected path %q", r.URL.Path)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"move":{"player":0,"action":"play","words":["CAT"],"score":9},"hints_remaining":2}`))
|
||||
_, _ = w.Write([]byte(`{"move":{"player":0,"action":"play","words":["CAT"],"score":9},"hints_remaining":2,"wallet_balance":1}`))
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
@@ -328,8 +328,8 @@ func TestHintRoundTrip(t *testing.T) {
|
||||
t.Fatalf("handler: %v", err)
|
||||
}
|
||||
hr := fb.GetRootAsHintResult(payload, 0)
|
||||
if hr.HintsRemaining() != 2 {
|
||||
t.Errorf("hints remaining = %d, want 2", hr.HintsRemaining())
|
||||
if hr.HintsRemaining() != 2 || hr.WalletBalance() != 1 {
|
||||
t.Errorf("hint decoded wrong: hints=%d wallet=%d", hr.HintsRemaining(), hr.WalletBalance())
|
||||
}
|
||||
var move fb.MoveRecord
|
||||
hr.Move(&move)
|
||||
|
||||
Reference in New Issue
Block a user