Skip to content

Commit c9b263a

Browse files
authored
Merge pull request #43 from ewdlop/ewdlop-patch-33
Create SokobanSolver.py
2 parents 857776b + b84ac7b commit c9b263a

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)