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

Solving, Grading, Generating, Enumerating

The four operations Penmark performs on a puzzle.

  • Solving — finding solutions. Solver trait; pluggable per-solver impls. Takes &Puzzle.
  • Grading — assigning a difficulty by simulating a human solver step by step. Grader trait; one shipped impl (StandardGrader). Takes &Puzzle.
  • Generating — producing a uniquely-solvable puzzle. Lives as genre.generate (an fn pointer on the runtime handle) rather than a separate trait, since generation needs intimate access to per-genre structural state.
  • Enumerating — streaming uniquely-solvable puzzles of a given size. Same shape: genre.enumerate.

Solving and grading are pluggable because real users want to compare solver / grader implementations side-by-side (the bench matrix shows FastSolver vs SimpleSolver vs OrToolsSolver). Generating and enumerating aren’t pluggable in the same way — each genre has at most one generation pipeline that makes sense for it. Folding them into the genre handle keeps the surface lean.

Pluggable solvers and graders

The algorithm traits (Solver, Grader) are non-generic and take &Puzzle. Dispatch on a puzzle’s genre happens at runtime through its &'static Genre handle, not by monomorphization.

The shipped set:

  • FastSolver — DFS + AC-3 propagation + depth-1 trial-1 sweep, driven through a Propagator that wraps a FastGame<'p> (bitmask domains, per-constraint aggregates, per-cell counters).
  • SimpleSolver — brute-force DFS over SimpleGame’s HashMap<Coord, Vec<Mark>> candidate sets. The “obvious correct” reference every faster impl is cross-checked against.
  • StandardGrader — fires named human-pattern techniques in priority order against the same Propagator engine, with trial-1 as backward escalation. Techniques are rule-kind-driven and apply across genres.
impl Solver for FastSolver { /* … */ }
impl Grader for StandardGrader { /* … */ }

Adding a new solver impl is one trait impl plus a row in the bench matrix. No per-genre wiring needed.

Genre-handle generators and enumerators

Generation and enumeration are fn pointers on the Genre struct, populated by the macro that builds each per-genre static from its GenreTrait impl:

pub struct Genre {
    pub generate: fn(&GenConfig, &AtomicBool) -> Option<Puzzle>,
    pub enumerate: fn(&EnumConfig, &AtomicBool,
                      &mut dyn FnMut(&Puzzle),
                      &mut dyn FnMut(EnumStats)) -> String,
    /* … */
}

// Dispatch at a call site:
let p = (puzzle.genre.generate)(&cfg, &cancel);

Both default to a universal recipe (scaffold_solve_derive / walk_canonical) that uses genre.material_vocab to know what to scatter and genre.derive_clue_at to derive a ClueValue per clueable cell from a bare-solve completion. The default derive_clue_at body dispatches through genre.clue_pattern (the Pin / Count / ConnectedSize declarative shortcut) and wraps the u8 result in ClueValue::Number; genres whose clues don’t fit that closed set (Tapa, future Yajilin) override derive_clue_at directly. Akari uses both defaults verbatim. Sudoku and slither override generate because they need a genre-specific structural seed (sudoku: random diagonal; slither: connected inside-region) before the bare-solve can find a useful completion.

See the Generator and Enumerator chapters for full configuration surfaces and extension points.