17 Commits

Author SHA1 Message Date
da7e b54e49122c README 2026-04-02 14:15:40 +02:00
maoake 53316f4e32 need to be merge 2026-04-02 12:36:10 +02:00
maoake 732cf25a57 trying a README 2026-04-01 23:20:11 +02:00
maoake 4c1955ace4 changing the import for mypy test 2026-04-01 23:00:08 +02:00
maoake 3e85cbe919 doing again the wheel 2026-04-01 22:41:11 +02:00
maoake 2edf61affa doing the wheel 2026-04-01 22:31:08 +02:00
da7e be997c5d17 fix lint + black formating + SITULITUPU 2026-04-01 18:02:38 +02:00
da7e a4d8f3fbfe Merge branch 'whl' 2026-04-01 17:44:20 +02:00
da7e 3fe46026ec Merge branch 'docstring' 2026-04-01 17:42:48 +02:00
Maoake Teriierooiterai d2f38468a4 need to be merge to the main and add some line for the makefile 2026-04-01 17:17:20 +02:00
Maoake Teriierooiterai b659871902 finish the mypy 2026-04-01 16:09:42 +02:00
Maoake Teriierooiterai c9e0cf0610 fix some mypy need to fix for the others 2026-04-01 15:25:38 +02:00
Maoake Teriierooiterai aadccfba53 finish the mypy strict 2026-04-01 15:19:46 +02:00
Maoake Teriierooiterai c7c7213fb9 fix some mypy strict on file a_maze_ing.py 2026-04-01 15:03:22 +02:00
da7e 843fe5f80c uv config for build wheel package
add clean and fclean method to Makefile
2026-04-01 14:31:11 +02:00
Maoake Teriierooiterai 03b5f9e6fd fix mypy strict on MazeSolver and Maze Generator 2026-04-01 14:12:39 +02:00
maoake ed16566677 finish to fix parsing mypy 2026-03-31 22:43:03 +02:00
24 changed files with 872 additions and 208 deletions
+1
View File
@@ -216,3 +216,4 @@ __marimo__/
.streamlit/secrets.toml
test.txt
mazegen-1.0.0-py3-none-any.whl
+21 -4
View File
@@ -1,3 +1,7 @@
build:
uv build --clear --wheel
cp dist/*.whl mazegen-1.0.0-py3-none-any.whl
install:
uv sync
uv pip install mlx-2.2-py3-none-any.whl
@@ -12,15 +16,26 @@ debug:
uv pdb python3 a_maze_ing.py config.txt
clean:
rm -rf __pycache__ .mypy_cache .venv
rm -rf */**/__pycache__ */__pycache__ __pycache__ .mypy_cache .venv dist build */**/*.egg-info */*.egg-info *.egg-info test.txt
fclean: clean
rm mazegen-1.0.0-py3-none-any.whl
lint:
uv run flake8 . --exclude=.venv
uv run mypy . --warn-return-any --warn-unused-ignores --ignore-missing-imports --disallow-untyped-defs --check-untyped-defs
uv run env PYTHONPATH=src python3 -m mypy --warn-return-any --warn-unused-ignores --ignore-missing-imports --disallow-untyped-defs --check-untyped-defs -p mazegen
uv run env PYTHONPATH=src python3 -m mypy --warn-return-any --warn-unused-ignores --ignore-missing-imports --disallow-untyped-defs --check-untyped-defs -p parsing
uv run env PYTHONPATH=src python3 -m mypy --warn-return-any --warn-unused-ignores --ignore-missing-imports --disallow-untyped-defs --check-untyped-defs src/AMazeIng.py
uv run env PYTHONPATH=src python3 -m mypy --warn-return-any --warn-unused-ignores --ignore-missing-imports --disallow-untyped-defs --check-untyped-defs tests
uv run env PYTHONPATH=src python3 -m mypy --warn-return-any --warn-unused-ignores --ignore-missing-imports --disallow-untyped-defs --check-untyped-defs a_maze_ing.py
lint-strict:
uv run flake8 .
uv run mypy . --strict
uv run flake8 . --exclude=.venv
uv run env PYTHONPATH=src python3 -m mypy --strict -p mazegen
uv run env PYTHONPATH=src python3 -m mypy --strict src/AMazeIng.py
uv run env PYTHONPATH=src python3 -m mypy --strict -p parsing
uv run env PYTHONPATH=src python3 -m mypy --strict tests
uv run env PYTHONPATH=src python3 -m mypy --strict a_maze_ing.py
run_test_parsing:
PYTHONPATH=src uv run pytest tests/test_parsing.py
@@ -34,3 +49,5 @@ run_test:
uv run pytest
mlx:
uv run python3 test.py
.PHONY: build install run debug clean fclean lint lint-strict run_test
+614 -2
View File
@@ -1,3 +1,615 @@
The Randomized Kruskal's Algorithm
This project has been created as part of the 42 curriculum by *mteriier*, *dgaillet*
The Randomized Prim's Algorithm
# A-Maze-ing
## Description
A-Maze-ing is a Python project that generates, solves, exports, and displays mazes.
The program:
- reads a configuration file,
- generates a maze according to the requested parameters,
- optionally enforces a **perfect maze** property,
- solves the maze from entry to exit,
- writes the maze to an output file using the required hexadecimal wall encoding,
- and displays the maze visually through an **MLX graphical window**.
This project was designed with **code reusability** in mind.
The maze generation and solving logic is exposed through a reusable Python package named **`mazegen`**, which can be built and installed independently for use in future projects.
---
## Features
- Maze generation from a config file
- Multiple generation algorithms:
- `DFS` (depth-first search / recursive backtracking style)
- `Kruskal`
- Multiple solving algorithms:
- `AStar`
- `DFS`
- Perfect and imperfect maze support
- Maze export using hexadecimal wall encoding
- Graphical rendering with MLX
- Animated generation
- Animated solution path display
- Wall color switching
- Reserved visual **“42” pattern** using fully closed cells when the maze is large enough
- Reusable `mazegen` package
---
## Project Structure
```text
.
├── a_maze_ing.py # Main executable script and MLX display
├── config.txt # Default configuration file
├── Makefile
├── README.md
├── src/
│ ├── AMazeIng.py
│ ├── mazegen/
│ │ ├── __init__.py
│ │ ├── Cell.py
│ │ ├── Maze.py
│ │ ├── MazeGenerator.py
│ │ └── MazeSolver.py
│ └── parsing/
│ └── Parsing.py
└── tests/
```
---
## Instructions
### Requirements
- Python **3.10+**
- `uv`, `pip`
- MLX Python binding used by the project
### Installation
Using the provided `Makefile`:
```bash
make install
```
This installs project dependencies and the MLX wheel used by the graphical display.
---
## Run
```bash
make run
```
---
## Debug
```bash
make debug
```
---
## Lint
Mandatory lint target:
```bash
make lint
```
Strict lint target:
```bash
make lint-strict
```
---
## Clean
```bash
make clean
```
Full cleanup:
```bash
make fclean
```
---
## Configuration File Format
The configuration file contains one `KEY=VALUE` pair per line.
### Mandatory keys
| Key | Description | Example |
|---|---|---|
| `WIDTH` | Maze width in cells | `WIDTH=20` |
| `HEIGHT` | Maze height in cells | `HEIGHT=15` |
| `ENTRY` | Entry coordinates `(x,y)` | `ENTRY=1,1` |
| `EXIT` | Exit coordinates `(x,y)` | `EXIT=20,15` |
| `OUTPUT_FILE` | Output filename | `OUTPUT_FILE=maze.txt` |
| `PERFECT` | Perfect maze or not | `PERFECT=True` |
| `GENERATOR` | Generation algorithm | `GENERATOR=DFS` |
| `SOLVER` | Solving algorithm | `SOLVER=AStar` |
### Supported values
#### GENERATOR
- `DFS`
- `Kruskal`
#### SOLVER
- `AStar`
- `DFS`
#### PERFECT
- `True`
- `False`
### Example config
```ini
WIDTH=20
HEIGHT=15
ENTRY=1,1
EXIT=20,15
OUTPUT_FILE=maze.txt
PERFECT=True
GENERATOR=DFS
SOLVER=AStar
SEED=31766516
```
### Notes
- Coordinates are handled as tuples in the form `x,y`.
- In the current implementation, coordinates are expected to be **inside maze bounds**.
- Entry and exit must be valid cells.
- The parser validates required keys and converts values to the correct Python types.
- You can add a `SEED` value
---
## Output File Format
The generated maze is written row by row using **one hexadecimal digit per cell**.
Each cell stores wall information using this bitmask:
| Bit | Direction |
|---|---|
| `1` | North |
| `2` | East |
| `4` | South |
| `8` | West |
A bit set to `1` means the wall is **closed**.
### Example
- `3` = `0011` → north and east closed
- `A` = `1010` → east and west closed
### Output layout
```text
<maze row 1>
<maze row 2>
...
<maze row n>
<entry coordinates>
<exit coordinates>
<solution path>
```
Example:
```text
FFFF
9A63
8C47
FFFF
1,1
4,4
EESSEN
```
---
## Visual Representation
This project provides a graphical rendering through **MLX**.
The display shows:
- maze walls,
- entry cell,
- exit cell,
- optional shortest path,
- reserved “42” pattern when present.
### Controls
In the MLX window:
- `1` / mapped equivalent: regenerate maze
- `2` / mapped equivalent: show/hide path
- `3` / mapped equivalent: change wall color
- `4` / mapped equivalent: quit
The code includes two key mappings to handle platform/layout differences.
### Visual Features
- animated generation,
- animated path display,
- color cycling for walls,
- separate color cycling for the “42” cells.
---
## Maze Generation Algorithm
This project supports two generation algorithms.
### 1. Depth-First Search (DFS)
This algorithm starts from a cell and repeatedly visits an unvisited neighbour, removing walls as it advances. When it reaches a dead end, it backtracks until it finds a cell with an unvisited neighbour.
#### Why this algorithm was chosen
- simple to implement,
- naturally produces connected mazes,
- works well for animation,
- produces visually interesting long corridors,
- easy to adapt for perfect mazes.
### 2. Kruskal
This algorithm treats each cell as its own set and removes walls between cells only when it connects two different sets. This avoids cycles and guarantees connectivity.
#### Why this algorithm was included
- classic maze generation algorithm,
- good complement to DFS,
- demonstrates modularity and algorithm interchangeability,
- naturally fits the reusable package requirement.
---
## Why These Algorithms Were Chosen
We chose DFS and Kruskal because together they provide:
- two well-known and complementary approaches,
- good pedagogical value,
- simple integration into a reusable class-based architecture,
- deterministic structure when used with a seed,
- compatibility with perfect maze generation.
DFS is particularly suitable for progressive visual rendering.
Kruskal is useful to show a different construction logic based on set merging.
---
## Perfect and Imperfect Mazes
When `PERFECT=True`:
- the maze is generated as a **perfect maze**,
- there is exactly one path between any two reachable cells,
- in particular, entry and exit have a unique valid path.
When `PERFECT=False`:
- additional walls may be removed after initial generation,
- loops can appear,
- the maze remains connected,
- the solver still computes a valid path.
---
## The “42” Pattern
For sufficiently large mazes, the generator reserves a group of fully closed cells to draw a visible **“42”** pattern in the visual rendering.
### Behaviour
- the pattern is added only if the maze is large enough,
- if the maze is too small, the pattern may be omitted,
- this should be reported to the user with a console message.
### Current implementation note
The current code includes support for reserving and rendering the “42” pattern using cells with value `15` (all walls closed).
The pattern is drawn in the central area when dimensions are large enough.
---
## Error Handling
The project is designed to fail gracefully and provide clear messages for common problems such as:
- missing configuration file,
- empty file,
- missing or invalid keys,
- invalid boolean values,
- invalid coordinates,
- invalid maze dimensions,
- solving an uninitialized maze.
The parser catches several common exceptions and prints user-friendly messages before exiting.
---
## Reusable Code
The reusable part of the project is the **`mazegen`** package.
It contains:
- `Cell`: wall bitmask representation,
- `Maze`: maze container and textual/ascii rendering,
- `MazeGenerator`: abstract generator interface,
- `DepthFirstSearch`: DFS-based maze generator,
- `Kruskal`: Kruskal-based maze generator,
- `MazeSolver`: abstract solver interface,
- `AStar`: shortest-path solver,
- `DepthFirstSearchSolver`: DFS-based path solver.
This package can be built as a wheel and reused independently of the MLX application.
---
## How to Use the Reusable Module
### Basic example
```python
from mazegen import Maze
from mazegen import DepthFirstSearch, AStar
generator = DepthFirstSearch(start=(1, 1), end=(10, 10), perfect=True)
solver = AStar(start=(1, 1), end=(10, 10))
maze = Maze()
for grid in generator.generator(height=10, width=10, seed=42):
maze.set_maze(grid)
path = solver.solve(maze, height=10, width=10)
print(maze)
print(path)
```
### With Kruskal
```python
from mazegen import Maze, Kruskal, AStar
generator = Kruskal(start=(1, 1), end=(20, 15), perfect=True)
solver = AStar(start=(1, 1), end=(20, 15))
maze = Maze()
for grid in generator.generator(height=15, width=20, seed=123):
maze.set_maze(grid)
print(solver.solve(maze, height=15, width=20))
```
### Accessing the generated structure
```python
maze_array = maze.get_maze()
```
Each element of `maze_array` is a `Cell` object exposing:
- `get_north()`
- `get_est()`
- `get_south()`
- `get_west()`
- `get_value()`
### Accessing a solution
```python
solution = solver.solve(maze, height=15, width=20)
print(solution) # Example: "EESSWN..."
```
---
## Packaging
The reusable package is distributed as **`mazegen-*`**.
Example expected artifact:
```text
mazegen-1.0.0-py3-none-any.whl
```
Build with:
```bash
make build
```
This produces a wheel suitable for later installation with `pip`/`uv`.
---
## Tests
Unit tests are recommended and partially integrated through `pytest` targets in the Makefile.
Start test with:
```bash
make run_test
```
These tests are useful to validate:
- parsing,
- generation,
- solver behavior,
- edge cases.
---
## Technical Choices
### Language
- Python 3.10+
### Libraries
- `numpy` for grid storage
- `pydantic` for model validation
- `mlx` for graphical rendering
- `pytest` for tests
- `mypy` for static typing
- `flake8` for style checking
### Architecture
The project is separated into three main parts:
1. **Main application**
- parsing,
- orchestration,
- MLX rendering,
- user interaction.
2. **Domain model**
- `AMazeIng`,
- maze configuration and lifecycle.
3. **Reusable package**
- generation,
- solving,
- maze structure.
This separation makes the generation logic portable to other projects.
---
## Team and Project Management
### Team roles
- **mteriier**
- Parsing
- DFS generator / solver
- Makefile
- some pytest
- Fix of mazegen package generation
- MLX
- **dgaillet**
- AMazeIng config class
- AStar solver
- Kruskal generator
- some pytest
- mazegen package generation
- MLX
- Cell / Maze class
### Initial planning
Our initial plan was:
1. define the maze data model,
2. implement one working generation algorithm,
3. export the maze to the required format,
4. implement solving,
5. add graphical rendering,
6. package reusable code,
7. write tests and documentation.
### How planning evolved
In practice:
- the reusable package structure had to be stabilized earlier than expected,
- coordinate handling between parser, generator, solver, and renderer required extra work,
- rendering and animation took longer than planned,
- algorithm modularity made later integration easier.
### What worked well
- clean separation between generation and display,
- abstract base classes for generator and solver,
- Makefile automation,
- packaging the reusable module.
### What could be improved
- stricter normalization of coordinate conventions,
- seed support should be exposed directly from configuration,
- more tests for edge cases and invalid inputs,
### Tools used
- Git
- `uv`
- `flake8`
- `mypy`
- `pytest`
- MLX
- optionally AI assistance for docstrings, README
---
## Resources
### Documentation and references
- [NumPy Documentation](https://numpy.org/doc/)
- [Pydantic Documentation](https://docs.pydantic.dev/)
- [A* Pathfinding explanation](https://matteo-tosato7.medium.com/exploring-the-depths-solving-mazes-with-a-search-algorithm-c15253104899)
- [Kruskal generation](https://medium.com/@anushidesilva28/understanding-kruskals-algorithm-44886bf8ba8b)
### How AI was used
AI was used as an assistant for:
- improving docstrings,
- helping structure the README,
---
## Reusable Module Summary
If you only want the reusable maze engine:
1. build/install `mazegen`,
2. import a generator and a solver,
3. generate a maze,
4. solve it,
5. access the grid through `Maze.get_maze()`.
This part is intended for reuse in future Python projects.
+32 -24
View File
@@ -1,8 +1,8 @@
from typing import Any
from src.AMazeIng import AMazeIng
from src.parsing import Parsing
from numpy.typing import NDArray
from AMazeIng import AMazeIng
from parsing.Parsing import DataMaze as Parsing
from mlx import Mlx
import numpy as np
import time
@@ -29,14 +29,12 @@ class MazeMLX:
self.buf, self.bpp, self.size_line, self.format = (
self.mlx.mlx_get_data_addr(self.img_ptr)
)
self.path_printer = None
self.generator = None
def close(self) -> None:
"""Destroy the image used by the renderer."""
self.mlx.mlx_destroy_image(self.mlx_ptr, self.img_ptr)
def close_loop(self, _: Any):
def close_loop(self, _: Any) -> None:
"""Stop the MLX event loop.
Args:
@@ -63,7 +61,9 @@ class MazeMLX:
"1: regen; 2: path; 3: color; 4: quit;",
)
def put_pixel(self, x, y, color: list | None = None) -> None:
def put_pixel(
self, x: int, y: int, color: list[Any] | None = None
) -> None:
"""Draw a single pixel into the image buffer.
Args:
@@ -93,7 +93,7 @@ class MazeMLX:
self,
start: tuple[int, int],
end: tuple[int, int],
color: list | None = None,
color: list[Any] | None = None,
) -> None:
"""Draw a horizontal or vertical line.
@@ -115,7 +115,7 @@ class MazeMLX:
self,
ul: tuple[int, int],
dr: tuple[int, int],
color: list | None = None,
color: list[Any] | None = None,
) -> None:
"""Draw a filled rectangular block.
@@ -165,7 +165,7 @@ class MazeMLX:
for color in colors:
yield color
def get_margin_line_len(self, maze: np.ndarray) -> tuple[int, int, int]:
def get_margin_line_len(self, maze: NDArray[Any]) -> tuple[int, int, int]:
"""Compute the cell size and margins for centering the maze.
Args:
@@ -188,7 +188,7 @@ class MazeMLX:
return (line_len, margin_x, margin_y)
def update_maze(self, maze: np.ndarray) -> None:
def update_maze(self, maze: NDArray[Any]) -> None:
"""Render the maze walls into the image buffer.
Args:
@@ -272,7 +272,7 @@ class MazeMLX:
self.put_block(ul, dr)
return
def put_start_end(self, amazing: AMazeIng):
def put_start_end(self, amazing: AMazeIng) -> None:
"""Draw highlighted blocks for the maze entry and exit.
Args:
@@ -306,8 +306,11 @@ class MazeMLX:
)
self.put_block(ul, dr, [0x00, 0xFF, 0x40, 0x9F])
def draw_ft(self, maze: np.ndarray, color: list | None = None):
"""Draw filled cells corresponding to the reserved fully walled pattern.
def draw_ft(
self, maze: NDArray[Any], color: list[Any] | None = None
) -> None:
"""Draw filled cells corresponding to the reserved fully
walled pattern.
Args:
maze: Maze grid to inspect.
@@ -325,33 +328,36 @@ class MazeMLX:
self.put_block((x0, y0), (x1, y1), color)
def draw_image(self, amazing: AMazeIng) -> None:
maze = amazing.maze.get_maze()
"""Main rendering callback used by the MLX loop.
Args:
amazing: Maze container to render.
"""
if self.render_maze(amazing):
if self.path_printer and self.print_path:
if self.print_path:
if self.render_path():
color = next(self.color_gen_ft)
self.draw_ft(amazing.maze.get_maze(), color)
if maze is not None:
self.draw_ft(maze, color)
next(self.timer_gen)
else:
self.time_gen()
self.update_maze(amazing.maze.get_maze())
self.draw_ft(amazing.maze.get_maze())
if maze is not None:
self.update_maze(maze)
self.draw_ft(maze)
self.put_start_end(amazing)
self.redraw_image()
def shift_color(self):
def shift_color(self) -> None:
"""Reset the maze color generator."""
self.color_gen = self.random_color()
def shift_color_ft(self):
def shift_color_ft(self) -> None:
"""Reset the reserved-pattern color generator."""
self.color_gen_ft = self.random_color_ft()
def time_gen(self):
def time_gen(self) -> None:
"""Reset the timing generator used for animation pacing."""
self.timer_gen = self.time_generator()
@@ -406,8 +412,10 @@ class MazeMLX:
``True`` if maze generation is complete, otherwise ``False``.
"""
try:
maze = amazing.maze.get_maze()
next(self.generator)
self.update_maze(amazing.maze.get_maze())
if maze is not None:
self.update_maze(maze)
return False
except StopIteration:
pass
@@ -466,7 +474,7 @@ class MazeMLX:
self.mlx.mlx_loop_hook(self.mlx_ptr, self.draw_image, amazing)
self.mlx.mlx_hook(self.win_ptr, 33, 0, self.close_loop, None)
self.mlx.mlx_hook(
self.win_ptr, 2, 1 << 0, self.handle_key_press, amazing
self.win_ptr, 2, 1 << 0, self.handle_key_press_mteriier, amazing
)
self.mlx.mlx_loop(self.mlx_ptr)
@@ -476,7 +484,7 @@ def main() -> None:
mlx = None
try:
mlx = MazeMLX(1000, 1000)
config = Parsing.DataMaze.get_data_maze("config.txt")
config = Parsing.get_data_maze("config.txt")
amazing = AMazeIng(**config)
mlx.start(amazing)
with open("test.txt", "w") as output:
-25
View File
@@ -1,25 +0,0 @@
# This script does not check for errors or malformed files.
# It only validates that neighbooring cells sharing a wall have
# both the correct encoding.
# Usage: python3 output_validator.py output_maze.txt
import sys
if len(sys.argv) != 2:
print(f"Usage: python3 {sys.argv[0]} <output_file>")
sys.exit(1)
g = []
for line in open(sys.argv[1]):
if line.strip() == '':
break
g.append([int(c, 16) for c in line.strip(' \t\n\r')])
for r in range(len(g)):
for c in range(len(g[0])):
v = g[r][c]
if not all([(r < 1 or v & 1 == (g[r-1][c] >> 2) & 1),
(c >= len(g[0])-1 or (v >> 1) & 1 == (g[r][c+1] >> 3) & 1),
(r >= len(g)-1 or (v >> 2) & 1 == g[r+1][c] & 1),
(c < 1 or (v >> 3) & 1 == (g[r][c-1] >> 1) & 1)]):
print(f'Wrong encoding for ({c},{r})')
+13 -1
View File
@@ -1,5 +1,5 @@
[project]
name = "A-Maze-ing"
name = "mazegen"
version = "0.1.0"
description = "This is the way"
readme = "README.md"
@@ -20,6 +20,18 @@ dev = [
[tool.mypy]
python_version = "3.10"
explicit_package_bases = true
[tool.pytest.ini_options]
pythonpath = ["src"]
[build-system]
requires = ["setuptools>=78.1.0", "wheel>=0.45.1"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
+4 -2
View File
@@ -2,11 +2,13 @@ from typing import Generator
from typing_extensions import Self
from pydantic import BaseModel, Field, model_validator, ConfigDict
from src.amaz_lib import Maze, MazeGenerator, MazeSolver
from mazegen import Maze, MazeGenerator, MazeSolver
class AMazeIng(BaseModel):
"""Represent a complete maze configuration, generation, and solving setup."""
"""Represent a complete maze configuration, generation,
and solving setup.
"""
model_config = ConfigDict(arbitrary_types_allowed=True)
-10
View File
@@ -1,10 +0,0 @@
from .Cell import Cell
from .Maze import Maze
from .MazeGenerator import MazeGenerator, DepthFirstSearch
from .MazeGenerator import Kruskal
from .MazeSolver import MazeSolver, AStar, DepthFirstSearchSolver
__version__ = "1.0.0"
__author__ = "us"
__all__ = ["Cell", "Maze", "MazeGenerator", "DepthFirstSearchSolver",
"MazeSolver", "AStar", "Kruskal", "DepthFirstSearch"]
@@ -1,14 +1,18 @@
from abc import ABC, abstractmethod
from typing import Generator, Set
from typing import Generator, Any
import numpy as np
from .Cell import Cell
from numpy.typing import NDArray
from mazegen.Cell import Cell
import math
import random
class MazeGenerator(ABC):
"""Define the common interface and helpers for maze generators."""
def __init__(self, start: tuple, end: tuple, perfect: bool) -> None:
def __init__(
self, start: tuple[int, int], end: tuple[int, int], perfect: bool
) -> None:
"""Initialize the maze generator.
Args:
@@ -23,7 +27,7 @@ class MazeGenerator(ABC):
@abstractmethod
def generator(
self, height: int, width: int, seed: int | None = None
) -> Generator[np.ndarray, None, np.ndarray]:
) -> Generator[NDArray[Any], None, NDArray[Any]]:
"""Generate a maze step by step.
Args:
@@ -40,7 +44,7 @@ class MazeGenerator(ABC):
...
@staticmethod
def get_cell_ft(width: int, height: int) -> set:
def get_cell_ft(width: int, height: int) -> set[tuple[int, int]]:
"""Return the coordinates used to reserve the '42' pattern.
Args:
@@ -76,11 +80,12 @@ class MazeGenerator(ABC):
def unperfect_maze(
width: int,
height: int,
maze: np.ndarray,
forty_two: set | None,
maze: NDArray[Any],
forty_two: set[tuple[int, int]] | None,
prob: float = 0.1,
) -> Generator[np.ndarray, None, np.ndarray]:
"""Add extra openings to transform a perfect maze into an imperfect one.
) -> Generator[NDArray[Any], None, NDArray[Any]]:
"""Add extra openings to transform a perfect maze into an imperfect
one.
Random walls are removed while optionally preserving the reserved
``forty_two`` area.
@@ -99,6 +104,7 @@ class MazeGenerator(ABC):
Returns:
The modified maze.
"""
directions = {"N": (0, -1), "S": (0, 1), "W": (-1, 0), "E": (1, 0)}
reverse = {"N": "S", "S": "N", "W": "E", "E": "W"}
@@ -139,7 +145,7 @@ class MazeGenerator(ABC):
class Kruskal(MazeGenerator):
"""Generate a maze using a Kruskal-based algorithm."""
class Set:
class KruskalSet:
"""Represent a connected component of maze cells."""
def __init__(self, cells: list[int]) -> None:
@@ -153,7 +159,7 @@ class Kruskal(MazeGenerator):
class Sets:
"""Store all connected components used during generation."""
def __init__(self, sets: list[Set]) -> None:
def __init__(self, sets: list["Kruskal.KruskalSet"]) -> None:
"""Initialize the collection of connected components.
Args:
@@ -163,8 +169,8 @@ class Kruskal(MazeGenerator):
@staticmethod
def walls_to_maze(
walls: np.ndarray, height: int, width: int
) -> np.ndarray:
walls: list[tuple[int, int]], height: int, width: int
) -> NDArray[Any]:
"""Convert a list of remaining walls into a maze grid.
Args:
@@ -176,7 +182,8 @@ class Kruskal(MazeGenerator):
A two-dimensional array of :class:`Cell` instances representing the
maze.
"""
maze: np.ndarray = np.array(
maze: NDArray[Any] = np.array(
[[Cell(value=0) for _ in range(width)] for _ in range(height)]
)
for wall in walls:
@@ -270,7 +277,7 @@ class Kruskal(MazeGenerator):
def generator(
self, height: int, width: int, seed: int | None = None
) -> Generator[np.ndarray, None, np.ndarray]:
) -> Generator[NDArray[Any], None, NDArray[Any]]:
"""Generate a maze using a Kruskal-based approach.
Args:
@@ -292,7 +299,7 @@ class Kruskal(MazeGenerator):
if seed is not None:
np.random.seed(seed)
sets = self.Sets([self.Set([i]) for i in range(height * width)])
sets = self.Sets([self.KruskalSet([i]) for i in range(height * width)])
walls = []
for h in range(height):
for w in range(width - 1):
@@ -330,7 +337,9 @@ class Kruskal(MazeGenerator):
class DepthFirstSearch(MazeGenerator):
"""Generate a maze using a depth-first search backtracking algorithm."""
def __init__(self, start: bool, end: bool, perfect: bool) -> None:
def __init__(
self, start: tuple[int, int], end: tuple[int, int], perfect: bool
) -> None:
"""Initialize the depth-first search generator.
Args:
@@ -341,11 +350,11 @@ class DepthFirstSearch(MazeGenerator):
self.start = (start[0] - 1, start[1] - 1)
self.end = (end[0] - 1, end[1] - 1)
self.perfect = perfect
self.forty_two: set | None = None
self.forty_two: set[tuple[int, int]] | None = None
def generator(
self, height: int, width: int, seed: int = None
) -> Generator[np.ndarray, None, np.ndarray]:
self, height: int, width: int, seed: int | None = None
) -> Generator[NDArray[Any], None, NDArray[Any]]:
"""Generate a maze using depth-first search.
Args:
@@ -364,14 +373,14 @@ class DepthFirstSearch(MazeGenerator):
maze = self.init_maze(width, height)
if width > 9 and height > 9:
self.forty_two = self.get_cell_ft(width, height)
visited = np.zeros((height, width), dtype=bool)
visited: NDArray[np.object_] = np.zeros((height, width), dtype=bool)
if (
self.forty_two
and self.start not in self.forty_two
and self.end not in self.forty_two
):
visited = self.lock_cell_ft(visited, self.forty_two)
path = list()
path: list[tuple[int, int]] = list()
w_h = (width, height)
coord = (0, 0)
x, y = coord
@@ -414,7 +423,7 @@ class DepthFirstSearch(MazeGenerator):
return maze
@staticmethod
def init_maze(width: int, height: int) -> np.ndarray:
def init_maze(width: int, height: int) -> NDArray[Any]:
"""Create a fully walled maze grid.
Args:
@@ -422,7 +431,8 @@ class DepthFirstSearch(MazeGenerator):
height: Number of rows in the maze.
Returns:
A two-dimensional array of cells initialized with all walls present.
A two-dimensional array of cells initialized with all
walls present.
"""
maze = np.array(
[[Cell(value=15) for _ in range(width)] for _ in range(height)]
@@ -430,7 +440,9 @@ class DepthFirstSearch(MazeGenerator):
return maze
@staticmethod
def add_cell_visited(coord: tuple, path: set) -> list:
def add_cell_visited(
coord: tuple[int, int], path: list[tuple[int, int]]
) -> list[tuple[int, int]]:
"""Append a visited coordinate to the current traversal path.
Args:
@@ -444,7 +456,9 @@ class DepthFirstSearch(MazeGenerator):
return path
@staticmethod
def random_cells(visited: np.array, coord: tuple, w_h: tuple) -> list:
def random_cells(
visited: NDArray[Any], coord: tuple[int, int], w_h: tuple[int, int]
) -> list[str]:
"""Return the list of unvisited neighboring directions.
Args:
@@ -456,7 +470,7 @@ class DepthFirstSearch(MazeGenerator):
A list of direction strings among ``"N"``, ``"S"``, ``"W"``, and
``"E"``.
"""
rand_cell = []
rand_cell: list[str] = []
x, y = coord
width, height = w_h
@@ -474,7 +488,7 @@ class DepthFirstSearch(MazeGenerator):
return rand_cell
@staticmethod
def next_step(rand_cell: list) -> str:
def next_step(rand_cell: list[str]) -> str:
"""Select the next direction at random.
Args:
@@ -483,7 +497,7 @@ class DepthFirstSearch(MazeGenerator):
Returns:
A randomly selected direction.
"""
return np.random.choice(rand_cell)
return random.choice(rand_cell)
@staticmethod
def broken_wall(cell: Cell, wall: str) -> Cell:
@@ -507,7 +521,7 @@ class DepthFirstSearch(MazeGenerator):
return cell
@staticmethod
def next_cell(x: int, y: int, next: str) -> tuple:
def next_cell(x: int, y: int, next: str) -> tuple[int, int]:
"""Return the coordinates of the adjacent cell in the given direction.
Args:
@@ -535,8 +549,13 @@ class DepthFirstSearch(MazeGenerator):
return {"N": "S", "S": "N", "W": "E", "E": "W"}[direction]
@staticmethod
def back_on_step(path: list, w_h: tuple, visited: np.ndarray) -> list:
"""Backtrack through the path until a cell with unvisited neighbors is found.
def back_on_step(
path: list[tuple[int, int]],
w_h: tuple[int, int],
visited: NDArray[Any],
) -> list[tuple[int, int]]:
"""Backtrack through the path until a cell with unvisited neighbors
is found.
Args:
path: Current traversal path.
@@ -555,8 +574,8 @@ class DepthFirstSearch(MazeGenerator):
@staticmethod
def lock_cell_ft(
visited: np.ndarray, forty_two: set[tuple[int]]
) -> np.ndarray:
visited: NDArray[Any], forty_two: set[tuple[int, int]]
) -> NDArray[Any]:
"""Mark the reserved '42' pattern cells as already visited.
Args:
@@ -2,6 +2,8 @@ from abc import ABC, abstractmethod
from .Maze import Maze
from typing import Any
import numpy as np
from numpy.typing import NDArray
import random
class MazeSolver(ABC):
@@ -83,7 +85,6 @@ class AStar(MazeSolver):
end: End coordinates using 1-based indexing.
"""
super().__init__(start, end)
self.path = []
def h(self, n: tuple[int, int]) -> int:
"""Compute the Manhattan distance heuristic to the goal.
@@ -103,9 +104,9 @@ class AStar(MazeSolver):
def get_paths(
self,
maze: np.ndarray,
maze: NDArray[Any],
actual: tuple[int, int],
close: list,
close: list["Node"],
) -> list[tuple[int, int]]:
"""Return all reachable neighboring coordinates.
@@ -153,7 +154,7 @@ class AStar(MazeSolver):
]
return [p for p in path if p is not None]
def get_path(self, maze: np.ndarray) -> list:
def get_path(self, maze: NDArray[Any]) -> list["Node"]:
"""Perform A* exploration until the destination is reached.
Args:
@@ -232,7 +233,7 @@ class AStar(MazeSolver):
else:
raise Exception("Translate error: AStar path not found")
def translate(self, close: list) -> str:
def translate(self, close: list["Node"]) -> str:
"""Translate a node chain into a path string.
Args:
@@ -263,14 +264,18 @@ class AStar(MazeSolver):
Returns:
A string representing the path using cardinal directions.
"""
path = self.get_path(maze.get_maze())
maze_arr = maze.get_maze()
if maze_arr is None:
raise Exception("Maze is not initialized")
path: list[AStar.Node] = self.get_path(maze_arr)
return self.translate(path)
class DepthFirstSearchSolver(MazeSolver):
"""Solve a maze using depth-first search with backtracking."""
def __init__(self, start, end):
def __init__(self, start: tuple[int, int], end: tuple[int, int]):
"""Initialize the depth-first search solver.
Args:
@@ -296,16 +301,20 @@ class DepthFirstSearchSolver(MazeSolver):
Exception: If no path can be found.
"""
path_str = ""
visited = np.zeros((height, width), dtype=bool)
path = list()
move = list()
if height is None or width is None:
raise Exception("We need Height and Width in the arg")
visited: NDArray[Any] = np.zeros((height, width), dtype=bool)
path: list[tuple[int, int]] = list()
move: list[str] = list()
maze_s = maze.get_maze()
if maze_s is None:
raise Exception("Maze is not initializef")
coord = self.start
h_w = (height, width)
h_w: tuple[int, int] = (height, width)
while coord != self.end:
visited[coord] = True
path.append(coord)
rand_p = self.random_path(visited, coord, maze_s, h_w)
rand_p: list[str] = self.random_path(visited, coord, maze_s, h_w)
if not rand_p:
path, move = self.back_on_step(
@@ -326,8 +335,11 @@ class DepthFirstSearchSolver(MazeSolver):
@staticmethod
def random_path(
visited: np.ndarray, coord: tuple, maze: np.ndarray, h_w: tuple
) -> list:
visited: NDArray[Any],
coord: tuple[int, int],
maze: NDArray[Any],
h_w: tuple[int, int],
) -> list[str]:
"""Return all valid unvisited directions from the current cell.
Args:
@@ -357,7 +369,7 @@ class DepthFirstSearchSolver(MazeSolver):
return random_p
@staticmethod
def next_path(rand_path: list) -> str:
def next_path(rand_path: list[str]) -> str:
"""Select the next move at random.
Args:
@@ -366,16 +378,17 @@ class DepthFirstSearchSolver(MazeSolver):
Returns:
A randomly selected direction.
"""
return np.random.choice(rand_path)
return random.choice(rand_path)
@staticmethod
def back_on_step(
path: list,
visited: np.ndarray,
maze: np.ndarray,
h_w: tuple,
move: list,
) -> list:
path: list[tuple[int, int]],
visited: NDArray[Any],
maze: NDArray[Any],
h_w: tuple[int, int],
move: list[str],
) -> tuple[list[Any], list[Any]]:
"""Backtrack until a cell with an unexplored path is found.
Args:
@@ -388,6 +401,7 @@ class DepthFirstSearchSolver(MazeSolver):
Returns:
A tuple containing the updated path and move list.
"""
while path:
last = path[-1]
if DepthFirstSearchSolver.random_path(visited, last, maze, h_w):
@@ -397,7 +411,7 @@ class DepthFirstSearchSolver(MazeSolver):
return path, move
@staticmethod
def next_cell(coord: tuple, next: str) -> tuple:
def next_cell(coord: tuple[int, int], next: str) -> tuple[int, int]:
"""Return the coordinates of the next cell in the given direction.
Args:
+18
View File
@@ -0,0 +1,18 @@
from mazegen.Cell import Cell
from mazegen.Maze import Maze
from mazegen.MazeGenerator import MazeGenerator, DepthFirstSearch
from mazegen.MazeGenerator import Kruskal
from mazegen.MazeSolver import MazeSolver, AStar, DepthFirstSearchSolver
__version__ = "1.0.0"
__author__ = "us"
__all__ = [
"Cell",
"Maze",
"MazeGenerator",
"DepthFirstSearchSolver",
"MazeSolver",
"AStar",
"Kruskal",
"DepthFirstSearch",
]
+20 -16
View File
@@ -1,5 +1,6 @@
from src.amaz_lib.MazeGenerator import DepthFirstSearch, Kruskal
from src.amaz_lib.MazeSolver import AStar, DepthFirstSearchSolver
from mazegen import DepthFirstSearch, Kruskal
from mazegen import AStar, DepthFirstSearchSolver
from typing import Any
class DataMaze:
@@ -25,7 +26,7 @@ class DataMaze:
return data
@staticmethod
def transform_data(data: str) -> dict:
def transform_data(data: str) -> dict[str, str]:
"""Transform raw configuration text into a dictionary.
Each non-empty line containing ``=`` is split into a key-value pair.
@@ -42,7 +43,7 @@ class DataMaze:
return data_t
@staticmethod
def verif_key_data(data: dict) -> None:
def verif_key_data(data: dict[str, str]) -> None:
"""Validate that the configuration contains the expected keys.
Args:
@@ -71,20 +72,20 @@ class DataMaze:
)
@staticmethod
def convert_values(data: dict):
def convert_values(data: dict[str, str]) -> dict[str, Any]:
"""Convert configuration values to their appropriate Python types.
Args:
data: Raw configuration dictionary with string values.
Returns:
A dictionary containing converted values and instantiated solver and
generator objects.
A dictionary containing converted values and instantiated
solver and generator objects.
"""
key_int = {"WIDTH", "HEIGHT"}
key_tuple = {"ENTRY", "EXIT"}
key_bool = {"PERFECT"}
res: dict = {}
res: dict[str, Any] = {}
for key in key_int:
res.update({key: int(data[key])})
for key in key_tuple:
@@ -104,11 +105,11 @@ class DataMaze:
@staticmethod
def get_solver_generator(
data: dict,
entry: tuple,
exit: tuple,
data: dict[str, str],
entry: tuple[int, int],
exit: tuple[int, int],
perfect: bool,
) -> dict:
) -> dict[str, Any]:
"""Instantiate the configured maze generator and solver.
Args:
@@ -121,11 +122,14 @@ class DataMaze:
A dictionary containing initialized ``GENERATOR`` and ``SOLVER``
objects.
"""
available_generator = {
available_generator: dict[str, Any] = {
"Kruskal": Kruskal,
"DFS": DepthFirstSearch,
}
available_solver = {"AStar": AStar, "DFS": DepthFirstSearchSolver}
available_solver: dict[str, Any] = {
"AStar": AStar,
"DFS": DepthFirstSearchSolver,
}
res = {}
res["GENERATOR"] = available_generator[data["GENERATOR"]](
entry,
@@ -136,7 +140,7 @@ class DataMaze:
return res
@staticmethod
def convert_tuple(data: str) -> tuple:
def convert_tuple(data: str) -> tuple[int, int]:
"""Convert a comma-separated coordinate string into a tuple.
Args:
@@ -178,7 +182,7 @@ class DataMaze:
return False
@staticmethod
def get_data_maze(name_file: str) -> dict:
def get_data_maze(name_file: str) -> dict[str, Any]:
"""Load, validate, and convert maze configuration data from a file.
Args:
-6
View File
@@ -1,6 +0,0 @@
__version__ = "1.0.0"
__author__ = "mteriier, dgaillet"
from .Parsing import DataMaze
__all__ = ["DataMaze"]
View File
+1 -1
View File
@@ -1,4 +1,4 @@
from amaz_lib.Cell import Cell
from mazegen import Cell
def test_cell_setter_getter() -> None:
+2 -2
View File
@@ -1,5 +1,5 @@
from amaz_lib.MazeGenerator import DepthFirstSearch
from amaz_lib.Cell import Cell
from mazegen import DepthFirstSearch
from mazegen import Cell
import numpy as np
+5 -3
View File
@@ -1,6 +1,6 @@
import numpy
from amaz_lib.Cell import Cell
from amaz_lib.Maze import Maze
from mazegen import Cell
from mazegen import Maze
def test_maze_setter_getter() -> None:
@@ -15,7 +15,9 @@ def test_maze_setter_getter() -> None:
)
maze.set_maze(test)
assert numpy.array_equal(maze.get_maze(), test) is True
m = maze.get_maze()
assert m is not None
assert numpy.array_equal(m, test) is True
def test_maze_str() -> None:
+1 -5
View File
@@ -1,5 +1,5 @@
import numpy
from amaz_lib.MazeGenerator import DepthFirstSearch, MazeGenerator
from mazegen import DepthFirstSearch
class TestMazeGenerator:
@@ -12,7 +12,3 @@ class TestMazeGenerator:
maze = output
assert maze.shape == w_h
def test_gen_broken(self) -> None:
test = MazeGenerator.gen_broken_set(50, 50)
assert len(test) > 0
+2 -2
View File
@@ -1,6 +1,6 @@
from amaz_lib.Cell import Cell
from mazegen import Cell
import numpy as np
from amaz_lib import AStar, Maze
from mazegen import AStar, Maze
def test_solver() -> None:
+17 -17
View File
@@ -4,71 +4,71 @@ import pytest
class TestParsing:
def test_get_data_valid(self):
def test_get_data_valid(self) -> None:
data = DataMaze.get_file_data("tests/test_txt/config_1.txt")
assert isinstance(data, str) is True
def test_file_error(self):
def test_file_error(self) -> None:
with pytest.raises(FileNotFoundError):
DataMaze.get_file_data("tete")
# def test_permission_error(self):
# def test_permission_error(self) -> None:
# with pytest.raises(PermissionError):
# DataMaze.get_file_data("tests/test_txt/error_1.txt")
def test_empty_file_error(self):
def test_empty_file_error(self) -> None:
with pytest.raises(ValueError):
DataMaze.get_file_data("tests/test_txt/error_6.txt")
def test_transform_data_valid(self):
def test_transform_data_valid(self) -> None:
data = DataMaze.get_file_data("tests/test_txt/config_1.txt")
data_2 = DataMaze.transform_data(data)
assert isinstance(data_2, dict)
def test_transform__index_error(self):
def test_transform__index_error(self) -> None:
with pytest.raises(IndexError):
DataMaze.transform_data("asdasdasdasdasdasda\nasdasdas=asdasd")
def test_key_data_error(self):
def test_key_data_error(self) -> None:
with pytest.raises(KeyError):
data = DataMaze.get_file_data("tests/test_txt/error_8.txt")
data2 = DataMaze.transform_data(data)
DataMaze.verif_key_data(data2)
def test_key_data_error_2(self):
def test_key_data_error_2(self) -> None:
with pytest.raises(KeyError):
data = DataMaze.get_file_data("tests/test_txt/error_9.txt")
data2 = DataMaze.transform_data(data)
DataMaze.verif_key_data(data2)
def test_convert_int(self):
def test_convert_int(self) -> None:
with pytest.raises(ValueError):
data = DataMaze.get_file_data("tests/test_txt/error_2.txt")
data2 = DataMaze.transform_data(data)
DataMaze.convert_values(data2)
def test_tuple_error(self):
def test_tuple_error(self) -> None:
with pytest.raises(ValueError):
DataMaze.convert_tuple("0,3,5,5")
def test_tuple_error1(self):
def test_tuple_error1(self) -> None:
with pytest.raises(AttributeError):
DataMaze.convert_tuple(None)
DataMaze.convert_tuple("None")
def test_bool_error(self):
def test_bool_error(self) -> None:
with pytest.raises(ValueError):
DataMaze.convert_bool("Trueeee")
def test_valid_tuple(self):
def test_valid_tuple(self) -> None:
assert DataMaze.convert_tuple("7534564654, 78") == (7534564654, 78)
def test_valid_bool(self):
def test_valid_bool(self) -> None:
assert DataMaze.convert_bool("False") is False
def test_valid_bool1(self):
def test_valid_bool1(self) -> None:
assert DataMaze.convert_bool("True") is True
def test_data_maze(self):
def test_data_maze(self) -> None:
data = DataMaze.get_data_maze("tests/test_txt/config_1.txt")
assert data["WIDTH"] == 200
assert data["HEIGHT"] == 100
Generated
+30 -30
View File
@@ -6,36 +6,6 @@ resolution-markers = [
"python_full_version < '3.11'",
]
[[package]]
name = "a-maze-ing"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "pydantic" },
]
[package.dev-dependencies]
dev = [
{ name = "flake8" },
{ name = "mypy" },
{ name = "pytest" },
]
[package.metadata]
requires-dist = [
{ name = "numpy", specifier = ">=2.2.6" },
{ name = "pydantic", specifier = ">=2.12.5" },
]
[package.metadata.requires-dev]
dev = [
{ name = "flake8", specifier = ">=7.3.0" },
{ name = "mypy", specifier = ">=1.19.1" },
{ name = "pytest", specifier = ">=9.0.2" },
]
[[package]]
name = "annotated-types"
version = "0.7.0"
@@ -174,6 +144,36 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" },
]
[[package]]
name = "mazegen"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "pydantic" },
]
[package.dev-dependencies]
dev = [
{ name = "flake8" },
{ name = "mypy" },
{ name = "pytest" },
]
[package.metadata]
requires-dist = [
{ name = "numpy", specifier = ">=2.2.6" },
{ name = "pydantic", specifier = ">=2.12.5" },
]
[package.metadata.requires-dev]
dev = [
{ name = "flake8", specifier = ">=7.3.0" },
{ name = "mypy", specifier = ">=1.19.1" },
{ name = "pytest", specifier = ">=9.0.2" },
]
[[package]]
name = "mccabe"
version = "0.7.0"