Initial commit
This commit is contained in:
3
src/__init__.py
Normal file
3
src/__init__.py
Normal 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
6
src/analysis/__init__.py
Normal 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
148
src/analysis/analyzer.py
Normal 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
172
src/analysis/benchmark.py
Normal 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
6
src/core/__init__.py
Normal 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
103
src/core/cell.py
Normal 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
193
src/core/maze.py
Normal 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})"
|
||||
23
src/generators/__init__.py
Normal file
23
src/generators/__init__.py
Normal 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"
|
||||
]
|
||||
51
src/generators/aldous_broder.py
Normal file
51
src/generators/aldous_broder.py
Normal 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
68
src/generators/base.py
Normal 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
88
src/generators/eller.py
Normal 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
|
||||
104
src/generators/hunt_and_kill.py
Normal file
104
src/generators/hunt_and_kill.py
Normal 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
89
src/generators/kruskal.py
Normal 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
75
src/generators/prim.py
Normal 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)
|
||||
73
src/generators/recursive_backtracking.py
Normal file
73
src/generators/recursive_backtracking.py
Normal 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
|
||||
60
src/generators/sidewinder.py
Normal file
60
src/generators/sidewinder.py
Normal 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
93
src/generators/wilson.py
Normal 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
7
src/solvers/__init__.py
Normal 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
139
src/solvers/base.py
Normal 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
62
src/solvers/bfs.py
Normal 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
61
src/solvers/dfs.py
Normal 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
5
src/storage/__init__.py
Normal 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
169
src/storage/file_handler.py
Normal 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
|
||||
6
src/visualization/__init__.py
Normal file
6
src/visualization/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Visualization and rendering utilities."""
|
||||
|
||||
from .image_renderer import ImageRenderer
|
||||
from .web_renderer import WebRenderer
|
||||
|
||||
__all__ = ["ImageRenderer", "WebRenderer"]
|
||||
145
src/visualization/image_renderer.py
Normal file
145
src/visualization/image_renderer.py
Normal 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
|
||||
)
|
||||
74
src/visualization/web_renderer.py
Normal file
74
src/visualization/web_renderer.py
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user