Explainable Sudoku solver in Python with human-style techniques, step-by-step reasoning, CLI and library APIs, benchmarking tools, and CI quality gates.
- Solves 9x9 Sudoku puzzles from a single puzzle string or a puzzle file.
- Applies a deterministic sequence of human-style techniques and records each applied step.
- Can optionally use a bounded uniqueness search after human techniques stall.
- Reports structured outcomes (
solved,stalled,invalid), difficulty rating, and optional telemetry. - Includes unit tests, property tests (when
hypothesisis installed), linting, typing checks, and coverage reporting.
- Techniques implemented:
- Naked Single
- Hidden Single
- Locked Candidates (pointing/claiming)
- Naked Pair
- Hidden Pair
- Naked Triple
- Hidden Triple
- XY-Wing
- XYZ-Wing
- X-Wing
- Naked Quad
- Hidden Quad
- W-Wing
- Swordfish
- Jellyfish
- Simple Coloring
- 3D Medusa (expanded)
- AIC (expanded)
- X-Cycles
- XY-Chain
- ALS-XZ (expanded)
- Sue de Coq (restricted)
- BUG+1
- Finned X-Wing / Sashimi X-Wing
- Finned Swordfish
- Empty Rectangle
- Remote Pairs
- Two-String Kite
- Skyscraper
- Unique Rectangle
- Grouped AIC (expanded)
- Nice Loops (expanded)
- ALS Chains (expanded)
- Death Blossom (restricted)
- Uniqueness Expansions (restricted)
- Fireworks (restricted)
- WXYZ-Wing (expanded)
- Exocet (restricted)
- Sue de Coq Full/Generalized (restricted)
- Kraken Fish (expanded)
- Sashimi Fish (expanded)
- Forcing Chains (expanded)
- Forcing Nets (expanded)
- Franken/Mutant Fish (expanded)
- Squirmbag See Technique Reference for detailed explanations and full-grid pattern tables.
- Default technique set:
All implemented human techniques run by default.
Runtime is controlled by the three-pass scheduler (primary, deferred, and
ultra-expensive groups), so expensive techniques are not run first on every
iteration.
API
techniques=[...]is still available when you want a custom subset or custom order. - Advanced-technique safety: High-risk eliminations are conservatively validated against solution existence checks before they are applied, reducing false-positive eliminations that can otherwise produce invalid states.
- Fallback search:
Uniqueness-aware backtracking is available when no configured technique can advance the grid.
It is disabled by default; use
allow_fallback_search=True(API) or--allow-fallback-search(CLI) to enable it. - Result metadata:
steps,technique_counts,difficulty, andused_fallback_searchare returned for every solve attempt.
Cell: one square in the 9x9 grid.Row: horizontal set of 9 cells.Column: vertical set of 9 cells.Box: one 3x3 subgrid.Unit: any row, column, or box.Candidate: a digit that is still legal for an empty cell.Peer: a cell sharing a row, column, or box with another cell.Placement: writing a final digit into a cell.Elimination: removing a candidate from a cell.
Detailed explanations, full-grid pattern tables, and implementation notes for every technique are in:
The list above remains the authoritative set of selectable technique keys. The solver still runs them in a deterministic order by default.
solve_from_string() returns a SolveResult with:
status:solved,stalled, orinvalidgrid_string: final grid stringsteps: ordered list of applied stepstechnique_counts: count per technique useddifficulty:easy,medium,hard,expert, orunsolvedused_fallback_search:trueif non-human fallback search was used to finishmessage: contextual message
Difficulty is derived from the hardest technique used (or fallback search usage).
python -m venv .venv
source .venv/bin/activate
pip install -e .Install developer tooling:
pip install -e .[dev]from sudoku_solver import solve_from_string
puzzle = "53..7....6..195....98....6.8...6...34..8..6...2...1.6....28....419..5....8..79"
result = solve_from_string(puzzle)
fallback_result = solve_from_string(puzzle, allow_fallback_search=True)
custom_techniques_result = solve_from_string(
puzzle,
techniques=[
"naked_single",
"hidden_single",
"locked_candidates",
"naked_pair",
"hidden_pair",
],
)
print(result.status)
print(result.difficulty)
print(result.used_fallback_search)
print(result.grid_string)
print(result.technique_counts)Single puzzle:
python -m sudoku_solver "<81-char-puzzle>"
python -m sudoku_solver "<81-char-puzzle>" --show-steps
python -m sudoku_solver "<81-char-puzzle>" --show-telemetry
python -m sudoku_solver "<81-char-puzzle>" --allow-fallback-search
python -m sudoku_solver "<81-char-puzzle>" --max-steps 200Puzzle file mode:
python -m sudoku_solver --puzzle-file puzzles/top1465.txt
python -m sudoku_solver --puzzle-file puzzles/top1465.txt --max-failures 2
python -m sudoku_solver --puzzle-file puzzles/top1465.txt --allow-fallback-search
python -m sudoku_solver --puzzle-file puzzles/top1465.txt --show-steps --show-telemetryNote:
CLI uses the full default human-technique set. Custom technique subsets/orders
are configured through the Python API (techniques=[...]).
python scripts/benchmark.py puzzles/top1465.txt
python scripts/benchmark.py puzzles/top1465.txt --allow-fallback-search
python scripts/benchmark.py puzzles/top1465.txt --limit 200 --top-slowest 10 --progress-every 500
python scripts/benchmark.py puzzles/top1465.txt --profile-techniques --top-techniques 20
python scripts/benchmark.py puzzles/top1465.txt --output-json benchmark.json --output-csv benchmark.csv
python scripts/check_benchmark_guardrail.py benchmark.json --max-avg-ms 700 --max-p95-ms 2500 --min-throughput 2.5Note:
scripts/benchmark.py pins import resolution to the local workspace root, so
it benchmarks the checked-out solver code without requiring PYTHONPATH=..
- Exactly 81 characters.
1-9for filled cells..or0for empty cells.
sudoku_solver/grid.py: parsing, formatting, and givens validation.sudoku_solver/candidates.py: candidate generation for empty cells.sudoku_solver/units.py: row/column/box helpers and peer calculation.sudoku_solver/engines/: shared family engines that reduce duplicated scans.sudoku_solver/engines/chain_engine.py: shared chain graph helpers (AIC/coloring/XY-chain support).sudoku_solver/engines/fish_engine.py: shared fish scanners (X-Wing/Swordfish/Jellyfish/finned families).sudoku_solver/engines/als_engine.py: shared ALS and petal-structure scans (ALS-XZ/ALS-Chains/Death Blossom).sudoku_solver/engines/uniqueness_engine.py: shared rectangle/pair scans for uniqueness-family rules.sudoku_solver/techniques/: individual technique implementations.sudoku_solver/techniques/README.md: detailed technique explanations and full-grid pattern illustrations.sudoku_solver/solver.py: orchestration loop, step application, optional fallback search, difficulty classification.sudoku_solver/cli.py: CLI parser, single/file runners, progress and reporting output.sudoku_solver/types.py: core dataclasses/enums (Grid,Step,SolveResult, etc.).scripts/benchmark.py: dataset timing and throughput reporting.tests/: unit, internal, regression, technique, and property tests.puzzles/: bundled puzzle corpora.
- Parse input (
parse_grid) and validate puzzle consistency. - Build candidate sets for empty cells (
get_candidates). - Iterate technique adapters in fixed order and request one
Stepat a time. - Technique adapters may delegate scanning to shared family engines.
- Apply step placements/eliminations (
_apply_step) and update state. - Repeat until solved or no technique can progress.
- If stalled by techniques and fallback is enabled, run uniqueness-aware fallback search.
- Return
SolveResultwith final status, steps, telemetry, difficulty, and fallback usage flag.
pre-commit install
pre-commit install --hook-type pre-push
pre-commit run --all-filessource venv/bin/activate
ruff format --check .
ruff check .
mypy
python -m unittest discover -s tests -t . -v
python -m coverage run --branch --source=sudoku_solver -m unittest discover -s tests -t .
python -m coverage report -msource venv/bin/activate
python -m unittest discover -s tests -t . -vsource venv/bin/activate
python -m coverage erase
python -m coverage run --branch --source=sudoku_solver -m unittest discover -s tests -t .
python -m coverage report -m
python -m coverage htmlNotes:
- Terminal summary is printed by
coverage report -m. - HTML report is written to
htmlcov/index.html. - CI enforces a minimum package coverage gate of 97%.
CIworkflow (.github/workflows/ci.yml) runs on pull requests and pushes tomain:- Ruff format check
- Ruff lint
- Mypy type checking
- Unit tests
- Branch coverage gate (minimum 97%)
Dataset Regressionworkflow (.github/workflows/dataset.yml) runs on pushes tomainand manual dispatch:- Dataset regression on
puzzles/top95.txtandpuzzles/top1465.txt - Benchmark artifact generation (
.txt,.json,.csv) - Benchmark performance guardrail checks on average latency, p95 latency, and throughput
- Dataset regression on
- Add a CI performance guardrail that checks benchmark metrics against configured thresholds.
- Add a technique cost profiler report (call count, hit count, total/runtime averages) during benchmark runs.
- Make benchmark execution path usage explicit so local runs always target workspace code.
- Add machine-readable benchmark outputs (JSON/CSV) for run-to-run comparisons and automation.
- Clean up legacy/noise artifacts in the repo (for example stray coverage byproducts).
- Add a technique index table in
sudoku_solver/techniques/README.md(family, complexity tier, status, expected cost). - Continue clarifying fallback-search docs and examples as optional/non-default behavior.
- Add more property/invariant tests to harden solver correctness guarantees.
- Create a branch for your change.
- Install dev dependencies:
pip install -e .[dev]. - Enable hooks:
pre-commit installandpre-commit install --hook-type pre-push. - Add or update tests with any code changes.
- Run local checks before opening a PR.
- Open a PR with a clear description of behavior changes and test evidence.
Recommended contribution pattern:
- Keep technique changes isolated per PR when possible.
- Include at least one regression test for each bug fix.
- If adding a technique, document it in
sudoku_solver/techniques/README.mdand add focused tests undertests/techniques/.