Building a Slitherlink Puzzle Engine in Rust + WebAssembly

Published: (February 18, 2026 at 09:41 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

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 a 10–50× speedup and the option to compile to WebAssembly 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 can make the puzzle harder, 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), and propagate each branch.
  • Uniqueness check – if both branches lead to valid solutions, the puzzle has multiple solutions and is rejected.
fn solve(puzzle: &Puzzle) -> usize {
    propagate_all_constraints(puzzle);
    if puzzle.is_contradictory() {
        return 0; // no solution
    }
    if puzzle.is_fully_decided() {
        return 1; // one solution found
    }
    let e = puzzle.pick_undecided_edge();
    let mut count = 0;
    // try edge = line
    let mut p1 = puzzle.clone();
    p1.set_edge(e, true);
    count += solve(&p1);
    // try edge = no line
    let mut p2 = puzzle.clone();
    p2.set_edge(e, false);
    count += solve(&p2);
    count // total solutions
}

If the total exceeds 1, the puzzle is ambiguous and discarded.

Phase 4: Difficulty Rating (10 Levels)

We don’t just count clues—we measure what techniques are required to solve:

LevelTechniques Required
1‑2Corner rules, 0‑elimination only
3‑4Adjacent number patterns (3‑3, 3‑0)
5‑6Vertex degree rule, edge patterns
7‑8Loop closure logic, inside/outside reasoning
9‑10Multi‑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 becomes the puzzle’s difficulty.

fn rate_puzzle(puzzle: &Puzzle) -> u8 {
    let mut max_level = 1;
    let mut state = SolverState::new(puzzle);

    loop {
        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 ~200 KB 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: 200 KB gzipped
  • Solve time (WASM, 10×10): . The difficulty ratings are real: Level 1 feels gentle; Level 8 will make you sweat.
0 views
Back to Blog

Related posts

Read more »