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