Initial commit

This commit is contained in:
2025-11-20 22:58:11 -05:00
commit 6d75c8e94e
51 changed files with 5141 additions and 0 deletions

3
src/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""Maze Generator - A comprehensive maze generation and solving application."""
__version__ = "1.0.0"

6
src/analysis/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
"""Analysis and benchmarking tools."""
from .analyzer import MazeAnalyzer
from .benchmark import Benchmark
__all__ = ["MazeAnalyzer", "Benchmark"]

148
src/analysis/analyzer.py Normal file
View File

@@ -0,0 +1,148 @@
"""Maze analysis tools for calculating statistics and metrics."""
from typing import Dict, List
from ..core.cell import Cell
from ..core.maze import Maze
class MazeAnalyzer:
"""Analyzes mazes to compute various metrics and statistics."""
@staticmethod
def analyze(maze: Maze) -> Dict:
"""Perform comprehensive analysis of a maze.
Args:
maze: Maze to analyze
Returns:
Dictionary with analysis results
"""
dead_ends = MazeAnalyzer.count_dead_ends(maze)
longest_path_info = MazeAnalyzer.find_longest_path(maze)
branching_factor = MazeAnalyzer.calculate_branching_factor(maze)
return {
'dimensions': f"{maze.rows}x{maze.cols}",
'total_cells': maze.rows * maze.cols,
'algorithm': maze.algorithm_used,
'generation_time_ms': maze.generation_time_ms,
'seed': maze.seed,
'dead_ends': dead_ends,
'dead_end_percentage': (dead_ends / (maze.rows * maze.cols)) * 100,
'longest_path_length': longest_path_info['length'],
'longest_path_start': longest_path_info['start'],
'longest_path_end': longest_path_info['end'],
'average_branching_factor': branching_factor
}
@staticmethod
def count_dead_ends(maze: Maze) -> int:
"""Count the number of dead ends in the maze.
A dead end is a cell with only one open passage.
Args:
maze: Maze to analyze
Returns:
Number of dead ends
"""
dead_end_count = 0
for row in maze.grid:
for cell in row:
# Count open passages
open_passages = sum(1 for wall in cell.walls.values() if not wall)
if open_passages == 1:
dead_end_count += 1
return dead_end_count
@staticmethod
def find_longest_path(maze: Maze) -> Dict:
"""Find the longest path in the maze.
Args:
maze: Maze to analyze
Returns:
Dictionary with longest path info
"""
max_length = 0
max_start = (0, 0)
max_end = (0, 0)
# Try BFS from each cell to find longest path
for start_row in maze.grid:
for start_cell in start_row:
maze.reset_visited()
distances = MazeAnalyzer._bfs_distances(maze, start_cell)
for end_cell, distance in distances.items():
if distance > max_length:
max_length = distance
max_start = (start_cell.row, start_cell.col)
max_end = (end_cell.row, end_cell.col)
maze.reset_visited()
return {
'length': max_length,
'start': max_start,
'end': max_end
}
@staticmethod
def _bfs_distances(maze: Maze, start: Cell) -> Dict[Cell, int]:
"""Calculate distances from start cell using BFS.
Args:
maze: The maze
start: Starting cell
Returns:
Dictionary mapping cells to their distance from start
"""
from collections import deque
queue = deque([(start, 0)])
distances = {start: 0}
start.visited = True
while queue:
current, dist = queue.popleft()
neighbors = maze.get_neighbors(current)
for neighbor, direction in neighbors:
if not neighbor.visited and not current.has_wall(direction):
neighbor.visited = True
distances[neighbor] = dist + 1
queue.append((neighbor, dist + 1))
return distances
@staticmethod
def calculate_branching_factor(maze: Maze) -> float:
"""Calculate the average branching factor of the maze.
Branching factor is the average number of passages per cell.
Args:
maze: Maze to analyze
Returns:
Average branching factor
"""
total_passages = 0
cell_count = 0
for row in maze.grid:
for cell in row:
# Count open passages
open_passages = sum(1 for wall in cell.walls.values() if not wall)
total_passages += open_passages
cell_count += 1
return total_passages / cell_count if cell_count > 0 else 0

172
src/analysis/benchmark.py Normal file
View File

@@ -0,0 +1,172 @@
"""Benchmarking tools for comparing maze algorithms."""
import time
from typing import Dict, List
from ..generators import (
RecursiveBacktrackingGenerator,
KruskalGenerator,
PrimGenerator,
SidewinderGenerator,
HuntAndKillGenerator,
EllerGenerator,
WilsonGenerator,
AldousBroderGenerator
)
from ..solvers import DFSSolver, BFSSolver
class Benchmark:
"""Benchmarks maze generation and solving algorithms."""
# All available generators
GENERATORS = [
RecursiveBacktrackingGenerator(),
KruskalGenerator(),
PrimGenerator(),
SidewinderGenerator(),
HuntAndKillGenerator(),
EllerGenerator(),
WilsonGenerator(),
AldousBroderGenerator()
]
# All available solvers
SOLVERS = [
DFSSolver(),
BFSSolver()
]
@staticmethod
def benchmark_generators(
sizes: List[tuple] = None,
iterations: int = 5,
seed: int = 42
) -> Dict:
"""Benchmark all maze generation algorithms.
Args:
sizes: List of (rows, cols) tuples to test
iterations: Number of iterations per configuration
seed: Base seed for reproducibility
Returns:
Dictionary with benchmark results
"""
if sizes is None:
sizes = [(5, 5), (10, 10), (25, 25), (50, 50)]
results = []
for rows, cols in sizes:
for generator in Benchmark.GENERATORS:
times = []
for i in range(iterations):
current_seed = seed + i
maze = generator.generate(rows, cols, current_seed)
times.append(maze.generation_time_ms)
avg_time = sum(times) / len(times)
min_time = min(times)
max_time = max(times)
results.append({
'algorithm': generator.name,
'size': f"{rows}x{cols}",
'rows': rows,
'cols': cols,
'iterations': iterations,
'avg_time_ms': round(avg_time, 3),
'min_time_ms': round(min_time, 3),
'max_time_ms': round(max_time, 3),
'times': [round(t, 3) for t in times]
})
return {
'benchmark_type': 'generators',
'sizes_tested': sizes,
'iterations_per_config': iterations,
'results': results
}
@staticmethod
def benchmark_solvers(
sizes: List[tuple] = None,
iterations: int = 5,
seed: int = 42
) -> Dict:
"""Benchmark all maze solving algorithms.
Args:
sizes: List of (rows, cols) tuples to test
iterations: Number of iterations per configuration
seed: Base seed for reproducibility
Returns:
Dictionary with benchmark results
"""
if sizes is None:
sizes = [(5, 5), (10, 10), (25, 25), (50, 50)]
results = []
# Use one generator for consistency
generator = RecursiveBacktrackingGenerator()
for rows, cols in sizes:
# Generate maze once per size
maze = generator.generate(rows, cols, seed)
for solver in Benchmark.SOLVERS:
times = []
path_lengths = []
for i in range(iterations):
result = solver.solve(maze)
times.append(result['time_ms'])
path_lengths.append(result['path_length'])
avg_time = sum(times) / len(times)
min_time = min(times)
max_time = max(times)
avg_path_length = sum(path_lengths) / len(path_lengths)
results.append({
'algorithm': solver.name,
'size': f"{rows}x{cols}",
'rows': rows,
'cols': cols,
'iterations': iterations,
'avg_time_ms': round(avg_time, 3),
'min_time_ms': round(min_time, 3),
'max_time_ms': round(max_time, 3),
'avg_path_length': round(avg_path_length, 1),
'times': [round(t, 3) for t in times]
})
return {
'benchmark_type': 'solvers',
'sizes_tested': sizes,
'iterations_per_config': iterations,
'maze_generator': generator.name,
'results': results
}
@staticmethod
def quick_benchmark() -> Dict:
"""Run a quick benchmark with default settings.
Returns:
Dictionary with benchmark results for both generators and solvers
"""
return {
'generators': Benchmark.benchmark_generators(
sizes=[(10, 10), (25, 25)],
iterations=3
),
'solvers': Benchmark.benchmark_solvers(
sizes=[(10, 10), (25, 25)],
iterations=3
)
}

6
src/core/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
"""Core maze data structures."""
from .cell import Cell
from .maze import Maze
__all__ = ["Cell", "Maze"]

103
src/core/cell.py Normal file
View File

@@ -0,0 +1,103 @@
"""Cell class representing a single cell in the maze."""
from typing import Dict, Set
class Cell:
"""Represents a single cell in the maze grid.
Each cell has four walls (north, south, east, west) and can be visited
during maze generation or solving algorithms.
"""
def __init__(self, row: int, col: int):
"""Initialize a cell at the given position.
Args:
row: Row index in the maze grid
col: Column index in the maze grid
"""
self.row = row
self.col = col
self.walls = {"north": True, "south": True, "east": True, "west": True}
self.visited = False
def remove_wall(self, direction: str) -> None:
"""Remove a wall in the specified direction.
Args:
direction: One of 'north', 'south', 'east', 'west'
"""
if direction in self.walls:
self.walls[direction] = False
def has_wall(self, direction: str) -> bool:
"""Check if the cell has a wall in the specified direction.
Args:
direction: One of 'north', 'south', 'east', 'west'
Returns:
True if wall exists, False otherwise
"""
return self.walls.get(direction, True)
def get_neighbors_directions(self) -> Dict[str, tuple]:
"""Get relative positions of neighbors in each direction.
Returns:
Dictionary mapping direction to (row_offset, col_offset)
"""
return {
"north": (-1, 0),
"south": (1, 0),
"east": (0, 1),
"west": (0, -1)
}
def reset(self) -> None:
"""Reset the cell to its initial state."""
self.walls = {"north": True, "south": True, "east": True, "west": True}
self.visited = False
def to_dict(self) -> Dict:
"""Convert cell to dictionary representation.
Returns:
Dictionary with cell data
"""
return {
"row": self.row,
"col": self.col,
"walls": self.walls.copy(),
"visited": self.visited
}
@staticmethod
def from_dict(data: Dict) -> 'Cell':
"""Create a cell from dictionary representation.
Args:
data: Dictionary with cell data
Returns:
New Cell instance
"""
cell = Cell(data["row"], data["col"])
cell.walls = data["walls"].copy()
cell.visited = data["visited"]
return cell
def __repr__(self) -> str:
"""String representation of the cell."""
return f"Cell({self.row}, {self.col})"
def __eq__(self, other) -> bool:
"""Check equality with another cell."""
if not isinstance(other, Cell):
return False
return self.row == other.row and self.col == other.col
def __hash__(self) -> int:
"""Hash function for using cells in sets/dicts."""
return hash((self.row, self.col))

193
src/core/maze.py Normal file
View File

@@ -0,0 +1,193 @@
"""Maze class representing the complete maze structure."""
import json
import random
from typing import Dict, List, Optional, Tuple
from .cell import Cell
class Maze:
"""Represents a complete maze with a grid of cells.
The maze maintains a 2D grid of cells and provides methods for
manipulation, serialization, and access to maze properties.
"""
def __init__(self, rows: int, cols: int, seed: Optional[int] = None):
"""Initialize a maze with the given dimensions.
Args:
rows: Number of rows (must be between 5 and 50)
cols: Number of columns (must be between 5 and 50)
seed: Random seed for reproducible generation (optional)
Raises:
ValueError: If dimensions are out of valid range
"""
if not (5 <= rows <= 50) or not (5 <= cols <= 50):
raise ValueError("Maze dimensions must be between 5 and 50")
self.rows = rows
self.cols = cols
self.seed = seed if seed is not None else random.randint(0, 1000000)
self.grid: List[List[Cell]] = []
self.start: Tuple[int, int] = (0, 0)
self.end: Tuple[int, int] = (rows - 1, cols - 1)
self.generation_time_ms: float = 0.0
self.algorithm_used: str = ""
# Initialize the grid
self._initialize_grid()
def _initialize_grid(self) -> None:
"""Initialize the grid with cells."""
self.grid = []
for row in range(self.rows):
row_cells = []
for col in range(self.cols):
row_cells.append(Cell(row, col))
self.grid.append(row_cells)
def get_cell(self, row: int, col: int) -> Optional[Cell]:
"""Get the cell at the specified position.
Args:
row: Row index
col: Column index
Returns:
Cell at the position, or None if out of bounds
"""
if 0 <= row < self.rows and 0 <= col < self.cols:
return self.grid[row][col]
return None
def get_neighbors(self, cell: Cell) -> List[Tuple[Cell, str]]:
"""Get all valid neighbors of a cell.
Args:
cell: The cell to find neighbors for
Returns:
List of tuples (neighbor_cell, direction)
"""
neighbors = []
directions = cell.get_neighbors_directions()
for direction, (dr, dc) in directions.items():
neighbor_row = cell.row + dr
neighbor_col = cell.col + dc
neighbor = self.get_cell(neighbor_row, neighbor_col)
if neighbor is not None:
neighbors.append((neighbor, direction))
return neighbors
def remove_wall_between(self, cell1: Cell, cell2: Cell) -> None:
"""Remove the wall between two adjacent cells.
Args:
cell1: First cell
cell2: Second cell
"""
row_diff = cell2.row - cell1.row
col_diff = cell2.col - cell1.col
# Determine direction and remove walls
if row_diff == -1: # cell2 is north of cell1
cell1.remove_wall("north")
cell2.remove_wall("south")
elif row_diff == 1: # cell2 is south of cell1
cell1.remove_wall("south")
cell2.remove_wall("north")
elif col_diff == -1: # cell2 is west of cell1
cell1.remove_wall("west")
cell2.remove_wall("east")
elif col_diff == 1: # cell2 is east of cell1
cell1.remove_wall("east")
cell2.remove_wall("west")
def reset_visited(self) -> None:
"""Reset the visited flag for all cells."""
for row in self.grid:
for cell in row:
cell.visited = False
def is_valid_position(self, row: int, col: int) -> bool:
"""Check if a position is within the maze bounds.
Args:
row: Row index
col: Column index
Returns:
True if position is valid, False otherwise
"""
return 0 <= row < self.rows and 0 <= col < self.cols
def to_dict(self) -> Dict:
"""Convert maze to dictionary representation.
Returns:
Dictionary with complete maze data
"""
return {
"rows": self.rows,
"cols": self.cols,
"seed": self.seed,
"start": self.start,
"end": self.end,
"generation_time_ms": self.generation_time_ms,
"algorithm_used": self.algorithm_used,
"grid": [[cell.to_dict() for cell in row] for row in self.grid]
}
def to_json(self) -> str:
"""Convert maze to JSON string.
Returns:
JSON string representation
"""
return json.dumps(self.to_dict(), indent=2)
@staticmethod
def from_dict(data: Dict) -> 'Maze':
"""Create a maze from dictionary representation.
Args:
data: Dictionary with maze data
Returns:
New Maze instance
"""
maze = Maze(data["rows"], data["cols"], data["seed"])
maze.start = tuple(data["start"])
maze.end = tuple(data["end"])
maze.generation_time_ms = data.get("generation_time_ms", 0.0)
maze.algorithm_used = data.get("algorithm_used", "")
# Restore grid
for row_idx, row_data in enumerate(data["grid"]):
for col_idx, cell_data in enumerate(row_data):
maze.grid[row_idx][col_idx] = Cell.from_dict(cell_data)
return maze
@staticmethod
def from_json(json_str: str) -> 'Maze':
"""Create a maze from JSON string.
Args:
json_str: JSON string with maze data
Returns:
New Maze instance
"""
data = json.loads(json_str)
return Maze.from_dict(data)
def __repr__(self) -> str:
"""String representation of the maze."""
return f"Maze({self.rows}x{self.cols}, seed={self.seed}, algorithm={self.algorithm_used})"

View File

@@ -0,0 +1,23 @@
"""Maze generation algorithms."""
from .base import BaseGenerator
from .recursive_backtracking import RecursiveBacktrackingGenerator
from .kruskal import KruskalGenerator
from .prim import PrimGenerator
from .sidewinder import SidewinderGenerator
from .hunt_and_kill import HuntAndKillGenerator
from .eller import EllerGenerator
from .wilson import WilsonGenerator
from .aldous_broder import AldousBroderGenerator
__all__ = [
"BaseGenerator",
"RecursiveBacktrackingGenerator",
"KruskalGenerator",
"PrimGenerator",
"SidewinderGenerator",
"HuntAndKillGenerator",
"EllerGenerator",
"WilsonGenerator",
"AldousBroderGenerator"
]

View File

@@ -0,0 +1,51 @@
"""Aldous-Broder Algorithm for maze generation."""
import random
from ..core.maze import Maze
from .base import BaseGenerator
class AldousBroderGenerator(BaseGenerator):
"""Generates mazes using the Aldous-Broder Algorithm.
This algorithm performs a random walk, carving passages to unvisited cells.
Very slow but generates truly uniform spanning trees. Can be inefficient
for large mazes as it continues random walking even when few cells remain.
Time Complexity: O(rows * cols * log(rows * cols)) expected
Space Complexity: O(1)
"""
def __init__(self):
"""Initialize the Aldous-Broder generator."""
super().__init__("Aldous-Broder Algorithm")
def _generate_maze(self, maze: Maze) -> None:
"""Generate maze using Aldous-Broder algorithm.
Args:
maze: Maze instance to generate
"""
# Start from random cell
current_cell = maze.grid[random.randint(0, maze.rows - 1)][random.randint(0, maze.cols - 1)]
current_cell.visited = True
unvisited_count = maze.rows * maze.cols - 1
# Continue until all cells are visited
while unvisited_count > 0:
# Get all neighbors
neighbors = maze.get_neighbors(current_cell)
if neighbors:
# Choose a random neighbor
next_cell, direction = random.choice(neighbors)
# If neighbor hasn't been visited, carve passage
if not next_cell.visited:
maze.remove_wall_between(current_cell, next_cell)
next_cell.visited = True
unvisited_count -= 1
# Move to neighbor (regardless of whether it was visited)
current_cell = next_cell

68
src/generators/base.py Normal file
View File

@@ -0,0 +1,68 @@
"""Base class for maze generation algorithms."""
import random
import time
from abc import ABC, abstractmethod
from typing import Optional
from ..core.maze import Maze
class BaseGenerator(ABC):
"""Abstract base class for maze generation algorithms.
All maze generation algorithms should inherit from this class
and implement the generate method.
"""
def __init__(self, name: str):
"""Initialize the generator.
Args:
name: Name of the algorithm
"""
self.name = name
@abstractmethod
def _generate_maze(self, maze: Maze) -> None:
"""Generate the maze (to be implemented by subclasses).
Args:
maze: Maze instance to generate
"""
pass
def generate(self, rows: int, cols: int, seed: Optional[int] = None) -> Maze:
"""Generate a maze with the specified dimensions.
Args:
rows: Number of rows
cols: Number of columns
seed: Random seed for reproducibility
Returns:
Generated Maze instance with timing information
"""
# Create maze
maze = Maze(rows, cols, seed)
# Set random seed
random.seed(maze.seed)
# Track generation time
start_time = time.time()
self._generate_maze(maze)
end_time = time.time()
# Store metadata
maze.generation_time_ms = (end_time - start_time) * 1000
maze.algorithm_used = self.name
# Reset visited flags
maze.reset_visited()
return maze
def __repr__(self) -> str:
"""String representation of the generator."""
return f"{self.__class__.__name__}('{self.name}')"

88
src/generators/eller.py Normal file
View File

@@ -0,0 +1,88 @@
"""Eller's Algorithm for maze generation."""
import random
from typing import Dict, List, Set
from ..core.cell import Cell
from ..core.maze import Maze
from .base import BaseGenerator
class EllerGenerator(BaseGenerator):
"""Generates mazes using Eller's Algorithm.
This algorithm generates mazes one row at a time and can create
infinitely long mazes. Each row maintains sets of connected cells.
Time Complexity: O(rows * cols)
Space Complexity: O(cols)
"""
def __init__(self):
"""Initialize the Eller generator."""
super().__init__("Eller's Algorithm")
def _generate_maze(self, maze: Maze) -> None:
"""Generate maze using Eller's algorithm.
Args:
maze: Maze instance to generate
"""
# Track which set each cell belongs to
sets: Dict[Cell, int] = {}
next_set_id = 0
for row_idx in range(maze.rows):
row = maze.grid[row_idx]
# Assign sets to new cells
for cell in row:
if cell not in sets:
sets[cell] = next_set_id
next_set_id += 1
# Randomly join adjacent cells in different sets
for col_idx in range(maze.cols - 1):
cell = row[col_idx]
east_cell = row[col_idx + 1]
# Join cells if in different sets and random choice
if sets[cell] != sets[east_cell] and random.choice([True, False]):
self._merge_sets(sets, sets[cell], sets[east_cell])
maze.remove_wall_between(cell, east_cell)
# Create vertical connections (except for last row)
if row_idx < maze.rows - 1:
# Group cells by set
set_groups: Dict[int, List[Cell]] = {}
for cell in row:
cell_set = sets[cell]
if cell_set not in set_groups:
set_groups[cell_set] = []
set_groups[cell_set].append(cell)
# For each set, create at least one vertical connection
for cell_set, cells in set_groups.items():
# Shuffle and pick at least one cell to connect down
random.shuffle(cells)
num_connections = random.randint(1, len(cells))
for i in range(num_connections):
cell = cells[i]
south_cell = maze.get_cell(cell.row + 1, cell.col)
if south_cell:
maze.remove_wall_between(cell, south_cell)
# South cell inherits the set
sets[south_cell] = cell_set
def _merge_sets(self, sets: Dict[Cell, int], set1: int, set2: int) -> None:
"""Merge two sets by replacing all set2 with set1.
Args:
sets: Dictionary mapping cells to set IDs
set1: First set ID
set2: Second set ID to merge into set1
"""
for cell, cell_set in sets.items():
if cell_set == set2:
sets[cell] = set1

