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

View File

@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(python:*)"
],
"deny": [],
"ask": []
}
}

25
.dockerignore Normal file
View File

@@ -0,0 +1,25 @@
__pycache__
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg
*.egg-info
dist
build
.env
.venv
venv/
ENV/
.pytest_cache
.coverage
htmlcov/
.git
.gitignore
*.md
Dockerfile
docker-compose.yml
.dockerignore
.vscode
.idea

141
.gitignore vendored Normal file
View File

@@ -0,0 +1,141 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
Pipfile.lock
# PEP 582
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Project specific
saved_mazes/
output_images/
*.png
*.jpg
*.jpeg
# OS
.DS_Store
Thumbs.db

395
PLAN.md Normal file
View File

@@ -0,0 +1,395 @@
# Maze Generator - Project Plan
## Project Overview
A comprehensive Python-based maze generation and solving application featuring multiple algorithms, interactive web interface, and advanced analysis tools with containerized deployment.
## Technology Stack
- **Backend:** Python 3.11+
- **Web Framework:** Flask or FastAPI
- **Frontend:** HTML5, CSS3, JavaScript (possibly React or Vue.js for interactivity)
- **Testing:** pytest, pytest-cov for coverage
- **Containerization:** Docker, Docker Compose
- **Visualization:** Pillow (PIL) for image generation, Canvas API for web rendering
- **Package Management:** pip, requirements.txt
## Project Structure
```
mazer/
├── src/
│ ├── __init__.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── maze.py # Core Maze class
│ │ └── cell.py # Cell representation
│ ├── generators/
│ │ ├── __init__.py
│ │ ├── base.py # Base generator interface
│ │ ├── recursive_backtracking.py
│ │ ├── kruskal.py
│ │ ├── prim.py
│ │ ├── sidewinder.py
│ │ ├── hunt_and_kill.py
│ │ ├── eller.py
│ │ ├── wilson.py
│ │ └── aldous_broder.py
│ ├── solvers/
│ │ ├── __init__.py
│ │ ├── base.py # Base solver interface
│ │ ├── dfs.py # Depth-First Search
│ │ └── bfs.py # Breadth-First Search
│ ├── visualization/
│ │ ├── __init__.py
│ │ ├── image_renderer.py # PNG/JPG generation
│ │ └── web_renderer.py # JSON format for web canvas
│ ├── analysis/
│ │ ├── __init__.py
│ │ ├── analyzer.py # Maze metrics and statistics
│ │ └── benchmark.py # Algorithm performance testing
│ └── storage/
│ ├── __init__.py
│ └── file_handler.py # Save/Load maze data
├── api/
│ ├── __init__.py
│ ├── app.py # Main Flask/FastAPI application
│ └── routes/
│ ├── __init__.py
│ ├── generate.py # Generation endpoints
│ ├── solve.py # Solving endpoints
│ └── analyze.py # Analysis endpoints
├── web/
│ ├── static/
│ │ ├── css/
│ │ │ └── styles.css
│ │ ├── js/
│ │ │ ├── app.js
│ │ │ ├── visualizer.js
│ │ │ └── controls.js
│ │ └── images/
│ └── templates/
│ └── index.html
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Pytest fixtures
│ ├── unit/
│ │ ├── test_generators.py
│ │ ├── test_solvers.py
│ │ ├── test_maze.py
│ │ └── test_analysis.py
│ ├── integration/
│ │ ├── test_api.py
│ │ └── test_workflow.py
│ └── performance/
│ └── test_benchmarks.py
├── docker/
│ ├── Dockerfile
│ └── docker-compose.yml
├── requirements.txt
├── requirements-dev.txt
├── pytest.ini
├── .dockerignore
├── .gitignore
├── README.md
└── PLAN.md
```
## Phase 1: Core Foundation (Days 1-3)
### 1.1 Project Setup
- [x] Initialize project structure
- [x] Create virtual environment setup
- [x] Configure pytest with coverage
- [x] Set up .gitignore for Python projects
- [x] Create requirements.txt and requirements-dev.txt
### 1.2 Core Data Structures
- [x] Implement `Cell` class (walls, visited status, coordinates)
- [x] Implement `Maze` class (grid, dimensions, seed management)
- [x] Add maze serialization/deserialization (JSON format)
- [x] Write unit tests for core classes (>90% coverage)
### 1.3 File Storage System
- [x] Implement save/load functionality (JSON format)
- [x] Add validation for loaded maze data
- [x] Create tests for file operations
## Phase 2: Maze Generation Algorithms (Days 4-7)
### 2.1 Base Generator Interface
- [x] Create abstract `BaseGenerator` class
- [x] Define standard interface (generate, step_by_step)
- [x] Implement timing/performance tracking
### 2.2 Implement Generation Algorithms
- [x] Recursive Backtracking (stack-based)
- [x] Kruskal's Algorithm (union-find)
- [x] Prim's Algorithm (minimum spanning tree)
- [x] Sidewinder Algorithm (row-by-row)
- [x] Hunt-and-Kill Algorithm
- [x] Eller's Algorithm (row-by-row with sets)
- [x] Wilson's Algorithm (loop-erased random walk)
- [x] Aldous-Broder Algorithm (random walk)
### 2.3 Testing
- [x] Unit tests for each algorithm
- [x] Verify maze validity (all cells reachable)
- [x] Test seed reproducibility
- [x] Performance tests (5x5 to 50x50)
## Phase 3: Maze Solving (Days 8-9)
### 3.1 Solver Implementation
- [x] Create abstract `BaseSolver` class
- [x] Implement DFS solver with path tracking
- [x] Implement BFS solver with path tracking
- [x] Add step-by-step visualization support
### 3.2 Testing
- [x] Unit tests for both solvers
- [x] Verify solution correctness
- [x] Test on various maze sizes
- [x] Performance benchmarking
## Phase 4: Visualization (Days 10-12)
### 4.1 Image Generation
- [x] Implement PNG/JPG renderer using Pillow
- [x] Support for different styles (walls, paths, solutions)
- [x] Color coding for solution paths
- [x] Configurable cell sizes and colors
### 4.2 Web Visualization
- [x] Create JSON format for web rendering
- [x] Implement Canvas-based renderer in JavaScript
- [x] Add animation support for generation/solving
- [x] Real-time step-by-step visualization
### 4.3 Testing
- [x] Test image generation for various sizes
- [x] Validate JSON output format
- [x] Visual regression tests (if applicable)
## Phase 5: Analysis & Benchmarking (Days 13-14)
### 5.1 Maze Analysis
- [x] Calculate dead ends count
- [x] Calculate longest path
- [x] Calculate branching factor
- [x] Calculate solution path length
- [x] Generate complexity metrics
### 5.2 Benchmarking System
- [x] Compare generation algorithms (time, memory)
- [x] Compare solving algorithms
- [x] Generate performance reports
- [x] Create visualization of benchmark results
### 5.3 Testing
- [x] Unit tests for analysis functions
- [x] Validate benchmark accuracy
- [x] Performance regression tests
## Phase 6: Web API (Days 15-17)
### 6.1 API Development
- [x] Set up Flask/FastAPI application
- [x] Create RESTful endpoints:
- `POST /api/generate` - Generate new maze
- `GET /api/maze/{id}` - Retrieve maze
- `POST /api/solve` - Solve maze
- `GET /api/analyze/{id}` - Analyze maze
- `POST /api/benchmark` - Run benchmarks
- `GET /api/download/{id}` - Download as image
- [x] Add request validation
- [x] Implement error handling
- [x] Add CORS support
### 6.2 API Testing
- [x] Integration tests for all endpoints
- [x] Test error cases
- [x] Load testing (optional)
## Phase 7: Web Interface (Days 18-21)
### 7.1 Frontend Development
- [x] Create responsive HTML layout
- [x] Implement Neo-Brutalism CSS styling:
- Bold, thick borders (4-8px)
- High contrast colors (black borders, bright backgrounds)
- Flat design with no gradients or shadows (except hard drop shadows)
- Offset/displaced drop shadows for depth
- Chunky, bold typography
- Asymmetric layouts and overlapping elements
- Vibrant color palette (neon yellows, pinks, blues)
- [x] Build JavaScript controls:
1. Generate new maze (algorithm selector, size, seed)
2. Visualize maze (canvas rendering)
3. Download maze as image
4. Save maze to file
5. Load maze from file
6. Solve maze (DFS) with visualization
7. Solve maze (BFS) with visualization
8. Analyze maze (show statistics)
9. Benchmark algorithms (show results)
### 7.2 Interactive Features
- [x] Real-time generation animation
- [x] Step-by-step solving visualization
- [x] Interactive controls (play/pause/speed)
- [x] Results display panel
### 7.3 Testing
- [x] Manual UI testing
- [x] Cross-browser compatibility
- [x] Responsive design testing
## Phase 8: Containerization (Days 22-23)
### 8.1 Docker Setup
- [x] Create Dockerfile (multi-stage build)
- [x] Create docker-compose.yml
- [x] Configure environment variables
- [x] Optimize image size
### 8.2 Docker Testing
- [x] Test container builds
- [x] Test container deployment
- [x] Verify all features work in container
- [x] Document deployment process
## Phase 9: Testing & Quality Assurance (Days 24-25)
### 9.1 Comprehensive Testing
- [x] Achieve >90% code coverage
- [x] Run full test suite
- [x] Fix any failing tests
- [x] Performance testing across all algorithms
### 9.2 Code Quality
- [x] Run linting (pylint/flake8)
- [x] Format code (black)
- [x] Type checking (mypy - optional)
- [x] Code review
## Phase 10: Documentation & Deployment (Days 26-27)
### 10.1 Documentation
- [x] Complete README.md with:
- Installation instructions
- Usage examples
- API documentation
- Algorithm descriptions
- [x] Add inline code documentation
- [x] Create user guide
- [x] Add deployment guide
### 10.2 Final Deployment
- [x] Final testing in production-like environment
- [x] Performance optimization
- [x] Security review
- [x] Deployment checklist
## Key Technical Requirements
### Maze Generation Requirements
- Support for 8 different algorithms
- Maze dimensions: 5x5 to 50x50
- Seed-based reproducibility
- Performance tracking (milliseconds)
### Testing Requirements
- Automated unit tests (pytest)
- Integration tests for API
- Code coverage >90%
- Performance benchmarks
- Self-contained test suite
### Containerization Requirements
- Docker container with all dependencies
- Docker Compose for easy deployment
- Environment-based configuration
- Health checks and logging
### UI/UX Requirements
- Neo-Brutalism design aesthetic:
- Thick black borders (4-8px) on all UI elements
- High contrast color scheme
- Hard drop shadows with offset (not soft/blurred)
- Bold, sans-serif typography (e.g., Space Grotesk, Inter Bold)
- Flat colors, no gradients
- Asymmetric, grid-breaking layouts
- Vibrant accent colors (neon pink, yellow, cyan)
- Raw, unpolished aesthetic with intentional roughness
### Performance Targets
- 5x5 maze: <10ms generation
- 25x25 maze: <100ms generation
- 50x50 maze: <1s generation
- API response time: <2s for all operations
## Testing Strategy
### Unit Tests
- All generator algorithms
- All solver algorithms
- Core maze operations
- Analysis functions
- File I/O operations
### Integration Tests
- API endpoints
- End-to-end workflows
- File save/load roundtrips
### Performance Tests
- Algorithm benchmarking
- Scalability testing (5x5 to 50x50)
- Memory usage monitoring
### Test Coverage Goals
- Overall: >90%
- Core modules: >95%
- Generators: >90%
- Solvers: >90%
- API: >85%
## Risk Mitigation
### Technical Risks
1. **Algorithm Complexity:** Start with simpler algorithms (Recursive Backtracking)
2. **Performance Issues:** Implement early benchmarking
3. **Browser Compatibility:** Use standard Canvas API, test on major browsers
4. **Container Size:** Use multi-stage builds, alpine base images
### Timeline Risks
1. **Scope Creep:** Stick to defined features
2. **Testing Overhead:** Write tests alongside implementation
3. **Integration Issues:** Regular integration testing
## Success Criteria
- [x] All 8 generation algorithms implemented and tested
- [ ] Both solving algorithms (DFS, BFS) working correctly
- [ ] Web interface with all 9 features functional
- [ ] UI follows Neo-Brutalism design concept
- [ ] Test coverage >90%
- [ ] Successful Docker deployment
- [ ] All mazes generated are valid (fully connected)
- [ ] Performance targets met
- [ ] Complete documentation
## Future Enhancements (Post-MVP)
- Additional solving algorithms (A*, Dijkstra)
- 3D maze support
- Multi-start/multi-end points
- Maze difficulty ratings
- User accounts and saved mazes
- REST API rate limiting
- WebSocket support for real-time updates
- Mobile app version
## Notes
- Prioritize code quality and test coverage
- Keep algorithms modular and extensible
- Document algorithm time/space complexity
- Use type hints throughout Python code
- Follow PEP 8 style guidelines
- Implement proper logging for debugging

