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.
139 lines
3.5 KiB
Go
139 lines
3.5 KiB
Go
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")
|
|
}
|
|
}
|