256999b42c
- 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.
122 lines
3.4 KiB
Go
122 lines
3.4 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/internal/encoding"
|
|
"gitea.iliadenisov.ru/developer/scrabble-solver/rack"
|
|
"gitea.iliadenisov.ru/developer/scrabble-solver/rules"
|
|
"gitea.iliadenisov.ru/developer/scrabble-solver/wordlist"
|
|
)
|
|
|
|
func makeRack(letters string, blanks int) rack.Rack {
|
|
r := rack.New(26)
|
|
for i := range len(letters) {
|
|
r.Add(letters[i] - 'a')
|
|
}
|
|
for range blanks {
|
|
r.AddBlank()
|
|
}
|
|
return r
|
|
}
|
|
|
|
func placeWord(b *board.Board, r, c int, dir Direction, word string) {
|
|
for i := range len(word) {
|
|
rr, cc := r, c+i
|
|
if dir == Vertical {
|
|
rr, cc = r+i, c
|
|
}
|
|
b.Set(rr, cc, encoding.Cell(word[i]-'a', false))
|
|
}
|
|
}
|
|
|
|
func genMoves(moves []Move) map[string]Move {
|
|
out := make(map[string]Move, len(moves))
|
|
for _, m := range moves {
|
|
out[moveKey(m.Dir, m.Tiles)] = m
|
|
}
|
|
return out
|
|
}
|
|
|
|
// testWords is a small lexicon with enough overlaps to form across and cross plays.
|
|
var testWords = []string{
|
|
"aa", "ace", "act", "arc", "are", "art", "as", "at", "ate",
|
|
"cab", "cap", "car", "care", "cars", "cart", "cat", "cats", "cot",
|
|
"oat", "oats", "ta", "tar", "tare", "tat", "tea", "teat",
|
|
}
|
|
|
|
func compareToBrute(t *testing.T, name string, gen Generator, b *board.Board, d dict, rk rack.Rack, mode Mode) {
|
|
t.Helper()
|
|
want := bruteForce(b, plainRulesShared, d, rk, mode)
|
|
got := genMoves(gen.GenerateMoves(b, rk, mode))
|
|
|
|
for k, wm := range want {
|
|
gm, ok := got[k]
|
|
if !ok {
|
|
t.Errorf("%s [%s]: %s missing %s (score %d)", name, gen.Name(), gen.Name(), k, wm.Score)
|
|
continue
|
|
}
|
|
if gm.Score != wm.Score {
|
|
t.Errorf("%s [%s]: %s score %d, want %d", name, gen.Name(), k, gm.Score, wm.Score)
|
|
}
|
|
}
|
|
for k := range got {
|
|
if _, ok := want[k]; !ok {
|
|
t.Errorf("%s [%s]: extra move %s", name, gen.Name(), k)
|
|
}
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Errorf("%s [%s]: %d moves, oracle has %d", name, gen.Name(), len(got), len(want))
|
|
}
|
|
}
|
|
|
|
func mustPlainRules() *rules.Ruleset {
|
|
eng := rules.English()
|
|
rs, err := rules.FromTemplate("plain7", eng.Alphabet, eng.Values, eng.Counts, 2, 7, 50, plain7)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return rs
|
|
}
|
|
|
|
var plainRulesShared = mustPlainRules()
|
|
|
|
type scenario struct {
|
|
name string
|
|
setup func(*board.Board)
|
|
rack rack.Rack
|
|
mode Mode
|
|
}
|
|
|
|
func genScenarios() []scenario {
|
|
return []scenario{
|
|
{"first move", func(*board.Board) {}, makeRack("cat", 0), Both},
|
|
{"first move blank", func(*board.Board) {}, makeRack("ca", 1), Both},
|
|
{"extend cat", func(b *board.Board) { placeWord(b, 3, 1, Horizontal, "cat") }, makeRack("srs", 0), Both},
|
|
{"cross cat", func(b *board.Board) { placeWord(b, 1, 3, Horizontal, "cat") }, makeRack("aort", 0), Both},
|
|
{"only horizontal", func(b *board.Board) { placeWord(b, 3, 1, Horizontal, "cat") }, makeRack("aser", 0), OnlyHorizontal},
|
|
{"only vertical", func(b *board.Board) { placeWord(b, 1, 3, Vertical, "cat") }, makeRack("aser", 0), OnlyVertical},
|
|
}
|
|
}
|
|
|
|
func TestDAWGGeneratorVsBruteForce(t *testing.T) {
|
|
rs := plainRulesShared
|
|
words := wordlist.Encode(testWords, alphabet.Latin(), 2, 15)
|
|
f, err := dictdawg.Build(alphabet.Latin(), words)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gen := NewDAWGGenerator(rs, f)
|
|
d := makeDict(words)
|
|
|
|
for _, c := range genScenarios() {
|
|
b := board.New(rs.Rows, rs.Cols)
|
|
c.setup(b)
|
|
compareToBrute(t, c.name, gen, b, d, c.rack, c.mode)
|
|
}
|
|
}
|