Game<'p>
Holds the candidate sets per coord. Borrows its puzzle for its
entire lifetime — the borrow checker enforces “this game is bound
to this puzzle.” Cloning a Game copies only the candidate state
plus a reference; the static puzzle is never duplicated.
pub trait Game<'p>: Sized + Clone {
/// Fresh game bound to a puzzle — every coord carries its full
/// domain.
fn new(puzzle: &'p Puzzle) -> Self;
/// The puzzle this game is playing.
fn puzzle(&self) -> &'p Puzzle;
/// Current candidate set at a coordinate.
fn candidates(&self, coord: Coord) -> impl Iterator<Item = Mark> + '_;
/// Apply a move. No validity checks — the caller is responsible.
/// `Move` is `Copy` and small (≤ 32 bytes), so we pass by value:
/// no indirection on the DFS hot path, and the data lives in
/// registers across the call.
fn apply(&mut self, mv: Move);
/// Undo the most recently applied move. Implementations track the
/// history themselves (per-game undo stack), so the caller doesn't
/// pass the move back — pop-the-stack semantics. Cheaper at the
/// call site (one fewer parameter to thread through DFS frames)
/// and lets fast impls undo via diff-records they already keep
/// for incremental propagator state.
fn undo(&mut self);
// ─── Derived ─────────────────────────────────────────────────
/// `Contradicted(v)` if any constraint is `Violated` — `v` is
/// the violation reported by the *first* such constraint in
/// `puzzle.constraints()` order. Else `Solved` if every `Goal`
/// constraint is `Satisfied`. Else `InProgress`.
fn status(&self) -> Status;
// ─── Player-mark accessors ───────────────────────────────────
/// Iterate every coord currently committed to a single mark,
/// paired with that mark. Skips undecided and contradicted
/// coords. Default impl walks `puzzle().coords()` and yields
/// singletons; overrides can stream from a denser store.
fn marks(&self) -> impl Iterator<Item = (Coord, Mark)> + '_ { /* default */ }
/// Read the committed mark at `coord`. `None` for undecided or
/// empty-domain coords. Default impl runs in O(domain size)
/// via `candidates()`.
fn get(&self, coord: Coord) -> Option<Mark> { /* default */ }
/// Force `coord`'s candidate set to `{mark}` — the player UI
/// path. Doesn't go through the apply/undo log; player edits
/// aren't solver moves. No default: the trait's
/// `apply(Move::Commit)` only works for multi-candidate
/// coords, so it can't express "overwrite a previously-
/// committed value" — a common player gesture.
fn set(&mut self, coord: Coord, mark: Mark);
/// Restore `coord` to its full domain — the player's "clear
/// this cell" gesture. Like `set`, not undo-stack-tracked.
fn clear(&mut self, coord: Coord);
// ─── Serialization (game-with-givens) ────────────────────────
/// Snapshot the game's current state: every puzzle coord paired
/// with its remaining candidate list. Forgiving by design — a
/// caller can save partial state and round-trip it. The
/// matching wire shape lives in `core::tagged::Marks` (see the
/// *Tagged serialization* chapter).
fn to_marks(&self) -> Marks { /* default impl walks coords */ }
/// Build a fresh game and replay each saved candidate
/// restriction into it. Coords absent from `marks` are left at
/// their full domain — partial saves stay loadable. Saved
/// marks outside a coord's full domain are silently dropped;
/// the engine's domain is the source of truth.
fn from_marks(puzzle: &'p Puzzle, marks: &Marks) -> Self { /* default impl */ }
}
Saved-game serialization round-trips through Puzzle’s tagged-JSON
methods (to_tagged_json_with_marks / from_tagged_json_with_marks,
see the Tagged serialization chapter); the loader then rebuilds
a Game in two steps because the borrow checker won’t let one
function return both the owned puzzle and a game that borrows it:
let (puzzle, marks) = Puzzle::from_tagged_json_with_marks(s)?;
let game = match marks {
Some(m) => FastGame::from_marks(&puzzle, &m),
None => FastGame::new(&puzzle),
};