View File

@@ -0,0 +1,104 @@
"""Hunt-and-Kill Algorithm for maze generation."""
import random
from typing import List, Optional
from ..core.cell import Cell
from ..core.maze import Maze
from .base import BaseGenerator
class HuntAndKillGenerator(BaseGenerator):
"""Generates mazes using the Hunt-and-Kill Algorithm.
This algorithm performs random walks (kill phase) and when stuck,
scans for unvisited cells adjacent to visited ones (hunt phase).
Creates mazes with fewer dead ends than recursive backtracking.
Time Complexity: O((rows * cols)^2) worst case
Space Complexity: O(1)
"""
def __init__(self):
"""Initialize the Hunt-and-Kill generator."""
super().__init__("Hunt-and-Kill Algorithm")
def _generate_maze(self, maze: Maze) -> None:
"""Generate maze using Hunt-and-Kill algorithm.
Args:
maze: Maze instance to generate
"""
# Start from random cell
current_cell = maze.grid[random.randint(0, maze.rows - 1)][random.randint(0, maze.cols - 1)]
current_cell.visited = True
while True:
# Kill phase: random walk from current cell
unvisited_neighbors = self._get_unvisited_neighbors(maze, current_cell)
if unvisited_neighbors:
# Choose random unvisited neighbor
next_cell, direction = random.choice(unvisited_neighbors)
# Remove wall and move to next cell
maze.remove_wall_between(current_cell, next_cell)
next_cell.visited = True
current_cell = next_cell
else:
# Hunt phase: scan for unvisited cell with visited neighbor
next_cell = self._hunt(maze)
if next_cell is None:
# All cells visited, done
break
current_cell = next_cell
def _get_unvisited_neighbors(self, maze: Maze, cell: Cell) -> List[tuple]:
"""Get all unvisited neighbors of a cell.
Args:
maze: The maze
cell: The cell to find neighbors for
Returns:
List of (neighbor_cell, direction) tuples
"""
neighbors = []
all_neighbors = maze.get_neighbors(cell)
for neighbor, direction in all_neighbors:
if not neighbor.visited:
neighbors.append((neighbor, direction))
return neighbors
def _hunt(self, maze: Maze) -> Optional[Cell]:
"""Hunt for an unvisited cell adjacent to a visited cell.
Args:
maze: The maze
Returns:
Unvisited cell with visited neighbor, or None if all visited
"""
for row in maze.grid:
for cell in row:
if not cell.visited:
# Check if this cell has visited neighbors
visited_neighbors = []
all_neighbors = maze.get_neighbors(cell)
for neighbor, direction in all_neighbors:
if neighbor.visited:
visited_neighbors.append((neighbor, direction))
if visited_neighbors:
# Connect to random visited neighbor
neighbor, direction = random.choice(visited_neighbors)
maze.remove_wall_between(cell, neighbor)
cell.visited = True
return cell
return None

