Stage 2: engine package over scrabble-solver (registry, bag, Game, replay)
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).
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"scrabble-solver/rules"
|
||||
"scrabble-solver/scrabble"
|
||||
)
|
||||
|
||||
// testVersion labels the single dictionary version the tests register.
|
||||
const testVersion = "test"
|
||||
|
||||
// testReg is the shared registry of all three variants, hydrated once by
|
||||
// TestMain and reused by the read-only tests.
|
||||
var testReg *Registry
|
||||
|
||||
// TestMain loads the committed dictionaries once and shares them with every
|
||||
// test. It fails loudly when the dictionary directory is absent (per
|
||||
// docs/TESTING.md) rather than skipping coverage.
|
||||
func TestMain(m *testing.M) {
|
||||
reg, err := Open(testDictDir(), testVersion)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "engine test setup:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
testReg = reg
|
||||
code := m.Run()
|
||||
_ = reg.Close()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
// testDictDir resolves the directory holding the committed scrabble-solver
|
||||
// DAWGs: BACKEND_DICT_DIR when set (used in CI), otherwise the sibling checkout
|
||||
// located relative to this test file.
|
||||
func testDictDir() string {
|
||||
if dir := os.Getenv("BACKEND_DICT_DIR"); dir != "" {
|
||||
return dir
|
||||
}
|
||||
_, file, _, _ := runtime.Caller(0)
|
||||
return filepath.Join(filepath.Dir(file), "..", "..", "..", "..", "scrabble-solver", "dawg")
|
||||
}
|
||||
|
||||
// centre returns the centre square coordinates of rs.
|
||||
func centre(rs *rules.Ruleset) (row, col int) {
|
||||
return rs.Center / rs.Cols, rs.Center % rs.Cols
|
||||
}
|
||||
|
||||
// placementsForWord lays word out from (row, col) along dir, resolving each rune
|
||||
// through the ruleset's alphabet. It expresses no blanks.
|
||||
func placementsForWord(t *testing.T, rs *rules.Ruleset, row, col int, dir scrabble.Direction, word string) []scrabble.Placement {
|
||||
t.Helper()
|
||||
var ps []scrabble.Placement
|
||||
for i, r := range []rune(word) {
|
||||
idx, err := rs.Alphabet.Index(string(r))
|
||||
if err != nil {
|
||||
t.Fatalf("index %q: %v", string(r), err)
|
||||
}
|
||||
rr, cc := row, col
|
||||
if dir == scrabble.Horizontal {
|
||||
cc += i
|
||||
} else {
|
||||
rr += i
|
||||
}
|
||||
ps = append(ps, scrabble.Placement{Row: rr, Col: cc, Letter: idx})
|
||||
}
|
||||
return ps
|
||||
}
|
||||
Reference in New Issue
Block a user