Solver
Solver finds solutions to a puzzle. Three live impls today, one
contract.
/// One valid solution: the candidate set per coord narrowed to a
/// singleton, expressed as a flat coord → mark map. Genre-specific
/// readers (e.g., "extract bulb positions") project from this.
pub type Solution = HashMap<Coord, Mark>;
/// Up to `max_solutions` solutions, in unspecified order. Length
/// ≤ `max_solutions`; equal to the true solution count if fewer.
/// The generator's uniqueness gate calls `solve(p, 2)` and checks
/// `len == 1`.
pub type SolutionSet = Vec<Solution>;
pub trait Solver {
type Config: Default + Serialize + DeserializeOwned;
const NAME: &'static str;
fn new(config: Self::Config) -> Self;
fn solve(&self, puzzle: &Puzzle, max_solutions: usize) -> SolutionSet;
}
/// Cross-puzzle solver options. Per-impl Configs typically embed
/// this with `#[serde(flatten)]`.
pub struct SolverParams {
pub time_budget: Duration,
}
One CSP engine for every genre
The generic FastSolver, driven through Propagator over FastGame,
is the single solve path for every genre. The rule vocabulary —
DegreeIn, CellMin, LoopFreezesCounts, plus the per-cell counter
machinery the engine maintains automatically — is expressive enough
that no per-genre solver override is needed. Per-genre fast solvers
(e.g. a future AkariFastSolver that registers fast genre-specific
propagators while keeping the same outer DFS shape) are post-MVP;
the generic impl earns its keep on every benchmark we run today.
OR-Tools as the sanity-check oracle
A separate algorithm/ortools/ impl wraps Google OR-Tools’ CP-SAT
behind the same Solver trait. It’s not on the hot path —
FastSolver outperforms it for everything we ship. What it provides
is a totally different code path with the same contract: every
generated puzzle and every imported dataset can be cross-checked
against an industry-grade CSP solver to confirm the answer set
matches. When FastSolver and OR-Tools disagree, the bug is in our
engine, full stop. Fallout from going hard on the CSP-engine path —
once the rule vocabulary maps cleanly onto CP-SAT primitives, getting
a second opinion is mostly plumbing.
Cross-checks
FastSolver, SimpleSolver, and the OR-Tools wrapper all implement
the same Solver trait. The benchmark harness runs them in the same
Criterion invocation on a fixed puzzle set; the differential tests
run them over a fuzzed corpus and assert solve(p, k).len() agrees
and the returned sets are equal as sets when fewer than
max_solutions exist. SimpleSolver is the brute-force baseline,
OR-Tools is the independent oracle, FastSolver is the production
path. Three impls, one contract, three angles on every disagreement.