89
src/generators/kruskal.py Normal file
View File

@@ -0,0 +1,89 @@
"""Kruskal's Algorithm for maze generation."""
import random
from typing import Dict, List, Tuple
from ..core.cell import Cell
from ..core.maze import Maze
from .base import BaseGenerator
class KruskalGenerator(BaseGenerator):
"""Generates mazes using Kruskal's Algorithm.
This algorithm treats the maze as a graph and uses a union-find structure
to create a minimum spanning tree. Creates mazes with many short paths.
Time Complexity: O(E log E) where E is number of edges
Space Complexity: O(V) where V is number of vertices
"""
def __init__(self):
"""Initialize the Kruskal generator."""
super().__init__("Kruskal's Algorithm")
def _generate_maze(self, maze: Maze) -> None:
"""Generate maze using Kruskal's algorithm.
Args:
maze: Maze instance to generate
"""
# Initialize union-find structure
parent: Dict[Cell, Cell] = {}
for row in maze.grid:
for cell in row:
parent[cell] = cell
# Create list of all possible walls (edges)
walls = self._get_all_walls(maze)
random.shuffle(walls)
# Process each wall
for cell1, cell2 in walls:
# Find roots of both cells
root1 = self._find(parent, cell1)
root2 = self._find(parent, cell2)
# If cells are in different sets, remove wall and union
if root1 != root2:
maze.remove_wall_between(cell1, cell2)
parent[root2] = root1
def _get_all_walls(self, maze: Maze) -> List[Tuple[Cell, Cell]]:
"""Get all possible walls between cells.
Args:
maze: The maze
Returns:
List of (cell1, cell2) tuples representing walls
"""
walls = []
for row in maze.grid:
for cell in row:
# Add wall to the south
south_cell = maze.get_cell(cell.row + 1, cell.col)
if south_cell:
walls.append((cell, south_cell))
# Add wall to the east
east_cell = maze.get_cell(cell.row, cell.col + 1)
if east_cell:
walls.append((cell, east_cell))
return walls
def _find(self, parent: Dict[Cell, Cell], cell: Cell) -> Cell:
"""Find the root of a cell's set with path compression.
Args:
parent: Union-find parent dictionary
cell: Cell to find root for
Returns:
Root cell of the set
"""
if parent[cell] != cell:
parent[cell] = self._find(parent, parent[cell])
return parent[cell]

