squared_maze 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- squared_maze-0.1.0/LICENSE +21 -0
- squared_maze-0.1.0/PKG-INFO +103 -0
- squared_maze-0.1.0/README.md +87 -0
- squared_maze-0.1.0/pyproject.toml +17 -0
- squared_maze-0.1.0/setup.cfg +4 -0
- squared_maze-0.1.0/src/squared_maze/__init__.py +17 -0
- squared_maze-0.1.0/src/squared_maze/generator.py +293 -0
- squared_maze-0.1.0/src/squared_maze/render.py +157 -0
- squared_maze-0.1.0/src/squared_maze/solver.py +90 -0
- squared_maze-0.1.0/src/squared_maze.egg-info/PKG-INFO +103 -0
- squared_maze-0.1.0/src/squared_maze.egg-info/SOURCES.txt +12 -0
- squared_maze-0.1.0/src/squared_maze.egg-info/dependency_links.txt +1 -0
- squared_maze-0.1.0/src/squared_maze.egg-info/requires.txt +1 -0
- squared_maze-0.1.0/src/squared_maze.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: squared_maze
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Maze generator and A* solver with ASCII and image rendering
|
|
5
|
+
Author-email: Jaime Pizarroso <jpizarroso@comillas.edu>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: maze,astar,generator,puzzle,visualization
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: Pillow>=9.0.0
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# squared_maze
|
|
18
|
+
|
|
19
|
+
Tiny Python package to generate grid mazes, solve them with A* and render
|
|
20
|
+
ASCII and PNG visualizations. The project includes utilities to pick valid
|
|
21
|
+
start/end cells, force a maze to become unsolvable, or introduce additional
|
|
22
|
+
solutions by breaking walls.
|
|
23
|
+
|
|
24
|
+
Features
|
|
25
|
+
- Generate perfect mazes (recursive backtracker) as a grid where `1` is
|
|
26
|
+
walkable and `0` is a wall.
|
|
27
|
+
- Solve with A* (`src/squared_maze/solver.py`).
|
|
28
|
+
- Render ASCII (`grid_to_ascii`) with customizable symbols.
|
|
29
|
+
- Render PNG images (Pillow) with start (green), end (red), path (blue),
|
|
30
|
+
walls (dark gray) and floors (light gray).
|
|
31
|
+
- Helpers: `find_valid_cell`, `make_unsolvable`, `make_multiple_solutions`.
|
|
32
|
+
|
|
33
|
+
Quick install
|
|
34
|
+
|
|
35
|
+
This project requires Python 3.8+ and Pillow for image output. Install the
|
|
36
|
+
dependency into your environment:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install pillow
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Running the example notebook
|
|
43
|
+
|
|
44
|
+
Open the example notebook `examples/maze_example.ipynb` with Jupyter or run it
|
|
45
|
+
in a supported environment (VS Code/Jupyter Lab). The notebook demonstrates:
|
|
46
|
+
- generating and solving a maze
|
|
47
|
+
- saving two images (with and without the path)
|
|
48
|
+
- creating an unsolvable variant
|
|
49
|
+
- creating a variant with multiple distinct solutions
|
|
50
|
+
|
|
51
|
+
If running the notebook from the repository you may need to add the project
|
|
52
|
+
`src` folder to `PYTHONPATH`. From the repository root you can run a small
|
|
53
|
+
script or open a Python REPL like this:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python3 -c "import sys; from pathlib import Path; sys.path.insert(0, str(Path('src').resolve())); from squared_maze import generate_maze, astar, grid_to_ascii; g=generate_maze(12,20,seed=42); print(grid_to_ascii(g, None))"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Basic usage (script)
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from squared_maze import (
|
|
63
|
+
generate_maze,
|
|
64
|
+
astar,
|
|
65
|
+
grid_to_ascii,
|
|
66
|
+
save_images,
|
|
67
|
+
find_valid_cell,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# generate
|
|
71
|
+
grid = generate_maze(12, 20, seed=42)
|
|
72
|
+
start = find_valid_cell(grid, seed=1)
|
|
73
|
+
end = find_valid_cell(grid, exclude={start}, seed=2)
|
|
74
|
+
path = astar(grid, start, end)
|
|
75
|
+
|
|
76
|
+
print(grid_to_ascii(grid, path, start, end))
|
|
77
|
+
save_images(grid, path, start, end, cell_size=24, out_prefix='maze_out')
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
API (key functions)
|
|
81
|
+
- `generate_maze(rows, cols, seed=None)` -> grid
|
|
82
|
+
- `astar(grid, start, end)` -> list of coordinates or `None`
|
|
83
|
+
- `grid_to_ascii(grid, path=None, start=None, end=None, ...)` -> str (customizable symbols)
|
|
84
|
+
- `save_images(grid, path=None, start=None, end=None, cell_size=16, out_prefix='maze')` -> (fn_no, fn_yes)
|
|
85
|
+
- `find_valid_cell(grid, exclude=None, seed=None)` -> (row, col)
|
|
86
|
+
- `make_unsolvable(grid, start, end, astar_fn, ...)` -> bool (modifies grid)
|
|
87
|
+
- `make_multiple_solutions(grid, start, end, astar_fn, ...)` -> bool (modifies grid)
|
|
88
|
+
|
|
89
|
+
Notes
|
|
90
|
+
- The generator produces a perfect maze (a spanning tree). That means there
|
|
91
|
+
is exactly one path between any two room cells until you deliberately
|
|
92
|
+
break walls (e.g. with `make_multiple_solutions`). Use `find_valid_cell`
|
|
93
|
+
to pick valid walkable start/end cells.
|
|
94
|
+
- Images are saved to the working directory. Filenames are returned by
|
|
95
|
+
`save_images`.
|
|
96
|
+
|
|
97
|
+
Contributing
|
|
98
|
+
- Small, self-contained patches are welcome. Please keep docstrings and
|
|
99
|
+
code PEP8-compatible and add a short test if you change core behaviour.
|
|
100
|
+
|
|
101
|
+
License
|
|
102
|
+
- This repository does not include an explicit license file. Add one if you
|
|
103
|
+
intend to publish or share the code.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# squared_maze
|
|
2
|
+
|
|
3
|
+
Tiny Python package to generate grid mazes, solve them with A* and render
|
|
4
|
+
ASCII and PNG visualizations. The project includes utilities to pick valid
|
|
5
|
+
start/end cells, force a maze to become unsolvable, or introduce additional
|
|
6
|
+
solutions by breaking walls.
|
|
7
|
+
|
|
8
|
+
Features
|
|
9
|
+
- Generate perfect mazes (recursive backtracker) as a grid where `1` is
|
|
10
|
+
walkable and `0` is a wall.
|
|
11
|
+
- Solve with A* (`src/squared_maze/solver.py`).
|
|
12
|
+
- Render ASCII (`grid_to_ascii`) with customizable symbols.
|
|
13
|
+
- Render PNG images (Pillow) with start (green), end (red), path (blue),
|
|
14
|
+
walls (dark gray) and floors (light gray).
|
|
15
|
+
- Helpers: `find_valid_cell`, `make_unsolvable`, `make_multiple_solutions`.
|
|
16
|
+
|
|
17
|
+
Quick install
|
|
18
|
+
|
|
19
|
+
This project requires Python 3.8+ and Pillow for image output. Install the
|
|
20
|
+
dependency into your environment:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install pillow
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Running the example notebook
|
|
27
|
+
|
|
28
|
+
Open the example notebook `examples/maze_example.ipynb` with Jupyter or run it
|
|
29
|
+
in a supported environment (VS Code/Jupyter Lab). The notebook demonstrates:
|
|
30
|
+
- generating and solving a maze
|
|
31
|
+
- saving two images (with and without the path)
|
|
32
|
+
- creating an unsolvable variant
|
|
33
|
+
- creating a variant with multiple distinct solutions
|
|
34
|
+
|
|
35
|
+
If running the notebook from the repository you may need to add the project
|
|
36
|
+
`src` folder to `PYTHONPATH`. From the repository root you can run a small
|
|
37
|
+
script or open a Python REPL like this:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
python3 -c "import sys; from pathlib import Path; sys.path.insert(0, str(Path('src').resolve())); from squared_maze import generate_maze, astar, grid_to_ascii; g=generate_maze(12,20,seed=42); print(grid_to_ascii(g, None))"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Basic usage (script)
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from squared_maze import (
|
|
47
|
+
generate_maze,
|
|
48
|
+
astar,
|
|
49
|
+
grid_to_ascii,
|
|
50
|
+
save_images,
|
|
51
|
+
find_valid_cell,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# generate
|
|
55
|
+
grid = generate_maze(12, 20, seed=42)
|
|
56
|
+
start = find_valid_cell(grid, seed=1)
|
|
57
|
+
end = find_valid_cell(grid, exclude={start}, seed=2)
|
|
58
|
+
path = astar(grid, start, end)
|
|
59
|
+
|
|
60
|
+
print(grid_to_ascii(grid, path, start, end))
|
|
61
|
+
save_images(grid, path, start, end, cell_size=24, out_prefix='maze_out')
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
API (key functions)
|
|
65
|
+
- `generate_maze(rows, cols, seed=None)` -> grid
|
|
66
|
+
- `astar(grid, start, end)` -> list of coordinates or `None`
|
|
67
|
+
- `grid_to_ascii(grid, path=None, start=None, end=None, ...)` -> str (customizable symbols)
|
|
68
|
+
- `save_images(grid, path=None, start=None, end=None, cell_size=16, out_prefix='maze')` -> (fn_no, fn_yes)
|
|
69
|
+
- `find_valid_cell(grid, exclude=None, seed=None)` -> (row, col)
|
|
70
|
+
- `make_unsolvable(grid, start, end, astar_fn, ...)` -> bool (modifies grid)
|
|
71
|
+
- `make_multiple_solutions(grid, start, end, astar_fn, ...)` -> bool (modifies grid)
|
|
72
|
+
|
|
73
|
+
Notes
|
|
74
|
+
- The generator produces a perfect maze (a spanning tree). That means there
|
|
75
|
+
is exactly one path between any two room cells until you deliberately
|
|
76
|
+
break walls (e.g. with `make_multiple_solutions`). Use `find_valid_cell`
|
|
77
|
+
to pick valid walkable start/end cells.
|
|
78
|
+
- Images are saved to the working directory. Filenames are returned by
|
|
79
|
+
`save_images`.
|
|
80
|
+
|
|
81
|
+
Contributing
|
|
82
|
+
- Small, self-contained patches are welcome. Please keep docstrings and
|
|
83
|
+
code PEP8-compatible and add a short test if you change core behaviour.
|
|
84
|
+
|
|
85
|
+
License
|
|
86
|
+
- This repository does not include an explicit license file. Add one if you
|
|
87
|
+
intend to publish or share the code.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "squared_maze"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Maze generator and A* solver with ASCII and image rendering"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.8"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [ { name = "Jaime Pizarroso", email = "jpizarroso@comillas.edu" } ]
|
|
9
|
+
keywords = ["maze", "astar", "generator", "puzzle", "visualization"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"Pillow>=9.0.0"
|
|
17
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Top-level package for maze generator and solver.
|
|
2
|
+
|
|
3
|
+
Expose main helpers: generate_maze, solve_maze, render helpers.
|
|
4
|
+
"""
|
|
5
|
+
from .generator import generate_maze, find_valid_cell, make_multiple_solutions, make_unsolvable
|
|
6
|
+
from .solver import astar
|
|
7
|
+
from .render import grid_to_ascii, save_images
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"generate_maze",
|
|
11
|
+
"find_valid_cell",
|
|
12
|
+
"make_multiple_solutions",
|
|
13
|
+
"make_unsolvable",
|
|
14
|
+
"astar",
|
|
15
|
+
"grid_to_ascii",
|
|
16
|
+
"save_images",
|
|
17
|
+
]
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Maze generation utilities.
|
|
2
|
+
|
|
3
|
+
This module implements a randomized depth-first search (recursive backtracker)
|
|
4
|
+
to carve a perfect maze into a grid that includes walls as cells. The
|
|
5
|
+
public function `generate_maze` accepts the number of logical rows and columns
|
|
6
|
+
and returns a grid of size (2*rows+1) x (2*cols+1) where 1 indicates a
|
|
7
|
+
walkable cell and 0 indicates a wall.
|
|
8
|
+
|
|
9
|
+
The grid layout uses odd indices for rooms/cells and even indices for walls.
|
|
10
|
+
This makes it simple to carve passages by stepping two cells at a time and
|
|
11
|
+
clearing the intermediate wall.
|
|
12
|
+
|
|
13
|
+
All functions follow Google style docstrings and minimal external deps.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import random
|
|
18
|
+
from typing import List, Optional, Tuple, Set, Callable
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _make_grid(rows: int, cols: int) -> List[List[int]]:
|
|
22
|
+
"""Create a grid initialized with walls (0).
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
rows: number of logical rows (rooms).
|
|
26
|
+
cols: number of logical columns (rooms).
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
A list of lists representing the grid with shape (2*rows+1) x (2*cols+1).
|
|
30
|
+
"""
|
|
31
|
+
height = 2 * rows + 1
|
|
32
|
+
width = 2 * cols + 1
|
|
33
|
+
return [[0 for _ in range(width)] for _ in range(height)]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def generate_maze(rows: int, cols: int, seed: Optional[int] = None) -> List[List[int]]:
|
|
37
|
+
"""Generate a perfect maze using recursive backtracker.
|
|
38
|
+
|
|
39
|
+
The returned grid is (2*rows+1) x (2*cols+1) with:
|
|
40
|
+
- 1: walkable cell
|
|
41
|
+
- 0: wall
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
rows: number of logical rows (rooms).
|
|
45
|
+
cols: number of logical columns (rooms).
|
|
46
|
+
seed: optional random seed for reproducibility.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
A 2D grid of ints (1 walkable, 0 wall).
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
ValueError: if rows or cols are less than 1.
|
|
53
|
+
"""
|
|
54
|
+
if rows < 1 or cols < 1:
|
|
55
|
+
raise ValueError("rows and cols must be >= 1")
|
|
56
|
+
|
|
57
|
+
rng = random.Random(seed)
|
|
58
|
+
grid = _make_grid(rows, cols)
|
|
59
|
+
|
|
60
|
+
# Helper to convert room coordinates (r,c) in [0,rows) to grid coords
|
|
61
|
+
def to_grid(rc: Tuple[int, int]) -> Tuple[int, int]:
|
|
62
|
+
r, c = rc
|
|
63
|
+
return 2 * r + 1, 2 * c + 1
|
|
64
|
+
|
|
65
|
+
visited = [[False for _ in range(cols)] for _ in range(rows)]
|
|
66
|
+
|
|
67
|
+
# Neighbor deltas in room coordinates (4-connectivity)
|
|
68
|
+
deltas = [(0, 1), (1, 0), (0, -1), (-1, 0)]
|
|
69
|
+
|
|
70
|
+
def carve(r: int, c: int) -> None:
|
|
71
|
+
"""Recursively carve passages starting from room (r,c)."""
|
|
72
|
+
visited[r][c] = True
|
|
73
|
+
gr, gc = to_grid((r, c))
|
|
74
|
+
grid[gr][gc] = 1
|
|
75
|
+
|
|
76
|
+
# randomize neighbor order
|
|
77
|
+
rng.shuffle(deltas)
|
|
78
|
+
for dr, dc in deltas:
|
|
79
|
+
nr, nc = r + dr, c + dc
|
|
80
|
+
if 0 <= nr < rows and 0 <= nc < cols and not visited[nr][nc]:
|
|
81
|
+
# remove wall between (r,c) and (nr,nc)
|
|
82
|
+
gr2, gc2 = to_grid((nr, nc))
|
|
83
|
+
wall_r, wall_c = (gr + gr2) // 2, (gc + gc2) // 2
|
|
84
|
+
grid[wall_r][wall_c] = 1
|
|
85
|
+
carve(nr, nc)
|
|
86
|
+
|
|
87
|
+
carve(0, 0)
|
|
88
|
+
|
|
89
|
+
return grid
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def find_valid_cell(grid: List[List[int]], exclude: Optional[Set[Tuple[int, int]]] = None,
|
|
93
|
+
seed: Optional[int] = None) -> Tuple[int, int]:
|
|
94
|
+
"""Return a random valid (walkable) cell coordinate from the grid.
|
|
95
|
+
|
|
96
|
+
The function scans the provided `grid` (where 1 means walkable and 0 means
|
|
97
|
+
wall) and returns a randomly chosen coordinate (row, col) such that the
|
|
98
|
+
cell is walkable and not in the optional `exclude` set.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
grid: 2D grid produced by :func:`generate_maze` where 1 is walkable.
|
|
102
|
+
exclude: optional set of coordinates to avoid (for example the start
|
|
103
|
+
when choosing an end cell).
|
|
104
|
+
seed: optional integer seed for reproducible selection. If omitted a
|
|
105
|
+
default random source is used.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
A tuple (row, col) pointing to a walkable cell in the grid.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
ValueError: if no valid cell can be found (for example a grid with no
|
|
112
|
+
walkable cells or all walkable cells are excluded).
|
|
113
|
+
"""
|
|
114
|
+
rng = random.Random(seed)
|
|
115
|
+
rows = len(grid)
|
|
116
|
+
if rows == 0:
|
|
117
|
+
raise ValueError("grid must be non-empty")
|
|
118
|
+
cols = len(grid[0])
|
|
119
|
+
|
|
120
|
+
exclude = exclude or set()
|
|
121
|
+
|
|
122
|
+
# collect all walkable cells not in exclude
|
|
123
|
+
candidates: List[Tuple[int, int]] = [
|
|
124
|
+
(r, c)
|
|
125
|
+
for r in range(rows)
|
|
126
|
+
for c in range(cols)
|
|
127
|
+
if grid[r][c] == 1 and (r, c) not in exclude
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
if not candidates:
|
|
131
|
+
raise ValueError("no valid walkable cells available to choose from")
|
|
132
|
+
|
|
133
|
+
return rng.choice(candidates)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def make_multiple_solutions(
|
|
137
|
+
grid: List[List[int]],
|
|
138
|
+
start: Tuple[int, int],
|
|
139
|
+
end: Tuple[int, int],
|
|
140
|
+
astar_fn: Callable,
|
|
141
|
+
max_tries: int = 200,
|
|
142
|
+
seed: Optional[int] = None,
|
|
143
|
+
) -> bool:
|
|
144
|
+
"""Modify ``grid`` by breaking walls until the maze has multiple solutions.
|
|
145
|
+
|
|
146
|
+
This function attempts to create an alternate path between ``start`` and
|
|
147
|
+
``end`` by turning wall cells (0) into walkable cells (1). After each
|
|
148
|
+
candidate wall removal it calls ``astar_fn(grid, start, end)`` to check if
|
|
149
|
+
the returned path differs from the original one. The grid is modified in
|
|
150
|
+
place and the function returns True on success.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
grid: 2D grid where 1 is walkable and 0 is wall.
|
|
154
|
+
start: start coordinate (row, col).
|
|
155
|
+
end: end coordinate (row, col).
|
|
156
|
+
astar_fn: function implementing A* search with signature
|
|
157
|
+
``astar_fn(grid, start, end) -> Optional[List[Tuple[int,int]]]``.
|
|
158
|
+
max_tries: maximum number of candidate walls to try.
|
|
159
|
+
seed: optional seed for reproducibility.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
True if the grid was modified to produce multiple distinct solutions
|
|
163
|
+
between start and end, False otherwise.
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
ValueError: if start or end are not walkable.
|
|
167
|
+
"""
|
|
168
|
+
rng = random.Random(seed)
|
|
169
|
+
|
|
170
|
+
if grid[start[0]][start[1]] != 1 or grid[end[0]][end[1]] != 1:
|
|
171
|
+
raise ValueError("start and end must be walkable cells (grid value 1)")
|
|
172
|
+
|
|
173
|
+
orig_path = astar_fn(grid, start, end)
|
|
174
|
+
if not orig_path:
|
|
175
|
+
# nothing to make multiple if they are not connected
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
rows = len(grid)
|
|
179
|
+
cols = len(grid[0])
|
|
180
|
+
|
|
181
|
+
# candidate wall cells that when removed connect two or more walkable neighbors
|
|
182
|
+
candidates: List[Tuple[int, int]] = []
|
|
183
|
+
for r in range(rows):
|
|
184
|
+
for c in range(cols):
|
|
185
|
+
if grid[r][c] != 0:
|
|
186
|
+
continue
|
|
187
|
+
neighs = 0
|
|
188
|
+
for dr, dc in ((0, 1), (1, 0), (0, -1), (-1, 0)):
|
|
189
|
+
nr, nc = r + dr, c + dc
|
|
190
|
+
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
|
|
191
|
+
neighs += 1
|
|
192
|
+
if neighs >= 2:
|
|
193
|
+
candidates.append((r, c))
|
|
194
|
+
|
|
195
|
+
rng.shuffle(candidates)
|
|
196
|
+
|
|
197
|
+
tries = 0
|
|
198
|
+
for (r, c) in candidates:
|
|
199
|
+
if tries >= max_tries:
|
|
200
|
+
break
|
|
201
|
+
tries += 1
|
|
202
|
+
# try removing this wall
|
|
203
|
+
grid[r][c] = 1
|
|
204
|
+
new_path = astar_fn(grid, start, end)
|
|
205
|
+
if new_path and new_path != orig_path:
|
|
206
|
+
return True
|
|
207
|
+
# revert
|
|
208
|
+
grid[r][c] = 0
|
|
209
|
+
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def make_unsolvable(
|
|
214
|
+
grid: List[List[int]],
|
|
215
|
+
start: Tuple[int, int],
|
|
216
|
+
end: Tuple[int, int],
|
|
217
|
+
astar_fn: Callable,
|
|
218
|
+
seed: Optional[int] = None,
|
|
219
|
+
max_tries: int = 100,
|
|
220
|
+
) -> bool:
|
|
221
|
+
"""Modify ``grid`` to make the maze unsolvable between ``start`` and
|
|
222
|
+
``end``.
|
|
223
|
+
|
|
224
|
+
The function finds an existing path between ``start`` and ``end`` and
|
|
225
|
+
then blocks one or more intermediate cells along that path (not start or
|
|
226
|
+
end) to disconnect the endpoints. Returns True if the grid was modified
|
|
227
|
+
and the two points are no longer connected.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
grid: 2D grid where 1 is walkable and 0 is wall.
|
|
231
|
+
start: start coordinate (row, col).
|
|
232
|
+
end: end coordinate (row, col).
|
|
233
|
+
astar_fn: function implementing A* search with signature
|
|
234
|
+
``astar_fn(grid, start, end) -> Optional[List[Tuple[int,int]]]``.
|
|
235
|
+
seed: optional seed for reproducibility.
|
|
236
|
+
max_tries: maximum attempts to block different intermediate cells.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
True if the grid was modified so that no path exists between start and end,
|
|
240
|
+
False if unsuccessful.
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
ValueError: if start or end are not walkable.
|
|
244
|
+
"""
|
|
245
|
+
rng = random.Random(seed)
|
|
246
|
+
|
|
247
|
+
if grid[start[0]][start[1]] != 1 or grid[end[0]][end[1]] != 1:
|
|
248
|
+
raise ValueError("start and end must be walkable cells (grid value 1)")
|
|
249
|
+
|
|
250
|
+
path = astar_fn(grid, start, end)
|
|
251
|
+
if not path:
|
|
252
|
+
# already unsolvable
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
# consider intermediate path cells (exclude start/end)
|
|
256
|
+
intermediates = [p for p in path[1:-1]]
|
|
257
|
+
if not intermediates:
|
|
258
|
+
# direct neighbors — blocking one neighbor should work though
|
|
259
|
+
intermediates = [path[1]] if len(path) > 1 else []
|
|
260
|
+
|
|
261
|
+
rng.shuffle(intermediates)
|
|
262
|
+
tries = 0
|
|
263
|
+
for cell in intermediates:
|
|
264
|
+
if tries >= max_tries:
|
|
265
|
+
break
|
|
266
|
+
tries += 1
|
|
267
|
+
r, c = cell
|
|
268
|
+
saved = grid[r][c]
|
|
269
|
+
grid[r][c] = 0
|
|
270
|
+
if not astar_fn(grid, start, end):
|
|
271
|
+
return True
|
|
272
|
+
# revert and try next
|
|
273
|
+
grid[r][c] = saved
|
|
274
|
+
|
|
275
|
+
# as a fallback, try blocking additional nearby walkable cells
|
|
276
|
+
rows = len(grid)
|
|
277
|
+
cols = len(grid[0])
|
|
278
|
+
all_walkables = [(r, c) for r in range(rows) for c in range(cols) if grid[r][c] == 1 and (r, c) not in (start, end)]
|
|
279
|
+
rng.shuffle(all_walkables)
|
|
280
|
+
for (r, c) in all_walkables[:max_tries]:
|
|
281
|
+
saved = grid[r][c]
|
|
282
|
+
grid[r][c] = 0
|
|
283
|
+
if not astar_fn(grid, start, end):
|
|
284
|
+
return True
|
|
285
|
+
grid[r][c] = saved
|
|
286
|
+
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
if __name__ == "__main__":
|
|
291
|
+
g = generate_maze(5, 10, seed=1)
|
|
292
|
+
for row in g:
|
|
293
|
+
print(''.join(['.' if c else '#' for c in row]))
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Rendering utilities for the maze: ASCII and images.
|
|
2
|
+
|
|
3
|
+
The image functions use Pillow to create a visualization of the grid. Walls are
|
|
4
|
+
dark gray, walkable light gray, start green, end red, and path cells blue.
|
|
5
|
+
Thin black separators are drawn between cells.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from PIL import Image, ImageDraw
|
|
12
|
+
|
|
13
|
+
Coord = Tuple[int, int]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def grid_to_ascii(
|
|
17
|
+
grid: List[List[int]],
|
|
18
|
+
path: Optional[List[Coord]] = None,
|
|
19
|
+
start: Optional[Coord] = None,
|
|
20
|
+
end: Optional[Coord] = None,
|
|
21
|
+
start_sym: str = "S",
|
|
22
|
+
end_sym: str = "E",
|
|
23
|
+
wall_sym: str = "█",
|
|
24
|
+
path_sym: str = "*",
|
|
25
|
+
walkable_sym: str = ".",
|
|
26
|
+
) -> str:
|
|
27
|
+
"""Return an ASCII representation of the maze using customizable symbols.
|
|
28
|
+
|
|
29
|
+
The function renders the grid into a multiline string. By default the
|
|
30
|
+
following symbols are used:
|
|
31
|
+
- start: ``S``
|
|
32
|
+
- end: ``E``
|
|
33
|
+
- wall: ``█``
|
|
34
|
+
- path (non start/end cells): ``*``
|
|
35
|
+
- walkable cells: ``.``
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
grid: 2D grid where 1 is walkable and 0 is wall.
|
|
39
|
+
path: optional list of coordinates that form the solution path.
|
|
40
|
+
start: optional start coordinate to mark with ``start_sym``.
|
|
41
|
+
end: optional end coordinate to mark with ``end_sym``.
|
|
42
|
+
start_sym: symbol used for the start cell. Defaults to ``'S'``.
|
|
43
|
+
end_sym: symbol used for the end cell. Defaults to ``'E'``.
|
|
44
|
+
wall_sym: symbol used for walls. Defaults to ``'█'``.
|
|
45
|
+
path_sym: symbol used for path cells (excluding start/end). Defaults to ``'*'``.
|
|
46
|
+
walkable_sym: symbol used for ordinary walkable cells. Defaults to ``'.'``.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
A multiline string with characters representing the maze.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
path_set = set(path) if path else set()
|
|
53
|
+
lines: List[str] = []
|
|
54
|
+
for r, row in enumerate(grid):
|
|
55
|
+
line_chars: List[str] = []
|
|
56
|
+
for c, cell in enumerate(row):
|
|
57
|
+
coord = (r, c)
|
|
58
|
+
if start is not None and coord == start:
|
|
59
|
+
line_chars.append(start_sym)
|
|
60
|
+
elif end is not None and coord == end:
|
|
61
|
+
line_chars.append(end_sym)
|
|
62
|
+
elif coord in path_set and coord != start and coord != end:
|
|
63
|
+
line_chars.append(path_sym)
|
|
64
|
+
elif cell == 0:
|
|
65
|
+
line_chars.append(wall_sym)
|
|
66
|
+
else:
|
|
67
|
+
line_chars.append(walkable_sym)
|
|
68
|
+
lines.append("".join(line_chars))
|
|
69
|
+
return "\n".join(lines)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def save_images(grid: List[List[int]], path: Optional[List[Coord]] = None,
|
|
73
|
+
start: Optional[Coord] = None, end: Optional[Coord] = None,
|
|
74
|
+
cell_size: int = 16, out_prefix: str = "maze") -> Tuple[str, str]:
|
|
75
|
+
"""Save two PNG images: one without the path and one including the path.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
grid: 2D grid where 1 is walkable and 0 is wall.
|
|
79
|
+
path: optional solution path.
|
|
80
|
+
start: optional start coordinate.
|
|
81
|
+
end: optional end coordinate.
|
|
82
|
+
cell_size: size in pixels of each cell.
|
|
83
|
+
out_prefix: prefix for output filenames.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Tuple of filenames (without_path_png, with_path_png).
|
|
87
|
+
"""
|
|
88
|
+
rows = len(grid)
|
|
89
|
+
cols = len(grid[0])
|
|
90
|
+
# line thickness 1 pixel between cells
|
|
91
|
+
img_w = cols * cell_size + (cols + 1)
|
|
92
|
+
img_h = rows * cell_size + (rows + 1)
|
|
93
|
+
|
|
94
|
+
def draw(with_path: bool) -> Image.Image:
|
|
95
|
+
img = Image.new("RGB", (img_w, img_h), color=(0, 0, 0))
|
|
96
|
+
draw = ImageDraw.Draw(img)
|
|
97
|
+
|
|
98
|
+
wall_color = (40, 40, 40) # very dark gray
|
|
99
|
+
floor_color = (211, 211, 211) # lightgray
|
|
100
|
+
start_color = (0, 200, 0)
|
|
101
|
+
end_color = (200, 0, 0)
|
|
102
|
+
path_color = (0, 0, 200)
|
|
103
|
+
|
|
104
|
+
def cell_bbox(r: int, c: int):
|
|
105
|
+
x0 = c * cell_size + (c + 1)
|
|
106
|
+
y0 = r * cell_size + (r + 1)
|
|
107
|
+
x1 = x0 + cell_size - 1
|
|
108
|
+
y1 = y0 + cell_size - 1
|
|
109
|
+
return x0, y0, x1, y1
|
|
110
|
+
|
|
111
|
+
path_set = set(path) if path else set()
|
|
112
|
+
|
|
113
|
+
for r in range(rows):
|
|
114
|
+
for c in range(cols):
|
|
115
|
+
bbox = cell_bbox(r, c)
|
|
116
|
+
if grid[r][c] == 0:
|
|
117
|
+
draw.rectangle(bbox, fill=wall_color)
|
|
118
|
+
else:
|
|
119
|
+
draw.rectangle(bbox, fill=floor_color)
|
|
120
|
+
|
|
121
|
+
# draw path on top if requested
|
|
122
|
+
if with_path and path:
|
|
123
|
+
for (r, c) in path:
|
|
124
|
+
if (r, c) == start or (r, c) == end:
|
|
125
|
+
continue
|
|
126
|
+
bbox = cell_bbox(r, c)
|
|
127
|
+
draw.rectangle(bbox, fill=path_color)
|
|
128
|
+
|
|
129
|
+
# draw start and end
|
|
130
|
+
if start:
|
|
131
|
+
draw.rectangle(cell_bbox(*start), fill=start_color)
|
|
132
|
+
if end:
|
|
133
|
+
draw.rectangle(cell_bbox(*end), fill=end_color)
|
|
134
|
+
|
|
135
|
+
# thin black separators already present as background; optionally draw outlines
|
|
136
|
+
return img
|
|
137
|
+
|
|
138
|
+
img_no = draw(False)
|
|
139
|
+
img_yes = draw(True)
|
|
140
|
+
|
|
141
|
+
fn_no = f"{out_prefix}.png"
|
|
142
|
+
fn_yes = f"{out_prefix}_path.png"
|
|
143
|
+
img_no.save(fn_no)
|
|
144
|
+
img_yes.save(fn_yes)
|
|
145
|
+
return fn_no, fn_yes
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
from .generator import generate_maze
|
|
150
|
+
from .solver import astar
|
|
151
|
+
|
|
152
|
+
g = generate_maze(10, 16, seed=1)
|
|
153
|
+
start = (1, 1)
|
|
154
|
+
end = (len(g) - 2, len(g[0]) - 2)
|
|
155
|
+
p = astar(g, start, end)
|
|
156
|
+
print(grid_to_ascii(g, p, start, end))
|
|
157
|
+
save_images(g, p, start, end, cell_size=12, out_prefix="demo_maze")
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""A* path finding for the maze grid.
|
|
2
|
+
|
|
3
|
+
This module implements a simple A* search over the grid produced by
|
|
4
|
+
`generate_maze`. It expects a grid where 1 means walkable and 0 means wall.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import heapq
|
|
9
|
+
from typing import List, Tuple, Optional, Dict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
Coord = Tuple[int, int]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _neighbors(grid: List[List[int]], node: Coord) -> List[Coord]:
|
|
16
|
+
rows = len(grid)
|
|
17
|
+
cols = len(grid[0])
|
|
18
|
+
r, c = node
|
|
19
|
+
nbrs = []
|
|
20
|
+
for dr, dc in ((0, 1), (1, 0), (0, -1), (-1, 0)):
|
|
21
|
+
nr, nc = r + dr, c + dc
|
|
22
|
+
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
|
|
23
|
+
nbrs.append((nr, nc))
|
|
24
|
+
return nbrs
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _heuristic(a: Coord, b: Coord) -> int:
|
|
28
|
+
# Manhattan distance
|
|
29
|
+
return abs(a[0] - b[0]) + abs(a[1] - b[1])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def astar(grid: List[List[int]], start: Coord, goal: Coord) -> Optional[List[Coord]]:
|
|
33
|
+
"""Find a path from start to goal using A*.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
grid: 2D grid where 1 is walkable and 0 is wall.
|
|
37
|
+
start: (row, col) start coordinate.
|
|
38
|
+
goal: (row, col) goal coordinate.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A list of coordinates from start to goal (inclusive) if a path exists,
|
|
42
|
+
otherwise None.
|
|
43
|
+
"""
|
|
44
|
+
if grid[start[0]][start[1]] != 1 or grid[goal[0]][goal[1]] != 1:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
open_set: List[Tuple[int, Coord]] = []
|
|
48
|
+
heapq.heappush(open_set, (0, start))
|
|
49
|
+
|
|
50
|
+
came_from: Dict[Coord, Coord] = {}
|
|
51
|
+
gscore: Dict[Coord, int] = {start: 0}
|
|
52
|
+
fscore: Dict[Coord, int] = {start: _heuristic(start, goal)}
|
|
53
|
+
|
|
54
|
+
closed = set()
|
|
55
|
+
|
|
56
|
+
while open_set:
|
|
57
|
+
_, current = heapq.heappop(open_set)
|
|
58
|
+
if current == goal:
|
|
59
|
+
# reconstruct path
|
|
60
|
+
path = [current]
|
|
61
|
+
while path[-1] != start:
|
|
62
|
+
path.append(came_from[path[-1]])
|
|
63
|
+
path.reverse()
|
|
64
|
+
return path
|
|
65
|
+
|
|
66
|
+
closed.add(current)
|
|
67
|
+
|
|
68
|
+
for nbr in _neighbors(grid, current):
|
|
69
|
+
if nbr in closed:
|
|
70
|
+
continue
|
|
71
|
+
tentative_g = gscore[current] + 1
|
|
72
|
+
if tentative_g < gscore.get(nbr, 1_000_000):
|
|
73
|
+
came_from[nbr] = current
|
|
74
|
+
gscore[nbr] = tentative_g
|
|
75
|
+
f = tentative_g + _heuristic(nbr, goal)
|
|
76
|
+
fscore[nbr] = f
|
|
77
|
+
heapq.heappush(open_set, (f, nbr))
|
|
78
|
+
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
# quick smoke test
|
|
84
|
+
from .generator import generate_maze
|
|
85
|
+
|
|
86
|
+
g = generate_maze(5, 8, seed=2)
|
|
87
|
+
start = (1, 1)
|
|
88
|
+
goal = (len(g) - 2, len(g[0]) - 2)
|
|
89
|
+
p = astar(g, start, goal)
|
|
90
|
+
print("Path length:", len(p) if p else None)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: squared_maze
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Maze generator and A* solver with ASCII and image rendering
|
|
5
|
+
Author-email: Jaime Pizarroso <jpizarroso@comillas.edu>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: maze,astar,generator,puzzle,visualization
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: Pillow>=9.0.0
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# squared_maze
|
|
18
|
+
|
|
19
|
+
Tiny Python package to generate grid mazes, solve them with A* and render
|
|
20
|
+
ASCII and PNG visualizations. The project includes utilities to pick valid
|
|
21
|
+
start/end cells, force a maze to become unsolvable, or introduce additional
|
|
22
|
+
solutions by breaking walls.
|
|
23
|
+
|
|
24
|
+
Features
|
|
25
|
+
- Generate perfect mazes (recursive backtracker) as a grid where `1` is
|
|
26
|
+
walkable and `0` is a wall.
|
|
27
|
+
- Solve with A* (`src/squared_maze/solver.py`).
|
|
28
|
+
- Render ASCII (`grid_to_ascii`) with customizable symbols.
|
|
29
|
+
- Render PNG images (Pillow) with start (green), end (red), path (blue),
|
|
30
|
+
walls (dark gray) and floors (light gray).
|
|
31
|
+
- Helpers: `find_valid_cell`, `make_unsolvable`, `make_multiple_solutions`.
|
|
32
|
+
|
|
33
|
+
Quick install
|
|
34
|
+
|
|
35
|
+
This project requires Python 3.8+ and Pillow for image output. Install the
|
|
36
|
+
dependency into your environment:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install pillow
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Running the example notebook
|
|
43
|
+
|
|
44
|
+
Open the example notebook `examples/maze_example.ipynb` with Jupyter or run it
|
|
45
|
+
in a supported environment (VS Code/Jupyter Lab). The notebook demonstrates:
|
|
46
|
+
- generating and solving a maze
|
|
47
|
+
- saving two images (with and without the path)
|
|
48
|
+
- creating an unsolvable variant
|
|
49
|
+
- creating a variant with multiple distinct solutions
|
|
50
|
+
|
|
51
|
+
If running the notebook from the repository you may need to add the project
|
|
52
|
+
`src` folder to `PYTHONPATH`. From the repository root you can run a small
|
|
53
|
+
script or open a Python REPL like this:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python3 -c "import sys; from pathlib import Path; sys.path.insert(0, str(Path('src').resolve())); from squared_maze import generate_maze, astar, grid_to_ascii; g=generate_maze(12,20,seed=42); print(grid_to_ascii(g, None))"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Basic usage (script)
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from squared_maze import (
|
|
63
|
+
generate_maze,
|
|
64
|
+
astar,
|
|
65
|
+
grid_to_ascii,
|
|
66
|
+
save_images,
|
|
67
|
+
find_valid_cell,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# generate
|
|
71
|
+
grid = generate_maze(12, 20, seed=42)
|
|
72
|
+
start = find_valid_cell(grid, seed=1)
|
|
73
|
+
end = find_valid_cell(grid, exclude={start}, seed=2)
|
|
74
|
+
path = astar(grid, start, end)
|
|
75
|
+
|
|
76
|
+
print(grid_to_ascii(grid, path, start, end))
|
|
77
|
+
save_images(grid, path, start, end, cell_size=24, out_prefix='maze_out')
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
API (key functions)
|
|
81
|
+
- `generate_maze(rows, cols, seed=None)` -> grid
|
|
82
|
+
- `astar(grid, start, end)` -> list of coordinates or `None`
|
|
83
|
+
- `grid_to_ascii(grid, path=None, start=None, end=None, ...)` -> str (customizable symbols)
|
|
84
|
+
- `save_images(grid, path=None, start=None, end=None, cell_size=16, out_prefix='maze')` -> (fn_no, fn_yes)
|
|
85
|
+
- `find_valid_cell(grid, exclude=None, seed=None)` -> (row, col)
|
|
86
|
+
- `make_unsolvable(grid, start, end, astar_fn, ...)` -> bool (modifies grid)
|
|
87
|
+
- `make_multiple_solutions(grid, start, end, astar_fn, ...)` -> bool (modifies grid)
|
|
88
|
+
|
|
89
|
+
Notes
|
|
90
|
+
- The generator produces a perfect maze (a spanning tree). That means there
|
|
91
|
+
is exactly one path between any two room cells until you deliberately
|
|
92
|
+
break walls (e.g. with `make_multiple_solutions`). Use `find_valid_cell`
|
|
93
|
+
to pick valid walkable start/end cells.
|
|
94
|
+
- Images are saved to the working directory. Filenames are returned by
|
|
95
|
+
`save_images`.
|
|
96
|
+
|
|
97
|
+
Contributing
|
|
98
|
+
- Small, self-contained patches are welcome. Please keep docstrings and
|
|
99
|
+
code PEP8-compatible and add a short test if you change core behaviour.
|
|
100
|
+
|
|
101
|
+
License
|
|
102
|
+
- This repository does not include an explicit license file. Add one if you
|
|
103
|
+
intend to publish or share the code.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/squared_maze/__init__.py
|
|
5
|
+
src/squared_maze/generator.py
|
|
6
|
+
src/squared_maze/render.py
|
|
7
|
+
src/squared_maze/solver.py
|
|
8
|
+
src/squared_maze.egg-info/PKG-INFO
|
|
9
|
+
src/squared_maze.egg-info/SOURCES.txt
|
|
10
|
+
src/squared_maze.egg-info/dependency_links.txt
|
|
11
|
+
src/squared_maze.egg-info/requires.txt
|
|
12
|
+
src/squared_maze.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Pillow>=9.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
squared_maze
|