diff --git a/README.md b/README.md
index fb0f13e..34ee5a8 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,9 @@ cargo run -- sudoku-examples/hard.sudoku
cargo run -- sudoku-examples/extreme.sudoku
```
+A visual explanation of the solver is available in
+[`docs/solver-visual-guide.pdf`](docs/solver-visual-guide.pdf).
+
## Project Decomposition
- Command-line interface: `src/main.rs`
diff --git a/docs/solver-visual-guide.html b/docs/solver-visual-guide.html
new file mode 100644
index 0000000..6b6cbfe
--- /dev/null
+++ b/docs/solver-visual-guide.html
@@ -0,0 +1,774 @@
+
+
+
+
+ sudoku-ai solver visual guide
+
+
+
+
+
sudoku-ai visual guide
+
+
+
How the solver turns blanks into certainty
+
The engine is a compact constraint solver: it models every empty cell as a bit mask, repeatedly applies deterministic Sudoku deductions, then searches only when deduction stalls.
BranchPick the tightest unsolved cell and order values by impact.
+
SolveReturn the first complete state that survives constraints.
+
+
01 / overview
+
+
+
+
1. Input becomes a constraint model
+
The grid is square, but the solver thinks in units and peers
+
After parsing, each cell knows the three units it belongs to: one row, one column, and one block. The union of those units, excluding the cell itself, becomes its peer list.
+
+
+
+
0 2 3 0
+0 0 0 1
+1 0 0 0
+0 4 2 0
+
Blank cells are written as 0. A 4x4 puzzle uses 2x2 blocks; a 9x9 puzzle uses 3x3 blocks; the same machinery handles both.
+
+
3unit kinds per cell
+
27units in a 9x9 puzzle
+
20peers for a 9x9 cell
+
1state object to search
+
+
+
+
+
+
Peers are precomputed once, so assigning a value later is just a quick walk over affected cells.
+
+
+
02 / model
+
+
+
+
2. Candidates are bits, not lists
+
A cell’s possible values fit into one u128
+
Each value maps to one bit. Eliminating a candidate clears its bit; placing a value replaces the whole mask with exactly that bit.
+
+
+
+
+
+
+
+
+
Why masks work well here
+
Bit masks make candidate checks tiny: membership is mask & bit != 0, removal is mask &= !bit, and a naked single is count_ones() == 1.
+
+
+
+
Full mask
+
(1 << size) - 1 turns on every legal value bit.
+
+
+
Placed value
+
Once a cell is assigned, its candidate mask becomes exactly value_bit(value).
+
+
+
Contradiction
+
If an unsolved cell’s mask becomes zero, that path is rejected.
+
+
+
+
+
03 / candidates
+
+
+
+
3. Deduction runs until it stalls
+
Every assignment immediately removes pressure from the board
+
The solver loops over simple, strong rules. If any rule places a value, it starts another pass because that placement may unlock more forced moves.
+
+
+
+
+
+
+
Naked single
+
A cell with one remaining candidate is forced. The solver detects this with count_ones() == 1, assigns it, and propagates again.
+
+
+
Hidden single
+
For each row, column, and block, the solver checks every value. If only one unsolved cell in that unit can still hold the value, that cell is forced.
+
+
If a unit has no legal place for a value, or a candidate mask goes empty, that path is rejected as a contradiction.
+
+
+
04 / deduction
+
+
+
+
4. Search begins only after deduction stalls
+
The branch point is chosen to make guessing as constrained as possible
+
When no rule can place another value, the solver picks one unresolved cell and tries each candidate in a cloned state.
+
+
+
+
+
+
+
+
+
Cell heuristic
+
The solver scans unresolved cells and keeps the cell with the fewest candidate bits. If two cells are equally tight, it picks the one touching more unsolved peers.
+
+
+
Candidate heuristic
+
For that cell, values are sorted by impact. A value has higher impact when more peer cells currently contain that same value as a candidate.
+
+
+
Tie break
+
If candidate impacts are equal, the smaller value is tried first for deterministic output.
+
+
+
+
05 / search
+
+
+
+
5. Backtracking is just recursive state cloning
+
Every assumption either solves the board or collapses quickly
+
Search clones the current state, assigns one candidate, and immediately re-enters deduction. Failed branches vanish; successful branches return the solved grid.
+
+
+
+
+
+
+
The core rhythm
+
+
Run deduction before every search decision.
+
If all cells are filled, return the state.
+
Otherwise choose one branch cell.
+
Try candidates in impact order.
+
Discard any branch that hits a contradiction.
+
+
+
Key idea Most of the “intelligence” is in shrinking the search tree before guessing and making each guess remove as much uncertainty as possible.
+
+
+
06 / backtracking
+
+
+
+
Implementation map
+
Where to look in the code
+
The solver is small enough that the visual model maps directly onto a few functions in src/lib.rs.
+
+
+
+
Parsing
+
Puzzle::parse validates square dimensions, block layout, and value ranges before solving starts.
+
+
+
Constraint model
+
Solver::new creates row, column, and block units; build_peers turns them into peer lists.
+
+
+
Candidate masks
+
full_mask and value_bit encode possible values into a single integer.
+
+
+
Propagation
+
assign places a value, checks duplicate peers, and clears that value from every unsolved peer.
+
+
+
Deduction
+
deduce repeatedly applies naked singles and hidden singles until no rule progresses.
+
+
+
Search
+
search clones states, tries ordered candidates, and returns the first branch that reaches a complete solution.
+
+
+
+
+
Correctness guardContradictions stop invalid givens and impossible branches early.
+
Performance trickPeer lists and bit masks make the hot path small.
+
Search controlFewest candidates narrows the branch factor.
+
Impact orderingHigher-impact values test stronger assumptions first.
+
+
07 / code map
+
+
+
diff --git a/docs/solver-visual-guide.pdf b/docs/solver-visual-guide.pdf
new file mode 100644
index 0000000..0bd2385
Binary files /dev/null and b/docs/solver-visual-guide.pdf differ