Skip to content

Commit a6e60ab

Browse files
Implement solver (#2)
* Implement SnakeSolver * Enhance board visualization with dynamic column and row sum alignment * Cleanup SnakeSolver * Update unittests * polish
1 parent 268978d commit a6e60ab

File tree

4 files changed

+474
-156
lines changed

4 files changed

+474
-156
lines changed

main.py

Lines changed: 88 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22
import time
33

44

5+
def example_3x3():
6+
"""Simple 3x3 example for testing"""
7+
puzzle = SnakePuzzle(
8+
row_sums=[2, 1, 2],
9+
col_sums=[1, 3, 1],
10+
start_cell=(0, 0),
11+
end_cell=(2, 2)
12+
)
13+
return puzzle
14+
15+
516
def example_8x8():
617
"""8x8 example from https://puzzlegenius.org/snake/"""
718
puzzle = SnakePuzzle(
@@ -13,75 +24,89 @@ def example_8x8():
1324
return puzzle
1425

1526

16-
def main():
17-
"""
18-
Example usage of the Snake solver.
27+
def example_diagonal_touching():
28+
"""Diagonal touching example (infeasible)"""
29+
puzzle = SnakePuzzle(
30+
row_sums=[2, 3, 3, 0, 0],
31+
col_sums=[0, 3, 2, 2, 1],
32+
start_cell=(0, 2),
33+
end_cell=(1, 4)
34+
)
35+
return puzzle
36+
37+
def example_adjacent_touching():
38+
"""Adjacent touching example (infeasible)"""
39+
puzzle = SnakePuzzle(
40+
row_sums=[1, 4, 3, 0],
41+
col_sums=[3, 2, 1, 2],
42+
start_cell=(0, 0),
43+
end_cell=(3, 3)
44+
)
45+
return puzzle
46+
47+
def example_12x12_evil():
48+
"""12x12 'Evil' puzzle from https://gridpuzzle.com/snake/evil-12"""
49+
puzzle = SnakePuzzle(
50+
row_sums=[11, 2, 7, 4, 4, None, None, None, 3, 2, None, 5],
51+
col_sums=[9, 7, None, 2, 5, 6, None, None, 5, None, None, None],
52+
start_cell=(2, 6),
53+
end_cell=(7, 5)
54+
)
55+
return puzzle
56+
57+
def solve_puzzle(puzzle, name):
58+
"""Solve a snake puzzle and display results"""
59+
print(f"\n" + "="*60)
60+
print(f"SOLVING {name.upper()}")
61+
print("="*60)
1962

20-
"""
21-
print("Snake MIP Solver - Example Usage")
22-
print("=" * 50)
63+
# Create and use the solver
64+
solver = SnakeSolver(puzzle)
2365

24-
25-
try:
26-
puzzle = example_8x8()
27-
print(f"Created puzzle: {puzzle}")
28-
29-
# Create solver
30-
solver = SnakeSolver(puzzle)
31-
print("Solver initialized successfully")
66+
print("Solver information:")
67+
info = solver.get_solver_info()
68+
for key, value in info.items():
69+
print(f" {key}: {value}")
70+
71+
print("\nSolving...")
72+
start_time = time.time()
73+
solution = solver.solve(verbose=False)
74+
solve_time = time.time() - start_time
75+
76+
if solution:
77+
print(f"\nSolution found in {solve_time:.3f} seconds!")
78+
print(f"Solution has {len(solution)} filled cells")
79+
print(f"Solution: {sorted(list(solution))}")
3280

33-
# Solve the puzzle
34-
print("\nSolving puzzle...")
35-
start_time = time.time()
36-
solution = solver.solve(verbose=True)
37-
solve_time = time.time() - start_time
81+
# Display the board with solution
82+
print("\nPuzzle with solution:")
83+
print(puzzle.get_board_visualization(solution, show_indices=True))
3884

39-
# Display results
40-
if solution is not None:
41-
print(f"\n✅ Solution found in {solve_time:.3f} seconds!")
42-
print(f"Solution: {solution}")
43-
print(puzzle.get_board_visualization())
44-
45-
manual_solution = set()
46-
manual_solution.add((2, 5))
47-
manual_solution.add((1, 5))
48-
manual_solution.add((0, 5))
49-
manual_solution.add((0, 4))
50-
manual_solution.add((0, 3))
51-
manual_solution.add((0, 2))
52-
manual_solution.add((1, 2))
53-
manual_solution.add((2, 2))
54-
manual_solution.add((3, 2))
55-
manual_solution.add((3, 1))
56-
manual_solution.add((3, 0))
57-
manual_solution.add((4, 0))
58-
manual_solution.add((5, 0))
59-
manual_solution.add((5, 1))
60-
manual_solution.add((5, 2))
61-
manual_solution.add((6, 2))
62-
manual_solution.add((7, 2))
63-
manual_solution.add((7, 3))
64-
manual_solution.add((7, 4))
65-
manual_solution.add((7, 5))
66-
manual_solution.add((7, 6))
67-
manual_solution.add((7, 7))
68-
manual_solution.add((6, 7))
69-
print(puzzle.get_board_visualization(manual_solution, show_indices=True))
70-
print(f"Manual solution valid? {puzzle.is_valid_solution(manual_solution)}")
71-
# TODO: Add solution visualization or validation
72-
# Example:
73-
# if puzzle.is_valid_solution(solution):
74-
# print("Solution is valid!")
75-
# display_solution(puzzle, solution)
76-
# else:
77-
# print("Solution validation failed!")
85+
# Validate solution
86+
if puzzle.is_valid_solution(solution):
87+
print("✅ Solution is valid!")
7888
else:
79-
print(f"\nNo solution found (took {solve_time:.3f} seconds)")
80-
81-
except Exception as e:
82-
print(f"Error: {e}")
83-
print("Make sure to implement your puzzle class and solver logic!")
89+
print("❌ Solution validation failed!")
90+
else:
91+
print(f"\nNo solution found (took {solve_time:.3f} seconds)")
92+
93+
94+
def main():
95+
# Solve different puzzle examples
96+
puzzle_3x3 = example_3x3()
97+
solve_puzzle(puzzle_3x3, "3x3 Simple")
98+
99+
puzzle_diag = example_diagonal_touching()
100+
solve_puzzle(puzzle_diag, "5x5 Diagonal Touching")
101+
102+
puzzle_adjacent_touching = example_adjacent_touching()
103+
solve_puzzle(puzzle_adjacent_touching, "4x4 Adjacent Touching")
104+
105+
puzzle_8x8 = example_8x8()
106+
solve_puzzle(puzzle_8x8, "8x8")
84107

108+
puzzle_12x12 = example_12x12_evil()
109+
solve_puzzle(puzzle_12x12, "12x12 Evil")
85110

86111
if __name__ == "__main__":
87112
main()

snake_mip_solver/puzzle.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -288,13 +288,28 @@ def get_board_visualization(self, snake_positions: Optional[Set[Tuple[int, int]]
288288
snake_positions = snake_positions or set()
289289
lines = []
290290

291+
# Calculate the maximum width needed for row sums to ensure proper alignment
292+
max_row_sum_width = max(
293+
len(str(s)) if s is not None else 1
294+
for s in self.row_sums
295+
)
296+
297+
# Calculate the maximum width needed for column indices if shown
298+
max_col_index_width = len(str(self.cols - 1)) if show_indices else 0
299+
291300
# Header with column indices and sums
292301
if show_indices:
293-
header = " " + " ".join(str(i) for i in range(self.cols))
302+
# Row index + row sum padding + space
303+
prefix_width = max_col_index_width + max_row_sum_width + 2
304+
header = " " * prefix_width + " ".join(f"{i:>{max_col_index_width}}" for i in range(self.cols))
294305
lines.append(header)
295-
col_sums_line = " " + " ".join(str(s) if s is not None else "?" for s in self.col_sums)
306+
col_sums_line = " " * prefix_width + " ".join(
307+
f"{str(s) if s is not None else '?':>{max_col_index_width}}" for s in self.col_sums
308+
)
296309
else:
297-
col_sums_line = " " + " ".join(str(s) if s is not None else "?" for s in self.col_sums)
310+
# Just row sum padding + space
311+
prefix_width = max_row_sum_width + 1
312+
col_sums_line = " " * prefix_width + " ".join(str(s) if s is not None else "?" for s in self.col_sums)
298313

299314
lines.append(col_sums_line)
300315

@@ -303,21 +318,26 @@ def get_board_visualization(self, snake_positions: Optional[Set[Tuple[int, int]]
303318
row_parts = []
304319

305320
if show_indices:
306-
row_parts.append(str(row))
321+
row_parts.append(f"{row:>{max_col_index_width}}")
307322

308323
row_sum_str = str(self.row_sums[row]) if self.row_sums[row] is not None else "?"
309-
row_parts.append(row_sum_str)
324+
row_parts.append(f"{row_sum_str:>{max_row_sum_width}}")
310325

311326
for col in range(self.cols):
312327
pos = (row, col)
313328
if pos == self.start_cell:
314-
row_parts.append('S')
329+
cell_str = 'S'
315330
elif pos == self.end_cell:
316-
row_parts.append('E')
331+
cell_str = 'E'
317332
elif pos in snake_positions:
318-
row_parts.append('x') # █
333+
cell_str = 'x'
334+
else:
335+
cell_str = '_'
336+
337+
if show_indices:
338+
row_parts.append(f"{cell_str:>{max_col_index_width}}")
319339
else:
320-
row_parts.append('_')
340+
row_parts.append(cell_str)
321341

322342
lines.append(' '.join(row_parts))
323343

0 commit comments

Comments
 (0)