283
README.md Normal file
View File

@@ -0,0 +1,283 @@
# Maze Generator
A comprehensive Python-based maze generation and solving application featuring **8 different algorithms**, an interactive **Neo-Brutalism web interface**, and advanced analysis tools.
![License](https://img.shields.io/badge/license-MIT-blue)
![Python](https://img.shields.io/badge/python-3.11%2B-blue)
![Tests](https://img.shields.io/badge/coverage-90%2B%25-green)
## Features
### 🎨 8 Maze Generation Algorithms
- **Recursive Backtracking** - Depth-first search with backtracking
- **Kruskal's Algorithm** - Minimum spanning tree using union-find
- **Prim's Algorithm** - Greedy minimum spanning tree
- **Sidewinder Algorithm** - Row-by-row generation with horizontal bias
- **Hunt-and-Kill Algorithm** - Random walks with hunting phase
- **Eller's Algorithm** - Memory-efficient row-by-row generation
- **Wilson's Algorithm** - Loop-erased random walks (uniform spanning trees)
- **Aldous-Broder Algorithm** - Random walk-based generation
### 🔍 2 Solving Algorithms
- **Depth-First Search (DFS)** - Memory-efficient pathfinding
- **Breadth-First Search (BFS)** - Guaranteed shortest path
### 🎯 Interactive Web Interface
Built with **Neo-Brutalism** design aesthetics featuring:
- Bold, thick borders (6px)
- High contrast neon colors (yellow, pink, cyan, green)
- Hard drop shadows
- Chunky typography (Space Grotesk font)
- Asymmetric layouts
### 📊 Analysis Tools
- Dead end counting and percentage
- Longest path detection
- Branching factor calculation
- Algorithm performance benchmarking
### 🖼️ Visualization
- Real-time canvas rendering
- PNG image export
- Solution path highlighting
- Visited cell tracking
### 💾 Persistence
- Save/load mazes as JSON
- File management system
- Reproducible generation with seeds
## Installation
### Local Setup
1. **Clone the repository:**
```bash
git clone <repository-url>
cd mazer
```
2. **Create virtual environment:**
```bash
python -m venv venv
# Windows
venv\Scripts\activate
# Linux/Mac
source venv/bin/activate
```
3. **Install dependencies:**
```bash
pip install -r requirements.txt
```
4. **Run the application:**
```bash
python api/app.py
```
5. **Open your browser:**
Navigate to `http://localhost:5000`
### Docker Setup
1. **Build and run with Docker Compose:**
```bash
cd docker
docker-compose up --build
```
2. **Access the application:**
Navigate to `http://localhost:5000`
## Usage
### Web Interface
The web interface provides 9 main operations:
1. **Generate Maze** - Create a new maze with selected algorithm and dimensions
2. **Visualize** - Display the maze on canvas
3. **Download Image** - Save maze as PNG file
4. **Save to File** - Persist maze as JSON
5. **Load from File** - Restore saved maze
6. **Solve (DFS)** - Find path using Depth-First Search
7. **Solve (BFS)** - Find shortest path using Breadth-First Search
8. **Analyze** - Compute maze statistics and metrics
9. **Benchmark** - Compare algorithm performance
### API Endpoints
#### Generate Maze
```bash
POST /api/generate
Content-Type: application/json
{
"algorithm": "recursive_backtracking",
"rows": 15,
"cols": 15,
"seed": 42
}
```
#### Solve Maze
```bash
POST /api/solve
Content-Type: application/json
{
"maze_id": 0,
"algorithm": "bfs"
}
```
#### Analyze Maze
```bash
GET /api/analyze/<maze_id>
```
#### Download Maze Image
```bash
GET /api/download/<maze_id>?solution=true&solver=bfs
```
#### Benchmark Algorithms
```bash
POST /api/benchmark
Content-Type: application/json
{
"type": "quick"
}
```
### Python API
```python
from src.generators import RecursiveBacktrackingGenerator
from src.solvers import BFSSolver
from src.analysis.analyzer import MazeAnalyzer
# Generate a maze
generator = RecursiveBacktrackingGenerator()
maze = generator.generate(rows=20, cols=20, seed=42)
# Solve the maze
solver = BFSSolver()
solution = solver.solve(maze)
# Analyze the maze
analysis = MazeAnalyzer.analyze(maze)
print(f"Dead ends: {analysis['dead_ends']}")
print(f"Longest path: {analysis['longest_path_length']}")
```
## Testing
Run the test suite with coverage:
```bash
# Run all tests
pytest
# Run with coverage report
pytest --cov=src --cov-report=html
# Run specific test file
pytest tests/unit/test_maze.py
# Run specific test
pytest tests/unit/test_generators.py::TestGenerators::test_generator_creates_valid_maze
```
### Test Coverage
- Overall: >90%
- Core modules: >95%
- Generators: >90%
- Solvers: >90%
- API: >85%
## Project Structure
```
mazer/
├── src/ # Core application code
│ ├── core/ # Maze and Cell classes
│ ├── generators/ # 8 generation algorithms
│ ├── solvers/ # DFS and BFS solvers
│ ├── visualization/ # Image and web rendering
│ ├── analysis/ # Analysis and benchmarking
│ └── storage/ # File I/O operations
├── api/ # Flask web API
│ └── app.py # Main application
├── web/ # Frontend
│ ├── static/
│ │ ├── css/ # Neo-Brutalism styles
│ │ └── js/ # Interactive controls
│ └── templates/ # HTML templates
├── tests/ # Test suite
│ ├── unit/ # Unit tests
│ └── integration/ # Integration tests
├── docker/ # Docker configuration
├── requirements.txt # Python dependencies
└── README.md # This file
```
## Algorithm Complexity
| Algorithm | Time Complexity | Space Complexity | Characteristics |
|-----------|----------------|------------------|-----------------|
| Recursive Backtracking | O(n) | O(n) | Long winding paths |
| Kruskal's | O(E log E) | O(V) | Many short paths |
| Prim's | O(E log V) | O(V) | Short dead ends |
| Sidewinder | O(n) | O(cols) | Horizontal bias |
| Hunt-and-Kill | O(n²) | O(1) | Few dead ends |
| Eller's | O(n) | O(cols) | Memory efficient |
| Wilson's | O(n) expected | O(n) | Uniform spanning tree |
| Aldous-Broder | O(n log n) | O(1) | Uniform, slow |
## Performance Targets
- 5×5 maze: <10ms generation
- 25×25 maze: <100ms generation
- 50×50 maze: <1s generation
- API response time: <2s for all operations
## Technologies Used
- **Backend:** Python 3.11+
- **Web Framework:** Flask 3.0
- **Image Processing:** Pillow
- **Testing:** pytest, pytest-cov
- **Containerization:** Docker, Docker Compose
- **Frontend:** HTML5, CSS3, JavaScript
- **Design:** Neo-Brutalism aesthetic
## Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
This project is licensed under the MIT License.
## Acknowledgments
- Maze generation algorithms based on ["Mazes for Programmers" by Jamis Buck](http://www.mazesforprogrammers.com/)
- Neo-Brutalism design inspiration from [neobrutalism.dev](https://neobrutalism.dev/)
## Future Enhancements
- Additional solving algorithms (A*, Dijkstra)
- 3D maze support
- Multi-start/multi-end points
- Difficulty ratings
- WebSocket real-time updates
- Mobile app version

1
api/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Web API for maze generation and solving."""

296
api/app.py Normal file
View File

@@ -0,0 +1,296 @@
"""Main Flask application for the Maze Generator API."""
import os
import sys
from pathlib import Path
# Add parent directory to path to import src modules
sys.path.insert(0, str(Path(__file__).parent.parent))
from flask import Flask, jsonify, request, send_file, render_template
from flask_cors import CORS
from src.generators import *
from src.solvers import *
from src.core.maze import Maze
from src.storage.file_handler import FileHandler
from src.visualization.image_renderer import ImageRenderer
from src.visualization.web_renderer import WebRenderer
from src.analysis.analyzer import MazeAnalyzer
from src.analysis.benchmark import Benchmark
app = Flask(__name__,
template_folder='../web/templates',
static_folder='../web/static')
CORS(app)
# Generator mapping
GENERATORS = {
'recursive_backtracking': RecursiveBacktrackingGenerator(),
'kruskal': KruskalGenerator(),
'prim': PrimGenerator(),
'sidewinder': SidewinderGenerator(),
'hunt_and_kill': HuntAndKillGenerator(),
'eller': EllerGenerator(),
'wilson': WilsonGenerator(),
'aldous_broder': AldousBroderGenerator()
}
# Solver mapping
SOLVERS = {
'dfs': DFSSolver(),
'bfs': BFSSolver()
}
# Store mazes in memory (keyed by ID)
mazes = {}
maze_counter = 0
@app.route('/')
def index():
"""Serve the main web interface."""
return render_template('index.html')
@app.route('/api/algorithms', methods=['GET'])
def get_algorithms():
"""Get list of available algorithms."""
return jsonify({
'generators': list(GENERATORS.keys()),
'solvers': list(SOLVERS.keys())
})
@app.route('/api/generate', methods=['POST'])
def generate_maze():
"""Generate a new maze."""
global maze_counter
data = request.json
# Validate input
algorithm = data.get('algorithm', 'recursive_backtracking')
rows = data.get('rows', 10)
cols = data.get('cols', 10)
seed = data.get('seed')
if algorithm not in GENERATORS:
return jsonify({'error': f'Unknown algorithm: {algorithm}'}), 400
if not (5 <= rows <= 50) or not (5 <= cols <= 50):
return jsonify({'error': 'Dimensions must be between 5 and 50'}), 400
try:
# Generate maze
generator = GENERATORS[algorithm]
maze = generator.generate(rows, cols, seed)
# Store maze
maze_id = maze_counter
mazes[maze_id] = maze
maze_counter += 1
# Return maze data
return jsonify({
'id': maze_id,
'maze': WebRenderer.to_json_format(maze),
'success': True
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/maze/<int:maze_id>', methods=['GET'])
def get_maze(maze_id):
"""Get maze by ID."""
if maze_id not in mazes:
return jsonify({'error': 'Maze not found'}), 404
maze = mazes[maze_id]
return jsonify({
'id': maze_id,
'maze': WebRenderer.to_json_format(maze)
})
@app.route('/api/solve', methods=['POST'])
def solve_maze():
"""Solve a maze."""
data = request.json
maze_id = data.get('maze_id')
algorithm = data.get('algorithm', 'bfs')
if maze_id is None or maze_id not in mazes:
return jsonify({'error': 'Maze not found'}), 404
if algorithm not in SOLVERS:
return jsonify({'error': f'Unknown solver: {algorithm}'}), 400
try:
maze = mazes[maze_id]
solver = SOLVERS[algorithm]
result = solver.solve(maze)
return jsonify({
'success': result['success'],
'path': result['path'],
'visited': result['visited'],
'time_ms': result['time_ms'],
'path_length': result['path_length'],
'algorithm': result['algorithm']
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/analyze/<int:maze_id>', methods=['GET'])
def analyze_maze(maze_id):
"""Analyze a maze."""
if maze_id not in mazes:
return jsonify({'error': 'Maze not found'}), 404
try:
maze = mazes[maze_id]
analysis = MazeAnalyzer.analyze(maze)
return jsonify(analysis)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/benchmark', methods=['POST'])
def benchmark():
"""Run algorithm benchmarks."""
data = request.json or {}
benchmark_type = data.get('type', 'quick')
try:
if benchmark_type == 'quick':
results = Benchmark.quick_benchmark()
elif benchmark_type == 'generators':
sizes = data.get('sizes', [(10, 10), (25, 25)])
iterations = data.get('iterations', 3)
results = Benchmark.benchmark_generators(sizes, iterations)
elif benchmark_type == 'solvers':
sizes = data.get('sizes', [(10, 10), (25, 25)])
iterations = data.get('iterations', 3)
results = Benchmark.benchmark_solvers(sizes, iterations)
else:
return jsonify({'error': 'Invalid benchmark type'}), 400
return jsonify(results)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/download/<int:maze_id>', methods=['GET'])
def download_maze_image(maze_id):
"""Download maze as an image."""
if maze_id not in mazes:
return jsonify({'error': 'Maze not found'}), 404
try:
maze = mazes[maze_id]
# Get optional parameters
include_solution = request.args.get('solution', 'false').lower() == 'true'
solver_algorithm = request.args.get('solver', 'bfs')
solution_path = None
visited_cells = None
if include_solution and solver_algorithm in SOLVERS:
solver = SOLVERS[solver_algorithm]
result = solver.solve(maze)
if result['success']:
solution_path = result['path']
visited_cells = result['visited']
# Render image
renderer = ImageRenderer(cell_size=20, wall_thickness=2)
filename = f"maze_{maze_id}_{maze.algorithm_used.replace(' ', '_')}"
filepath = renderer.render(
maze,
filename,
solution_path=solution_path,
visited_cells=visited_cells
)
return send_file(filepath, mimetype='image/png', as_attachment=True)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/save/<int:maze_id>', methods=['POST'])
def save_maze(maze_id):
"""Save a maze to file."""
if maze_id not in mazes:
return jsonify({'error': 'Maze not found'}), 404
data = request.json or {}
filename = data.get('filename', f'maze_{maze_id}')
try:
maze = mazes[maze_id]
filepath = FileHandler.save_maze(maze, filename)
return jsonify({
'success': True,
'filepath': filepath
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/load', methods=['POST'])
def load_maze():
"""Load a maze from file."""
global maze_counter
data = request.json
filename = data.get('filename')
if not filename:
return jsonify({'error': 'Filename required'}), 400
try:
maze = FileHandler.load_maze(filename)
# Store maze
maze_id = maze_counter
mazes[maze_id] = maze
maze_counter += 1
return jsonify({
'id': maze_id,
'maze': WebRenderer.to_json_format(maze),
'success': True
})
except FileNotFoundError:
return jsonify({'error': 'File not found'}), 404
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/saved-mazes', methods=['GET'])
def list_saved_mazes():
"""List all saved maze files."""
try:
files = FileHandler.list_saved_mazes()
return jsonify({'files': files})
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)

46
docker/Dockerfile Normal file
View File

@@ -0,0 +1,46 @@
# Multi-stage build for Maze Generator
# Stage 1: Builder
FROM python:3.11-slim as builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir --user -r requirements.txt
# Stage 2: Runtime
FROM python:3.11-slim
WORKDIR /app
# Copy Python dependencies from builder
COPY --from=builder /root/.local /root/.local
# Make sure scripts in .local are usable
ENV PATH=/root/.local/bin:$PATH
# Copy application code
COPY src/ ./src/
COPY api/ ./api/
COPY web/ ./web/
# Create directories for data
RUN mkdir -p saved_mazes output_images
# Expose port
EXPOSE 5000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:5000/api/algorithms')" || exit 1
# Run the application
CMD ["python", "api/app.py"]

29
docker/docker-compose.yml Normal file
View File

@@ -0,0 +1,29 @@
version: '3.8'
services:
maze-generator:
build:
context: ..
dockerfile: docker/Dockerfile
container_name: maze-generator
ports:
- "5100:5000"
volumes:
- maze-data:/app/saved_mazes
- maze-images:/app/output_images
environment:
- FLASK_ENV=production
- PYTHONUNBUFFERED=1
restart: unless-stopped
healthcheck:
test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:5000/api/algorithms')"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
volumes:
maze-data:
driver: local
maze-images:
driver: local

11
pytest.ini Normal file
View File

@@ -0,0 +1,11 @@
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--verbose
--cov=src
--cov-report=html
--cov-report=term-missing
--cov-fail-under=90

5
requirements-dev.txt Normal file
View File

@@ -0,0 +1,5 @@
-r requirements.txt
black==23.12.0
flake8==6.1.0
pylint==3.0.3
mypy==1.7.1

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
Flask==3.0.0
Flask-CORS==4.0.0
Pillow==10.1.0
pytest==7.4.3
pytest-cov==4.1.0

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
}

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Test suite for Maze Generator."""

38
tests/conftest.py Normal file
View File

@@ -0,0 +1,38 @@
"""Pytest configuration and fixtures."""
import pytest
import sys
from pathlib import Path
# Add src to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.core.maze import Maze
from src.generators import RecursiveBacktrackingGenerator
@pytest.fixture
def small_maze():
"""Create a small 5x5 maze for testing."""
generator = RecursiveBacktrackingGenerator()
return generator.generate(5, 5, seed=42)
@pytest.fixture
def medium_maze():
"""Create a medium 10x10 maze for testing."""
generator = RecursiveBacktrackingGenerator()
return generator.generate(10, 10, seed=42)
@pytest.fixture
def large_maze():
"""Create a large 25x25 maze for testing."""
generator = RecursiveBacktrackingGenerator()
return generator.generate(25, 25, seed=42)
@pytest.fixture
def empty_maze():
"""Create an empty maze (no walls removed)."""
return Maze(10, 10, seed=42)

View File

@@ -0,0 +1 @@
"""Integration tests."""

View File

@@ -0,0 +1,217 @@
"""Integration tests for complete workflows."""
import pytest
import tempfile
import shutil
from pathlib import Path
from src.generators import RecursiveBacktrackingGenerator, KruskalGenerator
from src.solvers import BFSSolver, DFSSolver
from src.storage.file_handler import FileHandler
from src.visualization.image_renderer import ImageRenderer
from src.analysis.analyzer import MazeAnalyzer
class TestCompleteWorkflow:
"""Test complete end-to-end workflows."""
def test_generate_solve_analyze_workflow(self):
"""Test generating, solving, and analyzing a maze."""
# Generate maze
generator = RecursiveBacktrackingGenerator()
maze = generator.generate(15, 15, seed=42)
assert maze is not None
assert maze.rows == 15
assert maze.cols == 15
# Solve maze
solver = BFSSolver()
result = solver.solve(maze)
assert result['success']
assert result['path_length'] > 0
# Analyze maze
analysis = MazeAnalyzer.analyze(maze)
assert analysis['total_cells'] == 225
assert analysis['dead_ends'] > 0
def test_generate_save_load_workflow(self):
"""Test generating, saving, and loading a maze."""
# Create temp directory
temp_dir = tempfile.mkdtemp()
try:
# Generate maze
generator = KruskalGenerator()
maze = generator.generate(10, 10, seed=123)
# Save maze
filepath = FileHandler.save_maze(maze, 'test_maze', temp_dir)
assert Path(filepath).exists()
# Load maze
loaded_maze = FileHandler.load_maze('test_maze', temp_dir)
assert loaded_maze.rows == maze.rows
assert loaded_maze.cols == maze.cols
assert loaded_maze.seed == maze.seed
# Verify walls are preserved
for row in range(maze.rows):
for col in range(maze.cols):
original_cell = maze.get_cell(row, col)
loaded_cell = loaded_maze.get_cell(row, col)
assert original_cell.walls == loaded_cell.walls
finally:
shutil.rmtree(temp_dir)
def test_generate_render_workflow(self):
"""Test generating and rendering a maze."""
temp_dir = tempfile.mkdtemp()
try:
# Generate maze
generator = RecursiveBacktrackingGenerator()
maze = generator.generate(10, 10, seed=42)
# Render image
renderer = ImageRenderer(cell_size=20)
filepath = renderer.render(maze, 'test_render', temp_dir)
assert Path(filepath).exists()
# File should have content
file_size = Path(filepath).stat().st_size
assert file_size > 0
finally:
shutil.rmtree(temp_dir)
def test_generate_solve_render_workflow(self):
"""Test generating, solving, and rendering with solution."""
temp_dir = tempfile.mkdtemp()
try:
# Generate maze
generator = RecursiveBacktrackingGenerator()
maze = generator.generate(15, 15, seed=42)
# Solve maze
solver = DFSSolver()
result = solver.solve(maze)
# Render with solution
renderer = ImageRenderer(cell_size=20)
filepath = renderer.render(
maze,
'maze_with_solution',
temp_dir,
solution_path=result['path'],
visited_cells=result['visited']
)
assert Path(filepath).exists()
assert Path(filepath).stat().st_size > 0
finally:
shutil.rmtree(temp_dir)
def test_multiple_algorithms_workflow(self):
"""Test workflow with multiple algorithms."""
from src.generators import (
PrimGenerator,
SidewinderGenerator,
WilsonGenerator
)
generators = [
PrimGenerator(),
SidewinderGenerator(),
WilsonGenerator()
]
for generator in generators:
# Generate
maze = generator.generate(10, 10, seed=42)
assert maze is not None
# Solve with both solvers
for solver in [DFSSolver(), BFSSolver()]:
result = solver.solve(maze)
assert result['success']
# Analyze
analysis = MazeAnalyzer.analyze(maze)
assert analysis['total_cells'] == 100
class TestFileOperations:
"""Test file operation workflows."""
def test_save_list_delete_workflow(self):
"""Test saving, listing, and deleting mazes."""
temp_dir = tempfile.mkdtemp()
try:
generator = RecursiveBacktrackingGenerator()
# Save multiple mazes
maze1 = generator.generate(5, 5, seed=1)
maze2 = generator.generate(10, 10, seed=2)
FileHandler.save_maze(maze1, 'maze1', temp_dir)
FileHandler.save_maze(maze2, 'maze2', temp_dir)
# List mazes
files = FileHandler.list_saved_mazes(temp_dir)
assert len(files) == 2
assert 'maze1.json' in files
assert 'maze2.json' in files
# Delete one
deleted = FileHandler.delete_maze('maze1', temp_dir)
assert deleted
# List again
files = FileHandler.list_saved_mazes(temp_dir)
assert len(files) == 1
assert 'maze2.json' in files
finally:
shutil.rmtree(temp_dir)
def test_load_nonexistent_file(self):
"""Test loading a file that doesn't exist."""
temp_dir = tempfile.mkdtemp()
try:
with pytest.raises(FileNotFoundError):
FileHandler.load_maze('nonexistent', temp_dir)
finally:
shutil.rmtree(temp_dir)
class TestVisualization:
"""Test visualization workflows."""
def test_render_different_sizes(self):
"""Test rendering mazes of different sizes."""
temp_dir = tempfile.mkdtemp()
try:
generator = RecursiveBacktrackingGenerator()
renderer = ImageRenderer()
for size in [5, 10, 15]:
maze = generator.generate(size, size, seed=42)
filepath = renderer.render(maze, f'maze_{size}', temp_dir)
assert Path(filepath).exists()
finally:
shutil.rmtree(temp_dir)

1
tests/unit/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Unit tests."""

141
tests/unit/test_analysis.py Normal file
View File

@@ -0,0 +1,141 @@
"""Tests for analysis and benchmarking tools."""
import pytest
from src.analysis.analyzer import MazeAnalyzer
from src.analysis.benchmark import Benchmark
class TestMazeAnalyzer:
"""Test maze analysis functionality."""
def test_analyze_returns_complete_data(self, medium_maze):
"""Test that analyze returns all required fields."""
result = MazeAnalyzer.analyze(medium_maze)
required_fields = [
'dimensions', 'total_cells', 'algorithm', 'generation_time_ms',
'seed', 'dead_ends', 'dead_end_percentage', 'longest_path_length',
'longest_path_start', 'longest_path_end', 'average_branching_factor'
]
for field in required_fields:
assert field in result
def test_dead_ends_count(self, small_maze):
"""Test dead ends counting."""
dead_ends = MazeAnalyzer.count_dead_ends(small_maze)
assert dead_ends >= 0
assert dead_ends <= small_maze.rows * small_maze.cols
def test_dead_end_percentage(self, medium_maze):
"""Test dead end percentage calculation."""
result = MazeAnalyzer.analyze(medium_maze)
assert 0 <= result['dead_end_percentage'] <= 100
def test_longest_path(self, small_maze):
"""Test longest path finding."""
result = MazeAnalyzer.find_longest_path(small_maze)
assert 'length' in result
assert 'start' in result
assert 'end' in result
assert result['length'] >= 0
def test_branching_factor(self, medium_maze):
"""Test branching factor calculation."""
branching_factor = MazeAnalyzer.calculate_branching_factor(medium_maze)
# Branching factor should be between 1 and 4
assert 1.0 <= branching_factor <= 4.0
def test_total_cells(self, medium_maze):
"""Test total cells calculation."""
result = MazeAnalyzer.analyze(medium_maze)
assert result['total_cells'] == medium_maze.rows * medium_maze.cols
class TestBenchmark:
"""Test benchmarking functionality."""
def test_benchmark_generators_runs(self):
"""Test that generator benchmark runs successfully."""
result = Benchmark.benchmark_generators(
sizes=[(5, 5), (10, 10)],
iterations=2,
seed=42
)
assert 'benchmark_type' in result
assert result['benchmark_type'] == 'generators'
assert 'results' in result
assert len(result['results']) > 0
def test_benchmark_solvers_runs(self):
"""Test that solver benchmark runs successfully."""
result = Benchmark.benchmark_solvers(
sizes=[(5, 5), (10, 10)],
iterations=2,
seed=42
)
assert 'benchmark_type' in result
assert result['benchmark_type'] == 'solvers'
assert 'results' in result
assert len(result['results']) > 0
def test_quick_benchmark(self):
"""Test quick benchmark runs."""
result = Benchmark.quick_benchmark()
assert 'generators' in result
assert 'solvers' in result
def test_benchmark_generator_results_structure(self):
"""Test benchmark generator results have correct structure."""
result = Benchmark.benchmark_generators(
sizes=[(5, 5)],
iterations=2,
seed=42
)
for r in result['results']:
assert 'algorithm' in r
assert 'size' in r
assert 'avg_time_ms' in r
assert 'min_time_ms' in r
assert 'max_time_ms' in r
assert r['avg_time_ms'] >= 0
def test_benchmark_solver_results_structure(self):
"""Test benchmark solver results have correct structure."""
result = Benchmark.benchmark_solvers(
sizes=[(5, 5)],
iterations=2,
seed=42
)
for r in result['results']:
assert 'algorithm' in r
assert 'size' in r
assert 'avg_time_ms' in r
assert 'avg_path_length' in r
assert r['avg_time_ms'] >= 0
assert r['avg_path_length'] > 0
def test_benchmark_multiple_sizes(self):
"""Test benchmark with multiple sizes."""
sizes = [(5, 5), (10, 10)]
result = Benchmark.benchmark_generators(
sizes=sizes,
iterations=2,
seed=42
)
# Should have results for each algorithm at each size
num_algorithms = len(Benchmark.GENERATORS)
expected_results = num_algorithms * len(sizes)
assert len(result['results']) == expected_results

View File

@@ -0,0 +1,148 @@
"""Tests for maze generation algorithms."""
import pytest
from src.generators import (
RecursiveBacktrackingGenerator,
KruskalGenerator,
PrimGenerator,
SidewinderGenerator,
HuntAndKillGenerator,
EllerGenerator,
WilsonGenerator,
AldousBroderGenerator
)
# All generators to test
GENERATORS = [
RecursiveBacktrackingGenerator(),
KruskalGenerator(),
PrimGenerator(),
SidewinderGenerator(),
HuntAndKillGenerator(),
EllerGenerator(),
WilsonGenerator(),
AldousBroderGenerator()
]
class TestGenerators:
"""Test all maze generation algorithms."""
@pytest.mark.parametrize("generator", GENERATORS)
def test_generator_creates_valid_maze(self, generator):
"""Test that generator creates a valid maze."""
maze = generator.generate(10, 10, seed=42)
assert maze is not None
assert maze.rows == 10
assert maze.cols == 10
assert maze.algorithm_used == generator.name
assert maze.generation_time_ms >= 0
@pytest.mark.parametrize("generator", GENERATORS)
def test_generator_with_different_sizes(self, generator):
"""Test generator with different maze sizes."""
# Small maze
maze_small = generator.generate(5, 5, seed=42)
assert maze_small.rows == 5
assert maze_small.cols == 5
# Large maze
maze_large = generator.generate(25, 25, seed=42)
assert maze_large.rows == 25
assert maze_large.cols == 25
@pytest.mark.parametrize("generator", GENERATORS)
def test_generator_reproducibility(self, generator):
"""Test that same seed produces same maze."""
maze1 = generator.generate(10, 10, seed=42)
maze2 = generator.generate(10, 10, seed=42)
# Compare wall structures
for row in range(10):
for col in range(10):
cell1 = maze1.get_cell(row, col)
cell2 = maze2.get_cell(row, col)
assert cell1.walls == cell2.walls
@pytest.mark.parametrize("generator", GENERATORS)
def test_maze_is_fully_connected(self, generator):
"""Test that all cells in maze are reachable."""
maze = generator.generate(10, 10, seed=42)
# Use BFS to check connectivity
start = maze.get_cell(0, 0)
visited = set()
queue = [start]
start.visited = True
while queue:
current = queue.pop(0)
visited.add((current.row, current.col))
neighbors = maze.get_neighbors(current)
for neighbor, direction in neighbors:
if not neighbor.visited and not current.has_wall(direction):
neighbor.visited = True
queue.append(neighbor)
# All cells should be reachable
assert len(visited) == maze.rows * maze.cols
@pytest.mark.parametrize("generator", GENERATORS)
def test_maze_has_passages(self, generator):
"""Test that maze has passages (some walls removed)."""
maze = generator.generate(10, 10, seed=42)
total_walls = 0
for row in maze.grid:
for cell in row:
total_walls += sum(1 for wall in cell.walls.values() if wall)
# Should have fewer walls than a completely walled maze
max_walls = 10 * 10 * 4
assert total_walls < max_walls
@pytest.mark.parametrize("generator", GENERATORS)
def test_generator_performance(self, generator):
"""Test generator meets performance targets."""
# 10x10 should be very fast
maze = generator.generate(10, 10, seed=42)
assert maze.generation_time_ms < 1000 # Less than 1 second
# Even 25x25 should be reasonable (except Aldous-Broder can be slow)
if generator.name != "Aldous-Broder Algorithm":
maze = generator.generate(25, 25, seed=42)
assert maze.generation_time_ms < 5000 # Less than 5 seconds
class TestSpecificGenerators:
"""Test specific generator properties."""
def test_recursive_backtracking_name(self):
"""Test recursive backtracking has correct name."""
gen = RecursiveBacktrackingGenerator()
assert gen.name == "Recursive Backtracking"
def test_kruskal_name(self):
"""Test Kruskal's has correct name."""
gen = KruskalGenerator()
assert gen.name == "Kruskal's Algorithm"
def test_prim_name(self):
"""Test Prim's has correct name."""
gen = PrimGenerator()
assert gen.name == "Prim's Algorithm"
def test_sidewinder_creates_valid_maze(self):
"""Test Sidewinder algorithm."""
gen = SidewinderGenerator()
maze = gen.generate(10, 10, seed=42)
# Top row should have all east walls removed (characteristic of Sidewinder)
# Check that top row is mostly connected horizontally
top_row = maze.grid[0]
east_walls = sum(1 for cell in top_row if cell.has_wall('east'))
# Should have mostly removed east walls in top row
assert east_walls < len(top_row)

173
tests/unit/test_maze.py Normal file
View File

@@ -0,0 +1,173 @@
"""Tests for core Maze and Cell classes."""
import pytest
from src.core.maze import Maze
from src.core.cell import Cell
class TestCell:
"""Test Cell class functionality."""
def test_cell_initialization(self):
"""Test cell is initialized with all walls."""
cell = Cell(0, 0)
assert cell.row == 0
assert cell.col == 0
assert all(cell.walls.values())
assert not cell.visited
def test_remove_wall(self):
"""Test removing walls from a cell."""
cell = Cell(0, 0)
cell.remove_wall('north')
assert not cell.has_wall('north')
assert cell.has_wall('south')
def test_cell_reset(self):
"""Test resetting a cell."""
cell = Cell(0, 0)
cell.remove_wall('north')
cell.visited = True
cell.reset()
assert cell.has_wall('north')
assert not cell.visited
def test_cell_serialization(self):
"""Test cell to_dict and from_dict."""
cell = Cell(2, 3)
cell.remove_wall('east')
cell.visited = True
data = cell.to_dict()
restored = Cell.from_dict(data)
assert restored.row == cell.row
assert restored.col == cell.col
assert restored.walls == cell.walls
assert restored.visited == cell.visited
def test_cell_equality(self):
"""Test cell equality."""
cell1 = Cell(0, 0)
cell2 = Cell(0, 0)
cell3 = Cell(1, 1)
assert cell1 == cell2
assert cell1 != cell3
class TestMaze:
"""Test Maze class functionality."""
def test_maze_initialization(self):
"""Test maze is initialized correctly."""
maze = Maze(10, 10, seed=42)
assert maze.rows == 10
assert maze.cols == 10
assert maze.seed == 42
assert len(maze.grid) == 10
assert len(maze.grid[0]) == 10
def test_maze_dimensions_validation(self):
"""Test maze dimension validation."""
with pytest.raises(ValueError):
Maze(3, 10) # Too small
with pytest.raises(ValueError):
Maze(10, 60) # Too large
def test_get_cell(self):
"""Test getting cells from maze."""
maze = Maze(10, 10)
cell = maze.get_cell(5, 5)
assert cell is not None
assert cell.row == 5
assert cell.col == 5
# Out of bounds
assert maze.get_cell(-1, 0) is None
assert maze.get_cell(0, 100) is None
def test_get_neighbors(self):
"""Test getting neighbors of a cell."""
maze = Maze(10, 10)
# Corner cell
cell = maze.get_cell(0, 0)
neighbors = maze.get_neighbors(cell)
assert len(neighbors) == 2 # Only south and east
# Middle cell
cell = maze.get_cell(5, 5)
neighbors = maze.get_neighbors(cell)
assert len(neighbors) == 4 # All directions
def test_remove_wall_between(self):
"""Test removing walls between cells."""
maze = Maze(10, 10)
cell1 = maze.get_cell(0, 0)
cell2 = maze.get_cell(0, 1)
maze.remove_wall_between(cell1, cell2)
assert not cell1.has_wall('east')
assert not cell2.has_wall('west')
def test_reset_visited(self):
"""Test resetting visited flags."""
maze = Maze(10, 10)
# Mark some cells as visited
for row in maze.grid[:5]:
for cell in row:
cell.visited = True
maze.reset_visited()
# Check all cells are unvisited
for row in maze.grid:
for cell in row:
assert not cell.visited
def test_maze_serialization(self):
"""Test maze to_dict and from_dict."""
maze = Maze(5, 5, seed=42)
maze.algorithm_used = "Test Algorithm"
maze.generation_time_ms = 10.5
# Modify some walls
cell1 = maze.get_cell(0, 0)
cell2 = maze.get_cell(0, 1)
maze.remove_wall_between(cell1, cell2)
# Serialize and deserialize
data = maze.to_dict()
restored = Maze.from_dict(data)
assert restored.rows == maze.rows
assert restored.cols == maze.cols
assert restored.seed == maze.seed
assert restored.algorithm_used == maze.algorithm_used
# Check walls are preserved
restored_cell1 = restored.get_cell(0, 0)
assert not restored_cell1.has_wall('east')
def test_maze_json(self):
"""Test JSON serialization."""
maze = Maze(5, 5, seed=42)
json_str = maze.to_json()
restored = Maze.from_json(json_str)
assert restored.rows == maze.rows
assert restored.cols == maze.cols
assert restored.seed == maze.seed
def test_is_valid_position(self):
"""Test position validation."""
maze = Maze(10, 10)
assert maze.is_valid_position(0, 0)
assert maze.is_valid_position(9, 9)
assert not maze.is_valid_position(-1, 0)
assert not maze.is_valid_position(0, 10)

143
tests/unit/test_solvers.py Normal file
View File

@@ -0,0 +1,143 @@
"""Tests for maze solving algorithms."""
import pytest
from src.solvers import DFSSolver, BFSSolver
from src.generators import RecursiveBacktrackingGenerator
SOLVERS = [DFSSolver(), BFSSolver()]
class TestSolvers:
"""Test maze solving algorithms."""
@pytest.mark.parametrize("solver", SOLVERS)
def test_solver_finds_solution(self, solver, small_maze):
"""Test that solver finds a solution."""
result = solver.solve(small_maze)
assert result['success']
assert result['path'] is not None
assert len(result['path']) > 0
assert result['path_length'] > 0
assert result['time_ms'] >= 0
@pytest.mark.parametrize("solver", SOLVERS)
def test_solution_path_validity(self, solver, medium_maze):
"""Test that solution path is valid."""
result = solver.solve(medium_maze)
assert result['success']
path = result['path']
# Path should start at maze start
assert path[0] == medium_maze.start
# Path should end at maze end
assert path[-1] == medium_maze.end
# Each step should be adjacent to previous
for i in range(len(path) - 1):
r1, c1 = path[i]
r2, c2 = path[i + 1]
# Manhattan distance should be 1
assert abs(r2 - r1) + abs(c2 - c1) == 1
@pytest.mark.parametrize("solver", SOLVERS)
def test_solver_visited_cells(self, solver, small_maze):
"""Test that solver tracks visited cells."""
result = solver.solve(small_maze)
assert 'visited' in result
assert len(result['visited']) > 0
# Solution path should be subset of visited cells
path_set = set(result['path'])
visited_set = set(result['visited'])
assert path_set.issubset(visited_set)
def test_bfs_finds_shortest_path(self):
"""Test that BFS finds shortest path."""
gen = RecursiveBacktrackingGenerator()
maze = gen.generate(10, 10, seed=42)
bfs = BFSSolver()
dfs = DFSSolver()
bfs_result = bfs.solve(maze)
dfs_result = dfs.solve(maze)
# BFS should find shortest or equal path
assert bfs_result['path_length'] <= dfs_result['path_length']
def test_solver_performance(self):
"""Test solver performance."""
gen = RecursiveBacktrackingGenerator()
maze = gen.generate(25, 25, seed=42)
for solver in SOLVERS:
result = solver.solve(maze)
# Should solve 25x25 maze quickly
assert result['time_ms'] < 1000
def test_solver_on_different_sizes(self):
"""Test solvers on different maze sizes."""
gen = RecursiveBacktrackingGenerator()
for size in [5, 10, 15, 20]:
maze = gen.generate(size, size, seed=42)
for solver in SOLVERS:
result = solver.solve(maze)
assert result['success']
assert result['path_length'] > 0
class TestDFSSolver:
"""Test DFS-specific functionality."""
def test_dfs_name(self):
"""Test DFS solver name."""
solver = DFSSolver()
assert "DFS" in solver.name or "Depth-First" in solver.name
def test_dfs_solves_maze(self, medium_maze):
"""Test DFS solves maze correctly."""
solver = DFSSolver()
result = solver.solve(medium_maze)
assert result['success']
assert result['algorithm'] == solver.name
class TestBFSSolver:
"""Test BFS-specific functionality."""
def test_bfs_name(self):
"""Test BFS solver name."""
solver = BFSSolver()
assert "BFS" in solver.name or "Breadth-First" in solver.name
def test_bfs_solves_maze(self, medium_maze):
"""Test BFS solves maze correctly."""
solver = BFSSolver()
result = solver.solve(medium_maze)
assert result['success']
assert result['algorithm'] == solver.name
def test_bfs_optimal_path(self):
"""Test BFS finds optimal path."""
gen = RecursiveBacktrackingGenerator()
# Test on multiple mazes
for seed in [42, 100, 200]:
maze = gen.generate(15, 15, seed=seed)
bfs = BFSSolver()
result = bfs.solve(maze)
# Verify path exists and is valid
assert result['success']
assert result['path_length'] > 0

372
web/static/css/styles.css Normal file
View File

@@ -0,0 +1,372 @@
/* Neo-Brutalism Maze Generator Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
/* Neo-Brutalism Color Palette */
--black: #000000;
--white: #FFFFFF;
--neon-yellow: #FFE500;
--neon-pink: #FF10F0;
--neon-cyan: #00F0FF;
--neon-green: #39FF14;
--gray-bg: #F5F5F5;
--gray-dark: #333333;
/* Spacing */
--border-thick: 6px;
--border-medium: 4px;
--shadow-offset: 8px;
--spacing-sm: 12px;
--spacing-md: 20px;
--spacing-lg: 32px;
}
body {
font-family: 'Space Grotesk', monospace, sans-serif;
background-color: var(--gray-bg);
color: var(--black);
line-height: 1.4;
padding: var(--spacing-md);
}
.container {
max-width: 1400px;
margin: 0 auto;
}
/* Header */
.header {
background-color: var(--neon-yellow);
border: var(--border-thick) solid var(--black);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--black);
transform: rotate(-1deg);
}
.title {
font-size: 4rem;
font-weight: 700;
letter-spacing: -2px;
text-transform: uppercase;
margin-bottom: var(--spacing-sm);
}
.subtitle {
font-size: 1.2rem;
font-weight: 700;
letter-spacing: 2px;
}
/* Main Grid Layout */
.main-grid {
display: grid;
grid-template-columns: 350px 1fr 400px;
gap: var(--spacing-lg);
}
@media (max-width: 1200px) {
.main-grid {
grid-template-columns: 1fr;
}
}
/* Panels */
.panel {
background-color: var(--white);
border: var(--border-thick) solid var(--black);
padding: var(--spacing-md);
}
.control-panel {
background-color: var(--neon-cyan);
box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--black);
}
.viz-panel {
background-color: var(--white);
box-shadow: calc(var(--shadow-offset) * -1) var(--shadow-offset) 0 var(--neon-pink);
min-height: 600px;
}
.results-panel {
background-color: var(--neon-green);
box-shadow: var(--shadow-offset) calc(var(--shadow-offset) * -1) 0 var(--black);
max-height: 800px;
overflow-y: auto;
}
.panel-title {
font-size: 1.5rem;
font-weight: 700;
text-transform: uppercase;
margin-bottom: var(--spacing-md);
padding-bottom: var(--spacing-sm);
border-bottom: var(--border-medium) solid var(--black);
}
/* Form Controls */
.control-group {
margin-bottom: var(--spacing-md);
}
.control-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.label {
display: block;
font-size: 0.9rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: var(--spacing-sm);
}
.text-input,
.select-input {
width: 100%;
padding: 12px;
font-family: 'Space Grotesk', monospace, sans-serif;
font-size: 1rem;
font-weight: 700;
background-color: var(--white);
border: var(--border-medium) solid var(--black);
box-shadow: 4px 4px 0 var(--black);
transition: transform 0.1s, box-shadow 0.1s;
}
.text-input:focus,
.select-input:focus {
outline: none;
transform: translate(2px, 2px);
box-shadow: 2px 2px 0 var(--black);
}
/* Buttons */
.btn {
width: 100%;
padding: 16px;
font-family: 'Space Grotesk', monospace, sans-serif;
font-size: 1rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
border: var(--border-medium) solid var(--black);
cursor: pointer;
transition: transform 0.1s, box-shadow 0.1s;
margin-bottom: var(--spacing-sm);
}
.btn:hover {
transform: translate(4px, 4px);
}
.btn:active {
transform: translate(6px, 6px);
box-shadow: none !important;
}
.btn-primary {
background-color: var(--neon-pink);
box-shadow: 6px 6px 0 var(--black);
font-size: 1.2rem;
}
.btn-secondary {
background-color: var(--neon-yellow);
box-shadow: 4px 4px 0 var(--black);
}
.btn-accent {
background-color: var(--white);
box-shadow: 4px 4px 0 var(--black);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Canvas Container */
.canvas-container {
background-color: var(--white);
border: var(--border-medium) solid var(--black);
padding: var(--spacing-md);
margin-bottom: var(--spacing-md);
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
#mazeCanvas {
border: 2px solid var(--black);
}
/* Info Box */
.info-box {
background-color: var(--neon-yellow);
border: var(--border-medium) solid var(--black);
padding: var(--spacing-md);
font-size: 0.9rem;
line-height: 1.8;
}
.info-box strong {
font-weight: 700;
}
/* Results Content */
.results-content {
font-size: 0.9rem;
line-height: 1.8;
}
.placeholder-text {
color: var(--gray-dark);
font-style: italic;
}
.result-section {
background-color: var(--white);
border: var(--border-medium) solid var(--black);
padding: var(--spacing-md);
margin-bottom: var(--spacing-md);
}
.result-section h3 {
font-size: 1.2rem;
margin-bottom: var(--spacing-sm);
text-transform: uppercase;
}
.result-item {
padding: 8px;
border-bottom: 2px solid var(--black);
}
.result-item:last-child {
border-bottom: none;
}
.result-table {
width: 100%;
border-collapse: collapse;
margin-top: var(--spacing-sm);
}
.result-table th,
.result-table td {
padding: 8px;
text-align: left;
border: 2px solid var(--black);
font-size: 0.85rem;
}
.result-table th {
background-color: var(--neon-yellow);
font-weight: 700;
text-transform: uppercase;
}
.result-table tr:nth-child(even) {
background-color: var(--gray-bg);
}
/* Modal */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background-color: var(--white);
border: var(--border-thick) solid var(--black);
box-shadow: 12px 12px 0 var(--neon-pink);
padding: var(--spacing-lg);
max-width: 500px;
width: 90%;
}
.modal-title {
font-size: 1.8rem;
font-weight: 700;
text-transform: uppercase;
margin-bottom: var(--spacing-md);
}
.file-list {
max-height: 300px;
overflow-y: auto;
margin-bottom: var(--spacing-md);
}
.file-item {
padding: 12px;
background-color: var(--gray-bg);
border: var(--border-medium) solid var(--black);
margin-bottom: 8px;
cursor: pointer;
font-weight: 700;
transition: background-color 0.2s;
}
.file-item:hover {
background-color: var(--neon-cyan);
}
/* Status Messages */
.status-message {
padding: var(--spacing-md);
border: var(--border-medium) solid var(--black);
margin-bottom: var(--spacing-md);
font-weight: 700;
}
.status-success {
background-color: var(--neon-green);
}
.status-error {
background-color: var(--neon-pink);
}
.status-info {
background-color: var(--neon-cyan);
}
/* Loading Spinner */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid var(--black);
border-radius: 0;
border-top-color: transparent;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}

385
web/static/js/app.js Normal file
View File

@@ -0,0 +1,385 @@
// Main application logic
const API_BASE = '/api';
let currentMazeId = null;
let currentMazeData = null;
// DOM Elements
const generateBtn = document.getElementById('generateBtn');
const visualizeBtn = document.getElementById('visualizeBtn');
const downloadBtn = document.getElementById('downloadBtn');
const saveBtn = document.getElementById('saveBtn');
const loadBtn = document.getElementById('loadBtn');
const solveDfsBtn = document.getElementById('solveDfsBtn');
const solveBfsBtn = document.getElementById('solveBfsBtn');
const analyzeBtn = document.getElementById('analyzeBtn');
const benchmarkBtn = document.getElementById('benchmarkBtn');
const algorithmSelect = document.getElementById('algorithm');
const rowsInput = document.getElementById('rows');
const colsInput = document.getElementById('cols');
const seedInput = document.getElementById('seed');
const solverSelect = document.getElementById('solver');
const resultsContent = document.getElementById('resultsContent');
const mazeInfo = document.getElementById('mazeInfo');
// Disable buttons initially
function disableActionButtons() {
visualizeBtn.disabled = true;
downloadBtn.disabled = true;
saveBtn.disabled = true;
solveDfsBtn.disabled = true;
solveBfsBtn.disabled = true;
analyzeBtn.disabled = true;
}
function enableActionButtons() {
visualizeBtn.disabled = false;
downloadBtn.disabled = false;
saveBtn.disabled = false;
solveDfsBtn.disabled = false;
solveBfsBtn.disabled = false;
analyzeBtn.disabled = false;
}
disableActionButtons();
// Event Listeners
generateBtn.addEventListener('click', generateMaze);
visualizeBtn.addEventListener('click', visualizeMaze);
downloadBtn.addEventListener('click', downloadMaze);
saveBtn.addEventListener('click', saveMaze);
loadBtn.addEventListener('click', showLoadModal);
solveDfsBtn.addEventListener('click', () => solveMaze('dfs'));
solveBfsBtn.addEventListener('click', () => solveMaze('bfs'));
analyzeBtn.addEventListener('click', analyzeMaze);
benchmarkBtn.addEventListener('click', runBenchmark);
// Generate Maze
async function generateMaze() {
const algorithm = algorithmSelect.value;
const rows = parseInt(rowsInput.value);
const cols = parseInt(colsInput.value);
const seed = seedInput.value ? parseInt(seedInput.value) : null;
if (rows < 5 || rows > 50 || cols < 5 || cols > 50) {
showError('Dimensions must be between 5 and 50');
return;
}
setLoading(generateBtn, true);
try {
const response = await fetch(`${API_BASE}/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ algorithm, rows, cols, seed })
});
const data = await response.json();
if (data.success) {
currentMazeId = data.id;
currentMazeData = data.maze;
enableActionButtons();
showSuccess('Maze generated successfully!');
displayMazeInfo(data.maze);
visualizeMaze();
} else {
showError(data.error || 'Failed to generate maze');
}
} catch (error) {
showError('Network error: ' + error.message);
} finally {
setLoading(generateBtn, false);
}
}
// Visualize Maze
function visualizeMaze() {
if (!currentMazeData) {
showError('No maze to visualize');
return;
}
renderMaze(currentMazeData);
showInfo('Maze visualized');
}
// Download Maze
async function downloadMaze() {
if (currentMazeId === null) {
showError('No maze to download');
return;
}
try {
window.open(`${API_BASE}/download/${currentMazeId}?solution=false`, '_blank');
showSuccess('Downloading maze image...');
} catch (error) {
showError('Failed to download: ' + error.message);
}
}
// Save Maze
async function saveMaze() {
if (currentMazeId === null) {
showError('No maze to save');
return;
}
const filename = prompt('Enter filename:', `maze_${currentMazeId}`);
if (!filename) return;
try {
const response = await fetch(`${API_BASE}/save/${currentMazeId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename })
});
const data = await response.json();
if (data.success) {
showSuccess(`Maze saved to: ${data.filepath}`);
} else {
showError(data.error || 'Failed to save maze');
}
} catch (error) {
showError('Network error: ' + error.message);
}
}
// Load Maze Modal
async function showLoadModal() {
const modal = document.getElementById('loadModal');
const fileList = document.getElementById('fileList');
const closeBtn = document.getElementById('closeModal');
try {
const response = await fetch(`${API_BASE}/saved-mazes`);
const data = await response.json();
if (data.files && data.files.length > 0) {
fileList.innerHTML = data.files.map(file =>
`<div class="file-item" onclick="loadMazeFile('${file}')">${file}</div>`
).join('');
} else {
fileList.innerHTML = '<p class="placeholder-text">No saved mazes found</p>';
}
modal.classList.add('active');
closeBtn.onclick = () => modal.classList.remove('active');
modal.onclick = (e) => {
if (e.target === modal) modal.classList.remove('active');
};
} catch (error) {
showError('Failed to load file list: ' + error.message);
}
}
async function loadMazeFile(filename) {
const modal = document.getElementById('loadModal');
modal.classList.remove('active');
try {
const response = await fetch(`${API_BASE}/load`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename })
});
const data = await response.json();
if (data.success) {
currentMazeId = data.id;
currentMazeData = data.maze;
enableActionButtons();
showSuccess(`Loaded: ${filename}`);
displayMazeInfo(data.maze);
visualizeMaze();
} else {
showError(data.error || 'Failed to load maze');
}
} catch (error) {
showError('Network error: ' + error.message);
}
}
// Solve Maze
async function solveMaze(algorithm) {
if (currentMazeId === null) {
showError('No maze to solve');
return;
}
const btn = algorithm === 'dfs' ? solveDfsBtn : solveBfsBtn;
setLoading(btn, true);
try {
const response = await fetch(`${API_BASE}/solve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ maze_id: currentMazeId, algorithm })
});
const data = await response.json();
if (data.success) {
renderMaze(currentMazeData, data.visited, data.path);
displaySolutionInfo(data);
showSuccess(`Maze solved using ${data.algorithm}!`);
} else {
showError('Failed to solve maze');
}
} catch (error) {
showError('Network error: ' + error.message);
} finally {
setLoading(btn, false);
}
}
// Analyze Maze
async function analyzeMaze() {
if (currentMazeId === null) {
showError('No maze to analyze');
return;
}
setLoading(analyzeBtn, true);
try {
const response = await fetch(`${API_BASE}/analyze/${currentMazeId}`);
const data = await response.json();
displayAnalysisResults(data);
showSuccess('Analysis complete!');
} catch (error) {
showError('Network error: ' + error.message);
} finally {
setLoading(analyzeBtn, false);
}
}
// Run Benchmark
async function runBenchmark() {
setLoading(benchmarkBtn, true);
showInfo('Running benchmarks... This may take a moment.');
try {
const response = await fetch(`${API_BASE}/benchmark`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'quick' })
});
const data = await response.json();
displayBenchmarkResults(data);
showSuccess('Benchmark complete!');
} catch (error) {
showError('Network error: ' + error.message);
} finally {
setLoading(benchmarkBtn, false);
}
}
// Display Functions
function displayMazeInfo(maze) {
mazeInfo.innerHTML = `
<strong>ALGORITHM:</strong> ${maze.algorithm}<br>
<strong>SIZE:</strong> ${maze.rows} × ${maze.cols}<br>
<strong>GENERATION TIME:</strong> ${maze.generationTime.toFixed(2)} ms
`;
}
function displaySolutionInfo(solution) {
const section = document.createElement('div');
section.className = 'result-section';
section.innerHTML = `
<h3>SOLUTION: ${solution.algorithm}</h3>
<div class="result-item"><strong>Path Length:</strong> ${solution.path_length} cells</div>
<div class="result-item"><strong>Cells Visited:</strong> ${solution.visited.length}</div>
<div class="result-item"><strong>Solve Time:</strong> ${solution.time_ms.toFixed(2)} ms</div>
`;
resultsContent.innerHTML = '';
resultsContent.appendChild(section);
}
function displayAnalysisResults(analysis) {
const section = document.createElement('div');
section.className = 'result-section';
section.innerHTML = `
<h3>MAZE ANALYSIS</h3>
<div class="result-item"><strong>Dimensions:</strong> ${analysis.dimensions}</div>
<div class="result-item"><strong>Total Cells:</strong> ${analysis.total_cells}</div>
<div class="result-item"><strong>Algorithm:</strong> ${analysis.algorithm}</div>
<div class="result-item"><strong>Dead Ends:</strong> ${analysis.dead_ends} (${analysis.dead_end_percentage.toFixed(1)}%)</div>
<div class="result-item"><strong>Longest Path:</strong> ${analysis.longest_path_length} cells</div>
<div class="result-item"><strong>Avg Branching:</strong> ${analysis.average_branching_factor.toFixed(2)}</div>
`;
resultsContent.innerHTML = '';
resultsContent.appendChild(section);
}
function displayBenchmarkResults(data) {
let html = '<div class="result-section"><h3>GENERATOR BENCHMARK</h3>';
html += '<table class="result-table"><tr><th>Algorithm</th><th>Size</th><th>Avg Time (ms)</th></tr>';
data.generators.results.forEach(r => {
html += `<tr><td>${r.algorithm}</td><td>${r.size}</td><td>${r.avg_time_ms}</td></tr>`;
});
html += '</table></div>';
html += '<div class="result-section"><h3>SOLVER BENCHMARK</h3>';
html += '<table class="result-table"><tr><th>Algorithm</th><th>Size</th><th>Avg Time (ms)</th><th>Path Length</th></tr>';
data.solvers.results.forEach(r => {
html += `<tr><td>${r.algorithm}</td><td>${r.size}</td><td>${r.avg_time_ms}</td><td>${r.avg_path_length}</td></tr>`;
});
html += '</table></div>';
resultsContent.innerHTML = html;
}
// Utility Functions
function showSuccess(message) {
showStatus(message, 'success');
}
function showError(message) {
showStatus(message, 'error');
}
function showInfo(message) {
showStatus(message, 'info');
}
function showStatus(message, type) {
const existing = document.querySelector('.status-message');
if (existing) existing.remove();
const div = document.createElement('div');
div.className = `status-message status-${type}`;
div.textContent = message;
resultsContent.insertBefore(div, resultsContent.firstChild);
setTimeout(() => div.remove(), 5000);
}
function setLoading(button, isLoading) {
if (isLoading) {
button.dataset.originalText = button.textContent;
button.textContent = 'LOADING...';
button.disabled = true;
} else {
button.textContent = button.dataset.originalText || button.textContent;
button.disabled = false;
}
}

125
web/static/js/visualizer.js Normal file
View File

@@ -0,0 +1,125 @@
// Canvas visualization for mazes
const canvas = document.getElementById('mazeCanvas');
const ctx = canvas.getContext('2d');
const COLORS = {
wall: '#000000',
background: '#FFFFFF',
start: '#FFE500', // Neon yellow
end: '#FF10F0', // Neon pink
solution: '#39FF14', // Neon green
visited: '#E0E0E0' // Light gray
};
function renderMaze(mazeData, visitedCells = null, solutionPath = null) {
const rows = mazeData.rows;
const cols = mazeData.cols;
const cellSize = Math.min(Math.floor(600 / Math.max(rows, cols)), 40);
const wallThickness = Math.max(2, Math.floor(cellSize / 10));
// Set canvas size
canvas.width = cols * cellSize + wallThickness;
canvas.height = rows * cellSize + wallThickness;
// Clear canvas
ctx.fillStyle = COLORS.background;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Create sets for quick lookup
const visitedSet = new Set();
if (visitedCells) {
visitedCells.forEach(([r, c]) => visitedSet.add(`${r},${c}`));
}
const solutionSet = new Set();
if (solutionPath) {
solutionPath.forEach(([r, c]) => solutionSet.add(`${r},${c}`));
}
// Draw cell backgrounds
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const x = col * cellSize + wallThickness;
const y = row * cellSize + wallThickness;
const key = `${row},${col}`;
// Determine cell color
let color = COLORS.background;
if (row === mazeData.start[0] && col === mazeData.start[1]) {
color = COLORS.start;
} else if (row === mazeData.end[0] && col === mazeData.end[1]) {
color = COLORS.end;
} else if (solutionSet.has(key)) {
color = COLORS.solution;
} else if (visitedSet.has(key)) {
color = COLORS.visited;
}
ctx.fillStyle = color;
ctx.fillRect(x, y, cellSize - 1, cellSize - 1);
}
}
// Draw walls
ctx.strokeStyle = COLORS.wall;
ctx.lineWidth = wallThickness;
ctx.lineCap = 'square';
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const cellWalls = mazeData.walls[row][col];
const x = col * cellSize + wallThickness / 2;
const y = row * cellSize + wallThickness / 2;
// Draw north wall
if (cellWalls.north) {
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + cellSize, y);
ctx.stroke();
}
// Draw south wall
if (cellWalls.south) {
ctx.beginPath();
ctx.moveTo(x, y + cellSize);
ctx.lineTo(x + cellSize, y + cellSize);
ctx.stroke();
}
// Draw west wall
if (cellWalls.west) {
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x, y + cellSize);
ctx.stroke();
}
// Draw east wall
if (cellWalls.east) {
ctx.beginPath();
ctx.moveTo(x + cellSize, y);
ctx.lineTo(x + cellSize, y + cellSize);
ctx.stroke();
}
}
}
// Draw start and end markers with text
drawMarker(mazeData.start[0], mazeData.start[1], 'S', cellSize, wallThickness);
drawMarker(mazeData.end[0], mazeData.end[1], 'E', cellSize, wallThickness);
}
function drawMarker(row, col, text, cellSize, wallThickness) {
const x = col * cellSize + wallThickness + cellSize / 2;
const y = row * cellSize + wallThickness + cellSize / 2;
ctx.fillStyle = '#000000';
ctx.font = `bold ${Math.max(12, cellSize / 2)}px Space Grotesk, monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, x, y);
}

127
web/templates/index.html Normal file
View File

@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MAZE GENERATOR</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/styles.css">
</head>
<body>
<div class="container">
<header class="header">
<h1 class="title">MAZE GENERATOR</h1>
<p class="subtitle">8 ALGORITHMS • INFINITE POSSIBILITIES</p>
</header>
<div class="main-grid">
<!-- Control Panel -->
<div class="panel control-panel">
<h2 class="panel-title">CONTROLS</h2>
<!-- Generation Controls -->
<div class="control-group">
<label class="label">ALGORITHM</label>
<select id="algorithm" class="select-input">
<option value="recursive_backtracking">Recursive Backtracking</option>
<option value="kruskal">Kruskal's Algorithm</option>
<option value="prim">Prim's Algorithm</option>
<option value="sidewinder">Sidewinder</option>
<option value="hunt_and_kill">Hunt & Kill</option>
<option value="eller">Eller's Algorithm</option>
<option value="wilson">Wilson's Algorithm</option>
<option value="aldous_broder">Aldous-Broder</option>
</select>
</div>
<div class="control-row">
<div class="control-group">
<label class="label">ROWS</label>
<input type="number" id="rows" class="text-input" min="5" max="50" value="15">
</div>
<div class="control-group">
<label class="label">COLS</label>
<input type="number" id="cols" class="text-input" min="5" max="50" value="15">
</div>
</div>
<div class="control-group">
<label class="label">SEED (OPTIONAL)</label>
<input type="number" id="seed" class="text-input" placeholder="Random">
</div>
<button id="generateBtn" class="btn btn-primary">
1. GENERATE MAZE
</button>
<!-- Solving Controls -->
<div class="control-group">
<label class="label">SOLVER</label>
<select id="solver" class="select-input">
<option value="dfs">Depth-First Search</option>
<option value="bfs">Breadth-First Search</option>
</select>
</div>
<button id="solveDfsBtn" class="btn btn-secondary">
6. SOLVE (DFS)
</button>
<button id="solveBfsBtn" class="btn btn-secondary">
7. SOLVE (BFS)
</button>
<!-- Action Buttons -->
<button id="visualizeBtn" class="btn btn-accent">
2. VISUALIZE
</button>
<button id="downloadBtn" class="btn btn-accent">
3. DOWNLOAD IMAGE
</button>
<button id="saveBtn" class="btn btn-accent">
4. SAVE TO FILE
</button>
<button id="loadBtn" class="btn btn-accent">
5. LOAD FROM FILE
</button>
<button id="analyzeBtn" class="btn btn-accent">
8. ANALYZE MAZE
</button>
<button id="benchmarkBtn" class="btn btn-accent">
9. BENCHMARK
</button>
</div>
<!-- Visualization Area -->
<div class="panel viz-panel">
<h2 class="panel-title">VISUALIZATION</h2>
<div class="canvas-container">
<canvas id="mazeCanvas"></canvas>
</div>
<div id="mazeInfo" class="info-box"></div>
</div>
<!-- Results Panel -->
<div class="panel results-panel">
<h2 class="panel-title">RESULTS</h2>
<div id="resultsContent" class="results-content">
<p class="placeholder-text">Generate a maze to see results...</p>
</div>
</div>
</div>
</div>
<!-- Load Modal -->
<div id="loadModal" class="modal">
<div class="modal-content">
<h2 class="modal-title">LOAD MAZE</h2>
<div id="fileList" class="file-list"></div>
<button id="closeModal" class="btn btn-secondary">CLOSE</button>
</div>
</div>
<script src="/static/js/app.js"></script>
<script src="/static/js/visualizer.js"></script>
</body>
</html>