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