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,138 @@
|
||||
package scrabble
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"scrabble-solver/board"
|
||||
"scrabble-solver/internal/encoding"
|
||||
"scrabble-solver/rules"
|
||||
)
|
||||
|
||||
const plain7 = `.......
|
||||
.......
|
||||
.......
|
||||
...+...
|
||||
.......
|
||||
.......
|
||||
.......`
|
||||
|
||||
// plainRules is a 7x7 board with no premiums and English tile values, for isolating
|
||||
// word-assembly and cross-word logic from premium multipliers.
|
||||
func plainRules(t *testing.T) *rules.Ruleset {
|
||||
t.Helper()
|
||||
eng := rules.English()
|
||||
rs, err := rules.FromTemplate("plain7", eng.Alphabet, eng.Values, eng.Counts, 2, 7, 50, plain7)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return rs
|
||||
}
|
||||
|
||||
// indices: a=0 c=2 o=14 t=19 x=23
|
||||
func TestEvaluateSimpleWord(t *testing.T) {
|
||||
rs := plainRules(t)
|
||||
b := board.New(7, 7)
|
||||
m, err := Evaluate(b, rs, Horizontal, []Placement{
|
||||
{Row: 3, Col: 1, Letter: 2}, {Row: 3, Col: 2, Letter: 0}, {Row: 3, Col: 3, Letter: 19},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m.Main.Score != 5 || m.Score != 5 {
|
||||
t.Errorf("cat: main=%d total=%d, want 5/5", m.Main.Score, m.Score)
|
||||
}
|
||||
if len(m.Cross) != 0 || m.Bonus != 0 {
|
||||
t.Errorf("cat: cross=%d bonus=%d, want 0/0", len(m.Cross), m.Bonus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateCrossWord(t *testing.T) {
|
||||
rs := plainRules(t)
|
||||
b := board.New(7, 7)
|
||||
b.Set(2, 3, encoding.Cell(14, false)) // o
|
||||
b.Set(3, 3, encoding.Cell(23, false)) // x
|
||||
|
||||
// Play "at" horizontally on row 4; the 'a' on col 3 forms the cross word "oxa".
|
||||
m, err := Evaluate(b, rs, Horizontal, []Placement{
|
||||
{Row: 4, Col: 3, Letter: 0}, {Row: 4, Col: 4, Letter: 19},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m.Main.Score != 2 {
|
||||
t.Errorf("main 'at' = %d, want 2", m.Main.Score)
|
||||
}
|
||||
if len(m.Cross) != 1 || m.Cross[0].Score != 10 {
|
||||
t.Errorf("cross = %+v, want one word scoring 10 (oxa)", m.Cross)
|
||||
}
|
||||
if m.Score != 12 {
|
||||
t.Errorf("total = %d, want 12", m.Score)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluatePremiums(t *testing.T) {
|
||||
rs := rules.English()
|
||||
|
||||
// (0,3) is a double-letter square: c(3)*2 + a(1) + t(1) = 8.
|
||||
b := board.New(15, 15)
|
||||
m, err := Evaluate(b, rs, Horizontal, []Placement{
|
||||
{Row: 0, Col: 3, Letter: 2}, {Row: 0, Col: 4, Letter: 0}, {Row: 0, Col: 5, Letter: 19},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m.Score != 8 {
|
||||
t.Errorf("DL cat = %d, want 8", m.Score)
|
||||
}
|
||||
|
||||
// (1,1) is a double-word square: (c(3) + a(1)) * 2 = 8.
|
||||
b2 := board.New(15, 15)
|
||||
m2, err := Evaluate(b2, rs, Horizontal, []Placement{
|
||||
{Row: 1, Col: 1, Letter: 2}, {Row: 1, Col: 2, Letter: 0},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m2.Score != 8 {
|
||||
t.Errorf("DW ca = %d, want 8", m2.Score)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateBingo(t *testing.T) {
|
||||
rs := plainRules(t)
|
||||
b := board.New(7, 7)
|
||||
tiles := make([]Placement, 7)
|
||||
for c := range 7 {
|
||||
tiles[c] = Placement{Row: 0, Col: c, Letter: 0} // seven a's
|
||||
}
|
||||
m, err := Evaluate(b, rs, Horizontal, tiles)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m.Bonus != 50 || m.Score != 7+50 {
|
||||
t.Errorf("bingo: bonus=%d total=%d, want 50/57", m.Bonus, m.Score)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateErrors(t *testing.T) {
|
||||
rs := plainRules(t)
|
||||
b := board.New(7, 7)
|
||||
b.Set(2, 3, encoding.Cell(14, false))
|
||||
|
||||
if _, err := Evaluate(b, rs, Horizontal, nil); err == nil {
|
||||
t.Error("empty play: want error")
|
||||
}
|
||||
if _, err := Evaluate(b, rs, Horizontal, []Placement{{Row: 2, Col: 3, Letter: 0}}); err == nil {
|
||||
t.Error("occupied square: want error")
|
||||
}
|
||||
if _, err := Evaluate(b, rs, Horizontal, []Placement{
|
||||
{Row: 3, Col: 1, Letter: 0}, {Row: 4, Col: 2, Letter: 0},
|
||||
}); err == nil {
|
||||
t.Error("non-collinear: want error")
|
||||
}
|
||||
if _, err := Evaluate(b, rs, Horizontal, []Placement{
|
||||
{Row: 5, Col: 1, Letter: 0}, {Row: 5, Col: 3, Letter: 0},
|
||||
}); err == nil {
|
||||
t.Error("gap: want error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user