Skip to content

sen-ltd/slitherlink

Repository files navigation

slitherlink

Slitherlink (the single-loop logic puzzle) with a constraint-propagation solver inside: sound clue and vertex-degree rules propagated to a fixpoint, then a backtracking search that prunes premature sub-loops and proves every bundled puzzle has a unique single-loop solution. TypeScript, no runtime dependencies.

Slitherlink

Live demo: https://sen.ltd/portfolio/slitherlink/

Run

npm install
npm run dev      # http://localhost:5173
npm test         # 32 vitest cases
npm run build    # production build to dist/

Click an edge to cycle line → ✕ → blank. Hint plays one guess-free deduction; Solve draws the unique loop.

The board is edges, not cells

A puzzle is an R×C grid of cells, some carrying a clue 0..3. You draw segments on the lattice edges to make one closed loop; a clue counts how many of its cell's four edges the loop uses. Two invariants make it solvable by pure logic:

  • Clue rule — a clued cell whose lines already equal its number crosses off its remaining edges; if the remaining edges are exactly what's still needed, they're all lines.
  • Vertex rule — every lattice vertex must finish with loop degree 0 or 2. Two lines at a vertex cross off the rest; one line with a single unknown left forces that unknown to a line; a lone unknown at an otherwise-empty vertex is crossed (one edge can't reach degree 2).

Each is a strict implication — never a guess. propagate() applies them to a fixpoint, recording the order edges were forced, which is what Hint replays.

When logic stalls: search that proves uniqueness

Sparse boards need a guess. The backtracking solver picks an unknown edge (preferring one that must continue a half-built path), tries both values, and lets propagation prune. The key accelerator is premature-loop pruning: since every vertex has degree ≤ 2, the drawn segments are disjoint paths and cycles, so a union-find back-edge means a loop just closed. A closed loop can't grow — if it isn't already the whole answer, the branch is dead:

if (isSolved(state, g, puzzle)) { solutions.push(crossUnknowns(state)); return; }
if (hasLineCycle(state, g)) return; // a stray sub-loop closed — dead end

hasUniqueSolution enumerates up to two solutions; the test suite asserts every bundled puzzle stops at exactly one.

Puzzles are solver-verified

Each puzzle is authored as a region mask — a simply-connected blob whose boundary is, by construction, one closed loop. Clues are derived from the mask (an edge is a line iff it separates inside from outside), so the picture is the single source of truth. The least-constraining clues (the 2s) are then blanked greedily, but only while the solution stays unique:

expect(hasUniqueSolution(toPuzzle(def))).toBe(true);        // exactly one answer
expect(foundLines).toEqual(expectedLines);                   // and it's the picture

Same discipline as the Nonogram and Sokoban entries: ship your levels through your solver.

src/slitherlink.ts      — pure engine: edge geometry, propagation, single-loop
                          check, backtracking solver, uniqueness count
src/slitherlink.test.ts — 32 cases (rules, contradictions, uniqueness, exactness)
src/puzzles.ts          — 5 region-mask puzzles, all solver-verified
src/main.ts             — SVG shell: clickable edges, Hint, animated Solve

License

MIT

Links

About

Slitherlink (single-loop puzzle) with a constraint-propagation solver — sound clue and vertex-degree (0-or-2) rules propagated to a fixpoint, then a backtracking search that prunes stray closed sub-loops via union-find and proves the solution is unique. All 5 puzzles derive clues from a region-mask picture, verified to be the one and only solution.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors