Files
scrabble-solver/scrabble/solver_test.go
T
Ilia Denisov 256999b42c Publish as versioned Gitea module; move dictionary pipeline out
- Rename module to gitea.iliadenisov.ru/developer/scrabble-solver so it can be
  consumed as a versioned dependency (no go.work replace / CI clone).
- De-internalize wordlist and dictdawg as public packages.
- Remove cmd/builddict, dictprep/, the dictionaries submodule and the dawg
  Makefile: the word-list parsing and DAWG build now live in the separate
  scrabble-dictionary repository, which publishes the DAWG set as a release artifact.
- internal/dict loads the committed dawg/en_sowpods.dawg fixture for cmd/stress.
- Update README/CLAUDE docs accordingly.
2026-06-04 19:11:46 +02:00

89 lines
3.0 KiB
Go

package scrabble
import (
"testing"
"github.com/iliadenisov/alphabet"
"gitea.iliadenisov.ru/developer/scrabble-solver/board"
"gitea.iliadenisov.ru/developer/scrabble-solver/dictdawg"
"gitea.iliadenisov.ru/developer/scrabble-solver/wordlist"
)
func newTestSolver(t *testing.T) *Solver {
t.Helper()
words := wordlist.Encode(testWords, alphabet.Latin(), 2, 15)
f, err := dictdawg.Build(alphabet.Latin(), words)
if err != nil {
t.Fatal(err)
}
return NewSolver(plainRulesShared, f)
}
func TestSolverGenerateMovesRanked(t *testing.T) {
s := newTestSolver(t)
b := board.New(s.rules.Rows, s.rules.Cols)
moves := s.GenerateMoves(b, makeRack("cat", 0), Both)
if len(moves) == 0 {
t.Fatal("no first moves generated")
}
for i := 1; i < len(moves); i++ {
if moves[i-1].Score < moves[i].Score {
t.Fatalf("moves not ranked: %d before %d", moves[i-1].Score, moves[i].Score)
}
}
}
func TestSolverValidatePlay(t *testing.T) {
s := newTestSolver(t)
// indices: c=2 a=0 t=19 z=25
cat := []Placement{{Row: 3, Col: 2, Letter: 2}, {Row: 3, Col: 3, Letter: 0}, {Row: 3, Col: 4, Letter: 19}}
// First move through the centre (3,3) is legal.
if _, err := s.ValidatePlay(board.New(s.rules.Rows, s.rules.Cols), Horizontal, cat); err != nil {
t.Errorf("valid first move rejected: %v", err)
}
// First move that misses the centre is rejected.
off := []Placement{{Row: 0, Col: 0, Letter: 2}, {Row: 0, Col: 1, Letter: 0}, {Row: 0, Col: 2, Letter: 19}}
if _, err := s.ValidatePlay(board.New(s.rules.Rows, s.rules.Cols), Horizontal, off); err == nil {
t.Error("first move off the centre was accepted")
}
// A non-word ("caz") is rejected.
caz := []Placement{{Row: 3, Col: 2, Letter: 2}, {Row: 3, Col: 3, Letter: 0}, {Row: 3, Col: 4, Letter: 25}}
if _, err := s.ValidatePlay(board.New(s.rules.Rows, s.rules.Cols), Horizontal, caz); err == nil {
t.Error("non-word 'caz' was accepted")
}
// A disconnected play on a non-empty board is rejected.
b := board.New(s.rules.Rows, s.rules.Cols)
placeWord(b, 3, 2, Horizontal, "cat")
disc := []Placement{{Row: 0, Col: 0, Letter: 0}, {Row: 0, Col: 1, Letter: 18}} // "as" far away
if _, err := s.ValidatePlay(b, Horizontal, disc); err == nil {
t.Error("disconnected play was accepted")
}
// Extending "cat" to "cats" connects and is a word.
cats := []Placement{{Row: 3, Col: 5, Letter: 18}} // s after cat
if m, err := s.ValidatePlay(b, Horizontal, cats); err != nil {
t.Errorf("valid extension rejected: %v", err)
} else if string(m.Main.Letters) != string([]byte{2, 0, 19, 18}) {
t.Errorf("main word = %v, want cats", m.Main.Letters)
}
}
func TestSolverScorePlay(t *testing.T) {
s := newTestSolver(t)
b := board.New(s.rules.Rows, s.rules.Cols)
m, err := s.ScorePlay(b, Horizontal, []Placement{
{Row: 3, Col: 2, Letter: 2}, {Row: 3, Col: 3, Letter: 0}, {Row: 3, Col: 4, Letter: 19},
})
if err != nil {
t.Fatal(err)
}
if m.Score != 5 { // c3 a1 t1, no premiums on the plain board
t.Errorf("cat score = %d, want 5", m.Score)
}
}