feat(stats): show the best move word per game variant
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m11s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m11s
Replace the single "best move" number on the statistics screen with a
full-width per-variant breakdown: the highest-scoring play in each variant
the player has played, drawn as game tiles (a wildcard shows its letter but
no value), with the words and scores right-aligned to shared edges.
- backend: new account_best_move table (PK account_id+variant) keeping the
main word as JSON tiles {letter,value,blank}; captured at game finish in
buildStats (blank flags taken from every placed blank — equivalent to the
final board), upserted in the finish transaction and replaced only by a
strictly higher-scoring play. Guest/honest-AI games still record nothing.
GetStats + statsDTO expose best_moves.
- wire: StatsView gains best_moves:[BestMoveView{variant,score,word:[BestMoveTile]}]
(trailing, backward-compatible); gateway encodeStats + UI codec updated.
- ui: new WordTiles component (board's tile look, fixed px size); Stats.svelte
drops the maxWord card and adds the full-width best-move card (catalogue
order, empty variants omitted).
- docs: ARCHITECTURE §9 + schema, FUNCTIONAL (+ru), UI_DESIGN.
Tests: mainWordTiles unit + buildStats end-to-end (inttest) + gateway and UI
codec round-trips (incl. a blank tile) + e2e.
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"scrabble/backend/internal/account"
|
||||
"scrabble/backend/internal/engine"
|
||||
"scrabble/backend/internal/game"
|
||||
)
|
||||
@@ -120,8 +121,8 @@ func TestGameLifecycleAndStats(t *testing.T) {
|
||||
t.Fatalf("final game not finished: %+v", last.Game)
|
||||
}
|
||||
|
||||
w0, l0, d0, mg0, _, ok0 := readStats(t, seats[0])
|
||||
w1, l1, d1, mg1, _, ok1 := readStats(t, seats[1])
|
||||
w0, l0, d0, mg0, mw0, ok0 := readStats(t, seats[0])
|
||||
w1, l1, d1, mg1, mw1, ok1 := readStats(t, seats[1])
|
||||
if !ok0 || !ok1 {
|
||||
t.Fatal("both players must have a stats row")
|
||||
}
|
||||
@@ -133,6 +134,44 @@ func TestGameLifecycleAndStats(t *testing.T) {
|
||||
if !decisive && !draw {
|
||||
t.Errorf("inconsistent W/L/D: p0(%d/%d/%d) p1(%d/%d/%d)", w0, l0, d0, w1, l1, d1)
|
||||
}
|
||||
|
||||
// Each player who made a scoring play gets exactly one per-variant best move (only
|
||||
// scrabble_en was played here); its score equals the aggregate max_word_points and it
|
||||
// carries the decoded word as tiles.
|
||||
accounts := account.NewStore(testDB)
|
||||
for _, p := range []struct {
|
||||
id uuid.UUID
|
||||
maxWord int
|
||||
}{{seats[0], mw0}, {seats[1], mw1}} {
|
||||
if p.maxWord == 0 {
|
||||
continue // a player who only passed has no best move
|
||||
}
|
||||
st, err := accounts.GetStats(ctx, p.id)
|
||||
if err != nil {
|
||||
t.Fatalf("get stats: %v", err)
|
||||
}
|
||||
if len(st.BestMoves) != 1 {
|
||||
t.Fatalf("want one best move (only scrabble_en played), got %d: %+v", len(st.BestMoves), st.BestMoves)
|
||||
}
|
||||
bm := st.BestMoves[0]
|
||||
if bm.Variant != engine.VariantEnglish.String() {
|
||||
t.Errorf("best move variant = %q, want %q", bm.Variant, engine.VariantEnglish.String())
|
||||
}
|
||||
if bm.Score != p.maxWord {
|
||||
t.Errorf("best move score %d != max_word_points %d", bm.Score, p.maxWord)
|
||||
}
|
||||
if len(bm.Tiles) == 0 {
|
||||
t.Error("best move word is empty")
|
||||
}
|
||||
for _, tl := range bm.Tiles {
|
||||
if tl.Letter == "" {
|
||||
t.Error("best move tile has empty letter")
|
||||
}
|
||||
if tl.Blank && tl.Value != 0 {
|
||||
t.Errorf("blank tile must score 0, got %d", tl.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestReplayEquivalence plays a few moves through one service, then proves a
|
||||
|
||||
Reference in New Issue
Block a user