Implement Scrabble move generator (DAWG) with English and Russian rules
A Go library that returns every legal play ranked by score and scores or validates plays, using the Appel-Jacobson DAWG algorithm over github.com/iliadenisov/dafsa v1.1.0. - DAWG move generation (across / down / both), full tournament scoring with a per-tile breakdown; public Solver: GenerateMoves (ranked), ScorePlay, ValidatePlay. - Rulesets: English Scrabble, Russian Scrabble, Эрудит (parameterizable Ruleset). - cmd/builddict (build the DAWG from the dictionaries submodule), cmd/stress (self-play benchmark), selfplay engine; brute-force test oracle. - A GADDAG was implemented, benchmarked and removed (the DAWG was smaller and faster for a scoring solver); see RESULTS.md and ALGORITHM.md.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
package scrabble
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/iliadenisov/alphabet"
|
||||
dawg "github.com/iliadenisov/dafsa"
|
||||
|
||||
"scrabble-solver/internal/dictdawg"
|
||||
"scrabble-solver/internal/wordlist"
|
||||
)
|
||||
|
||||
func bruteCrossSet(words [][]byte, above, below []byte, size int) letterSet {
|
||||
set := make(map[string]bool, len(words))
|
||||
for _, w := range words {
|
||||
set[string(w)] = true
|
||||
}
|
||||
var out letterSet
|
||||
for x := range size {
|
||||
w := make([]byte, 0, len(above)+1+len(below))
|
||||
w = append(w, above...)
|
||||
w = append(w, byte(x))
|
||||
w = append(w, below...)
|
||||
if set[string(w)] {
|
||||
out |= letterSet(1) << uint(x)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestDAWGCrossSetMatchesBruteForce(t *testing.T) {
|
||||
const size = 26
|
||||
words := wordlist.Encode(
|
||||
[]string{"cat", "cot", "cut", "cap", "cab", "at", "it"},
|
||||
alphabet.Latin(), 2, 15)
|
||||
|
||||
finder, err := dictdawg.Build(alphabet.Latin(), words)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cur, err := dawg.NewCursor(finder)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
above, below []byte
|
||||
}{
|
||||
{"c_t", []byte{2}, []byte{19}}, // expect {a,o,u}
|
||||
{"_t", nil, []byte{19}}, // expect {a,i}
|
||||
{"c_", []byte{2}, nil}, // expect {} (no two-letter c-words)
|
||||
{"a_t", []byte{0}, []byte{19}}, // expect {}
|
||||
}
|
||||
for _, tc := range cases {
|
||||
want := bruteCrossSet(words, tc.above, tc.below, size)
|
||||
if got := dawgCrossSet(cur, tc.above, tc.below, size); got != want {
|
||||
t.Errorf("%s: dawgCrossSet = %026b, want %026b", tc.name, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// c_t must be exactly {a(0), o(14), u(20)}.
|
||||
want := letterSet(0)
|
||||
for _, x := range []byte{0, 14, 20} {
|
||||
want |= letterSet(1) << x
|
||||
}
|
||||
if got := dawgCrossSet(cur, []byte{2}, []byte{19}, size); got != want {
|
||||
t.Errorf("c_t cross-set = %026b, want {a,o,u} = %026b", got, want)
|
||||
}
|
||||
|
||||
// No perpendicular neighbours: every letter is allowed.
|
||||
if got := dawgCrossSet(cur, nil, nil, size); got != fullSet(size) {
|
||||
t.Errorf("empty context = %026b, want full", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user