Stage 2: engine package over scrabble-solver (registry, bag, Game, replay)
backend/internal/engine wraps the sibling scrabble-solver library in-process: - Registry: versioned DAWG load via dafsa.Load, keyed by (variant, dict_version), latest-per-variant; English / Russian / Эрудит handled uniformly. - Bag: own deterministic, seeded tile bag with Draw + Return (for exchanges), since the solver's self-play bag cannot return tiles. - Game: pure rules engine — deal, play/pass/exchange/resign, refill, per-move scoring, turn order, and end-condition detection (empty bag + empty rack, six scoreless turns, resignation) with end-game rack adjustment. - decode/ReplayBoard: dictionary-independent MoveRecords and board replay via scrabble.Apply (no internal/encoding), realising ARCHITECTURE §9.1. Wiring: go.work gains "replace scrabble-solver => ../scrabble-solver"; backend requires scrabble-solver (placeholder) and github.com/iliadenisov/dafsa directly. Both Go CI workflows clone the public solver sibling (master HEAD, no token) and set BACKEND_DICT_DIR. Docs: ARCHITECTURE §5/§14, TESTING engine layer, backend README, and PLAN refinements + deferred TODOs (publish/version solver; split engine vs dictionary generator).
This commit is contained in:
+39
-12
@@ -100,20 +100,43 @@ arrive from a platform rather than completing a mandatory registration).
|
||||
|
||||
## 5. Game engine integration (`scrabble-solver`)
|
||||
|
||||
`backend` embeds the solver library (see [`CLAUDE.md`](../CLAUDE.md) for the
|
||||
exact public API and constraints). Key points:
|
||||
`backend` embeds the solver library in-process behind `internal/engine`, the
|
||||
only package that imports `scrabble-solver` (see [`CLAUDE.md`](../CLAUDE.md) for
|
||||
the solver's public API and constraints). The engine is a self-contained rules
|
||||
library — no persistence, transport or scheduling; the game domain drives it.
|
||||
Key points:
|
||||
|
||||
- Variants at launch: **English Scrabble**, **Russian Scrabble**, **Эрудит** —
|
||||
`rules.English()`, `rules.RussianScrabble()`, `rules.Erudit()`.
|
||||
- Dictionaries are committed DAWGs loaded with `dawg.Load`; held in memory and
|
||||
addressed by `(variant, dict_version)`.
|
||||
- **Dictionary versioning — pin per game.** A game records the `dict_version`
|
||||
it started on and finishes on that version; new games use the latest. Multiple
|
||||
versions may be resident at once. An admin reload endpoint *(planned)* adds a
|
||||
new version; delivery is the DAWG file in the image / a mounted volume.
|
||||
- Variants at launch: **English Scrabble**, **Russian Scrabble**, **Эрудит**
|
||||
(`engine.Variant`, mapping to `rules.English()` / `RussianScrabble()` /
|
||||
`Erudit()`). Эрудит's specifics (non-doubling centre, `ё` with no tiles, 3
|
||||
blanks, a 15-point bonus) live entirely in the solver ruleset, so the engine
|
||||
treats every variant uniformly.
|
||||
- **Dictionaries** are committed DAWGs loaded with `dawg.Load` from a directory
|
||||
(a parameter today; a configurable `BACKEND_DICT_DIR` is wired when the first
|
||||
consumer needs it). The `engine.Registry` holds them in memory addressed by
|
||||
`(variant, dict_version)`, tracking the latest version per variant.
|
||||
- **Dictionary versioning — pin per game.** A game records the `dict_version` it
|
||||
started on and finishes on that version; new games use the latest. Multiple
|
||||
versions may be resident at once. An admin reload *(planned, Stage 9)*
|
||||
registers a new version through `Registry.Load`; delivery is the DAWG file in
|
||||
the image / a volume mounted at the dictionary directory. (A future split of
|
||||
the solver into engine + dictionary generator with versioned artifacts is
|
||||
recorded in [`../PLAN.md`](../PLAN.md) TODO-2.)
|
||||
- Move generation/validation/scoring use `Solver.GenerateMoves` (ranked),
|
||||
`Solver.ValidatePlay`, `Solver.ScorePlay`; board mutation uses
|
||||
`scrabble.Apply`. Tile bag follows the `selfplay.Bag` pattern.
|
||||
`Solver.ValidatePlay` and `Solver.ScorePlay`; board mutation uses
|
||||
`scrabble.Apply`. The engine adds its own deterministic, seeded tile **bag**
|
||||
that can return tiles (an exchange needs this; the solver's self-play bag
|
||||
cannot).
|
||||
- **`engine.Game`** is the in-memory match state and the pure rules engine: it
|
||||
deals racks, applies legal plays / passes / exchanges / resignations, refills
|
||||
from the bag, keeps the scores and whose turn it is, and **detects the end of
|
||||
the game** — empty bag with an empty rack, six consecutive scoreless turns, or
|
||||
a resignation — applying the end-game rack-value adjustment. The 24-hour
|
||||
timeout / auto-resign, turn scheduling and persistence belong to the game
|
||||
domain *(Stage 3)*.
|
||||
- History is dictionary-independent (§9.1): the engine emits decoded
|
||||
`MoveRecord`s and reconstructs the board from them with `engine.ReplayBoard`
|
||||
(alphabet only, no dictionary).
|
||||
|
||||
## 6. Game rules
|
||||
|
||||
@@ -242,5 +265,9 @@ is something to deploy.
|
||||
`integration` build tag (testcontainers `postgres:17-alpine`, Ryuk disabled,
|
||||
serial). Further workflows (ui-test, deploy) are added with the components they
|
||||
cover.
|
||||
- Since Stage 2 both Go workflows clone the public `scrabble-solver` sibling
|
||||
(master HEAD, no credentials) into `../scrabble-solver` before building, so the
|
||||
`go.work` `replace` resolves; the engine tests read the committed DAWGs from
|
||||
that checkout via `BACKEND_DICT_DIR`.
|
||||
- After any push, the run is watched to green before a stage is declared done
|
||||
(`python3 ~/.claude/bin/gitea-ci-watch.py`).
|
||||
|
||||
Reference in New Issue
Block a user