caefc8f579
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
Decide who moves first by the official rule: each seated player draws one tile, the one closest to "A" leads (a blank beats every letter), ties re-drawing until a single leader remains. Each draw uses honest per-draw crypto/rand entropy (not the deterministic bag seed), so the recorded draw — not a seed — is the only account of the outcome. The leader takes seat 0, so the engine and journal replay are unchanged. The draw is recorded with the game (game_setup_draws, migration 00013) for future tournaments, designed as a discrete "player N draws a tile" step. Friend/AI games draw at creation. Auto-match draws when the game opens, against a synthetic uuid.Nil opponent whose draw rows (NULL account_id) are back-filled to the real opponent on join — so the opener's seat is fixed up front and the existing open-game pre-move is preserved with no reseating. Admin /_gm/games/:id gains the recorded draw list and a simple step-by-step board replay (game.ReplayTimeline + a vanilla-JS stepper): a board with A-O/1-15 headers and highlighted premium squares, placed letters with their tile value as a subscript, rack panels around the board (seat 0 top, 1 bottom, 2 left, 3 right) with the current player highlighted, and a per-move log with the tiles drawn and the bag remainder. Docs: ARCHITECTURE §6/§9, FUNCTIONAL (+_ru), PRERELEASE (FM row), design spec.
51 lines
1.6 KiB
Go
51 lines
1.6 KiB
Go
package game
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"scrabble/backend/internal/engine"
|
|
)
|
|
|
|
func TestUsedTiles(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mv HistoryMove
|
|
want []string
|
|
}{
|
|
{"pass", HistoryMove{Action: "pass"}, nil},
|
|
{"resign", HistoryMove{Action: "resign"}, nil},
|
|
{"play with blank", HistoryMove{Action: "play", Tiles: []engine.TileRecord{{Letter: "a"}, {Letter: "b", Blank: true}}}, []string{"a", "?"}},
|
|
{"exchange", HistoryMove{Action: "exchange", Exchanged: []string{"a", "?"}}, []string{"a", "?"}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := usedTiles(tt.mv); !reflect.DeepEqual(got, tt.want) {
|
|
t.Fatalf("usedTiles = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDrawnTiles(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
before, used []string
|
|
after []string
|
|
want []string
|
|
}{
|
|
{"play refill", []string{"a", "b", "c", "d"}, []string{"a", "b"}, []string{"c", "d", "e", "f"}, []string{"e", "f"}},
|
|
{"blank played", []string{"?", "a"}, []string{"?"}, []string{"a", "x"}, []string{"x"}},
|
|
{"pass keeps rack", []string{"a", "b"}, nil, []string{"a", "b"}, nil},
|
|
{"duplicate letters", []string{"e", "e", "e"}, []string{"e"}, []string{"e", "e", "q"}, []string{"q"}},
|
|
{"empty bag no refill", []string{"a", "b"}, []string{"a"}, []string{"b"}, nil},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := drawnTiles(tt.before, tt.after, tt.used); !reflect.DeepEqual(got, tt.want) {
|
|
t.Fatalf("drawnTiles = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|