Files
scrabble-solver/scrabble/score_test.go
T
Ilia Denisov 15c7959d96 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.
2026-06-01 16:07:32 +02:00

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")
}
}