Nonogram (picross), the Japanese picture-logic puzzle, built for the browser with a constraint-propagation line solver inside. Three hinges: (1) the line solver = enumerate and intersect — walk every legal run placement for a row/column with a memoized recursion over (position, clue index); cells filled in every placement are certain, cells empty in every placement are certain. The classic human "overlap" trick falls out automatically. (2) Every new certainty re-queues the crossing line; propagation runs to a fixpoint. (3) All five bundled puzzles are verified line-solvable (no guessing) and verified to reproduce their pixel art exactly — clues are derived from the art, so they can't drift apart. Solver-equipped puzzle #5.
🌐 Live demo: https://sen.ltd/portfolio/nonogram/
📦 GitHub: https://github.com/sen-ltd/nonogram
The line solver: enumerate and intersect
One line with clue [3,1] admits only finitely many run placements consistent
with the already-known cells. The solver walks them with a memoized recursion
over (position, clue index) — O(L·k) subproblems — recording per cell
whether it can be FILLED and whether it can be EMPTY in at least one placement:
if (canFill[j] && !canEmpty[j]) out[j] = FILLED; // filled in EVERY placement
else if (!canFill[j] && canEmpty[j]) out[j] = EMPTY;
That intersection formalizes every human picross technique. The famous
"overlap" opening — a run of 3 in 5 cells pins the middle — falls out
automatically, and is pinned as a test:
expect(solveLine([U, U, U, U, U], [3])).toEqual([U, U, F, U, U]);
If no placement fits, the line is contradictory and the solver says so — which
is how the UI can tell you "your grid contradicts the clues" instead of
letting you dig deeper into a broken state.
Propagation to a fixpoint
When a line pass determines a cell, the crossing line becomes worth
re-solving. A dirty-queue loop runs line passes until nothing changes — that's
the whole board solver. Puzzles that finish this way are line-solvable: the
class humans solve without bifurcation. The solver also records the order in
which cells were determined, which is exactly what the demo's Solve button
animates — you watch the cat emerge from pure deduction.
No-guessing puzzles, by construction
The five bundled pixel-art puzzles (heart, arrow, house, cup, cat) are original.
The tests demand each one is line-solvable and that the solved grid
reproduces the art exactly:
expect(r.solved, `${def.name} must be line-solvable (no guessing)`).toBe(true);
expect(asBool, `${def.name} must reproduce the art`).toEqual(solution);
Clues are derived from the art (fromPixels), so the art is the single source
of truth — clue/picture drift is structurally impossible. Same philosophy as the
Sokoban entry (#286): ship your levels through your solver.
Takeaway
A nonogram line solver is just enumerate and intersect, and it subsumes
every human technique; fixpoint propagation does the rest. That makes five
solver-equipped puzzles — Sudoku, Lights Out (GF(2)), the 15-puzzle
(parity + IDA*), Sokoban (pull-BFS), and Nonogram (constraint propagation) —
each wearing a different piece of mathematics. Hit Solve in the demo and watch
the cat appear from reasoning alone.
🌐 Live demo: https://sen.ltd/portfolio/nonogram/
📦 GitHub: https://github.com/sen-ltd/nonogram

Top comments (0)