Skip to content

Commit b84ac7b

Browse files
authored
Create SokobanSolver.py
```python import heapq from collections import deque from typing import List, Tuple, Dict, Set class SokobanState: """Represents a state in the Sokoban game""" def __init__(self, player_pos: Tuple[int, int], boxes: Set[Tuple[int, int]], level: List[List[str]]): self.player_pos = player_pos self.boxes = frozenset(boxes) # Use frozenset for hashing self.level = level self.goals = self._find_goals() def _find_goals(self) -> Set[Tuple[int, int]]: """Find all goal positions in the level""" goals = set() for y, row in enumerate(self.level): for x, cell in enumerate(row): if cell in '.+*': goals.add((x, y)) return goals def is_goal_state(self) -> bool: """Check if all boxes are on goal positions""" return all(box in self.goals for box in self.boxes) def __hash__(self): return hash((self.player_pos, self.boxes)) def __eq__(self, other): return self.player_pos == other.player_pos and self.boxes == other.boxes class SokobanSolver: """Solves Sokoban puzzles using A* search algorithm""" def __init__(self, level: List[str]): self.level = self._parse_level(level) self.initial_state = self._create_initial_state() self.moves = [(0, 1), (1, 0), (0, -1), (-1, 0)] # down, right, up, left self.move_names = ['D', 'R', 'U', 'L'] def _parse_level(self, level: List[str]) -> List[List[str]]: """Convert string level to 2D list""" return [list(row) for row in level] def _create_initial_state(self) -> SokobanState: """Create initial state from level""" player_pos = None boxes = set() for y, row in enumerate(self.level): for x, cell in enumerate(row): if cell in '@+': player_pos = (x, y) if cell in '$*': boxes.add((x, y)) return SokobanState(player_pos, boxes, self.level) def _is_wall(self, x: int, y: int) -> bool: """Check if position is a wall""" return self.level[y][x] == '#' def _is_valid_position(self, x: int, y: int) -> bool: """Check if position is within bounds and not a wall""" if x < 0 or x >= len(self.level[0]) or y < 0 or y >= len(self.level): return False return not self._is_wall(x, y) def _calculate_heuristic(self, state: SokobanState) -> int: """Calculate heuristic (Manhattan distance from boxes to nearest goals)""" if not state.goals: return 0 total_distance = 0 for box in state.boxes: min_distance = float('inf') for goal in state.goals: distance = abs(box[0] - goal[0]) + abs(box[1] - goal[1]) min_distance = min(min_distance, distance) total_distance += min_distance return total_distance def _get_successors(self, state: SokobanState) -> List[Tuple[SokobanState, str]]: """Get possible next states and the moves that lead to them""" successors = [] for i, (dx, dy) in enumerate(self.moves): # Calculate new player position new_x = state.player_pos[0] + dx new_y = state.player_pos[1] + dy if not self._is_valid_position(new_x, new_y): continue # Check if player is pushing a box box_pos = (new_x, new_y) if box_pos in state.boxes: # Calculate new box position new_box_x = new_x + dx new_box_y = new_y + dy # Check if box can be pushed if not self._is_valid_position(new_box_x, new_box_y): continue # Check if there's already a box at the new position new_box_pos = (new_box_x, new_box_y) if new_box_pos in state.boxes: continue # Create new state with pushed box new_boxes = set(state.boxes) new_boxes.remove(box_pos) new_boxes.add(new_box_pos) new_state = SokobanState((new_x, new_y), new_boxes, state.level) else: # Create new state with moved player new_state = SokobanState((new_x, new_y), state.boxes, state.level) successors.append((new_state, self.move_names[i])) return successors def solve(self) -> List[str]: """Solve the Sokoban puzzle using A* search""" frontier = [] heapq.heappush(frontier, (0, 0, self.initial_state, [])) explored = set() explored.add(self.initial_state) while frontier: _, cost, current_state, moves = heapq.heappop(frontier) if current_state.is_goal_state(): return moves for next_state, move in self._get_successors(current_state): if next_state not in explored: new_cost = cost + 1 priority = new_cost + self._calculate_heuristic(next_state) heapq.heappush(frontier, (priority, new_cost, next_state, moves + [move])) explored.add(next_state) return [] # No solution found # Example usage def print_level(level): """Print the level""" for row in level: print(''.join(row)) print() def apply_moves(level, moves): """Apply a sequence of moves to the level""" # Create a copy of the level as list of lists current_level = [list(row) for row in level] # Find player position player_pos = None for y, row in enumerate(current_level): for x, cell in enumerate(row): if cell in '@+': player_pos = (x, y) break if player_pos: break # Define move directions directions = {'D': (0, 1), 'R': (1, 0), 'U': (0, -1), 'L': (-1, 0)} for move in moves: dx, dy = directions[move] new_x, new_y = player_pos[0] + dx, player_pos[1] + dy # Check if player is pushing a box if current_level[new_y][new_x] in '$*': box_new_x, box_new_y = new_x + dx, new_y + dy # Update box position if current_level[new_y][new_x] == '$': current_level[new_y][new_x] = ' ' else: # current_level[new_y][new_x] == '*' current_level[new_y][new_x] = '.' if current_level[box_new_y][box_new_x] == '.': current_level[box_new_y][box_new_x] = '*' else: current_level[box_new_y][box_new_x] = '$' # Update player position if current_level[player_pos[1]][player_pos[0]] == '@': current_level[player_pos[1]][player_pos[0]] = ' ' else: # current_level[player_pos[1]][player_pos[0]] == '+' current_level[player_pos[1]][player_pos[0]] = '.' if current_level[new_y][new_x] == '.': current_level[new_y][new_x] = '+' else: current_level[new_y][new_x] = '@' player_pos = (new_x, new_y) print(f"Move: {move}") print_level(current_level) return current_level # Test with a simple level if __name__ == "__main__": # Simple example level level = [ "#####", "#@ $#", "# . #", "# #", "#####" ] print("Initial level:") print_level(level) solver = SokobanSolver(level) solution = solver.solve() if solution: print(f"Solution found: {solution}") print("\nApplying solution:") final_level = apply_moves(level, solution) else: print("No solution found!") ```
1 parent 857776b commit b84ac7b