75
src/generators/prim.py Normal file
View File

@@ -0,0 +1,75 @@
"""Prim's Algorithm for maze generation."""
import random
from typing import List, Set, Tuple
from ..core.cell import Cell
from ..core.maze import Maze
from .base import BaseGenerator
class PrimGenerator(BaseGenerator):
"""Generates mazes using Prim's Algorithm.
This algorithm starts with a cell and grows the maze by adding the
lowest-cost adjacent cells. Creates mazes with many short dead ends.
Time Complexity: O(E log V) where E is edges and V is vertices
Space Complexity: O(V)
"""
def __init__(self):
"""Initialize the Prim generator."""
super().__init__("Prim's Algorithm")
def _generate_maze(self, maze: Maze) -> None:
"""Generate maze using Prim's algorithm.
Args:
maze: Maze instance to generate
"""
# Start with a random cell
start_cell = maze.grid[random.randint(0, maze.rows - 1)][random.randint(0, maze.cols - 1)]
start_cell.visited = True
# List of frontier walls (cell pairs)
frontier: List[Tuple[Cell, Cell]] = []
# Add all walls of start cell to frontier
self._add_frontier_walls(maze, start_cell, frontier)
# Process frontier until empty
while frontier:
# Pick a random wall from frontier
wall_idx = random.randint(0, len(frontier) - 1)
cell1, cell2 = frontier.pop(wall_idx)
# If only one of the cells is visited
if cell1.visited != cell2.visited:
# Remove the wall
maze.remove_wall_between(cell1, cell2)
# Mark unvisited cell as visited
unvisited_cell = cell2 if not cell2.visited else cell1
unvisited_cell.visited = True
# Add new frontier walls
self._add_frontier_walls(maze, unvisited_cell, frontier)
def _add_frontier_walls(self, maze: Maze, cell: Cell, frontier: List[Tuple[Cell, Cell]]) -> None:
"""Add walls of a cell to the frontier.
Args:
maze: The maze
cell: Cell whose walls to add
frontier: Frontier list to add to
"""
neighbors = maze.get_neighbors(cell)
for neighbor, direction in neighbors:
if not neighbor.visited:
# Add wall if not already in frontier
wall = (cell, neighbor)
reverse_wall = (neighbor, cell)
if wall not in frontier and reverse_wall not in frontier:
frontier.append(wall)

