Most puzzle websites generate puzzles with simple random placement. We built a proper constraint-based engine in Rust that generates, solves, and rates Slitherlink puzzles on a 10-level difficulty scale — then compiles to WebAssembly for browser use.
Why Rust?
Slitherlink generation is computationally expensive. A 10×10 grid has 220 possible edges. The engine needs to:
- Generate a valid closed loop
- Place number clues
- Verify the puzzle has exactly one solution
- Rate the difficulty based on required solving techniques
JavaScript couldn't handle this efficiently. Java was our first attempt (it worked), but Rust gave us 10-50x speedup and the option to compile to WASM for client-side solving.
The Architecture
Phase 1: Loop Generation
Start with an empty grid. Build a random Hamiltonian-like path that forms a single closed loop:
·───· ·───·───·
│ │ │
· ·───· · ·
│ │ │ │
· · ·───· ·
│ │ │ │
·───· ·───·───·
The algorithm uses randomized DFS with backtracking. Key constraint: every vertex touched by the loop must have exactly degree 2 (one line enters, one leaves).
Phase 2: Clue Placement
Count the edges around each cell to generate numbers:
·───· ·───·───·
│ 2 1 │ 2 3 │
· ·───· · ·
│ 2 │ 2 2 │ 2
· · ·───· ·
│ 1 │ 1 │ 2 2 │
·───· ·───·───·
Then selectively remove some numbers to create the puzzle. More numbers removed = potentially harder puzzle, but difficulty depends on which numbers remain, not just how many.
Phase 3: Uniqueness Verification
This is the expensive part. The solver must confirm exactly one solution exists by:
- Constraint propagation: Apply all known patterns (corner rules, adjacent 3-3, vertex degree) exhaustively
- Backtracking: When propagation stalls, pick an undecided edge, try both states (line/no-line), propagate each branch
- Uniqueness check: If both branches lead to valid solutions, the puzzle has multiple solutions — reject it
solve(puzzle):
propagate all constraints
if contradiction → no solution
if all edges decided → one solution found
pick undecided edge E
try E = line:
solutions_a = solve(puzzle + E=line)
try E = no_line:
solutions_b = solve(puzzle + E=no_line)
return solutions_a + solutions_b
// if total > 1, puzzle is ambiguous — reject
Phase 4: Difficulty Rating (10 Levels)
This is where it gets interesting. We don't just count clues — we measure what techniques are required to solve:
| Level | Techniques Required |
|---|---|
| 1-2 | Corner rules, 0-elimination only |
| 3-4 | Adjacent number patterns (3-3, 3-0) |
| 5-6 | Vertex degree rule, edge patterns |
| 7-8 | Loop closure logic, inside/outside reasoning |
| 9-10 | Multi-step bifurcation (trial and error) |
The rating engine simulates a human solver: apply techniques in order of complexity, track which level of technique was needed to make progress. The highest technique level used = the puzzle's difficulty.
fn rate_puzzle(puzzle: &Puzzle) -> u8 {
let mut max_level = 1;
let mut state = SolverState::new(puzzle);
loop {
// Try techniques from simplest to most complex
if state.apply_corner_rules() { max_level = max(max_level, 1); continue; }
if state.apply_adjacent_patterns() { max_level = max(max_level, 3); continue; }
if state.apply_vertex_degree() { max_level = max(max_level, 5); continue; }
if state.apply_loop_closure() { max_level = max(max_level, 7); continue; }
if state.apply_bifurcation() { max_level = max(max_level, 9); continue; }
break;
}
max_level
}
Compiling to WebAssembly
The Rust engine compiles to WASM via wasm-pack:
wasm-pack build --target web --release
The resulting .wasm binary is ~200KB and runs in the browser for:
- Client-side puzzle validation (anti-cheat)
- Hint generation
- Solution verification
For batch generation (we pre-generate 3000+ puzzles), we use the native binary:
./slitherlink-engine gen --grid-size 10 --level 6 --count 100
This outputs JSON to stdout — one puzzle per line with grid_size, clues, solution, and difficulty rating.
Production Pipeline
Rust engine (batch generate)
→ JSON puzzles
→ Import to Cloudflare D1 database
→ CF Worker API serves puzzles
→ Next.js frontend renders with Phaser 3
The database holds 3000+ puzzles across 5 grid sizes (5×5, 7×7, 8×8, 10×10, 12×12) and 10 difficulty levels. The API supports random puzzle selection filtered by size and difficulty, daily challenges, and leaderboards.
Numbers
- Generation speed: ~50 puzzles/minute for 10×10 Level 5-6
- WASM bundle: 200KB gzipped
- Solve time (WASM, 10×10): < 100ms
- Database: 3000+ puzzles, 1.9MB total in D1
Try It
Play the puzzles at slitherlinks.com — the difficulty ratings are real. Level 1 feels gentle; Level 8 will make you sweat.
Top comments (0)