Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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),
};