Files
scrabble-solver/README.md
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

90 lines
3.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](https://github.com/iliadenisov/dafsa).
See [`ALGORITHM.md`](ALGORITHM.md) for the algorithm (the single source of truth) and
[`RESULTS.md`](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.Ruleset` is
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
```sh
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
```go
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
```sh
go run ./cmd/stress -games 100 # greedy AI-vs-AI self-play; reports speed and memory
```
## Tests
```sh
go test ./... # unit tests + a brute-force move-generation oracle
go test ./... -short # skips the full-dictionary game test
```