View File

@@ -0,0 +1,73 @@
"""Recursive Backtracking maze generation algorithm."""
import random
from typing import List
from ..core.cell import Cell
from ..core.maze import Maze
from .base import BaseGenerator
class RecursiveBacktrackingGenerator(BaseGenerator):
"""Generates mazes using the Recursive Backtracking algorithm.
This algorithm uses a depth-first search approach with backtracking.
It creates mazes with a high "river" characteristic - long, winding paths.
Time Complexity: O(rows * cols)
Space Complexity: O(rows * cols) for the stack
"""
def __init__(self):
"""Initialize the Recursive Backtracking generator."""
super().__init__("Recursive Backtracking")
def _generate_maze(self, maze: Maze) -> None:
"""Generate maze using recursive backtracking.
Args:
maze: Maze instance to generate
"""
# Start from a random cell
start_cell = maze.grid[0][0]
stack = [start_cell]
start_cell.visited = True
while stack:
current_cell = stack[-1]
# Get unvisited neighbors
unvisited_neighbors = self._get_unvisited_neighbors(maze, current_cell)
if unvisited_neighbors:
# Choose a random unvisited neighbor
next_cell, direction = random.choice(unvisited_neighbors)
# Remove wall between current and next cell
maze.remove_wall_between(current_cell, next_cell)
# Mark as visited and add to stack
next_cell.visited = True
stack.append(next_cell)
else:
# Backtrack
stack.pop()
def _get_unvisited_neighbors(self, maze: Maze, cell: Cell) -> List[tuple]:
"""Get all unvisited neighbors of a cell.
Args:
maze: The maze
cell: The cell to find neighbors for
Returns:
List of (neighbor_cell, direction) tuples
"""
neighbors = []
all_neighbors = maze.get_neighbors(cell)
for neighbor, direction in all_neighbors:
if not neighbor.visited:
neighbors.append((neighbor, direction))
return neighbors

View File

@@ -0,0 +1,60 @@
"""Sidewinder Algorithm for maze generation."""
import random
from typing import List
from ..core.cell import Cell
from ..core.maze import Maze
from .base import BaseGenerator
class SidewinderGenerator(BaseGenerator):
"""Generates mazes using the Sidewinder Algorithm.
This algorithm works row by row, creating horizontal runs and randomly
carving north. Creates mazes with a bias toward horizontal passages.
Time Complexity: O(rows * cols)
Space Complexity: O(cols) for the run
"""
def __init__(self):
"""Initialize the Sidewinder generator."""
super().__init__("Sidewinder Algorithm")
def _generate_maze(self, maze: Maze) -> None:
"""Generate maze using Sidewinder algorithm.
Args:
maze: Maze instance to generate
"""
# Process each row
for row_idx in range(maze.rows):
run: List[Cell] = []
for col_idx in range(maze.cols):
cell = maze.grid[row_idx][col_idx]
run.append(cell)
# Decide whether to carve east or north
at_eastern_boundary = (col_idx == maze.cols - 1)
at_northern_boundary = (row_idx == 0)
should_close_run = at_eastern_boundary or (
not at_northern_boundary and random.choice([True, False])
)
if should_close_run:
# Pick a random cell from run and carve north
if not at_northern_boundary:
random_cell = random.choice(run)
north_cell = maze.get_cell(random_cell.row - 1, random_cell.col)
if north_cell:
maze.remove_wall_between(random_cell, north_cell)
# Clear the run
run = []
else:
# Carve east
east_cell = maze.get_cell(cell.row, cell.col + 1)
if east_cell:
maze.remove_wall_between(cell, east_cell)

