Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions PathPlanning/BreadthFirstSearch/dynamic_maze_solver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy as np
from collections import deque
import random
import matplotlib.animation as animation


class MazeVisualizer:
"""
Dynamic BFS maze-solving visualizer with moving target and evolving obstacles.
"""

def __init__(self, maze, start, target):
self.maze = np.array(maze, dtype=int)
self.start_pos = start
self.target_pos = target
self.solver_pos = start

self.rows, self.cols = self.maze.shape
self.step_delay_ms = 200 # Animation frame delay
self.target_move_interval = 5 # Target moves every N frames
self.obstacle_change_prob = 0.01 # Random obstacle toggle probability

# --- State Tracking ---
self.path = []
self.visited_nodes = set()
self.breadcrumb_trail = [self.solver_pos]
self.frame_count = 0

# --- Plot Setup ---
self.fig, self.ax = plt.subplots(figsize=(8, 6))
plt.style.use('seaborn-v0_8-darkgrid')
self.fig.patch.set_facecolor('#2c2c2c')
self.ax.set_facecolor('#1e1e1e')

self.ax.set_xticks([])
self.ax.set_yticks([])

# Base maze
self.maze_plot = self.ax.imshow(self.maze, cmap='magma', interpolation='nearest')

# Visited overlay
self.visited_overlay = np.zeros((*self.maze.shape, 4))
self.visited_plot = self.ax.imshow(self.visited_overlay, interpolation='nearest')

# Path, breadcrumbs, solver, target
self.path_line, = self.ax.plot([], [], 'g-', linewidth=3, alpha=0.7, label='Path')
self.breadcrumbs_plot = self.ax.scatter([], [], c=[], cmap='viridis_r', s=50, alpha=0.6, label='Trail')
self.solver_plot, = self.ax.plot(
[self.solver_pos[1]], [self.solver_pos[0]],
'o', markersize=15, color='#00ffdd', label='Solver'
)
self.target_plot, = self.ax.plot(
[self.target_pos[1]], [self.target_pos[0]],
'*', markersize=20, color='#ff006a', label='Target'
)

self.ax.legend(facecolor='gray', framealpha=0.5, loc='upper right')
self.title = self.ax.set_title("Initializing Maze...", color='white', fontsize=14)

def _bfs(self):
"""Performs BFS to find shortest path."""
queue = deque([(self.solver_pos, [self.solver_pos])])
visited = {self.solver_pos}

while queue:
(r, c), path = queue.popleft()

if (r, c) == self.target_pos:
return path, visited

for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nr, nc = r + dr, c + dc
if 0 <= nr < self.rows and 0 <= nc < self.cols and \
self.maze[nr][nc] == 0 and (nr, nc) not in visited:
visited.add((nr, nc))
queue.append(((nr, nc), path + [(nr, nc)]))

return None, visited

def _update_target(self):
"""Moves the target randomly to an adjacent open cell."""
tr, tc = self.target_pos
moves = [(-1, 0), (1, 0), (0, -1), (0, 1)]
random.shuffle(moves)
for dr, dc in moves:
nr, nc = tr + dr, tc + dc
if 0 <= nr < self.rows and 0 <= nc < self.cols and self.maze[nr][nc] == 0:
self.target_pos = (nr, nc)
break

def _update_obstacles(self):
"""Randomly toggle a few obstacles."""
for r in range(self.rows):
for c in range(self.cols):
if (r, c) in [self.solver_pos, self.target_pos]:
continue
if random.random() < self.obstacle_change_prob:
self.maze[r, c] = 1 - self.maze[r, c]

def _update_frame(self, frame):
"""Main animation loop."""
self.frame_count += 1

# --- State ---
if self.frame_count % self.target_move_interval == 0:
self._update_target()
self._update_obstacles()

self.path, self.visited_nodes = self._bfs()

# Move solver one step
if self.path and len(self.path) > 1:
self.solver_pos = self.path[1]
self.breadcrumb_trail.append(self.solver_pos)

# --- Visuals ---
self.maze_plot.set_data(self.maze)

# Visited overlay
self.visited_overlay.fill(0)
visited_color = mcolors.to_rgba('#0077b6', alpha=0.3)
for r, c in self.visited_nodes:
self.visited_overlay[r, c] = visited_color
self.visited_plot.set_data(self.visited_overlay)

# Path line
if self.path:
y, x = zip(*self.path)
self.path_line.set_data(x, y)
else:
self.path_line.set_data([], [])

# set_data() now receives sequences
self.solver_plot.set_data([self.solver_pos[1]], [self.solver_pos[0]])
self.target_plot.set_data([self.target_pos[1]], [self.target_pos[0]])

# Breadcrumbs
if self.breadcrumb_trail:
y, x = zip(*self.breadcrumb_trail)
colors = np.linspace(0.1, 1.0, len(y))
self.breadcrumbs_plot.set_offsets(np.c_[x, y])
self.breadcrumbs_plot.set_array(colors)

