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.
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.
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:
| Rule | What it does |
|---|---|
| Island numbers | Subtract what the other connections already commit to, and narrow this one |
| No crossings | A bridge that exists forces every crossing candidate to zero |
| Connectivity | A group that cannot reach the rest of the board is a contradiction |
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:
| Board | Level 3 yield, before | after |
|---|---|---|
| 10×10 | < 0.1% | 30–60% |
Tuning the generator had not touched it, because the generator was never the problem.
Levels are defined by which technique a puzzle requires, not by how many clues it has:
| Level | Requires |
|---|---|
| Easy | Saturation only — the numbers alone force every bridge |
| Medium | Counting what each island has left to spend |
| Hard | The connectivity reasoning above |
| Expert | A few "what if" steps |
| Extreme | Long "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.
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.
The whole thing is Node with an empty dependency tree. That includes the parts you would normally reach for a library for:
| Part | Why it's hand-rolled |
|---|---|
| PDF writer | The book draws lines, circles and digits. That is a small operator set |
| TrueType embedding | Print-on-demand wants embedded fonts; parse head/hhea/hmtx/cmap and emit FontFile2 |
| Cover geometry | Spine width from page count, bleed, barcode keep-out — all arithmetic from a spec |
| PDF → SVG proofer | Built 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.
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 |
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.