93
src/generators/wilson.py Normal file
View File

@@ -0,0 +1,93 @@
"""Wilson's Algorithm for maze generation."""
import random
from typing import Dict, List, Optional
from ..core.cell import Cell
from ..core.maze import Maze
from .base import BaseGenerator
class WilsonGenerator(BaseGenerator):
"""Generates mazes using Wilson's Algorithm.
This algorithm uses loop-erased random walks to generate unbiased mazes.
Starts slowly but speeds up as more cells are added to the maze.
Creates truly uniform spanning trees.
Time Complexity: O(rows * cols) expected
Space Complexity: O(rows * cols)
"""
def __init__(self):
"""Initialize the Wilson generator."""
super().__init__("Wilson's Algorithm")
def _generate_maze(self, maze: Maze) -> None:
"""Generate maze using Wilson's algorithm.
Args:
maze: Maze instance to generate
"""
# Start with one random cell in the maze
all_cells = [cell for row in maze.grid for cell in row]
first_cell = random.choice(all_cells)
first_cell.visited = True
in_maze = {first_cell}
# Process remaining cells
unvisited = [cell for cell in all_cells if cell not in in_maze]
while unvisited:
# Start random walk from random unvisited cell
start_cell = random.choice(unvisited)
path = self._random_walk(maze, start_cell, in_maze)
# Add path to maze
for i in range(len(path) - 1):
current = path[i]
next_cell = path[i + 1]
maze.remove_wall_between(current, next_cell)
current.visited = True
in_maze.add(current)
# Update unvisited list
unvisited = [cell for cell in all_cells if cell not in in_maze]
def _random_walk(self, maze: Maze, start: Cell, in_maze: set) -> List[Cell]:
"""Perform a loop-erased random walk.
Args:
maze: The maze
start: Starting cell
in_maze: Set of cells already in the maze
Returns:
List of cells forming the loop-erased path
"""
path: List[Cell] = [start]
current = start
# Walk until we hit a cell in the maze
while current not in in_maze:
# Get all neighbors
neighbors = maze.get_neighbors(current)
if not neighbors:
break
# Choose random neighbor
next_cell, direction = random.choice(neighbors)
# If we've been to this cell before in this walk, erase the loop
if next_cell in path:
# Erase loop by truncating path
loop_start = path.index(next_cell)
path = path[:loop_start + 1]
current = next_cell
else:
# Add to path
path.append(next_cell)
current = next_cell
return path

7
src/solvers/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
"""Maze solving algorithms."""
from .base import BaseSolver
from .dfs import DFSSolver
from .bfs import BFSSolver
__all__ = ["BaseSolver", "DFSSolver", "BFSSolver"]

139
src/solvers/base.py Normal file
View File

@@ -0,0 +1,139 @@
"""Base class for maze solving algorithms."""
import time
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Tuple
from ..core.cell import Cell
from ..core.maze import Maze
class BaseSolver(ABC):
"""Abstract base class for maze solving algorithms.
All maze solving algorithms should inherit from this class
and implement the solve method.
"""
def __init__(self, name: str):
"""Initialize the solver.
Args:
name: Name of the algorithm
"""
self.name = name
@abstractmethod
def _solve_maze(self, maze: Maze, start: Cell, end: Cell) -> Tuple[Optional[List[Cell]], List[Cell]]:
"""Solve the maze (to be implemented by subclasses).
Args:
maze: Maze instance to solve
start: Starting cell
end: Ending cell
Returns:
Tuple of (solution_path, visited_cells_in_order)
"""
pass
def solve(self, maze: Maze) -> Dict:
"""Solve the maze and return solution with metadata.
Args:
maze: Maze instance to solve
Returns:
Dictionary with solution path, visited cells, and timing
"""
# Reset visited flags
maze.reset_visited()
# Get start and end cells
start = maze.get_cell(maze.start[0], maze.start[1])
end = maze.get_cell(maze.end[0], maze.end[1])
if not start or not end:
return {
"success": False,
"path": None,
"visited": [],
"time_ms": 0,
"algorithm": self.name,
"path_length": 0
}
# Track solving time
start_time = time.time()
path, visited = self._solve_maze(maze, start, end)
end_time = time.time()
# Reset visited flags again
maze.reset_visited()
return {
"success": path is not None,
"path": [(cell.row, cell.col) for cell in path] if path else None,
"visited": [(cell.row, cell.col) for cell in visited],
"time_ms": (end_time - start_time) * 1000,
"algorithm": self.name,
"path_length": len(path) if path else 0
}
def can_move(self, maze: Maze, from_cell: Cell, to_cell: Cell) -> bool:
"""Check if movement between two cells is possible.
Args:
maze: The maze
from_cell: Starting cell
to_cell: Target cell
Returns:
True if movement is possible, False otherwise
"""
# Calculate direction
row_diff = to_cell.row - from_cell.row
col_diff = to_cell.col - from_cell.col
# Determine direction
if row_diff == -1:
direction = "north"
elif row_diff == 1:
direction = "south"
elif col_diff == -1:
direction = "west"
elif col_diff == 1:
direction = "east"
else:
return False
# Check if wall exists
return not from_cell.has_wall(direction)
def reconstruct_path(self, came_from: Dict[Cell, Cell], start: Cell, end: Cell) -> List[Cell]:
"""Reconstruct path from start to end using came_from dict.
Args:
came_from: Dictionary mapping cell to its predecessor
start: Starting cell
end: Ending cell
Returns:
List of cells forming the path
"""
path = []
current = end
while current != start:
path.append(current)
current = came_from.get(current)
if current is None:
return []
path.append(start)
path.reverse()
return path
def __repr__(self) -> str:
"""String representation of the solver."""
return f"{self.__class__.__name__}('{self.name}')"

62
src/solvers/bfs.py Normal file
View File

