Reference
Architecture
House composition, custom variant definitions, and solver internals.
Architecture
How @reetesh/sudoku-engine supports multiple Sudoku variants and how to extend it. For product usage see Overview and Guides.
Design choice: presets + houses
We evaluated two APIs:
// A — named presets (public)
createEngine({ variant: "hyper" });
// B — constraint composition (future / advanced)
createEngine({
constraints: [rowConstraint(), columnConstraint(), boxConstraint(), hyperConstraint()],
});Chosen: named presets publicly, implemented as composed house sets internally. Advanced users inject a full SudokuVariant via createEngine({ definition }) + defineVariant / composeHouses.
| Approach | Pros | Cons |
|---|---|---|
| Presets only | Simple docs, stable semver, easy batch/daily | Custom layouts need a preset or definition |
| Raw constraints only | Maximum flexibility | Harder to document; easy to build invalid rule sets |
| Hybrid (shipped) | Presets for 99% of users; definition for custom houses | Callers must keep house integrity (defineVariant) |
Uniqueness constraints map to houses: fixed sets of cells where digits must be all different.
| Variant | Houses |
|---|---|
| Classic | 9 rows + 9 columns + 9 regions |
| 6×6 | Same pattern on 6×6 grid |
| Diagonal | Classic + main + anti diagonal |
| Hyper | Classic + four edge-centered 3×3 windows |
| Windoku | Classic + four inner-corner 3×3 windows |
| Jigsaw | 9 rows + 9 columns + 9 irregular regions (no 3×3 boxes) |
| Killer / Thermo / Kropki | Classic houses + per-puzzle MetaConstraints |
| Samurai | Five overlapping 9×9 house sets on a 21×21 canvas (playableCells) |
Beyond uniqueness, meta-constraints (cage-sum, thermometer, kropki) live on SudokuVariant.metaConstraints and are usually attached per puzzle via withMeta / GeneratedPuzzle.metaConstraints.
Layers
Public API
├─ createEngine({ variant | definition }) → PuzzleEngine
├─ defineVariant / composeHouses → custom rulesets
├─ BatchEngine → seeded batch generation
└─ Classic top-level exports → lazy classicEngine() facades
PuzzleEngine
└─ SudokuVariant (grid + houses + meta + playable + generation)
Core (source of truth)
├─ GridSpec / HouseSet / BitGrid / meta
├─ validation / solver / generator / hints / play
└─ errors (SudokuEngineError)
Compat shims (classic folders)
└─ thin classicEngine() wrappers — no new logic
Variants (registry)
└─ named presets onlyBitGrid
Each house stores a bitmask of used digits. For a cell:
candidates = fullMask & ~(OR of house masks for that cell)MRV picks the empty cell with the fewest candidates. House uniqueness uses house masks; meta variants also prune via meta candidate masks.
Solver flow
validateBoard(shape + house duplicates + meta when present)BitGrid.fromBoard- MRV backtracking: pick cell, try each candidate bit, recurse
countSolutionsuses the same search with a solution limit (typically 2 for uniqueness)
Generator flow
- Seed one or more disjoint regions with shuffled digits
- Backtrack-fill remaining cells using candidate masks (+ meta)
- Carve clues: shuffle cell order, remove while
countSolutions === 1 - For meta variants, generate cages / thermos / dots consistent with the solution
- Retry with alternate target clue counts; return
nullif all attempts fail
Generation tuning lives on SudokuVariant.generation (GenerationProfile), not in variant.id switches:
| Field | Purpose |
|---|---|
puzzleMaxAttempts | Outer retry loop |
puzzleFallbackAttempts | Relaxed carving retries |
carveMaxAttempts | Per-carve attempt budget |
tryFullClueRange | Try max/min/mid clue targets (tight variants) |
Tight variants (diagonal, hyper, windoku) use conservative seeding; extra houses make aggressive multi-box seeds unreliable during fill.
Hyper vs Windoku regions
Hyper — four edge-centered 3×3 windows (0-indexed):
| Region | Rows | Columns |
|---|---|---|
| Top | 1–3 | 3–5 |
| Left | 3–5 | 1–3 |
| Right | 3–5 | 5–7 |
| Bottom | 5–7 | 3–5 |
Windoku — four inner-corner windows anchored at (1,1), (1,5), (5,1), (5,5).
Built with buildHyperRegionHouses / hyperSudokuHouses and buildWindokuRegionHouses / windokuSudokuHouses in core/houses.ts.
Board type
Board remains (number | null)[][]. Shape and digit range are validated through the active variant.
Classic 9×9 exports (GRID_SIZE, Digit, etc.) stay for backward compatibility. Samurai uses size === 21 on its GridSpec.
Adding a variant
- Prefer a preset in
variants/registry.ts(GridSpec, houses, clue ranges, generation, optional meta/playable). - For one-off layouts, build houses with
composeHouses/ map helpers, then
createEngine({ definition: defineVariant({ ...base, houses }) }). - Keep algorithm code in
core/— never add behavior to classic facade folders. - Extend
VariantIdonly when shipping a named preset. - Add tests via
ALL_VARIANTS/META_VARIANTSand the QA rigorous suite. - Document the user-facing rules in OVERVIEW.md and recipes in GUIDES.md.
Files
| Path | Role |
|---|---|
core/grid-spec.ts | Grid dimensions and digit mask |
core/houses.ts | House builders and composition |
core/bitgrid.ts | Parameterized solver grid |
core/variant.ts | Variant types and generation profiles |
core/define-variant.ts | Validate/normalize custom definitions |
core/meta.ts / meta-generate.ts | Meta constraints + per-puzzle generation |
core/classic-base.ts | Registry-free classic fill base for meta gen |
core/errors.ts | Shared SudokuEngineError |
core/engine.ts | createEngine, PuzzleEngine |
types/results.ts | Shared result DTOs (play/hints/import) |
variants/registry.ts | Built-in variant presets |
Classic folders (solver/, …) | Thin classicEngine() compat facades |
classic/api.ts | Lazy classic singleton (not public) |
batch/BatchEngine.ts | Seeded batch generation |