15c7959d96
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.
89 lines
2.9 KiB
Go
89 lines
2.9 KiB
Go
package scrabble
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/iliadenisov/alphabet"
|
|
|
|
"scrabble-solver/board"
|
|
"scrabble-solver/internal/dictdawg"
|
|
"scrabble-solver/internal/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)
|
|
}
|
|
}
|