Replay 17 real tournament games (cross-tables.com, including the 700+ club) and
check, move for move, that:
- ScorePlay reproduces the protocol score and running total exactly;
- the move generator produces every dictionary-valid played move with that score.
The GCG parser handles coordinates (across/down), blanks (lowercase), played-through
tiles ("."), passes/exchanges, challenge bonuses, end-game rack adjustments and phony
withdrawals ("--", un-applied). Uses the committed dawg/en_sowpods.dawg.
scrabble-solver
A Go library that, given a dictionary, a board position and a rack, returns every
legal play ranked by score, and also scores or validates arbitrary plays. The
move generator is the DAWG algorithm of Appel & Jacobson, The World's Fastest Scrabble
Program. It operates on compact byte-indexed inputs/outputs and is dictionary-driven via
github.com/iliadenisov/dafsa.
See ALGORITHM.md for the algorithm (the single source of truth) and
RESULTS.md for the DAWG-vs-GADDAG benchmark that settled the design.
Status
- DAWG move generation (across / down / both orientations), with full tournament scoring (cross-words, premiums, all-tiles bonus) and a per-tile breakdown.
- Public
Solver:GenerateMoves(ranked),ScorePlay,ValidatePlay. - Rulesets: English Scrabble, Russian Scrabble, Эрудит;
rules.Rulesetis fully parameterizable (board, premiums, tile values/counts, blanks, rack, bonus). - A GADDAG (Gordon) was implemented, benchmarked and then removed — for a scoring solver it was ~7× larger and no faster.
Layout
scrabble/ public API: Solver, Move/Play types, DAWG generator, scoring, validation
board/ rack/ rules/ board grid (+transpose), rack, rulesets (English/Russian/Эрудит)
internal/ encoding (byte conventions), wordlist, dictdawg, dict, graph
cmd/builddict/ word list -> serialized DAWG in testdata
cmd/stress/ greedy self-play benchmark of the generator
selfplay/ bag + greedy player + game loop
Setup
git submodule update --init # the dictionaries submodule (SOWPODS, TWL06, …)
go run ./cmd/builddict # build testdata/sowpods.dawg (≈0.2 s, ~730 KB)
go.mod carries replace github.com/iliadenisov/dafsa => ../dafsa: the solver needs
dafsa's low-level traversal Cursor (see the patch notes in ../dafsa/SCRABBLE_API.md).
Usage
rs := rules.English()
finder, _ := dict.EnglishDAWG() // loads testdata/sowpods.dawg
s := scrabble.NewSolver(rs, finder)
b := board.New(rs.Rows, rs.Cols) // empty board (first move)
r := rack.New(rs.Size()) // rack "friends"
tiles, _ := rs.Alphabet.Encode("friends")
for _, t := range tiles {
r.Add(t)
}
moves := s.GenerateMoves(b, r, scrabble.Both) // ranked, highest score first
best := moves[0]
// best.Main / best.Cross hold the words (alphabet indexes; decode via rs.Alphabet),
// best.Tiles the placed tiles (with blank flags), best.Score the total.
// Score or validate an arbitrary play (placed tiles + direction):
m, err := s.ValidatePlay(b, scrabble.Horizontal, best.Tiles)
_ = m
_ = err
Words and tiles are alphabet indexes throughout (no string wrapper); convert with the
ruleset's alphabet.Indexer (Encode/Decode) when you need text.
Rulesets
rules.English(), rules.RussianScrabble(), rules.Erudit(), or build your own with
rules.FromTemplate(...). For Эрудит, fold Ё→Е while preparing the dictionary with
wordlist.FoldYo (the engine treats them as one letter; it is a dictionary-prep step).
Benchmark
go run ./cmd/stress -games 100 # greedy AI-vs-AI self-play; reports speed and memory
Tests
go test ./... # unit tests + a brute-force move-generation oracle
go test ./... -short # skips the full-dictionary game test