6d0dd4fb14
backend/internal/engine wraps the sibling scrabble-solver library in-process: - Registry: versioned DAWG load via dafsa.Load, keyed by (variant, dict_version), latest-per-variant; English / Russian / Эрудит handled uniformly. - Bag: own deterministic, seeded tile bag with Draw + Return (for exchanges), since the solver's self-play bag cannot return tiles. - Game: pure rules engine — deal, play/pass/exchange/resign, refill, per-move scoring, turn order, and end-condition detection (empty bag + empty rack, six scoreless turns, resignation) with end-game rack adjustment. - decode/ReplayBoard: dictionary-independent MoveRecords and board replay via scrabble.Apply (no internal/encoding), realising ARCHITECTURE §9.1. Wiring: go.work gains "replace scrabble-solver => ../scrabble-solver"; backend requires scrabble-solver (placeholder) and github.com/iliadenisov/dafsa directly. Both Go CI workflows clone the public solver sibling (master HEAD, no token) and set BACKEND_DICT_DIR. Docs: ARCHITECTURE §5/§14, TESTING engine layer, backend README, and PLAN refinements + deferred TODOs (publish/version solver; split engine vs dictionary generator).
69 lines
1.9 KiB
Go
69 lines
1.9 KiB
Go
package engine
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"scrabble-solver/scrabble"
|
|
)
|
|
|
|
// blankCellFlag is the bit board cells set for a blank tile (board.go encoding).
|
|
const blankCellFlag byte = 0x80
|
|
|
|
// TestDecodeBlankPlayAndReplay places "cat" with the C drawn from a blank, then
|
|
// checks the decoded record keeps the concrete letter and the blank flag, and
|
|
// that ReplayBoard — using only the ruleset, no dictionary — reproduces the
|
|
// blank on the board.
|
|
func TestDecodeBlankPlayAndReplay(t *testing.T) {
|
|
g := newEnglishGame(t, 1)
|
|
rs := g.rules
|
|
row, col := centre(rs)
|
|
|
|
idx := func(s string) byte {
|
|
t.Helper()
|
|
i, err := rs.Alphabet.Index(s)
|
|
if err != nil {
|
|
t.Fatalf("index %q: %v", s, err)
|
|
}
|
|
return i
|
|
}
|
|
ps := []scrabble.Placement{
|
|
{Row: row, Col: col, Letter: idx("c"), Blank: true},
|
|
{Row: row, Col: col + 1, Letter: idx("a")},
|
|
{Row: row, Col: col + 2, Letter: idx("t")},
|
|
}
|
|
|
|
move, err := g.solver.ValidatePlay(g.board, scrabble.Horizontal, ps)
|
|
if err != nil {
|
|
t.Fatalf("validate: %v", err)
|
|
}
|
|
rec := g.recordPlay(0, move)
|
|
|
|
if rec.Action != ActionPlay || len(rec.Tiles) != 3 {
|
|
t.Fatalf("record = %+v, want a 3-tile play", rec)
|
|
}
|
|
if blank := rec.Tiles[0]; blank.Letter != "c" || !blank.Blank {
|
|
t.Errorf("blank tile = %+v, want letter \"c\" with Blank=true", blank)
|
|
}
|
|
if rec.Tiles[1].Blank || rec.Tiles[1].Letter != "a" {
|
|
t.Errorf("second tile = %+v, want plain \"a\"", rec.Tiles[1])
|
|
}
|
|
if len(rec.Words) == 0 || rec.Words[0] != "cat" {
|
|
t.Errorf("words = %v, want main word \"cat\"", rec.Words)
|
|
}
|
|
|
|
rs2, err := Ruleset(VariantEnglish)
|
|
if err != nil {
|
|
t.Fatalf("ruleset: %v", err)
|
|
}
|
|
b, err := ReplayBoard(rs2, []MoveRecord{rec})
|
|
if err != nil {
|
|
t.Fatalf("replay: %v", err)
|
|
}
|
|
if b.At(row, col)&blankCellFlag == 0 {
|
|
t.Error("replayed centre cell lost its blank flag")
|
|
}
|
|
if !b.Filled(row, col+1) || b.At(row, col+1)&blankCellFlag != 0 {
|
|
t.Error("replayed \"a\" cell should be a filled, non-blank tile")
|
|
}
|
|
}
|