Skip to content

Commit 6a4edbe

Browse files
committed
dynamic maze solver
1 parent c4c7d59 commit 6a4edbe

File tree

1 file changed

+190
-0
lines changed

1 file changed

+190
-0
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import matplotlib.pyplot as plt
2+
import matplotlib.colors as mcolors
3+
import numpy as np
4+
from collections import deque
5+
import random
6+
import matplotlib.animation as animation
7+
8+
class MazeVisualizer:
9+
"""
10+
A class to create a beautiful and interesting visualization
11+
of a dynamic maze-solving algorithm (BFS).
12+
"""
13+
14+
def __init__(self, maze, start, target):
15+
self.maze = np.array(maze)
16+
self.start_pos = start
17+
self.target_pos = target
18+
self.solver_pos = start
19+
20+
self.rows, self.cols = self.maze.shape
21+
22+
# --- Configurable Parameters ---
23+
self.step_delay_ms = 200 # Animation frame delay in milliseconds
24+
self.target_move_interval = 5 # Target moves every N frames
25+
self.obstacle_change_prob = 0.01 # Probability of a wall changing
26+
27+
# --- State Tracking ---
28+
self.path = []
29+
self.visited_nodes = set()
30+
self.breadcrumb_trail = [self.solver_pos]
31+
self.frame_count = 0
32+
33+
# --- Plotting Setup ---
34+
self.fig, self.ax = plt.subplots(figsize=(8, 6))
35+
plt.style.use('seaborn-v0_8-darkgrid')
36+
self.fig.patch.set_facecolor('#2c2c2c')
37+
self.ax.set_facecolor('#1e1e1e')
38+
39+
# Hide axes ticks and labels for a cleaner look
40+
self.ax.set_xticks([])
41+
self.ax.set_yticks([])
42+
43+
# Maze plot
44+
self.maze_plot = self.ax.imshow(self.maze, cmap='magma', interpolation='nearest')
45+
46+
# Visited nodes plot (semi-transparent overlay)
47+
self.visited_overlay = np.zeros((*self.maze.shape, 4)) # RGBA
48+
self.visited_plot = self.ax.imshow(self.visited_overlay, interpolation='nearest')
49+
50+
# Path, solver, target, and breadcrumbs plots
51+
self.path_line, = self.ax.plot([], [], 'g-', linewidth=3, alpha=0.7, label='Path')
52+
self.breadcrumbs_plot = self.ax.scatter([], [], c=[], cmap='viridis_r', s=50, alpha=0.6, label='Trail')
53+
self.solver_plot, = self.ax.plot(self.solver_pos[1], self.solver_pos[0], 'o', markersize=15, color='#00ffdd', label='Solver')
54+
self.target_plot, = self.ax.plot(self.target_pos[1], self.target_pos[0], '*', markersize=20, color='#ff006a', label='Target')
55+
56+
self.ax.legend(facecolor='gray', framealpha=0.5, loc='upper right')
57+
self.title = self.ax.set_title("Initializing Maze...", color='white', fontsize=14)
58+
59+
def _bfs(self):
60+
"""Performs BFS to find the shortest path and returns path and visited nodes."""
61+
queue = deque([(self.solver_pos, [self.solver_pos])])
62+
visited = {self.solver_pos}
63+
64+
while queue:
65+
(r, c), path = queue.popleft()
66+
67+
if (r, c) == self.target_pos:
68+
return path, visited
69+
70+
for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
71+
nr, nc = r + dr, c + dc
72+
if 0 <= nr < self.rows and 0 <= nc < self.cols and \
73+
self.maze[nr][nc] == 0 and (nr, nc) not in visited:
74+
visited.add((nr, nc))
75+
new_path = list(path)
76+
new_path.append((nr, nc))
77+
queue.append(((nr, nc), new_path))
78+
79+
return None, visited # No path found
80+
81+
def _update_target(self):
82+
"""Moves the target to a random adjacent valid cell."""
83+
tr, tc = self.target_pos
84+
moves = [(-1, 0), (1, 0), (0, -1), (0, 1)]
85+
random.shuffle(moves)
86+
for dr, dc in moves:
87+
nr, nc = tr + dr, tc + dc
88+
if 0 <= nr < self.rows and 0 <= nc < self.cols and self.maze[nr][nc] == 0:
89+
self.target_pos = (nr, nc)
90+
break
91+
92+
def _update_obstacles(self):
93+
"""Randomly toggles a few obstacle cells."""
94+
for r in range(self.rows):
95+
for c in range(self.cols):
96+
# Avoid changing start/target positions
97+
if (r,c) == self.solver_pos or (r,c) == self.target_pos:
98+
continue
99+
if random.random() < self.obstacle_change_prob:
100+
self.maze[r, c] = 1 - self.maze[r, c] # Toggle 0 to 1 or 1 to 0
101+
102+
def _update_frame(self, frame):
103+
"""Main animation loop function."""
104+
self.frame_count += 1
105+
106+
# --- Update Game State ---
107+
if self.frame_count % self.target_move_interval == 0:
108+
self._update_target()
109+
110+
self._update_obstacles()
111+
112+
self.path, self.visited_nodes = self._bfs()
113+
114+
if self.path and len(self.path) > 1:
115+
self.solver_pos = self.path[1] # Move solver one step
116+
self.breadcrumb_trail.append(self.solver_pos)
117+
118+
# --- Update Visuals ---
119+
# Update maze and visited nodes overlay
120+
self.maze_plot.set_data(self.maze)
121+
self.visited_overlay.fill(0) # Reset overlay
122+
visited_color = mcolors.to_rgba('#0077b6', alpha=0.3)
123+
for r, c in self.visited_nodes:
124+
self.visited_overlay[r, c] = visited_color
125+
self.visited_plot.set_data(self.visited_overlay)
126+
127+
# Update path line
128+
if self.path:
129+
path_y, path_x = zip(*self.path)
130+
self.path_line.set_data(path_x, path_y)
131+
else:
132+
self.path_line.set_data([], [])
133+
134+
# Update solver and target positions
135+
self.solver_plot.set_data(self.solver_pos[1], self.solver_pos[0])
136+
self.target_plot.set_data(self.target_pos[1], self.target_pos[0])
137+
138+
# Update breadcrumbs
139+
if self.breadcrumb_trail:
140+
trail_y, trail_x = zip(*self.breadcrumb_trail)
141+
colors = np.linspace(0.1, 1.0, len(trail_y))
142+
self.breadcrumbs_plot.set_offsets(np.c_[trail_x, trail_y])
143+
self.breadcrumbs_plot.set_array(colors)
144+
145+
# Update title and check for win condition
146+
if self.solver_pos == self.target_pos:
147+
self.title.set_text("Target Reached! 🎉")
148+
self.title.set_color('lightgreen')
149+
self.anim.event_source.stop() # Stop animation
150+
else:
151+
path_len_str = len(self.path) if self.path else "N/A"
152+
self.title.set_text(f"Frame: {self.frame_count} | Path Length: {path_len_str}")
153+
if not self.path:
154+
self.title.set_color('coral')
155+
else:
156+
self.title.set_color('white')
157+
158+
return [self.maze_plot, self.visited_plot, self.path_line, self.solver_plot,
159+
self.target_plot, self.breadcrumbs_plot, self.title]
160+
161+
def run(self):
162+
"""Starts the animation."""
163+
self.anim = animation.FuncAnimation(
164+
self.fig,
165+
self._update_frame,
166+
frames=200, # Can be increased for longer animation
167+
interval=self.step_delay_ms,
168+
blit=True,
169+
repeat=False
170+
)
171+
plt.show()
172+
173+
if __name__ == "__main__":
174+
initial_maze = [
175+
[0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
176+
[0, 1, 0, 1, 1, 0, 1, 0, 1, 0],
177+
[0, 0, 0, 1, 0, 0, 1, 0, 0, 0],
178+
[0, 1, 0, 1, 0, 1, 1, 1, 1, 0],
179+
[0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
180+
[0, 1, 1, 1, 1, 1, 1, 0, 1, 0],
181+
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
182+
[1, 1, 1, 1, 0, 1, 1, 1, 1, 0],
183+
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
184+
]
185+
186+
start_point = (0, 0)
187+
end_point = (8, 9)
188+
189+
visualizer = MazeVisualizer(maze=initial_maze, start=start_point, target=end_point)
190+
visualizer.run()

0 commit comments

Comments
 (0)