# Title update
if self.solver_pos == self.target_pos:
self.title.set_text("Dynamic Maze Solver")
self.title.set_color('lightgreen')
self.anim.event_source.stop()
else:
path_len = len(self.path) if self.path else "N/A"
self.title.set_text(f"Frame: {self.frame_count} | Path Length: {path_len}")
self.title.set_color('white' if self.path else 'coral')

return [
self.maze_plot, self.visited_plot, self.path_line,
self.solver_plot, self.target_plot, self.breadcrumbs_plot, self.title
]

def run(self):
"""Starts the animation."""
self.anim = animation.FuncAnimation(
self.fig,
self._update_frame,
frames=500,
interval=self.step_delay_ms,
blit=False,
repeat=False
)
plt.show()


if __name__ == "__main__":
initial_maze = [
[0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 1, 0, 1, 1, 0, 1, 0, 1, 0],
[0, 0, 0, 1, 0, 0, 1, 0, 0, 0],
[0, 1, 0, 1, 0, 1, 1, 1, 1, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[1, 1, 1, 1, 0, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
]

start_point = (0, 0)
end_point = (8, 9)

visualizer = MazeVisualizer(initial_maze, start_point, end_point)
visualizer.run()
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
Dynamic Maze Solver using Breadth-First Search (BFS)
====================================================

.. contents:: Table of Contents
:local:
:depth: 2

Overview
--------

This example demonstrates a **dynamic maze-solving algorithm** based on the
**Breadth-First Search (BFS)** strategy. The visualizer dynamically updates a maze
in real-time while the solver attempts to reach a moving target.

Unlike static pathfinding examples, this version introduces:

- **A moving target** that relocates periodically.
- **Randomly evolving obstacles** that can appear or disappear.
- **Animated BFS exploration**, showing visited cells, computed paths, and breadcrumbs.

This simulation provides intuition for dynamic pathfinding problems such as
robot navigation in unpredictable environments.


Algorithmic Background
----------------------

### Breadth-First Search (BFS)

The BFS algorithm is a graph traversal method that explores nodes in layers,
guaranteeing the shortest path in an unweighted grid.

Let the maze be represented as a grid:

.. math::

M = \{ (i, j) \mid 0 \leq i < R, 0 \leq j < C \}

where each cell is either *free (0)* or *obstacle (1)*.

The BFS frontier expands as:

.. math::

Q = [(s, [s])]

where *s* is the start position, and the second term is the path history.

At each iteration:

.. math::

(r, c), path = Q.pop(0)

\text{for each neighbor } (r', c') \text{ in } N(r, c):
\text{if } (r', c') \text{ is free and unvisited:}
Q.append((r', c'), path + [(r', c')])

The algorithm halts when the target node *t* is reached.

Because BFS explores all nodes in increasing distance order, the path returned
is the shortest (in terms of number of moves).


Dynamic Components
------------------

### Moving Target

Every few frames, the target moves randomly to an adjacent open cell:

.. math::

T_{new} = T_{old} + \Delta

where :math:`\Delta \in \{ (-1,0), (1,0), (0,-1), (0,1) \}`.

This simulates dynamic goals or moving entities in robotic navigation.

### Evolving Obstacles

With a small probability :math:`p`, each cell toggles between *free* and *blocked*:

.. math::

M_{i,j}^{t+1} =
\begin{cases}
1 - M_{i,j}^{t} & \text{with probability } p \\
M_{i,j}^{t} & \text{otherwise}
\end{cases}

This reflects real-world conditions like temporary obstructions or environment changes.


Visualization
-------------

The maze, solver, target, and BFS layers are visualized using **Matplotlib**.

Elements include:

- **Maze cells** – magma colormap (black = wall, bright = open)
- **Visited nodes** – blue overlay with transparency
- **Path line** – green connecting line
- **Solver (robot)** – cyan circle
- **Target** – magenta star
- **Breadcrumbs** – trail of previously visited solver positions

A sample animation frame:

.. image:: ezgif.com-crop.jpg
:alt: Maze BFS dynamic visualizer frame
:align: center
:scale: 80 %


Mathematical Insights
---------------------

- **BFS guarantees optimality** in unweighted grids.
- The evolving maze introduces **non-stationarity**, requiring recomputation per frame.
- The path length :math:`L_t` fluctuates as the environment changes.

If :math:`E_t` is the set of explored nodes at frame :math:`t`, then:

.. math::

L_t = |P_t|, \quad E_t = |V_t|

where :math:`P_t` is the discovered path and :math:`V_t` is the visited node set.

The solver continually re-estimates the path to accommodate new maze configurations.


References
----------

- **Algorithm:** Breadth-First Search (BFS) :-`<https://en.wikipedia.org/wiki/Breadth-first_search>`_
- **Visualization:** Matplotlib animation
- **Maze Solver:**:-`<https://medium.com/@luthfisauqi17_68455/artificial-intelligence-search-problem-solve-maze-using-breadth-first-search-bfs-algorithm-255139c6e1a3>`__