Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 164 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@ A Snake puzzle solver using mathematical programming.

## Overview

Snake is a logic puzzle where ...
Snake is a logic puzzle where you must create a single connected path on a grid according to these rules:

- **Single connected path** - the snake forms one continuous line from start to end cell
- **No self-touching** - the snake body never touches itself, neither orthogonally nor diagonally
- **Row and column constraints** - each row/column must have a specific number of filled cells
- **Missing constraints variant** - supports puzzles where some row/column constraints are unknown (specified as `None`)

This solver models the puzzle as a **Mixed Integer Programming (MIP)** problem to find solutions.

Expand All @@ -70,16 +75,164 @@ pip install snake-mip-solver

## Requirements

- Python >=3.9
- Python 3.9+
- Google OR-Tools
- pytest (for testing)

## Example Puzzles

### 6x6 Easy Puzzle

This 6x6 puzzle demonstrates a straightforward Snake puzzle with clear constraints:

| Puzzle | Solution |
|--------|----------|
| <img src="https://github.com/DenHvideDvaerg/snake-mip-solver/raw/main/images/6x6_Easy.png" width="200"> | <img src="https://github.com/DenHvideDvaerg/snake-mip-solver/raw/main/images/6x6_Easy_solution.png" width="200"> |

```python
def example_6x6_easy():
"""6x6 easy example"""
puzzle = SnakePuzzle(
row_sums=[1, 1, 1, 3, 2, 5],
col_sums=[4, 3, 1, 1, 1, 3],
start_cell=(0, 0),
end_cell=(3, 5)
)
return puzzle
```

### 12x12 Evil Puzzle with Missing Constraints

This 12x12 puzzle demonstrates advanced features: a large grid with missing row/column constraints (shown as `None`):

| Puzzle | Solution |
|--------|----------|
| <img src="https://github.com/DenHvideDvaerg/snake-mip-solver/raw/main/images/12x12_Evil.png" width="200"> | <img src="https://github.com/DenHvideDvaerg/snake-mip-solver/raw/main/images/12x12_Evil_solution.png" width="200"> |

```python
def example_12x12_evil():
"""12x12 'Evil' puzzle from https://gridpuzzle.com/snake/evil-12"""
puzzle = SnakePuzzle(
row_sums=[11, 2, 7, 4, 4, None, None, None, 3, 2, None, 5],
col_sums=[9, 7, None, 2, 5, 6, None, None, 5, None, None, None],
start_cell=(2, 6),
end_cell=(7, 5)
)
return puzzle
```

## Usage

```python
from snake_mip_solver import SnakePuzzle, SnakeSolver
import time

def solve_puzzle(puzzle, name):
"""Solve a snake puzzle and display results"""
print(f"\n" + "="*60)
print(f"SOLVING {name.upper()}")
print("="*60)

# Create and use the solver
solver = SnakeSolver(puzzle)

print("Solver information:")
info = solver.get_solver_info()
for key, value in info.items():
print(f" {key}: {value}")

print("\nSolving...")
start_time = time.time()
solution = solver.solve(verbose=False)
solve_time = time.time() - start_time

if solution:
print(f"\nSolution found in {solve_time:.3f} seconds!")
print(f"Solution has {len(solution)} filled cells")
print(f"Solution: {sorted(list(solution))}")

# Display the board with solution
print("\nPuzzle with solution:")
print(puzzle.get_board_visualization(solution, show_indices=False))

# Validate solution
if puzzle.is_valid_solution(solution):
print("✅ Solution is valid!")
else:
print("❌ Solution validation failed!")
else:
print(f"\nNo solution found (took {solve_time:.3f} seconds)")

# Load and solve example puzzles
puzzle_6x6 = example_6x6_easy()
solve_puzzle(puzzle_6x6, "6x6 Easy")

puzzle_12x12 = example_12x12_evil()
solve_puzzle(puzzle_12x12, "12x12 Evil")
```

# TODO: Make example either a direct copy of or very similar to main.py
### Output