1 file changed

Lines changed: 231 additions & 0 deletions

File tree

Python/Claude/SokobanSolver.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import heapq
2+
from collections import deque
3+
from typing import List, Tuple, Dict, Set
4+
5+
class SokobanState:
6+
"""Represents a state in the Sokoban game"""
7+
def __init__(self, player_pos: Tuple[int, int], boxes: Set[Tuple[int, int]], level: List[List[str]]):
8+
self.player_pos = player_pos
9+
self.boxes = frozenset(boxes) # Use frozenset for hashing
10+
self.level = level
11+
self.goals = self._find_goals()
12+
13+
def _find_goals(self) -> Set[Tuple[int, int]]:
14+
"""Find all goal positions in the level"""
15+
goals = set()
16+
for y, row in enumerate(self.level):
17+
for x, cell in enumerate(row):
18+
if cell in '.+*':
19+
goals.add((x, y))
20+
return goals
21+
22+
def is_goal_state(self) -> bool:
23+
"""Check if all boxes are on goal positions"""
24+
return all(box in self.goals for box in self.boxes)
25+
26+
def __hash__(self):
27+
return hash((self.player_pos, self.boxes))
28+
29+
def __eq__(self, other):
30+
return self.player_pos == other.player_pos and self.boxes == other.boxes
31+
32+
class SokobanSolver:
33+
"""Solves Sokoban puzzles using A* search algorithm"""
34+
35+
def __init__(self, level: List[str]):
36+
self.level = self._parse_level(level)
37+
self.initial_state = self._create_initial_state()
38+
self.moves = [(0, 1), (1, 0), (0, -1), (-1, 0)] # down, right, up, left
39+
self.move_names = ['D', 'R', 'U', 'L']
40+
41+
def _parse_level(self, level: List[str]) -> List[List[str]]:
42+
"""Convert string level to 2D list"""
43+
return [list(row) for row in level]
44+
45+
def _create_initial_state(self) -> SokobanState:
46+
"""Create initial state from level"""
47+
player_pos = None
48+
boxes = set()
49+
50+
for y, row in enumerate(self.level):
51+
for x, cell in enumerate(row):
52+
if cell in '@+':
53+
player_pos = (x, y)
54+
if cell in '$*':
55+
boxes.add((x, y))
56+
57+
return SokobanState(player_pos, boxes, self.level)
58+
59+
def _is_wall(self, x: int, y: int) -> bool:
60+
"""Check if position is a wall"""
61+
return self.level[y][x] == '#'
62+
63+
def _is_valid_position(self, x: int, y: int) -> bool:
64+
"""Check if position is within bounds and not a wall"""
65+
if x < 0 or x >= len(self.level[0]) or y < 0 or y >= len(self.level):
66+
return False
67+
return not self._is_wall(x, y)
68+
69+
def _calculate_heuristic(self, state: SokobanState) -> int:
70+
"""Calculate heuristic (Manhattan distance from boxes to nearest goals)"""
71+
if not state.goals:
72+
return 0
73+
74+
total_distance = 0
75+
for box in state.boxes:
76+
min_distance = float('inf')
77+
for goal in state.goals:
78+
distance = abs(box[0] - goal[0]) + abs(box[1] - goal[1])
79+
min_distance = min(min_distance, distance)
80+
total_distance += min_distance
81+
82+
return total_distance
83+
84+
def _get_successors(self, state: SokobanState) -> List[Tuple[SokobanState, str]]:
85+
"""Get possible next states and the moves that lead to them"""
86+
successors = []
87+
88+
for i, (dx, dy) in enumerate(self.moves):
89+
# Calculate new player position
90+
new_x = state.player_pos[0] + dx
91+
new_y = state.player_pos[1] + dy
92+
93+
if not self._is_valid_position(new_x, new_y):
94+
continue
95+
96+
# Check if player is pushing a box
97+
box_pos = (new_x, new_y)
98+
if box_pos in state.boxes:
99+
# Calculate new box position
100+
new_box_x = new_x + dx
101+
new_box_y = new_y + dy
102+
103+
# Check if box can be pushed
104+
if not self._is_valid_position(new_box_x, new_box_y):
105+
continue
106+
107+
# Check if there's already a box at the new position
108+
new_box_pos = (new_box_x, new_box_y)
109+
if new_box_pos in state.boxes:
110+
continue
111+
112+
# Create new state with pushed box
113+
new_boxes = set(state.boxes)
114+
new_boxes.remove(box_pos)
115+
new_boxes.add(new_box_pos)
116+
new_state = SokobanState((new_x, new_y), new_boxes, state.level)
117+
else:
118+
# Create new state with moved player
119+
new_state = SokobanState((new_x, new_y), state.boxes, state.level)
120+
121+
successors.append((new_state, self.move_names[i]))
122+
123+
return successors
124+
125+
def solve(self) -> List[str]:
126+
"""Solve the Sokoban puzzle using A* search"""
127+
frontier = []
128+
heapq.heappush(frontier, (0, 0, self.initial_state, []))
129+
explored = set()
130+
explored.add(self.initial_state)
131+
132+
while frontier:
133+
_, cost, current_state, moves = heapq.heappop(frontier)
134+
135+
if current_state.is_goal_state():
136+
return moves
137+
138+
for next_state, move in self._get_successors(current_state):
139+
if next_state not in explored:
140+
new_cost = cost + 1
141+
priority = new_cost + self._calculate_heuristic(next_state)
142+
heapq.heappush(frontier, (priority, new_cost, next_state, moves + [move]))
143+
explored.add(next_state)
144+
145+
return [] # No solution found
146+
147+
# Example usage
148+
def print_level(level):
149+
"""Print the level"""
150+
for row in level:
151+
print(''.join(row))
152+
print()
153+
154+
def apply_moves(level, moves):
155+
"""Apply a sequence of moves to the level"""
156+
# Create a copy of the level as list of lists
157+
current_level = [list(row) for row in level]
158+
159+
# Find player position
160+
player_pos = None
161+
for y, row in enumerate(current_level):
162+
for x, cell in enumerate(row):
163+
if cell in '@+':
164+
player_pos = (x, y)
165+
break
166+
if player_pos:
167+
break
168+
169+
# Define move directions
170+
directions = {'D': (0, 1), 'R': (1, 0), 'U': (0, -1), 'L': (-1, 0)}
171+
172+
for move in moves:
173+
dx, dy = directions[move]
174+
new_x, new_y = player_pos[0] + dx, player_pos[1] + dy
175+
176+
# Check if player is pushing a box
177+
if current_level[new_y][new_x] in '$*':
178+
box_new_x, box_new_y = new_x + dx, new_y + dy
179+
180+
# Update box position
181+
if current_level[new_y][new_x] == '$':
182+
current_level[new_y][new_x] = ' '
183+
else: # current_level[new_y][new_x] == '*'
184+
current_level[new_y][new_x] = '.'
185+
186+
if current_level[box_new_y][box_new_x] == '.':
187+
current_level[box_new_y][box_new_x] = '*'
188+
else:
189+
current_level[box_new_y][box_new_x] = '$'
190+
191+
# Update player position
192+
if current_level[player_pos[1]][player_pos[0]] == '@':
193+
current_level[player_pos[1]][player_pos[0]] = ' '
194+
else: # current_level[player_pos[1]][player_pos[0]] == '+'
195+
current_level[player_pos[1]][player_pos[0]] = '.'
196+
197+
if current_level[new_y][new_x] == '.':
198+
current_level[new_y][new_x] = '+'
199+
else:
200+
current_level[new_y][new_x] = '@'
201+
202+
player_pos = (new_x, new_y)
203+
204+
print(f"Move: {move}")
205+
print_level(current_level)
206+
207+
return current_level
208+
209+
# Test with a simple level
210+
if __name__ == "__main__":
211+
# Simple example level
212+
level = [
213+
"#####",
214+
"#@ $#",
215+
"# . #",
216+
"# #",
217+
"#####"
218+
]
219+
220+
print("Initial level:")
221+
print_level(level)
222+
223+
solver = SokobanSolver(level)
224+
solution = solver.solve()
225+
226+
if solution:
227+
print(f"Solution found: {solution}")
228+
print("\nApplying solution:")
229+
final_level = apply_moves(level, solution)
230+
else:
231+
print("No solution found!")

0 commit comments

Comments
 (0)