@@ -0,0 +1,62 @@
"""Breadth-First Search maze solver."""
from collections import deque
from typing import Dict, List, Optional, Tuple
from ..core.cell import Cell
from ..core.maze import Maze
from .base import BaseSolver
class BFSSolver(BaseSolver):
"""Solves mazes using Breadth-First Search.
BFS explores all neighbors at the current depth before moving deeper.
Guarantees the shortest path in unweighted graphs.
Time Complexity: O(V + E) where V is vertices and E is edges
Space Complexity: O(V) for the queue
"""
def __init__(self):
"""Initialize the BFS solver."""
super().__init__("Breadth-First Search (BFS)")
def _solve_maze(self, maze: Maze, start: Cell, end: Cell) -> Tuple[Optional[List[Cell]], List[Cell]]:
"""Solve maze using BFS.
Args:
maze: Maze instance to solve
start: Starting cell
end: Ending cell
Returns:
Tuple of (solution_path, visited_cells_in_order)
"""
queue = deque([start])
came_from: Dict[Cell, Cell] = {}
visited_order = []
start.visited = True
visited_order.append(start)
while queue:
current = queue.popleft()
# Check if we reached the end
if current == end:
path = self.reconstruct_path(came_from, start, end)
return path, visited_order
# Explore neighbors
neighbors = maze.get_neighbors(current)
for neighbor, direction in neighbors:
if not neighbor.visited and self.can_move(maze, current, neighbor):
neighbor.visited = True
visited_order.append(neighbor)
came_from[neighbor] = current
queue.append(neighbor)
# No path found
return None, visited_order

61
src/solvers/dfs.py Normal file
View File

@@ -0,0 +1,61 @@
"""Depth-First Search maze solver."""
from typing import Dict, List, Optional, Tuple
from ..core.cell import Cell
from ..core.maze import Maze
from .base import BaseSolver
class DFSSolver(BaseSolver):
"""Solves mazes using Depth-First Search.
DFS explores as far as possible along each branch before backtracking.
Does not guarantee shortest path but is memory efficient.
Time Complexity: O(V + E) where V is vertices and E is edges
Space Complexity: O(V) for the stack
"""
def __init__(self):
"""Initialize the DFS solver."""
super().__init__("Depth-First Search (DFS)")
def _solve_maze(self, maze: Maze, start: Cell, end: Cell) -> Tuple[Optional[List[Cell]], List[Cell]]:
"""Solve maze using DFS.
Args:
maze: Maze instance to solve
start: Starting cell
end: Ending cell
Returns:
Tuple of (solution_path, visited_cells_in_order)
"""
stack = [start]
came_from: Dict[Cell, Cell] = {}
visited_order = []
start.visited = True
visited_order.append(start)
while stack:
current = stack.pop()
# Check if we reached the end
if current == end:
path = self.reconstruct_path(came_from, start, end)
return path, visited_order
# Explore neighbors
neighbors = maze.get_neighbors(current)
for neighbor, direction in neighbors:
if not neighbor.visited and self.can_move(maze, current, neighbor):
neighbor.visited = True
visited_order.append(neighbor)
came_from[neighbor] = current
stack.append(neighbor)
# No path found
return None, visited_order

5
src/storage/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
"""File storage and persistence utilities."""
from .file_handler import FileHandler
__all__ = ["FileHandler"]

169
src/storage/file_handler.py Normal file
View File

@@ -0,0 +1,169 @@
"""File handling for saving and loading mazes."""
import json
import os
from pathlib import Path
from typing import Optional
from ..core.maze import Maze
class FileHandler:
"""Handles saving and loading mazes to/from files."""
DEFAULT_SAVE_DIR = "saved_mazes"
@staticmethod
def ensure_save_directory(directory: Optional[str] = None) -> Path:
"""Ensure the save directory exists.
Args:
directory: Directory path (uses default if None)
Returns:
Path object for the directory
"""
save_dir = Path(directory) if directory else Path(FileHandler.DEFAULT_SAVE_DIR)
save_dir.mkdir(parents=True, exist_ok=True)
return save_dir
@staticmethod
def save_maze(maze: Maze, filename: str, directory: Optional[str] = None) -> str:
"""Save a maze to a JSON file.
Args:
maze: Maze to save
filename: Name of the file (without extension)
directory: Directory to save to (uses default if None)
Returns:
Full path to the saved file
Raises:
IOError: If file cannot be written
"""
save_dir = FileHandler.ensure_save_directory(directory)
# Ensure .json extension
if not filename.endswith('.json'):
filename += '.json'
file_path = save_dir / filename
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(maze.to_json())
return str(file_path)
except Exception as e:
raise IOError(f"Failed to save maze to {file_path}: {e}")
@staticmethod
def load_maze(filename: str, directory: Optional[str] = None) -> Maze:
"""Load a maze from a JSON file.
Args:
filename: Name of the file to load
directory: Directory to load from (uses default if None)
Returns:
Loaded Maze instance
Raises:
FileNotFoundError: If file doesn't exist
ValueError: If file contains invalid maze data
IOError: If file cannot be read
"""
save_dir = Path(directory) if directory else Path(FileHandler.DEFAULT_SAVE_DIR)
# Ensure .json extension
if not filename.endswith('.json'):
filename += '.json'
file_path = save_dir / filename
if not file_path.exists():
raise FileNotFoundError(f"Maze file not found: {file_path}")
try:
with open(file_path, 'r', encoding='utf-8') as f:
json_str = f.read()
# Validate JSON structure
data = json.loads(json_str)
FileHandler._validate_maze_data(data)
return Maze.from_json(json_str)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in maze file: {e}")
except Exception as e:
raise IOError(f"Failed to load maze from {file_path}: {e}")
@staticmethod
def _validate_maze_data(data: dict) -> None:
"""Validate maze data structure.
Args:
data: Dictionary with maze data
Raises:
ValueError: If data is invalid
"""
required_fields = ["rows", "cols", "seed", "grid"]
for field in required_fields:
if field not in data:
raise ValueError(f"Missing required field: {field}")
# Validate dimensions
rows = data["rows"]
cols = data["cols"]
if not (5 <= rows <= 50) or not (5 <= cols <= 50):
raise ValueError("Invalid maze dimensions")
# Validate grid structure
grid = data["grid"]
if len(grid) != rows:
raise ValueError("Grid row count doesn't match maze rows")
for row_idx, row in enumerate(grid):
if len(row) != cols:
raise ValueError(f"Grid column count doesn't match at row {row_idx}")
@staticmethod
def list_saved_mazes(directory: Optional[str] = None) -> list:
"""List all saved maze files.
Args:
directory: Directory to list from (uses default if None)
Returns:
List of maze filenames
"""
save_dir = Path(directory) if directory else Path(FileHandler.DEFAULT_SAVE_DIR)
if not save_dir.exists():
return []
return [f.name for f in save_dir.glob("*.json")]
@staticmethod
def delete_maze(filename: str, directory: Optional[str] = None) -> bool:
"""Delete a saved maze file.
Args:
filename: Name of the file to delete
directory: Directory containing the file (uses default if None)
Returns:
True if deleted successfully, False if file didn't exist
"""
save_dir = Path(directory) if directory else Path(FileHandler.DEFAULT_SAVE_DIR)
if not filename.endswith('.json'):
filename += '.json'
file_path = save_dir / filename
if file_path.exists():
file_path.unlink()
return True
return False

