Proving every puzzle has exactly one solution

Notes from building a bridges-puzzle generator that refuses to ship a puzzle it cannot prove.

Free puzzle generators are everywhere, and a lot of them are quietly broken. They hand you a grid with two valid answers, or none at all. You only find out after twenty minutes of work.

Generating a puzzle is the easy half. The hard half is proving the solution is unique, and grading difficulty in a way that means something. Here is what that took.

The puzzle

Bridges (Hashiwokakero): join the islands so that each island has exactly as many bridge ends as its number. Bridges run straight, never cross, never pass over an island, and at most two connect any pair. When you are done, every island must be reachable from every other one.

2 2 3 3 1 1 2 2
Left: a solved puzzle. Right: the rule every human learns first — those two 1s can never be joined to each other, because the pair would close off from the rest of the board.

The solver is the product

The generator builds a random solution first, reads the island numbers off it, and then asks the solver to prove that those numbers admit exactly one answer. If the proof fails, the puzzle is thrown away. Nothing ships unproven.

The solver keeps an interval [lo, hi] of bridge counts for every candidate connection and only ever narrows it, so undo is a trail of old intervals rather than a copy of the board. Three kinds of reasoning drive it:

RuleWhat it does
Island numbersSubtract what the other connections already commit to, and narrow this one
No crossingsA bridge that exists forces every crossing candidate to zero
ConnectivityA group that cannot reach the rest of the board is a contradiction

The rule I was missing

For a long time the connectivity rule only worked backwards: place bridges, notice a group had sealed itself off, backtrack. What it never did was look ahead — the thing in the figure above, where you can see that joining two 1s would strand them before you place anything.

The general form turned out to be one line of arithmetic. Give each group of islands a remaining capacity: the sum over its islands of number − bridges already committed. Joining two groups with v bridges leaves the merged group with rem(a) + rem(b) − 2v. If that is zero and the merge does not swallow every island on the board, the board has just sealed itself shut — so v bridges are impossible.

Two 1s are the case 1 + 1 − 2·1 = 0. Two 2s not taking a double bridge is 2 + 2 − 2·2 = 0. Both fall out of the same check.

The effect was not subtle. Difficulty level 3 is defined as "needs the connectivity rule and no guessing". Before the look-ahead, that level was essentially impossible to generate — puzzles that should have been solvable by pure reasoning were being pushed into the "requires case analysis" bucket instead:

BoardLevel 3 yield, beforeafter
10×10< 0.1%30–60%

Tuning the generator had not touched it, because the generator was never the problem.

Difficulty means what it says

Levels are defined by which technique a puzzle requires, not by how many clues it has:

LevelRequires
EasySaturation only — the numbers alone force every bridge
MediumCounting what each island has left to spend
HardThe connectivity reasoning above
ExpertA few "what if" steps
ExtremeLong "what if" chains

Which gives a claim worth making and worth testing: Easy, Medium and Hard are provably solvable without guessing once. The test suite runs the solver with search disabled and asserts it still finishes — and asserts that Expert and Extreme do not, so the grades can't silently drift.

Soundness is the thing that can quietly break

Every inference rule prunes the search space. A rule that prunes one branch too many turns "two solutions" into "one solution", and the uniqueness guarantee becomes a lie that nothing visibly contradicts. So the suite enumerates all solutions with inference on and with inference off, and asserts the two sets are identical, over thousands of boards.

That test earned its keep. A later review found the look-ahead reading a stale copy of the group partition while computing remaining capacity from updated counts. No lost solution ever showed up in practice, but the shipping gate is "the solver found exactly one answer" — so a wrongly pruned second answer would have broken the guarantee silently. One-line fix, caught before anything was printed.

No dependencies, including the boring parts

The whole thing is Node with an empty dependency tree. That includes the parts you would normally reach for a library for:

PartWhy it's hand-rolled
PDF writerThe book draws lines, circles and digits. That is a small operator set
TrueType embeddingPrint-on-demand wants embedded fonts; parse head/hhea/hmtx/cmap and emit FontFile2
Cover geometrySpine width from page count, bleed, barcode keep-out — all arithmetic from a spec
PDF → SVG prooferBuilt to proofread layout in an environment with no PDF viewer

End to end — 615 puzzles, six books, 110 pages each, covers and all — the build takes about seven seconds.

What the tests actually guard

63 of them. The ones that matter are not about code paths:

Every shipped puzzle has exactly one solution, verified independently of the generator
Inference never changes the solution set
Levels 1–3 need no search; levels 4–5 do
No bridge is ever drawn on a puzzle page — the answers cannot leak onto the paper
No text crosses the printer's minimum margin, measured from real font metrics
The web version numbers its board identically to the generator, or every published solution would point at the wrong bridge

Play it free See the book

The free version runs entirely in your browser: no sign-up, no analytics, nothing sent anywhere. The paperback is the same generator's output, printed.