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).
101 lines
3.1 KiB
Go
101 lines
3.1 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"scrabble-solver/board"
|
|
"scrabble-solver/scrabble"
|
|
)
|
|
|
|
// TestRegistryOpensEveryVariant checks that Open loads all three variants at the
|
|
// requested version and reports them through Latest and Versions.
|
|
func TestRegistryOpensEveryVariant(t *testing.T) {
|
|
for _, v := range Variants() {
|
|
version, solver, err := testReg.Latest(v)
|
|
if err != nil {
|
|
t.Fatalf("latest %s: %v", v, err)
|
|
}
|
|
if version != testVersion {
|
|
t.Errorf("latest %s version = %q, want %q", v, version, testVersion)
|
|
}
|
|
if solver == nil {
|
|
t.Errorf("latest %s solver is nil", v)
|
|
}
|
|
if got := testReg.Versions(v); len(got) != 1 || got[0] != testVersion {
|
|
t.Errorf("versions %s = %v, want [%q]", v, got, testVersion)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRegistryValidatesKnownWords is the per-variant smoke test: a known word
|
|
// laid over the centre validates against the loaded dictionary, including the
|
|
// Эрудит variant.
|
|
func TestRegistryValidatesKnownWords(t *testing.T) {
|
|
cases := []struct {
|
|
variant Variant
|
|
word string
|
|
}{
|
|
{VariantEnglish, "cat"},
|
|
{VariantRussianScrabble, "кот"},
|
|
{VariantErudit, "кот"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.variant.String(), func(t *testing.T) {
|
|
solver, err := testReg.Solver(tc.variant, testVersion)
|
|
if err != nil {
|
|
t.Fatalf("solver: %v", err)
|
|
}
|
|
rs := solver.Rules()
|
|
row, col := centre(rs)
|
|
ps := placementsForWord(t, rs, row, col, scrabble.Horizontal, tc.word)
|
|
if _, err := solver.ValidatePlay(board.New(rs.Rows, rs.Cols), scrabble.Horizontal, ps); err != nil {
|
|
t.Fatalf("validate %q against %s: %v", tc.word, tc.variant, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRegistryUnknownLookups covers the not-found error taxonomy.
|
|
func TestRegistryUnknownLookups(t *testing.T) {
|
|
reg, err := Open(testDictDir(), testVersion, VariantEnglish)
|
|
if err != nil {
|
|
t.Fatalf("open english-only registry: %v", err)
|
|
}
|
|
defer reg.Close()
|
|
|
|
if _, err := reg.Solver(VariantEnglish, "absent"); !errors.Is(err, ErrUnknownVersion) {
|
|
t.Errorf("solver with bad version: got %v, want ErrUnknownVersion", err)
|
|
}
|
|
if _, err := reg.Solver(VariantErudit, testVersion); !errors.Is(err, ErrUnknownVariant) {
|
|
t.Errorf("solver for unloaded variant: got %v, want ErrUnknownVariant", err)
|
|
}
|
|
if _, _, err := reg.Latest(VariantErudit); !errors.Is(err, ErrUnknownVariant) {
|
|
t.Errorf("latest for unloaded variant: got %v, want ErrUnknownVariant", err)
|
|
}
|
|
if got := reg.Versions(VariantErudit); got != nil {
|
|
t.Errorf("versions for unloaded variant = %v, want nil", got)
|
|
}
|
|
}
|
|
|
|
// TestRegistryCloseIdempotent verifies Close may be called more than once.
|
|
func TestRegistryCloseIdempotent(t *testing.T) {
|
|
reg, err := Open(testDictDir(), testVersion, VariantEnglish)
|
|
if err != nil {
|
|
t.Fatalf("open: %v", err)
|
|
}
|
|
if err := reg.Close(); err != nil {
|
|
t.Fatalf("first close: %v", err)
|
|
}
|
|
if err := reg.Close(); err != nil {
|
|
t.Fatalf("second close: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestRegistryOpenMissingDir fails when a dictionary file is absent.
|
|
func TestRegistryOpenMissingDir(t *testing.T) {
|
|
if _, err := Open(t.TempDir(), testVersion, VariantEnglish); err == nil {
|
|
t.Fatal("expected an error opening a registry over an empty directory")
|
|
}
|
|
}
|