```
============================================================
SOLVING 6X6 EASY
============================================================
Solver information:
solver_type: SCIP 9.2.2 [LP solver: SoPlex 7.1.3]
num_variables: 36
num_constraints: 159
puzzle_size: 6x6
start_cell: (0, 0)
end_cell: (3, 5)

Solving...

Solution found in 0.002 seconds!
Solution has 13 filled cells
Solution: [(0, 0), (1, 0), (2, 0), (3, 0), (3, 1), (3, 5), (4, 1), (4, 5), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)]

Puzzle with solution:
4 3 1 1 1 3
1 S _ _ _ _ _
1 x _ _ _ _ _
1 x _ _ _ _ _
3 x x _ _ _ E
2 _ x _ _ _ x
5 _ x x x x x
✅ Solution is valid!

============================================================
SOLVING 12X12 EVIL
============================================================
Solver information:
solver_type: SCIP 9.2.2 [LP solver: SoPlex 7.1.3]
num_variables: 144
num_constraints: 665
puzzle_size: 12x12
start_cell: (2, 6)
end_cell: (7, 5)

Solving...

Solution found in 0.255 seconds!
Solution has 49 filled cells
Solution: [(0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 9), (0, 10), (0, 11), (1, 1), (1, 11), (2, 0), (2, 1), (2, 6), (2, 8), (2, 9), (2, 10), (2, 11), (3, 0), (3, 5), (3, 6), (3, 8), (4, 0), (4, 1), (4, 5), (4, 8), (5, 1), (5, 5), (5, 6), (5, 7), (5, 8), (6, 0), (6, 1), (7, 0), (7, 5), (8, 0), (8, 4), (8, 5), (9, 0), (9, 4), (10, 0), (10, 4), (11, 0), (11, 1), (11, 2), (11, 3), (11, 4)]

Puzzle with solution:
9 7 ? 2 5 6 ? ? 5 ? ? ?
11 _ x x x x x x x x x x x
2 _ x _ _ _ _ _ _ _ _ _ x
7 x x _ _ _ _ S _ x x x x
4 x _ _ _ _ x x _ x _ _ _
4 x x _ _ _ x _ _ x _ _ _
? _ x _ _ _ x x x x _ _ _
? x x _ _ _ _ _ _ _ _ _ _
? x _ _ _ _ E _ _ _ _ _ _
3 x _ _ _ x x _ _ _ _ _ _
2 x _ _ _ x _ _ _ _ _ _ _
? x _ _ _ x _ _ _ _ _ _ _
5 x x x x x _ _ _ _ _ _ _
✅ Solution is valid!
```

### Running the example
Expand All @@ -103,8 +256,15 @@ pytest --cov=snake_mip_solver # Run with coverage

The solver uses **Mixed Integer Programming (MIP)** to model the puzzle constraints. Google OR-Tools provides the optimization framework, with SCIP as the default solver.

See the complete formulation in **[Complete Mathematical Model Documentation](https://github.com/DenHvideDvaerg/snake-mip-solver/blob/main/model.md)**
The mathematical formulation includes six types of constraints:
1. **Start and End Cell Constraints** - fixing the path endpoints
2. **Row Sum Constraints** - ensuring correct number of cells per row
3. **Column Sum Constraints** - ensuring correct number of cells per column
4. **Snake Path Connectivity Constraints** - forming a single connected path
5. **Diagonal Non-Touching Constraints** - preventing diagonal self-contact
6. **No 2×2 Block Constraints** - preventing disconnected filled blocks

See the complete formulation in **[Complete Mathematical Model Documentation](https://github.com/DenHvideDvaerg/snake-mip-solver/blob/main/model.md)**

## License

Expand Down
Binary file added images/12x12_Evil.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/12x12_Evil_solution.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/6x6_Easy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/6x6_Easy_solution.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 13 additions & 23 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,6 @@ def example_3x3():
return puzzle


def example_8x8():
"""8x8 example from https://puzzlegenius.org/snake/"""
puzzle = SnakePuzzle(
row_sums=[4, 2, 2, 3, 1, 3, 2, 6],
col_sums=[3, 2, 7, 2, 2, 4, 1, 2],
start_cell=(2, 5),
end_cell=(6, 7)
)
return puzzle


def example_diagonal_touching():
"""Diagonal touching example (infeasible)"""
puzzle = SnakePuzzle(
Expand All @@ -44,6 +33,16 @@ def example_adjacent_touching():
)
return puzzle

def example_6x6_easy():
"""6x6 easy example"""
puzzle = SnakePuzzle(
row_sums=[1, 1, 1, 3, 2, 5],
col_sums=[4, 3, 1, 1, 1, 3],
start_cell=(0, 0),
end_cell=(3, 5)
)
return puzzle

def example_12x12_evil():
"""12x12 'Evil' puzzle from https://gridpuzzle.com/snake/evil-12"""
puzzle = SnakePuzzle(
Expand Down Expand Up @@ -80,7 +79,7 @@ def solve_puzzle(puzzle, name):

# Display the board with solution
print("\nPuzzle with solution:")
print(puzzle.get_board_visualization(solution, show_indices=True))
print(puzzle.get_board_visualization(solution, show_indices=False))

# Validate solution
if puzzle.is_valid_solution(solution):
Expand All @@ -93,17 +92,8 @@ def solve_puzzle(puzzle, name):

def main():
# Solve different puzzle examples
puzzle_3x3 = example_3x3()
solve_puzzle(puzzle_3x3, "3x3 Simple")

puzzle_diag = example_diagonal_touching()
solve_puzzle(puzzle_diag, "5x5 Diagonal Touching")

puzzle_adjacent_touching = example_adjacent_touching()
solve_puzzle(puzzle_adjacent_touching, "4x4 Adjacent Touching")

puzzle_8x8 = example_8x8()
solve_puzzle(puzzle_8x8, "8x8")
puzzle_6x6 = example_6x6_easy()
solve_puzzle(puzzle_6x6, "6x6 Easy")

puzzle_12x12 = example_12x12_evil()
solve_puzzle(puzzle_12x12, "12x12 Evil")
Expand Down
Loading