Initial commit
This commit is contained in:
9
.claude/settings.local.json
Normal file
9
.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(python:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
25
.dockerignore
Normal file
25
.dockerignore
Normal 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
141
.gitignore
vendored
Normal 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
395
PLAN.md
Normal 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
283
README.md
Normal 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.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## 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
1
api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Web API for maze generation and solving."""
|
||||
296
api/app.py
Normal file
296
api/app.py
Normal 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
46
docker/Dockerfile
Normal 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
29
docker/docker-compose.yml
Normal 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
11
pytest.ini
Normal 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
5
requirements-dev.txt
Normal 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
5
requirements.txt
Normal 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
3
src/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Maze Generator - A comprehensive maze generation and solving application."""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
6
src/analysis/__init__.py
Normal file
6
src/analysis/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Analysis and benchmarking tools."""
|
||||
|
||||
from .analyzer import MazeAnalyzer
|
||||
from .benchmark import Benchmark
|
||||
|
||||
__all__ = ["MazeAnalyzer", "Benchmark"]
|
||||
148
src/analysis/analyzer.py
Normal file
148
src/analysis/analyzer.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""Maze analysis tools for calculating statistics and metrics."""
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from ..core.cell import Cell
|
||||
from ..core.maze import Maze
|
||||
|
||||
|
||||
class MazeAnalyzer:
|
||||
"""Analyzes mazes to compute various metrics and statistics."""
|
||||
|
||||
@staticmethod
|
||||
def analyze(maze: Maze) -> Dict:
|
||||
"""Perform comprehensive analysis of a maze.
|
||||
|
||||
Args:
|
||||
maze: Maze to analyze
|
||||
|
||||
Returns:
|
||||
Dictionary with analysis results
|
||||
"""
|
||||
dead_ends = MazeAnalyzer.count_dead_ends(maze)
|
||||
longest_path_info = MazeAnalyzer.find_longest_path(maze)
|
||||
branching_factor = MazeAnalyzer.calculate_branching_factor(maze)
|
||||
|
||||
return {
|
||||
'dimensions': f"{maze.rows}x{maze.cols}",
|
||||
'total_cells': maze.rows * maze.cols,
|
||||
'algorithm': maze.algorithm_used,
|
||||
'generation_time_ms': maze.generation_time_ms,
|
||||
'seed': maze.seed,
|
||||
'dead_ends': dead_ends,
|
||||
'dead_end_percentage': (dead_ends / (maze.rows * maze.cols)) * 100,
|
||||
'longest_path_length': longest_path_info['length'],
|
||||
'longest_path_start': longest_path_info['start'],
|
||||
'longest_path_end': longest_path_info['end'],
|
||||
'average_branching_factor': branching_factor
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def count_dead_ends(maze: Maze) -> int:
|
||||
"""Count the number of dead ends in the maze.
|
||||
|
||||
A dead end is a cell with only one open passage.
|
||||
|
||||
Args:
|
||||
maze: Maze to analyze
|
||||
|
||||
Returns:
|
||||
Number of dead ends
|
||||
"""
|
||||
dead_end_count = 0
|
||||
|
||||
for row in maze.grid:
|
||||
for cell in row:
|
||||
# Count open passages
|
||||
open_passages = sum(1 for wall in cell.walls.values() if not wall)
|
||||
|
||||
if open_passages == 1:
|
||||
dead_end_count += 1
|
||||
|
||||
return dead_end_count
|
||||
|
||||
@staticmethod
|
||||
def find_longest_path(maze: Maze) -> Dict:
|
||||
"""Find the longest path in the maze.
|
||||
|
||||
Args:
|
||||
maze: Maze to analyze
|
||||
|
||||
Returns:
|
||||
Dictionary with longest path info
|
||||
"""
|
||||
max_length = 0
|
||||
max_start = (0, 0)
|
||||
max_end = (0, 0)
|
||||
|
||||
# Try BFS from each cell to find longest path
|
||||
for start_row in maze.grid:
|
||||
for start_cell in start_row:
|
||||
maze.reset_visited()
|
||||
distances = MazeAnalyzer._bfs_distances(maze, start_cell)
|
||||
|
||||
for end_cell, distance in distances.items():
|
||||
if distance > max_length:
|
||||
max_length = distance
|
||||
max_start = (start_cell.row, start_cell.col)
|
||||
max_end = (end_cell.row, end_cell.col)
|
||||
|
||||
maze.reset_visited()
|
||||
return {
|
||||
'length': max_length,
|
||||
'start': max_start,
|
||||
'end': max_end
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _bfs_distances(maze: Maze, start: Cell) -> Dict[Cell, int]:
|
||||
"""Calculate distances from start cell using BFS.
|
||||
|
||||
Args:
|
||||
maze: The maze
|
||||
start: Starting cell
|
||||
|
||||
Returns:
|
||||
Dictionary mapping cells to their distance from start
|
||||
"""
|
||||
from collections import deque
|
||||
|
||||
queue = deque([(start, 0)])
|
||||
distances = {start: 0}
|
||||
start.visited = True
|
||||
|
||||
while queue:
|
||||
current, dist = queue.popleft()
|
||||
|
||||
neighbors = maze.get_neighbors(current)
|
||||
for neighbor, direction in neighbors:
|
||||
if not neighbor.visited and not current.has_wall(direction):
|
||||
neighbor.visited = True
|
||||
distances[neighbor] = dist + 1
|
||||
queue.append((neighbor, dist + 1))
|
||||
|
||||
return distances
|
||||
|
||||
@staticmethod
|
||||
def calculate_branching_factor(maze: Maze) -> float:
|
||||
"""Calculate the average branching factor of the maze.
|
||||
|
||||
Branching factor is the average number of passages per cell.
|
||||
|
||||
Args:
|
||||
maze: Maze to analyze
|
||||
|
||||
Returns:
|
||||
Average branching factor
|
||||
"""
|
||||
total_passages = 0
|
||||
cell_count = 0
|
||||
|
||||
for row in maze.grid:
|
||||
for cell in row:
|
||||
# Count open passages
|
||||
open_passages = sum(1 for wall in cell.walls.values() if not wall)
|
||||
total_passages += open_passages
|
||||
cell_count += 1
|
||||
|
||||
return total_passages / cell_count if cell_count > 0 else 0
|
||||
172
src/analysis/benchmark.py
Normal file
172
src/analysis/benchmark.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""Benchmarking tools for comparing maze algorithms."""
|
||||
|
||||
import time
|
||||
from typing import Dict, List
|
||||
|
||||
from ..generators import (
|
||||
RecursiveBacktrackingGenerator,
|
||||
KruskalGenerator,
|
||||
PrimGenerator,
|
||||
SidewinderGenerator,
|
||||
HuntAndKillGenerator,
|
||||
EllerGenerator,
|
||||
WilsonGenerator,
|
||||
AldousBroderGenerator
|
||||
)
|
||||
from ..solvers import DFSSolver, BFSSolver
|
||||
|
||||
|
||||
class Benchmark:
|
||||
"""Benchmarks maze generation and solving algorithms."""
|
||||
|
||||
# All available generators
|
||||
GENERATORS = [
|
||||
RecursiveBacktrackingGenerator(),
|
||||
KruskalGenerator(),
|
||||
PrimGenerator(),
|
||||
SidewinderGenerator(),
|
||||
HuntAndKillGenerator(),
|
||||
EllerGenerator(),
|
||||
WilsonGenerator(),
|
||||
AldousBroderGenerator()
|
||||
]
|
||||
|
||||
# All available solvers
|
||||
SOLVERS = [
|
||||
DFSSolver(),
|
||||
BFSSolver()
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def benchmark_generators(
|
||||
sizes: List[tuple] = None,
|
||||
iterations: int = 5,
|
||||
seed: int = 42
|
||||
) -> Dict:
|
||||
"""Benchmark all maze generation algorithms.
|
||||
|
||||
Args:
|
||||
sizes: List of (rows, cols) tuples to test
|
||||
iterations: Number of iterations per configuration
|
||||
seed: Base seed for reproducibility
|
||||
|
||||
Returns:
|
||||
Dictionary with benchmark results
|
||||
"""
|
||||
if sizes is None:
|
||||
sizes = [(5, 5), (10, 10), (25, 25), (50, 50)]
|
||||
|
||||
results = []
|
||||
|
||||
for rows, cols in sizes:
|
||||
for generator in Benchmark.GENERATORS:
|
||||
times = []
|
||||
|
||||
for i in range(iterations):
|
||||
current_seed = seed + i
|
||||
maze = generator.generate(rows, cols, current_seed)
|
||||
times.append(maze.generation_time_ms)
|
||||
|
||||
avg_time = sum(times) / len(times)
|
||||
min_time = min(times)
|
||||
max_time = max(times)
|
||||
|
||||
results.append({
|
||||
'algorithm': generator.name,
|
||||
'size': f"{rows}x{cols}",
|
||||
'rows': rows,
|
||||
'cols': cols,
|
||||
'iterations': iterations,
|
||||
'avg_time_ms': round(avg_time, 3),
|
||||
'min_time_ms': round(min_time, 3),
|
||||
'max_time_ms': round(max_time, 3),
|
||||
'times': [round(t, 3) for t in times]
|
||||
})
|
||||
|
||||
return {
|
||||
'benchmark_type': 'generators',
|
||||
'sizes_tested': sizes,
|
||||
'iterations_per_config': iterations,
|
||||
'results': results
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def benchmark_solvers(
|
||||
sizes: List[tuple] = None,
|
||||
iterations: int = 5,
|
||||
seed: int = 42
|
||||
) -> Dict:
|
||||
"""Benchmark all maze solving algorithms.
|
||||
|
||||
Args:
|
||||
sizes: List of (rows, cols) tuples to test
|
||||
iterations: Number of iterations per configuration
|
||||
seed: Base seed for reproducibility
|
||||
|
||||
Returns:
|
||||
Dictionary with benchmark results
|
||||
"""
|
||||
if sizes is None:
|
||||
sizes = [(5, 5), (10, 10), (25, 25), (50, 50)]
|
||||
|
||||
results = []
|
||||
|
||||
# Use one generator for consistency
|
||||
generator = RecursiveBacktrackingGenerator()
|
||||
|
||||
for rows, cols in sizes:
|
||||
# Generate maze once per size
|
||||
maze = generator.generate(rows, cols, seed)
|
||||
|
||||
for solver in Benchmark.SOLVERS:
|
||||
times = []
|
||||
path_lengths = []
|
||||
|
||||
for i in range(iterations):
|
||||
result = solver.solve(maze)
|
||||
times.append(result['time_ms'])
|
||||
path_lengths.append(result['path_length'])
|
||||
|
||||
avg_time = sum(times) / len(times)
|
||||
min_time = min(times)
|
||||
max_time = max(times)
|
||||
avg_path_length = sum(path_lengths) / len(path_lengths)
|
||||
|
||||
results.append({
|
||||
'algorithm': solver.name,
|
||||
'size': f"{rows}x{cols}",
|
||||
'rows': rows,
|
||||
'cols': cols,
|
||||
'iterations': iterations,
|
||||
'avg_time_ms': round(avg_time, 3),
|
||||
'min_time_ms': round(min_time, 3),
|
||||
'max_time_ms': round(max_time, 3),
|
||||
'avg_path_length': round(avg_path_length, 1),
|
||||
'times': [round(t, 3) for t in times]
|
||||
})
|
||||
|
||||
return {
|
||||
'benchmark_type': 'solvers',
|
||||
'sizes_tested': sizes,
|
||||
'iterations_per_config': iterations,
|
||||
'maze_generator': generator.name,
|
||||
'results': results
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def quick_benchmark() -> Dict:
|
||||
"""Run a quick benchmark with default settings.
|
||||
|
||||
Returns:
|
||||
Dictionary with benchmark results for both generators and solvers
|
||||
"""
|
||||
return {
|
||||
'generators': Benchmark.benchmark_generators(
|
||||
sizes=[(10, 10), (25, 25)],
|
||||
iterations=3
|
||||
),
|
||||
'solvers': Benchmark.benchmark_solvers(
|
||||
sizes=[(10, 10), (25, 25)],
|
||||
iterations=3
|
||||
)
|
||||
}
|
||||
6
src/core/__init__.py
Normal file
6
src/core/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Core maze data structures."""
|
||||
|
||||
from .cell import Cell
|
||||
from .maze import Maze
|
||||
|
||||
__all__ = ["Cell", "Maze"]
|
||||
103
src/core/cell.py
Normal file
103
src/core/cell.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Cell class representing a single cell in the maze."""
|
||||
|
||||
from typing import Dict, Set
|
||||
|
||||
|
||||
class Cell:
|
||||
"""Represents a single cell in the maze grid.
|
||||
|
||||
Each cell has four walls (north, south, east, west) and can be visited
|
||||
during maze generation or solving algorithms.
|
||||
"""
|
||||
|
||||
def __init__(self, row: int, col: int):
|
||||
"""Initialize a cell at the given position.
|
||||
|
||||
Args:
|
||||
row: Row index in the maze grid
|
||||
col: Column index in the maze grid
|
||||
"""
|
||||
self.row = row
|
||||
self.col = col
|
||||
self.walls = {"north": True, "south": True, "east": True, "west": True}
|
||||
self.visited = False
|
||||
|
||||
def remove_wall(self, direction: str) -> None:
|
||||
"""Remove a wall in the specified direction.
|
||||
|
||||
Args:
|
||||
direction: One of 'north', 'south', 'east', 'west'
|
||||
"""
|
||||
if direction in self.walls:
|
||||
self.walls[direction] = False
|
||||
|
||||
def has_wall(self, direction: str) -> bool:
|
||||
"""Check if the cell has a wall in the specified direction.
|
||||
|
||||
Args:
|
||||
direction: One of 'north', 'south', 'east', 'west'
|
||||
|
||||
Returns:
|
||||
True if wall exists, False otherwise
|
||||
"""
|
||||
return self.walls.get(direction, True)
|
||||
|
||||
def get_neighbors_directions(self) -> Dict[str, tuple]:
|
||||
"""Get relative positions of neighbors in each direction.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping direction to (row_offset, col_offset)
|
||||
"""
|
||||
return {
|
||||
"north": (-1, 0),
|
||||
"south": (1, 0),
|
||||
"east": (0, 1),
|
||||
"west": (0, -1)
|
||||
}
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the cell to its initial state."""
|
||||
self.walls = {"north": True, "south": True, "east": True, "west": True}
|
||||
self.visited = False
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""Convert cell to dictionary representation.
|
||||
|
||||
Returns:
|
||||
Dictionary with cell data
|
||||
"""
|
||||
return {
|
||||
"row": self.row,
|
||||
"col": self.col,
|
||||
"walls": self.walls.copy(),
|
||||
"visited": self.visited
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: Dict) -> 'Cell':
|
||||
"""Create a cell from dictionary representation.
|
||||
|
||||
Args:
|
||||
data: Dictionary with cell data
|
||||
|
||||
Returns:
|
||||
New Cell instance
|
||||
"""
|
||||
cell = Cell(data["row"], data["col"])
|
||||
cell.walls = data["walls"].copy()
|
||||
cell.visited = data["visited"]
|
||||
return cell
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of the cell."""
|
||||
return f"Cell({self.row}, {self.col})"
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
"""Check equality with another cell."""
|
||||
if not isinstance(other, Cell):
|
||||
return False
|
||||
return self.row == other.row and self.col == other.col
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Hash function for using cells in sets/dicts."""
|
||||
return hash((self.row, self.col))
|
||||
193
src/core/maze.py
Normal file
193
src/core/maze.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""Maze class representing the complete maze structure."""
|
||||
|
||||
import json
|
||||
import random
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from .cell import Cell
|
||||
|
||||
|
||||
class Maze:
|
||||
"""Represents a complete maze with a grid of cells.
|
||||
|
||||
The maze maintains a 2D grid of cells and provides methods for
|
||||
manipulation, serialization, and access to maze properties.
|
||||
"""
|
||||
|
||||
def __init__(self, rows: int, cols: int, seed: Optional[int] = None):
|
||||
"""Initialize a maze with the given dimensions.
|
||||
|
||||
Args:
|
||||
rows: Number of rows (must be between 5 and 50)
|
||||
cols: Number of columns (must be between 5 and 50)
|
||||
seed: Random seed for reproducible generation (optional)
|
||||
|
||||
Raises:
|
||||
ValueError: If dimensions are out of valid range
|
||||
"""
|
||||
if not (5 <= rows <= 50) or not (5 <= cols <= 50):
|
||||
raise ValueError("Maze dimensions must be between 5 and 50")
|
||||
|
||||
self.rows = rows
|
||||
self.cols = cols
|
||||
self.seed = seed if seed is not None else random.randint(0, 1000000)
|
||||
self.grid: List[List[Cell]] = []
|
||||
self.start: Tuple[int, int] = (0, 0)
|
||||
self.end: Tuple[int, int] = (rows - 1, cols - 1)
|
||||
self.generation_time_ms: float = 0.0
|
||||
self.algorithm_used: str = ""
|
||||
|
||||
# Initialize the grid
|
||||
self._initialize_grid()
|
||||
|
||||
def _initialize_grid(self) -> None:
|
||||
"""Initialize the grid with cells."""
|
||||
self.grid = []
|
||||
for row in range(self.rows):
|
||||
row_cells = []
|
||||
for col in range(self.cols):
|
||||
row_cells.append(Cell(row, col))
|
||||
self.grid.append(row_cells)
|
||||
|
||||
def get_cell(self, row: int, col: int) -> Optional[Cell]:
|
||||
"""Get the cell at the specified position.
|
||||
|
||||
Args:
|
||||
row: Row index
|
||||
col: Column index
|
||||
|
||||
Returns:
|
||||
Cell at the position, or None if out of bounds
|
||||
"""
|
||||
if 0 <= row < self.rows and 0 <= col < self.cols:
|
||||
return self.grid[row][col]
|
||||
return None
|
||||
|
||||
def get_neighbors(self, cell: Cell) -> List[Tuple[Cell, str]]:
|
||||
"""Get all valid neighbors of a cell.
|
||||
|
||||
Args:
|
||||
cell: The cell to find neighbors for
|
||||
|
||||
Returns:
|
||||
List of tuples (neighbor_cell, direction)
|
||||
"""
|
||||
neighbors = []
|
||||
directions = cell.get_neighbors_directions()
|
||||
|
||||
for direction, (dr, dc) in directions.items():
|
||||
neighbor_row = cell.row + dr
|
||||
neighbor_col = cell.col + dc
|
||||
neighbor = self.get_cell(neighbor_row, neighbor_col)
|
||||
|
||||
if neighbor is not None:
|
||||
neighbors.append((neighbor, direction))
|
||||
|
||||
return neighbors
|
||||
|
||||
def remove_wall_between(self, cell1: Cell, cell2: Cell) -> None:
|
||||
"""Remove the wall between two adjacent cells.
|
||||
|
||||
Args:
|
||||
cell1: First cell
|
||||
cell2: Second cell
|
||||
"""
|
||||
row_diff = cell2.row - cell1.row
|
||||
col_diff = cell2.col - cell1.col
|
||||
|
||||
# Determine direction and remove walls
|
||||
if row_diff == -1: # cell2 is north of cell1
|
||||
cell1.remove_wall("north")
|
||||
cell2.remove_wall("south")
|
||||
elif row_diff == 1: # cell2 is south of cell1
|
||||
cell1.remove_wall("south")
|
||||
cell2.remove_wall("north")
|
||||
elif col_diff == -1: # cell2 is west of cell1
|
||||
cell1.remove_wall("west")
|
||||
cell2.remove_wall("east")
|
||||
elif col_diff == 1: # cell2 is east of cell1
|
||||
cell1.remove_wall("east")
|
||||
cell2.remove_wall("west")
|
||||
|
||||
def reset_visited(self) -> None:
|
||||
"""Reset the visited flag for all cells."""
|
||||
for row in self.grid:
|
||||
for cell in row:
|
||||
cell.visited = False
|
||||
|
||||
def is_valid_position(self, row: int, col: int) -> bool:
|
||||
"""Check if a position is within the maze bounds.
|
||||
|
||||
Args:
|
||||
row: Row index
|
||||
col: Column index
|
||||
|
||||
Returns:
|
||||
True if position is valid, False otherwise
|
||||
"""
|
||||
return 0 <= row < self.rows and 0 <= col < self.cols
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""Convert maze to dictionary representation.
|
||||
|
||||
Returns:
|
||||
Dictionary with complete maze data
|
||||
"""
|
||||
return {
|
||||
"rows": self.rows,
|
||||
"cols": self.cols,
|
||||
"seed": self.seed,
|
||||
"start": self.start,
|
||||
"end": self.end,
|
||||
"generation_time_ms": self.generation_time_ms,
|
||||
"algorithm_used": self.algorithm_used,
|
||||
"grid": [[cell.to_dict() for cell in row] for row in self.grid]
|
||||
}
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Convert maze to JSON string.
|
||||
|
||||
Returns:
|
||||
JSON string representation
|
||||
"""
|
||||
return json.dumps(self.to_dict(), indent=2)
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: Dict) -> 'Maze':
|
||||
"""Create a maze from dictionary representation.
|
||||
|
||||
Args:
|
||||
data: Dictionary with maze data
|
||||
|
||||
Returns:
|
||||
New Maze instance
|
||||
"""
|
||||
maze = Maze(data["rows"], data["cols"], data["seed"])
|
||||
maze.start = tuple(data["start"])
|
||||
maze.end = tuple(data["end"])
|
||||
maze.generation_time_ms = data.get("generation_time_ms", 0.0)
|
||||
maze.algorithm_used = data.get("algorithm_used", "")
|
||||
|
||||
# Restore grid
|
||||
for row_idx, row_data in enumerate(data["grid"]):
|
||||
for col_idx, cell_data in enumerate(row_data):
|
||||
maze.grid[row_idx][col_idx] = Cell.from_dict(cell_data)
|
||||
|
||||
return maze
|
||||
|
||||
@staticmethod
|
||||
def from_json(json_str: str) -> 'Maze':
|
||||
"""Create a maze from JSON string.
|
||||
|
||||
Args:
|
||||
json_str: JSON string with maze data
|
||||
|
||||
Returns:
|
||||
New Maze instance
|
||||
"""
|
||||
data = json.loads(json_str)
|
||||
return Maze.from_dict(data)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of the maze."""
|
||||
return f"Maze({self.rows}x{self.cols}, seed={self.seed}, algorithm={self.algorithm_used})"
|
||||
23
src/generators/__init__.py
Normal file
23
src/generators/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Maze generation algorithms."""
|
||||
|
||||
from .base import BaseGenerator
|
||||
from .recursive_backtracking import RecursiveBacktrackingGenerator
|
||||
from .kruskal import KruskalGenerator
|
||||
from .prim import PrimGenerator
|
||||
from .sidewinder import SidewinderGenerator
|
||||
from .hunt_and_kill import HuntAndKillGenerator
|
||||
from .eller import EllerGenerator
|
||||
from .wilson import WilsonGenerator
|
||||
from .aldous_broder import AldousBroderGenerator
|
||||
|
||||
__all__ = [
|
||||
"BaseGenerator",
|
||||
"RecursiveBacktrackingGenerator",
|
||||
"KruskalGenerator",
|
||||
"PrimGenerator",
|
||||
"SidewinderGenerator",
|
||||
"HuntAndKillGenerator",
|
||||
"EllerGenerator",
|
||||
"WilsonGenerator",
|
||||
"AldousBroderGenerator"
|
||||
]
|
||||
51
src/generators/aldous_broder.py
Normal file
51
src/generators/aldous_broder.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Aldous-Broder Algorithm for maze generation."""
|
||||
|
||||
import random
|
||||
|
||||
from ..core.maze import Maze
|
||||
from .base import BaseGenerator
|
||||
|
||||
|
||||
class AldousBroderGenerator(BaseGenerator):
|
||||
"""Generates mazes using the Aldous-Broder Algorithm.
|
||||
|
||||
This algorithm performs a random walk, carving passages to unvisited cells.
|
||||
Very slow but generates truly uniform spanning trees. Can be inefficient
|
||||
for large mazes as it continues random walking even when few cells remain.
|
||||
|
||||
Time Complexity: O(rows * cols * log(rows * cols)) expected
|
||||
Space Complexity: O(1)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Aldous-Broder generator."""
|
||||
super().__init__("Aldous-Broder Algorithm")
|
||||
|
||||
def _generate_maze(self, maze: Maze) -> None:
|
||||
"""Generate maze using Aldous-Broder algorithm.
|
||||
|
||||
Args:
|
||||
maze: Maze instance to generate
|
||||
"""
|
||||
# Start from random cell
|
||||
current_cell = maze.grid[random.randint(0, maze.rows - 1)][random.randint(0, maze.cols - 1)]
|
||||
current_cell.visited = True
|
||||
unvisited_count = maze.rows * maze.cols - 1
|
||||
|
||||
# Continue until all cells are visited
|
||||
while unvisited_count > 0:
|
||||
# Get all neighbors
|
||||
neighbors = maze.get_neighbors(current_cell)
|
||||
|
||||
if neighbors:
|
||||
# Choose a random neighbor
|
||||
next_cell, direction = random.choice(neighbors)
|
||||
|
||||
# If neighbor hasn't been visited, carve passage
|
||||
if not next_cell.visited:
|
||||
maze.remove_wall_between(current_cell, next_cell)
|
||||
next_cell.visited = True
|
||||
unvisited_count -= 1
|
||||
|
||||
# Move to neighbor (regardless of whether it was visited)
|
||||
current_cell = next_cell
|
||||
68
src/generators/base.py
Normal file
68
src/generators/base.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""Base class for maze generation algorithms."""
|
||||
|
||||
import random
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
|
||||
from ..core.maze import Maze
|
||||
|
||||
|
||||
class BaseGenerator(ABC):
|
||||
"""Abstract base class for maze generation algorithms.
|
||||
|
||||
All maze generation algorithms should inherit from this class
|
||||
and implement the generate method.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
"""Initialize the generator.
|
||||
|
||||
Args:
|
||||
name: Name of the algorithm
|
||||
"""
|
||||
self.name = name
|
||||
|
||||
@abstractmethod
|
||||
def _generate_maze(self, maze: Maze) -> None:
|
||||
"""Generate the maze (to be implemented by subclasses).
|
||||
|
||||
Args:
|
||||
maze: Maze instance to generate
|
||||
"""
|
||||
pass
|
||||
|
||||
def generate(self, rows: int, cols: int, seed: Optional[int] = None) -> Maze:
|
||||
"""Generate a maze with the specified dimensions.
|
||||
|
||||
Args:
|
||||
rows: Number of rows
|
||||
cols: Number of columns
|
||||
seed: Random seed for reproducibility
|
||||
|
||||
Returns:
|
||||
Generated Maze instance with timing information
|
||||
"""
|
||||
# Create maze
|
||||
maze = Maze(rows, cols, seed)
|
||||
|
||||
# Set random seed
|
||||
random.seed(maze.seed)
|
||||
|
||||
# Track generation time
|
||||
start_time = time.time()
|
||||
self._generate_maze(maze)
|
||||
end_time = time.time()
|
||||
|
||||
# Store metadata
|
||||
maze.generation_time_ms = (end_time - start_time) * 1000
|
||||
maze.algorithm_used = self.name
|
||||
|
||||
# Reset visited flags
|
||||
maze.reset_visited()
|
||||
|
||||
return maze
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of the generator."""
|
||||
return f"{self.__class__.__name__}('{self.name}')"
|
||||
88
src/generators/eller.py
Normal file
88
src/generators/eller.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""Eller's Algorithm for maze generation."""
|
||||
|
||||
import random
|
||||
from typing import Dict, List, Set
|
||||
|
||||
from ..core.cell import Cell
|
||||
from ..core.maze import Maze
|
||||
from .base import BaseGenerator
|
||||
|
||||
|
||||
class EllerGenerator(BaseGenerator):
|
||||
"""Generates mazes using Eller's Algorithm.
|
||||
|
||||
This algorithm generates mazes one row at a time and can create
|
||||
infinitely long mazes. Each row maintains sets of connected cells.
|
||||
|
||||
Time Complexity: O(rows * cols)
|
||||
Space Complexity: O(cols)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Eller generator."""
|
||||
super().__init__("Eller's Algorithm")
|
||||
|
||||
def _generate_maze(self, maze: Maze) -> None:
|
||||
"""Generate maze using Eller's algorithm.
|
||||
|
||||
Args:
|
||||
maze: Maze instance to generate
|
||||
"""
|
||||
# Track which set each cell belongs to
|
||||
sets: Dict[Cell, int] = {}
|
||||
next_set_id = 0
|
||||
|
||||
for row_idx in range(maze.rows):
|
||||
row = maze.grid[row_idx]
|
||||
|
||||
# Assign sets to new cells
|
||||
for cell in row:
|
||||
if cell not in sets:
|
||||
sets[cell] = next_set_id
|
||||
next_set_id += 1
|
||||
|
||||
# Randomly join adjacent cells in different sets
|
||||
for col_idx in range(maze.cols - 1):
|
||||
cell = row[col_idx]
|
||||
east_cell = row[col_idx + 1]
|
||||
|
||||
# Join cells if in different sets and random choice
|
||||
if sets[cell] != sets[east_cell] and random.choice([True, False]):
|
||||
self._merge_sets(sets, sets[cell], sets[east_cell])
|
||||
maze.remove_wall_between(cell, east_cell)
|
||||
|
||||
# Create vertical connections (except for last row)
|
||||
if row_idx < maze.rows - 1:
|
||||
# Group cells by set
|
||||
set_groups: Dict[int, List[Cell]] = {}
|
||||
for cell in row:
|
||||
cell_set = sets[cell]
|
||||
if cell_set not in set_groups:
|
||||
set_groups[cell_set] = []
|
||||
set_groups[cell_set].append(cell)
|
||||
|
||||
# For each set, create at least one vertical connection
|
||||
for cell_set, cells in set_groups.items():
|
||||
# Shuffle and pick at least one cell to connect down
|
||||
random.shuffle(cells)
|
||||
num_connections = random.randint(1, len(cells))
|
||||
|
||||
for i in range(num_connections):
|
||||
cell = cells[i]
|
||||
south_cell = maze.get_cell(cell.row + 1, cell.col)
|
||||
if south_cell:
|
||||
maze.remove_wall_between(cell, south_cell)
|
||||
# South cell inherits the set
|
||||
sets[south_cell] = cell_set
|
||||
|
||||
def _merge_sets(self, sets: Dict[Cell, int], set1: int, set2: int) -> None:
|
||||
"""Merge two sets by replacing all set2 with set1.
|
||||
|
||||
Args:
|
||||
sets: Dictionary mapping cells to set IDs
|
||||
set1: First set ID
|
||||
set2: Second set ID to merge into set1
|
||||
"""
|
||||
for cell, cell_set in sets.items():
|
||||
if cell_set == set2:
|
||||
sets[cell] = set1
|
||||
104
src/generators/hunt_and_kill.py
Normal file
104
src/generators/hunt_and_kill.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""Hunt-and-Kill Algorithm for maze generation."""
|
||||
|
||||
import random
|
||||
from typing import List, Optional
|
||||
|
||||
from ..core.cell import Cell
|
||||
from ..core.maze import Maze
|
||||
from .base import BaseGenerator
|
||||
|
||||
|
||||
class HuntAndKillGenerator(BaseGenerator):
|
||||
"""Generates mazes using the Hunt-and-Kill Algorithm.
|
||||
|
||||
This algorithm performs random walks (kill phase) and when stuck,
|
||||
scans for unvisited cells adjacent to visited ones (hunt phase).
|
||||
Creates mazes with fewer dead ends than recursive backtracking.
|
||||
|
||||
Time Complexity: O((rows * cols)^2) worst case
|
||||
Space Complexity: O(1)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Hunt-and-Kill generator."""
|
||||
super().__init__("Hunt-and-Kill Algorithm")
|
||||
|
||||
def _generate_maze(self, maze: Maze) -> None:
|
||||
"""Generate maze using Hunt-and-Kill algorithm.
|
||||
|
||||
Args:
|
||||
maze: Maze instance to generate
|
||||
"""
|
||||
# Start from random cell
|
||||
current_cell = maze.grid[random.randint(0, maze.rows - 1)][random.randint(0, maze.cols - 1)]
|
||||
current_cell.visited = True
|
||||
|
||||
while True:
|
||||
# Kill phase: random walk from current cell
|
||||
unvisited_neighbors = self._get_unvisited_neighbors(maze, current_cell)
|
||||
|
||||
if unvisited_neighbors:
|
||||
# Choose random unvisited neighbor
|
||||
next_cell, direction = random.choice(unvisited_neighbors)
|
||||
|
||||
# Remove wall and move to next cell
|
||||
maze.remove_wall_between(current_cell, next_cell)
|
||||
next_cell.visited = True
|
||||
current_cell = next_cell
|
||||
else:
|
||||
# Hunt phase: scan for unvisited cell with visited neighbor
|
||||
next_cell = self._hunt(maze)
|
||||
|
||||
if next_cell is None:
|
||||
# All cells visited, done
|
||||
break
|
||||
|
||||
current_cell = next_cell
|
||||
|
||||
def _get_unvisited_neighbors(self, maze: Maze, cell: Cell) -> List[tuple]:
|
||||
"""Get all unvisited neighbors of a cell.
|
||||
|
||||
Args:
|
||||
maze: The maze
|
||||
cell: The cell to find neighbors for
|
||||
|
||||
Returns:
|
||||
List of (neighbor_cell, direction) tuples
|
||||
"""
|
||||
neighbors = []
|
||||
all_neighbors = maze.get_neighbors(cell)
|
||||
|
||||
for neighbor, direction in all_neighbors:
|
||||
if not neighbor.visited:
|
||||
neighbors.append((neighbor, direction))
|
||||
|
||||
return neighbors
|
||||
|
||||
def _hunt(self, maze: Maze) -> Optional[Cell]:
|
||||
"""Hunt for an unvisited cell adjacent to a visited cell.
|
||||
|
||||
Args:
|
||||
maze: The maze
|
||||
|
||||
Returns:
|
||||
Unvisited cell with visited neighbor, or None if all visited
|
||||
"""
|
||||
for row in maze.grid:
|
||||
for cell in row:
|
||||
if not cell.visited:
|
||||
# Check if this cell has visited neighbors
|
||||
visited_neighbors = []
|
||||
all_neighbors = maze.get_neighbors(cell)
|
||||
|
||||
for neighbor, direction in all_neighbors:
|
||||
if neighbor.visited:
|
||||
visited_neighbors.append((neighbor, direction))
|
||||
|
||||
if visited_neighbors:
|
||||
# Connect to random visited neighbor
|
||||
neighbor, direction = random.choice(visited_neighbors)
|
||||
maze.remove_wall_between(cell, neighbor)
|
||||
cell.visited = True
|
||||
return cell
|
||||
|
||||
return None
|
||||
89
src/generators/kruskal.py
Normal file
89
src/generators/kruskal.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Kruskal's Algorithm for maze generation."""
|
||||
|
||||
import random
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from ..core.cell import Cell
|
||||
from ..core.maze import Maze
|
||||
from .base import BaseGenerator
|
||||
|
||||
|
||||
class KruskalGenerator(BaseGenerator):
|
||||
"""Generates mazes using Kruskal's Algorithm.
|
||||
|
||||
This algorithm treats the maze as a graph and uses a union-find structure
|
||||
to create a minimum spanning tree. Creates mazes with many short paths.
|
||||
|
||||
Time Complexity: O(E log E) where E is number of edges
|
||||
Space Complexity: O(V) where V is number of vertices
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Kruskal generator."""
|
||||
super().__init__("Kruskal's Algorithm")
|
||||
|
||||
def _generate_maze(self, maze: Maze) -> None:
|
||||
"""Generate maze using Kruskal's algorithm.
|
||||
|
||||
Args:
|
||||
maze: Maze instance to generate
|
||||
"""
|
||||
# Initialize union-find structure
|
||||
parent: Dict[Cell, Cell] = {}
|
||||
for row in maze.grid:
|
||||
for cell in row:
|
||||
parent[cell] = cell
|
||||
|
||||
# Create list of all possible walls (edges)
|
||||
walls = self._get_all_walls(maze)
|
||||
random.shuffle(walls)
|
||||
|
||||
# Process each wall
|
||||
for cell1, cell2 in walls:
|
||||
# Find roots of both cells
|
||||
root1 = self._find(parent, cell1)
|
||||
root2 = self._find(parent, cell2)
|
||||
|
||||
# If cells are in different sets, remove wall and union
|
||||
if root1 != root2:
|
||||
maze.remove_wall_between(cell1, cell2)
|
||||
parent[root2] = root1
|
||||
|
||||
def _get_all_walls(self, maze: Maze) -> List[Tuple[Cell, Cell]]:
|
||||
"""Get all possible walls between cells.
|
||||
|
||||
Args:
|
||||
maze: The maze
|
||||
|
||||
Returns:
|
||||
List of (cell1, cell2) tuples representing walls
|
||||
"""
|
||||
walls = []
|
||||
|
||||
for row in maze.grid:
|
||||
for cell in row:
|
||||
# Add wall to the south
|
||||
south_cell = maze.get_cell(cell.row + 1, cell.col)
|
||||
if south_cell:
|
||||
walls.append((cell, south_cell))
|
||||
|
||||
# Add wall to the east
|
||||
east_cell = maze.get_cell(cell.row, cell.col + 1)
|
||||
if east_cell:
|
||||
walls.append((cell, east_cell))
|
||||
|
||||
return walls
|
||||
|
||||
def _find(self, parent: Dict[Cell, Cell], cell: Cell) -> Cell:
|
||||
"""Find the root of a cell's set with path compression.
|
||||
|
||||
Args:
|
||||
parent: Union-find parent dictionary
|
||||
cell: Cell to find root for
|
||||
|
||||
Returns:
|
||||
Root cell of the set
|
||||
"""
|
||||
if parent[cell] != cell:
|
||||
parent[cell] = self._find(parent, parent[cell])
|
||||
return parent[cell]
|
||||
75
src/generators/prim.py
Normal file
75
src/generators/prim.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""Prim's Algorithm for maze generation."""
|
||||
|
||||
import random
|
||||
from typing import List, Set, Tuple
|
||||
|
||||
from ..core.cell import Cell
|
||||
from ..core.maze import Maze
|
||||
from .base import BaseGenerator
|
||||
|
||||
|
||||
class PrimGenerator(BaseGenerator):
|
||||
"""Generates mazes using Prim's Algorithm.
|
||||
|
||||
This algorithm starts with a cell and grows the maze by adding the
|
||||
lowest-cost adjacent cells. Creates mazes with many short dead ends.
|
||||
|
||||
Time Complexity: O(E log V) where E is edges and V is vertices
|
||||
Space Complexity: O(V)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Prim generator."""
|
||||
super().__init__("Prim's Algorithm")
|
||||
|
||||
def _generate_maze(self, maze: Maze) -> None:
|
||||
"""Generate maze using Prim's algorithm.
|
||||
|
||||
Args:
|
||||
maze: Maze instance to generate
|
||||
"""
|
||||
# Start with a random cell
|
||||
start_cell = maze.grid[random.randint(0, maze.rows - 1)][random.randint(0, maze.cols - 1)]
|
||||
start_cell.visited = True
|
||||
|
||||
# List of frontier walls (cell pairs)
|
||||
frontier: List[Tuple[Cell, Cell]] = []
|
||||
|
||||
# Add all walls of start cell to frontier
|
||||
self._add_frontier_walls(maze, start_cell, frontier)
|
||||
|
||||
# Process frontier until empty
|
||||
while frontier:
|
||||
# Pick a random wall from frontier
|
||||
wall_idx = random.randint(0, len(frontier) - 1)
|
||||
cell1, cell2 = frontier.pop(wall_idx)
|
||||
|
||||
# If only one of the cells is visited
|
||||
if cell1.visited != cell2.visited:
|
||||
# Remove the wall
|
||||
maze.remove_wall_between(cell1, cell2)
|
||||
|
||||
# Mark unvisited cell as visited
|
||||
unvisited_cell = cell2 if not cell2.visited else cell1
|
||||
unvisited_cell.visited = True
|
||||
|
||||
# Add new frontier walls
|
||||
self._add_frontier_walls(maze, unvisited_cell, frontier)
|
||||
|
||||
def _add_frontier_walls(self, maze: Maze, cell: Cell, frontier: List[Tuple[Cell, Cell]]) -> None:
|
||||
"""Add walls of a cell to the frontier.
|
||||
|
||||
Args:
|
||||
maze: The maze
|
||||
cell: Cell whose walls to add
|
||||
frontier: Frontier list to add to
|
||||
"""
|
||||
neighbors = maze.get_neighbors(cell)
|
||||
|
||||
for neighbor, direction in neighbors:
|
||||
if not neighbor.visited:
|
||||
# Add wall if not already in frontier
|
||||
wall = (cell, neighbor)
|
||||
reverse_wall = (neighbor, cell)
|
||||
if wall not in frontier and reverse_wall not in frontier:
|
||||
frontier.append(wall)
|
||||
73
src/generators/recursive_backtracking.py
Normal file
73
src/generators/recursive_backtracking.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Recursive Backtracking maze generation algorithm."""
|
||||
|
||||
import random
|
||||
from typing import List
|
||||
|
||||
from ..core.cell import Cell
|
||||
from ..core.maze import Maze
|
||||
from .base import BaseGenerator
|
||||
|
||||
|
||||
class RecursiveBacktrackingGenerator(BaseGenerator):
|
||||
"""Generates mazes using the Recursive Backtracking algorithm.
|
||||
|
||||
This algorithm uses a depth-first search approach with backtracking.
|
||||
It creates mazes with a high "river" characteristic - long, winding paths.
|
||||
|
||||
Time Complexity: O(rows * cols)
|
||||
Space Complexity: O(rows * cols) for the stack
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Recursive Backtracking generator."""
|
||||
super().__init__("Recursive Backtracking")
|
||||
|
||||
def _generate_maze(self, maze: Maze) -> None:
|
||||
"""Generate maze using recursive backtracking.
|
||||
|
||||
Args:
|
||||
maze: Maze instance to generate
|
||||
"""
|
||||
# Start from a random cell
|
||||
start_cell = maze.grid[0][0]
|
||||
stack = [start_cell]
|
||||
start_cell.visited = True
|
||||
|
||||
while stack:
|
||||
current_cell = stack[-1]
|
||||
|
||||
# Get unvisited neighbors
|
||||
unvisited_neighbors = self._get_unvisited_neighbors(maze, current_cell)
|
||||
|
||||
if unvisited_neighbors:
|
||||
# Choose a random unvisited neighbor
|
||||
next_cell, direction = random.choice(unvisited_neighbors)
|
||||
|
||||
# Remove wall between current and next cell
|
||||
maze.remove_wall_between(current_cell, next_cell)
|
||||
|
||||
# Mark as visited and add to stack
|
||||
next_cell.visited = True
|
||||
stack.append(next_cell)
|
||||
else:
|
||||
# Backtrack
|
||||
stack.pop()
|
||||
|
||||
def _get_unvisited_neighbors(self, maze: Maze, cell: Cell) -> List[tuple]:
|
||||
"""Get all unvisited neighbors of a cell.
|
||||
|
||||
Args:
|
||||
maze: The maze
|
||||
cell: The cell to find neighbors for
|
||||
|
||||
Returns:
|
||||
List of (neighbor_cell, direction) tuples
|
||||
"""
|
||||
neighbors = []
|
||||
all_neighbors = maze.get_neighbors(cell)
|
||||
|
||||
for neighbor, direction in all_neighbors:
|
||||
if not neighbor.visited:
|
||||
neighbors.append((neighbor, direction))
|
||||
|
||||
return neighbors
|
||||
60
src/generators/sidewinder.py
Normal file
60
src/generators/sidewinder.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Sidewinder Algorithm for maze generation."""
|
||||
|
||||
import random
|
||||
from typing import List
|
||||
|
||||
from ..core.cell import Cell
|
||||
from ..core.maze import Maze
|
||||
from .base import BaseGenerator
|
||||
|
||||
|
||||
class SidewinderGenerator(BaseGenerator):
|
||||
"""Generates mazes using the Sidewinder Algorithm.
|
||||
|
||||
This algorithm works row by row, creating horizontal runs and randomly
|
||||
carving north. Creates mazes with a bias toward horizontal passages.
|
||||
|
||||
Time Complexity: O(rows * cols)
|
||||
Space Complexity: O(cols) for the run
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Sidewinder generator."""
|
||||
super().__init__("Sidewinder Algorithm")
|
||||
|
||||
def _generate_maze(self, maze: Maze) -> None:
|
||||
"""Generate maze using Sidewinder algorithm.
|
||||
|
||||
Args:
|
||||
maze: Maze instance to generate
|
||||
"""
|
||||
# Process each row
|
||||
for row_idx in range(maze.rows):
|
||||
run: List[Cell] = []
|
||||
|
||||
for col_idx in range(maze.cols):
|
||||
cell = maze.grid[row_idx][col_idx]
|
||||
run.append(cell)
|
||||
|
||||
# Decide whether to carve east or north
|
||||
at_eastern_boundary = (col_idx == maze.cols - 1)
|
||||
at_northern_boundary = (row_idx == 0)
|
||||
|
||||
should_close_run = at_eastern_boundary or (
|
||||
not at_northern_boundary and random.choice([True, False])
|
||||
)
|
||||
|
||||
if should_close_run:
|
||||
# Pick a random cell from run and carve north
|
||||
if not at_northern_boundary:
|
||||
random_cell = random.choice(run)
|
||||
north_cell = maze.get_cell(random_cell.row - 1, random_cell.col)
|
||||
if north_cell:
|
||||
maze.remove_wall_between(random_cell, north_cell)
|
||||
# Clear the run
|
||||
run = []
|
||||
else:
|
||||
# Carve east
|
||||
east_cell = maze.get_cell(cell.row, cell.col + 1)
|
||||
if east_cell:
|
||||
maze.remove_wall_between(cell, east_cell)
|
||||
93
src/generators/wilson.py
Normal file
93
src/generators/wilson.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Wilson's Algorithm for maze generation."""
|
||||
|
||||
import random
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from ..core.cell import Cell
|
||||
from ..core.maze import Maze
|
||||
from .base import BaseGenerator
|
||||
|
||||
|
||||
class WilsonGenerator(BaseGenerator):
|
||||
"""Generates mazes using Wilson's Algorithm.
|
||||
|
||||
This algorithm uses loop-erased random walks to generate unbiased mazes.
|
||||
Starts slowly but speeds up as more cells are added to the maze.
|
||||
Creates truly uniform spanning trees.
|
||||
|
||||
Time Complexity: O(rows * cols) expected
|
||||
Space Complexity: O(rows * cols)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Wilson generator."""
|
||||
super().__init__("Wilson's Algorithm")
|
||||
|
||||
def _generate_maze(self, maze: Maze) -> None:
|
||||
"""Generate maze using Wilson's algorithm.
|
||||
|
||||
Args:
|
||||
maze: Maze instance to generate
|
||||
"""
|
||||
# Start with one random cell in the maze
|
||||
all_cells = [cell for row in maze.grid for cell in row]
|
||||
first_cell = random.choice(all_cells)
|
||||
first_cell.visited = True
|
||||
in_maze = {first_cell}
|
||||
|
||||
# Process remaining cells
|
||||
unvisited = [cell for cell in all_cells if cell not in in_maze]
|
||||
|
||||
while unvisited:
|
||||
# Start random walk from random unvisited cell
|
||||
start_cell = random.choice(unvisited)
|
||||
path = self._random_walk(maze, start_cell, in_maze)
|
||||
|
||||
# Add path to maze
|
||||
for i in range(len(path) - 1):
|
||||
current = path[i]
|
||||
next_cell = path[i + 1]
|
||||
|
||||
maze.remove_wall_between(current, next_cell)
|
||||
current.visited = True
|
||||
in_maze.add(current)
|
||||
|
||||
# Update unvisited list
|
||||
unvisited = [cell for cell in all_cells if cell not in in_maze]
|
||||
|
||||
def _random_walk(self, maze: Maze, start: Cell, in_maze: set) -> List[Cell]:
|
||||
"""Perform a loop-erased random walk.
|
||||
|
||||
Args:
|
||||
maze: The maze
|
||||
start: Starting cell
|
||||
in_maze: Set of cells already in the maze
|
||||
|
||||
Returns:
|
||||
List of cells forming the loop-erased path
|
||||
"""
|
||||
path: List[Cell] = [start]
|
||||
current = start
|
||||
|
||||
# Walk until we hit a cell in the maze
|
||||
while current not in in_maze:
|
||||
# Get all neighbors
|
||||
neighbors = maze.get_neighbors(current)
|
||||
if not neighbors:
|
||||
break
|
||||
|
||||
# Choose random neighbor
|
||||
next_cell, direction = random.choice(neighbors)
|
||||
|
||||
# If we've been to this cell before in this walk, erase the loop
|
||||
if next_cell in path:
|
||||
# Erase loop by truncating path
|
||||
loop_start = path.index(next_cell)
|
||||
path = path[:loop_start + 1]
|
||||
current = next_cell
|
||||
else:
|
||||
# Add to path
|
||||
path.append(next_cell)
|
||||
current = next_cell
|
||||
|
||||
return path
|
||||
7
src/solvers/__init__.py
Normal file
7
src/solvers/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Maze solving algorithms."""
|
||||
|
||||
from .base import BaseSolver
|
||||
from .dfs import DFSSolver
|
||||
from .bfs import BFSSolver
|
||||
|
||||
__all__ = ["BaseSolver", "DFSSolver", "BFSSolver"]
|
||||
139
src/solvers/base.py
Normal file
139
src/solvers/base.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""Base class for maze solving algorithms."""
|
||||
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from ..core.cell import Cell
|
||||
from ..core.maze import Maze
|
||||
|
||||
|
||||
class BaseSolver(ABC):
|
||||
"""Abstract base class for maze solving algorithms.
|
||||
|
||||
All maze solving algorithms should inherit from this class
|
||||
and implement the solve method.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
"""Initialize the solver.
|
||||
|
||||
Args:
|
||||
name: Name of the algorithm
|
||||
"""
|
||||
self.name = name
|
||||
|
||||
@abstractmethod
|
||||
def _solve_maze(self, maze: Maze, start: Cell, end: Cell) -> Tuple[Optional[List[Cell]], List[Cell]]:
|
||||
"""Solve the maze (to be implemented by subclasses).
|
||||
|
||||
Args:
|
||||
maze: Maze instance to solve
|
||||
start: Starting cell
|
||||
end: Ending cell
|
||||
|
||||
Returns:
|
||||
Tuple of (solution_path, visited_cells_in_order)
|
||||
"""
|
||||
pass
|
||||
|
||||
def solve(self, maze: Maze) -> Dict:
|
||||
"""Solve the maze and return solution with metadata.
|
||||
|
||||
Args:
|
||||
maze: Maze instance to solve
|
||||
|
||||
Returns:
|
||||
Dictionary with solution path, visited cells, and timing
|
||||
"""
|
||||
# Reset visited flags
|
||||
maze.reset_visited()
|
||||
|
||||
# Get start and end cells
|
||||
start = maze.get_cell(maze.start[0], maze.start[1])
|
||||
end = maze.get_cell(maze.end[0], maze.end[1])
|
||||
|
||||
if not start or not end:
|
||||
return {
|
||||
"success": False,
|
||||
"path": None,
|
||||
"visited": [],
|
||||
"time_ms": 0,
|
||||
"algorithm": self.name,
|
||||
"path_length": 0
|
||||
}
|
||||
|
||||
# Track solving time
|
||||
start_time = time.time()
|
||||
path, visited = self._solve_maze(maze, start, end)
|
||||
end_time = time.time()
|
||||
|
||||
# Reset visited flags again
|
||||
maze.reset_visited()
|
||||
|
||||
return {
|
||||
"success": path is not None,
|
||||
"path": [(cell.row, cell.col) for cell in path] if path else None,
|
||||
"visited": [(cell.row, cell.col) for cell in visited],
|
||||
"time_ms": (end_time - start_time) * 1000,
|
||||
"algorithm": self.name,
|
||||
"path_length": len(path) if path else 0
|
||||
}
|
||||
|
||||
def can_move(self, maze: Maze, from_cell: Cell, to_cell: Cell) -> bool:
|
||||
"""Check if movement between two cells is possible.
|
||||
|
||||
Args:
|
||||
maze: The maze
|
||||
from_cell: Starting cell
|
||||
to_cell: Target cell
|
||||
|
||||
Returns:
|
||||
True if movement is possible, False otherwise
|
||||
"""
|
||||
# Calculate direction
|
||||
row_diff = to_cell.row - from_cell.row
|
||||
col_diff = to_cell.col - from_cell.col
|
||||
|
||||
# Determine direction
|
||||
if row_diff == -1:
|
||||
direction = "north"
|
||||
elif row_diff == 1:
|
||||
direction = "south"
|
||||
elif col_diff == -1:
|
||||
direction = "west"
|
||||
elif col_diff == 1:
|
||||
direction = "east"
|
||||
else:
|
||||
return False
|
||||
|
||||
# Check if wall exists
|
||||
return not from_cell.has_wall(direction)
|
||||
|
||||
def reconstruct_path(self, came_from: Dict[Cell, Cell], start: Cell, end: Cell) -> List[Cell]:
|
||||
"""Reconstruct path from start to end using came_from dict.
|
||||
|
||||
Args:
|
||||
came_from: Dictionary mapping cell to its predecessor
|
||||
start: Starting cell
|
||||
end: Ending cell
|
||||
|
||||
Returns:
|
||||
List of cells forming the path
|
||||
"""
|
||||
path = []
|
||||
current = end
|
||||
|
||||
while current != start:
|
||||
path.append(current)
|
||||
current = came_from.get(current)
|
||||
if current is None:
|
||||
return []
|
||||
|
||||
path.append(start)
|
||||
path.reverse()
|
||||
return path
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of the solver."""
|
||||
return f"{self.__class__.__name__}('{self.name}')"
|
||||
62
src/solvers/bfs.py
Normal file
62
src/solvers/bfs.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Breadth-First Search maze solver."""
|
||||
|
||||
from collections import deque
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from ..core.cell import Cell
|
||||
from ..core.maze import Maze
|
||||
from .base import BaseSolver
|
||||
|
||||
|
||||
class BFSSolver(BaseSolver):
|
||||
"""Solves mazes using Breadth-First Search.
|
||||
|
||||
BFS explores all neighbors at the current depth before moving deeper.
|
||||
Guarantees the shortest path in unweighted graphs.
|
||||
|
||||
Time Complexity: O(V + E) where V is vertices and E is edges
|
||||
Space Complexity: O(V) for the queue
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the BFS solver."""
|
||||
super().__init__("Breadth-First Search (BFS)")
|
||||
|
||||
def _solve_maze(self, maze: Maze, start: Cell, end: Cell) -> Tuple[Optional[List[Cell]], List[Cell]]:
|
||||
"""Solve maze using BFS.
|
||||
|
||||
Args:
|
||||
maze: Maze instance to solve
|
||||
start: Starting cell
|
||||
end: Ending cell
|
||||
|
||||
Returns:
|
||||
Tuple of (solution_path, visited_cells_in_order)
|
||||
"""
|
||||
queue = deque([start])
|
||||
came_from: Dict[Cell, Cell] = {}
|
||||
visited_order = []
|
||||
|
||||
start.visited = True
|
||||
visited_order.append(start)
|
||||
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
|
||||
# Check if we reached the end
|
||||
if current == end:
|
||||
path = self.reconstruct_path(came_from, start, end)
|
||||
return path, visited_order
|
||||
|
||||
# Explore neighbors
|
||||
neighbors = maze.get_neighbors(current)
|
||||
|
||||
for neighbor, direction in neighbors:
|
||||
if not neighbor.visited and self.can_move(maze, current, neighbor):
|
||||
neighbor.visited = True
|
||||
visited_order.append(neighbor)
|
||||
came_from[neighbor] = current
|
||||
queue.append(neighbor)
|
||||
|
||||
# No path found
|
||||
return None, visited_order
|
||||
61
src/solvers/dfs.py
Normal file
61
src/solvers/dfs.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Depth-First Search maze solver."""
|
||||
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from ..core.cell import Cell
|
||||
from ..core.maze import Maze
|
||||
from .base import BaseSolver
|
||||
|
||||
|
||||
class DFSSolver(BaseSolver):
|
||||
"""Solves mazes using Depth-First Search.
|
||||
|
||||
DFS explores as far as possible along each branch before backtracking.
|
||||
Does not guarantee shortest path but is memory efficient.
|
||||
|
||||
Time Complexity: O(V + E) where V is vertices and E is edges
|
||||
Space Complexity: O(V) for the stack
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the DFS solver."""
|
||||
super().__init__("Depth-First Search (DFS)")
|
||||
|
||||
def _solve_maze(self, maze: Maze, start: Cell, end: Cell) -> Tuple[Optional[List[Cell]], List[Cell]]:
|
||||
"""Solve maze using DFS.
|
||||
|
||||
Args:
|
||||
maze: Maze instance to solve
|
||||
start: Starting cell
|
||||
end: Ending cell
|
||||
|
||||
Returns:
|
||||
Tuple of (solution_path, visited_cells_in_order)
|
||||
"""
|
||||
stack = [start]
|
||||
came_from: Dict[Cell, Cell] = {}
|
||||
visited_order = []
|
||||
|
||||
start.visited = True
|
||||
visited_order.append(start)
|
||||
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
|
||||
# Check if we reached the end
|
||||
if current == end:
|
||||
path = self.reconstruct_path(came_from, start, end)
|
||||
return path, visited_order
|
||||
|
||||
# Explore neighbors
|
||||
neighbors = maze.get_neighbors(current)
|
||||
|
||||
for neighbor, direction in neighbors:
|
||||
if not neighbor.visited and self.can_move(maze, current, neighbor):
|
||||
neighbor.visited = True
|
||||
visited_order.append(neighbor)
|
||||
came_from[neighbor] = current
|
||||
stack.append(neighbor)
|
||||
|
||||
# No path found
|
||||
return None, visited_order
|
||||
5
src/storage/__init__.py
Normal file
5
src/storage/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""File storage and persistence utilities."""
|
||||
|
||||
from .file_handler import FileHandler
|
||||
|
||||
__all__ = ["FileHandler"]
|
||||
169
src/storage/file_handler.py
Normal file
169
src/storage/file_handler.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""File handling for saving and loading mazes."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from ..core.maze import Maze
|
||||
|
||||
|
||||
class FileHandler:
|
||||
"""Handles saving and loading mazes to/from files."""
|
||||
|
||||
DEFAULT_SAVE_DIR = "saved_mazes"
|
||||
|
||||
@staticmethod
|
||||
def ensure_save_directory(directory: Optional[str] = None) -> Path:
|
||||
"""Ensure the save directory exists.
|
||||
|
||||
Args:
|
||||
directory: Directory path (uses default if None)
|
||||
|
||||
Returns:
|
||||
Path object for the directory
|
||||
"""
|
||||
save_dir = Path(directory) if directory else Path(FileHandler.DEFAULT_SAVE_DIR)
|
||||
save_dir.mkdir(parents=True, exist_ok=True)
|
||||
return save_dir
|
||||
|
||||
@staticmethod
|
||||
def save_maze(maze: Maze, filename: str, directory: Optional[str] = None) -> str:
|
||||
"""Save a maze to a JSON file.
|
||||
|
||||
Args:
|
||||
maze: Maze to save
|
||||
filename: Name of the file (without extension)
|
||||
directory: Directory to save to (uses default if None)
|
||||
|
||||
Returns:
|
||||
Full path to the saved file
|
||||
|
||||
Raises:
|
||||
IOError: If file cannot be written
|
||||
"""
|
||||
save_dir = FileHandler.ensure_save_directory(directory)
|
||||
|
||||
# Ensure .json extension
|
||||
if not filename.endswith('.json'):
|
||||
filename += '.json'
|
||||
|
||||
file_path = save_dir / filename
|
||||
|
||||
try:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(maze.to_json())
|
||||
return str(file_path)
|
||||
except Exception as e:
|
||||
raise IOError(f"Failed to save maze to {file_path}: {e}")
|
||||
|
||||
@staticmethod
|
||||
def load_maze(filename: str, directory: Optional[str] = None) -> Maze:
|
||||
"""Load a maze from a JSON file.
|
||||
|
||||
Args:
|
||||
filename: Name of the file to load
|
||||
directory: Directory to load from (uses default if None)
|
||||
|
||||
Returns:
|
||||
Loaded Maze instance
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If file doesn't exist
|
||||
ValueError: If file contains invalid maze data
|
||||
IOError: If file cannot be read
|
||||
"""
|
||||
save_dir = Path(directory) if directory else Path(FileHandler.DEFAULT_SAVE_DIR)
|
||||
|
||||
# Ensure .json extension
|
||||
if not filename.endswith('.json'):
|
||||
filename += '.json'
|
||||
|
||||
file_path = save_dir / filename
|
||||
|
||||
if not file_path.exists():
|
||||
raise FileNotFoundError(f"Maze file not found: {file_path}")
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
json_str = f.read()
|
||||
|
||||
# Validate JSON structure
|
||||
data = json.loads(json_str)
|
||||
FileHandler._validate_maze_data(data)
|
||||
|
||||
return Maze.from_json(json_str)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Invalid JSON in maze file: {e}")
|
||||
except Exception as e:
|
||||
raise IOError(f"Failed to load maze from {file_path}: {e}")
|
||||
|
||||
@staticmethod
|
||||
def _validate_maze_data(data: dict) -> None:
|
||||
"""Validate maze data structure.
|
||||
|
||||
Args:
|
||||
data: Dictionary with maze data
|
||||
|
||||
Raises:
|
||||
ValueError: If data is invalid
|
||||
"""
|
||||
required_fields = ["rows", "cols", "seed", "grid"]
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
raise ValueError(f"Missing required field: {field}")
|
||||
|
||||
# Validate dimensions
|
||||
rows = data["rows"]
|
||||
cols = data["cols"]
|
||||
if not (5 <= rows <= 50) or not (5 <= cols <= 50):
|
||||
raise ValueError("Invalid maze dimensions")
|
||||
|
||||
# Validate grid structure
|
||||
grid = data["grid"]
|
||||
if len(grid) != rows:
|
||||
raise ValueError("Grid row count doesn't match maze rows")
|
||||
|
||||
for row_idx, row in enumerate(grid):
|
||||
if len(row) != cols:
|
||||
raise ValueError(f"Grid column count doesn't match at row {row_idx}")
|
||||
|
||||
@staticmethod
|
||||
def list_saved_mazes(directory: Optional[str] = None) -> list:
|
||||
"""List all saved maze files.
|
||||
|
||||
Args:
|
||||
directory: Directory to list from (uses default if None)
|
||||
|
||||
Returns:
|
||||
List of maze filenames
|
||||
"""
|
||||
save_dir = Path(directory) if directory else Path(FileHandler.DEFAULT_SAVE_DIR)
|
||||
|
||||
if not save_dir.exists():
|
||||
return []
|
||||
|
||||
return [f.name for f in save_dir.glob("*.json")]
|
||||
|
||||
@staticmethod
|
||||
def delete_maze(filename: str, directory: Optional[str] = None) -> bool:
|
||||
"""Delete a saved maze file.
|
||||
|
||||
Args:
|
||||
filename: Name of the file to delete
|
||||
directory: Directory containing the file (uses default if None)
|
||||
|
||||
Returns:
|
||||
True if deleted successfully, False if file didn't exist
|
||||
"""
|
||||
save_dir = Path(directory) if directory else Path(FileHandler.DEFAULT_SAVE_DIR)
|
||||
|
||||
if not filename.endswith('.json'):
|
||||
filename += '.json'
|
||||
|
||||
file_path = save_dir / filename
|
||||
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
return True
|
||||
return False
|
||||
6
src/visualization/__init__.py
Normal file
6
src/visualization/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Visualization and rendering utilities."""
|
||||
|
||||
from .image_renderer import ImageRenderer
|
||||
from .web_renderer import WebRenderer
|
||||
|
||||
__all__ = ["ImageRenderer", "WebRenderer"]
|
||||
145
src/visualization/image_renderer.py
Normal file
145
src/visualization/image_renderer.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""Image rendering for mazes using Pillow."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from ..core.maze import Maze
|
||||
|
||||
|
||||
class ImageRenderer:
|
||||
"""Renders mazes as PNG/JPG images."""
|
||||
|
||||
DEFAULT_OUTPUT_DIR = "output_images"
|
||||
DEFAULT_CELL_SIZE = 20
|
||||
DEFAULT_WALL_THICKNESS = 2
|
||||
|
||||
def __init__(self, cell_size: int = DEFAULT_CELL_SIZE, wall_thickness: int = DEFAULT_WALL_THICKNESS):
|
||||
"""Initialize the image renderer.
|
||||
|
||||
Args:
|
||||
cell_size: Size of each cell in pixels
|
||||
wall_thickness: Thickness of walls in pixels
|
||||
"""
|
||||
self.cell_size = cell_size
|
||||
self.wall_thickness = wall_thickness
|
||||
|
||||
def render(
|
||||
self,
|
||||
maze: Maze,
|
||||
filename: str,
|
||||
directory: Optional[str] = None,
|
||||
solution_path: Optional[List[Tuple[int, int]]] = None,
|
||||
visited_cells: Optional[List[Tuple[int, int]]] = None
|
||||
) -> str:
|
||||
"""Render maze as an image.
|
||||
|
||||
Args:
|
||||
maze: Maze to render
|
||||
filename: Output filename (without extension)
|
||||
directory: Output directory (uses default if None)
|
||||
solution_path: Optional list of (row, col) tuples for solution
|
||||
visited_cells: Optional list of (row, col) tuples for visited cells
|
||||
|
||||
Returns:
|
||||
Path to the saved image file
|
||||
"""
|
||||
# Ensure output directory exists
|
||||
output_dir = Path(directory) if directory else Path(self.DEFAULT_OUTPUT_DIR)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Ensure .png extension
|
||||
if not filename.endswith('.png'):
|
||||
filename += '.png'
|
||||
|
||||
# Calculate image dimensions
|
||||
width = maze.cols * self.cell_size + self.wall_thickness
|
||||
height = maze.rows * self.cell_size + self.wall_thickness
|
||||
|
||||
# Create image
|
||||
img = Image.new('RGB', (width, height), color='white')
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Draw visited cells (light gray)
|
||||
if visited_cells:
|
||||
for row, col in visited_cells:
|
||||
self._draw_cell_background(draw, row, col, '#E0E0E0')
|
||||
|
||||
# Draw solution path (light green)
|
||||
if solution_path:
|
||||
for row, col in solution_path:
|
||||
self._draw_cell_background(draw, row, col, '#90EE90')
|
||||
|
||||
# Draw start and end markers
|
||||
self._draw_cell_background(draw, maze.start[0], maze.start[1], '#FFD700') # Gold
|
||||
self._draw_cell_background(draw, maze.end[0], maze.end[1], '#FF69B4') # Hot pink
|
||||
|
||||
# Draw walls
|
||||
for row in maze.grid:
|
||||
for cell in row:
|
||||
self._draw_cell_walls(draw, cell)
|
||||
|
||||
# Save image
|
||||
file_path = output_dir / filename
|
||||
img.save(file_path)
|
||||
return str(file_path)
|
||||
|
||||
def _draw_cell_background(self, draw: ImageDraw, row: int, col: int, color: str) -> None:
|
||||
"""Draw a colored background for a cell.
|
||||
|
||||
Args:
|
||||
draw: ImageDraw object
|
||||
row: Row index
|
||||
col: Column index
|
||||
color: Background color
|
||||
"""
|
||||
x = col * self.cell_size + self.wall_thickness
|
||||
y = row * self.cell_size + self.wall_thickness
|
||||
draw.rectangle(
|
||||
[x, y, x + self.cell_size - 1, y + self.cell_size - 1],
|
||||
fill=color
|
||||
)
|
||||
|
||||
def _draw_cell_walls(self, draw: ImageDraw, cell) -> None:
|
||||
"""Draw walls for a cell.
|
||||
|
||||
Args:
|
||||
draw: ImageDraw object
|
||||
cell: Cell to draw walls for
|
||||
"""
|
||||
x = cell.col * self.cell_size + self.wall_thickness // 2
|
||||
y = cell.row * self.cell_size + self.wall_thickness // 2
|
||||
|
||||
# Draw north wall
|
||||
if cell.has_wall('north'):
|
||||
draw.line(
|
||||
[x, y, x + self.cell_size, y],
|
||||
fill='black',
|
||||
width=self.wall_thickness
|
||||
)
|
||||
|
||||
# Draw south wall
|
||||
if cell.has_wall('south'):
|
||||
draw.line(
|
||||
[x, y + self.cell_size, x + self.cell_size, y + self.cell_size],
|
||||
fill='black',
|
||||
width=self.wall_thickness
|
||||
)
|
||||
|
||||
# Draw west wall
|
||||
if cell.has_wall('west'):
|
||||
draw.line(
|
||||
[x, y, x, y + self.cell_size],
|
||||
fill='black',
|
||||
width=self.wall_thickness
|
||||
)
|
||||
|
||||
# Draw east wall
|
||||
if cell.has_wall('east'):
|
||||
draw.line(
|
||||
[x + self.cell_size, y, x + self.cell_size, y + self.cell_size],
|
||||
fill='black',
|
||||
width=self.wall_thickness
|
||||
)
|
||||
74
src/visualization/web_renderer.py
Normal file
74
src/visualization/web_renderer.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Web rendering for mazes (JSON format for Canvas)."""
|
||||
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from ..core.maze import Maze
|
||||
|
||||
|
||||
class WebRenderer:
|
||||
"""Renders mazes as JSON data for web canvas visualization."""
|
||||
|
||||
@staticmethod
|
||||
def to_json_format(
|
||||
maze: Maze,
|
||||
solution_path: Optional[List[Tuple[int, int]]] = None,
|
||||
visited_cells: Optional[List[Tuple[int, int]]] = None
|
||||
) -> Dict:
|
||||
"""Convert maze to JSON format for web rendering.
|
||||
|
||||
Args:
|
||||
maze: Maze to render
|
||||
solution_path: Optional list of (row, col) tuples for solution
|
||||
visited_cells: Optional list of (row, col) tuples for visited cells
|
||||
|
||||
Returns:
|
||||
Dictionary with maze data for web rendering
|
||||
"""
|
||||
# Build walls array
|
||||
walls = []
|
||||
for row in maze.grid:
|
||||
row_walls = []
|
||||
for cell in row:
|
||||
cell_walls = {
|
||||
'north': cell.has_wall('north'),
|
||||
'south': cell.has_wall('south'),
|
||||
'east': cell.has_wall('east'),
|
||||
'west': cell.has_wall('west')
|
||||
}
|
||||
row_walls.append(cell_walls)
|
||||
walls.append(row_walls)
|
||||
|
||||
# Convert paths to sets for quick lookup
|
||||
solution_set = set(solution_path) if solution_path else set()
|
||||
visited_set = set(visited_cells) if visited_cells else set()
|
||||
|
||||
# Build cell states
|
||||
cell_states = []
|
||||
for row_idx in range(maze.rows):
|
||||
row_states = []
|
||||
for col_idx in range(maze.cols):
|
||||
pos = (row_idx, col_idx)
|
||||
state = 'normal'
|
||||
|
||||
if pos == maze.start:
|
||||
state = 'start'
|
||||
elif pos == maze.end:
|
||||
state = 'end'
|
||||
elif pos in solution_set:
|
||||
state = 'solution'
|
||||
elif pos in visited_set:
|
||||
state = 'visited'
|
||||
|
||||
row_states.append(state)
|
||||
cell_states.append(row_states)
|
||||
|
||||
return {
|
||||
'rows': maze.rows,
|
||||
'cols': maze.cols,
|
||||
'walls': walls,
|
||||
'cellStates': cell_states,
|
||||
'start': list(maze.start),
|
||||
'end': list(maze.end),
|
||||
'algorithm': maze.algorithm_used,
|
||||
'generationTime': maze.generation_time_ms
|
||||
}
|
||||
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Test suite for Maze Generator."""
|
||||
38
tests/conftest.py
Normal file
38
tests/conftest.py
Normal 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)
|
||||
1
tests/integration/__init__.py
Normal file
1
tests/integration/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Integration tests."""
|
||||
217
tests/integration/test_workflow.py
Normal file
217
tests/integration/test_workflow.py
Normal 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
1
tests/unit/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Unit tests."""
|
||||
141
tests/unit/test_analysis.py
Normal file
141
tests/unit/test_analysis.py
Normal 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
|
||||
148
tests/unit/test_generators.py
Normal file
148
tests/unit/test_generators.py
Normal 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
173
tests/unit/test_maze.py
Normal 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
143
tests/unit/test_solvers.py
Normal 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
372
web/static/css/styles.css
Normal 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
385
web/static/js/app.js
Normal 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
125
web/static/js/visualizer.js
Normal 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
127
web/templates/index.html
Normal 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>
|
||||
Reference in New Issue
Block a user