Solving, Grading, Generating, Enumerating
The four operations Penmark performs on a puzzle.
- Solving — finding solutions.
Solvertrait; pluggable per-solver impls. Takes&Puzzle. - Grading — assigning a difficulty by simulating a human solver
step by step.
Gradertrait; 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 aPropagatorthat wraps aFastGame<'p>(bitmask domains, per-constraint aggregates, per-cell counters).SimpleSolver— brute-force DFS overSimpleGame’sHashMap<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 samePropagatorengine, 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.