View File

@@ -0,0 +1,6 @@
"""Visualization and rendering utilities."""
from .image_renderer import ImageRenderer
from .web_renderer import WebRenderer
__all__ = ["ImageRenderer", "WebRenderer"]

View File

@@ -0,0 +1,145 @@
"""Image rendering for mazes using Pillow."""
import os
from pathlib import Path
from typing import List, Optional, Tuple
from PIL import Image, ImageDraw
from ..core.maze import Maze
class ImageRenderer:
"""Renders mazes as PNG/JPG images."""
DEFAULT_OUTPUT_DIR = "output_images"
DEFAULT_CELL_SIZE = 20
DEFAULT_WALL_THICKNESS = 2
def __init__(self, cell_size: int = DEFAULT_CELL_SIZE, wall_thickness: int = DEFAULT_WALL_THICKNESS):
"""Initialize the image renderer.
Args:
cell_size: Size of each cell in pixels
wall_thickness: Thickness of walls in pixels
"""
self.cell_size = cell_size
self.wall_thickness = wall_thickness
def render(
self,
maze: Maze,
filename: str,
directory: Optional[str] = None,
solution_path: Optional[List[Tuple[int, int]]] = None,
visited_cells: Optional[List[Tuple[int, int]]] = None
) -> str:
"""Render maze as an image.
Args:
maze: Maze to render
filename: Output filename (without extension)
directory: Output directory (uses default if None)
solution_path: Optional list of (row, col) tuples for solution
visited_cells: Optional list of (row, col) tuples for visited cells
Returns:
Path to the saved image file
"""
# Ensure output directory exists
output_dir = Path(directory) if directory else Path(self.DEFAULT_OUTPUT_DIR)
output_dir.mkdir(parents=True, exist_ok=True)
# Ensure .png extension
if not filename.endswith('.png'):
filename += '.png'
# Calculate image dimensions
width = maze.cols * self.cell_size + self.wall_thickness
height = maze.rows * self.cell_size + self.wall_thickness
# Create image
img = Image.new('RGB', (width, height), color='white')
draw = ImageDraw.Draw(img)
# Draw visited cells (light gray)
if visited_cells:
for row, col in visited_cells:
self._draw_cell_background(draw, row, col, '#E0E0E0')
# Draw solution path (light green)
if solution_path:
for row, col in solution_path:
self._draw_cell_background(draw, row, col, '#90EE90')
# Draw start and end markers
self._draw_cell_background(draw, maze.start[0], maze.start[1], '#FFD700') # Gold
self._draw_cell_background(draw, maze.end[0], maze.end[1], '#FF69B4') # Hot pink
# Draw walls
for row in maze.grid:
for cell in row:
self._draw_cell_walls(draw, cell)
# Save image
file_path = output_dir / filename
img.save(file_path)
return str(file_path)
def _draw_cell_background(self, draw: ImageDraw, row: int, col: int, color: str) -> None:
"""Draw a colored background for a cell.
Args:
draw: ImageDraw object
row: Row index
col: Column index
color: Background color
"""
x = col * self.cell_size + self.wall_thickness
y = row * self.cell_size + self.wall_thickness
draw.rectangle(
[x, y, x + self.cell_size - 1, y + self.cell_size - 1],
fill=color
)
def _draw_cell_walls(self, draw: ImageDraw, cell) -> None:
"""Draw walls for a cell.
Args:
draw: ImageDraw object
cell: Cell to draw walls for
"""
x = cell.col * self.cell_size + self.wall_thickness // 2
y = cell.row * self.cell_size + self.wall_thickness // 2
# Draw north wall
if cell.has_wall('north'):
draw.line(
[x, y, x + self.cell_size, y],
fill='black',
width=self.wall_thickness
)
# Draw south wall
if cell.has_wall('south'):
draw.line(
[x, y + self.cell_size, x + self.cell_size, y + self.cell_size],
fill='black',
width=self.wall_thickness
)
# Draw west wall
if cell.has_wall('west'):
draw.line(
[x, y, x, y + self.cell_size],
fill='black',
width=self.wall_thickness
)
# Draw east wall
if cell.has_wall('east'):
draw.line(
[x + self.cell_size, y, x + self.cell_size, y + self.cell_size],
fill='black',
width=self.wall_thickness
)

View File

@@ -0,0 +1,74 @@
"""Web rendering for mazes (JSON format for Canvas)."""
from typing import Dict, List, Optional, Tuple
from ..core.maze import Maze
class WebRenderer:
"""Renders mazes as JSON data for web canvas visualization."""
@staticmethod
def to_json_format(
maze: Maze,
solution_path: Optional[List[Tuple[int, int]]] = None,
visited_cells: Optional[List[Tuple[int, int]]] = None
) -> Dict:
"""Convert maze to JSON format for web rendering.
Args:
maze: Maze to render
solution_path: Optional list of (row, col) tuples for solution
visited_cells: Optional list of (row, col) tuples for visited cells
Returns:
Dictionary with maze data for web rendering
"""
# Build walls array
walls = []
for row in maze.grid:
row_walls = []
for cell in row:
cell_walls = {
'north': cell.has_wall('north'),
'south': cell.has_wall('south'),
'east': cell.has_wall('east'),
'west': cell.has_wall('west')
}
row_walls.append(cell_walls)
walls.append(row_walls)
# Convert paths to sets for quick lookup
solution_set = set(solution_path) if solution_path else set()
visited_set = set(visited_cells) if visited_cells else set()
# Build cell states
cell_states = []
for row_idx in range(maze.rows):
row_states = []
for col_idx in range(maze.cols):
pos = (row_idx, col_idx)
state = 'normal'
if pos == maze.start:
state = 'start'
elif pos == maze.end:
state = 'end'
elif pos in solution_set:
state = 'solution'
elif pos in visited_set:
state = 'visited'
row_states.append(state)
cell_states.append(row_states)
return {
'rows': maze.rows,
'cols': maze.cols,
'walls': walls,
'cellStates': cell_states,
'start': list(maze.start),
'end': list(maze.end),
'algorithm': maze.algorithm_used,
'generationTime': maze.generation_time_ms
}