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