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,89 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user