mazeengine 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.
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mazeengine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A maze generator stand-alone module that will generate a maze based on config
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: aymel-ha
|
|
7
|
+
Requires-Python: >=3.10.12
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Dist: numpy (>=1.26.4)
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from typing import List, Tuple, Optional, Set, Dict, Any, Union
|
|
3
|
+
import numpy as n
|
|
4
|
+
class MazeGenerator:
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
height: int,
|
|
8
|
+
width: int,
|
|
9
|
+
entry: Tuple[int, int],
|
|
10
|
+
exit_: Tuple[int, int],
|
|
11
|
+
perfect: bool,
|
|
12
|
+
output_file: Optional[str],
|
|
13
|
+
seed: Optional[int]
|
|
14
|
+
) -> None:
|
|
15
|
+
self.width: int = width
|
|
16
|
+
self.height: int = height
|
|
17
|
+
self.entry: Tuple[int, int] = entry
|
|
18
|
+
self.exit: Tuple[int, int] = exit_
|
|
19
|
+
self.output_file: Optional[str] = output_file
|
|
20
|
+
self.seed: Optional[int] = seed
|
|
21
|
+
self.perfect: bool = perfect
|
|
22
|
+
self.generated_maze: Optional[n.ndarray] = None
|
|
23
|
+
self.path: Optional[str] = None
|
|
24
|
+
def pattern_42(self) -> Set[Tuple[int, int]]:
|
|
25
|
+
base_pattern = {
|
|
26
|
+
(0,0) ,(0, 1), (0, 2), (1, 2), (2, 2), (2, 3), (2, 4),
|
|
27
|
+
(4, 0), (5, 0), (6, 0), (6, 1), (4, 2), (5, 2), (6, 2), (4, 3),
|
|
28
|
+
(4,4), (5,4), (6,4)
|
|
29
|
+
}
|
|
30
|
+
pattern_width = max(x for x, _ in base_pattern) + 1
|
|
31
|
+
pattern_height = max(y for _, y in base_pattern) + 1
|
|
32
|
+
if self.height <= pattern_height or self.width <= pattern_width:
|
|
33
|
+
print(f"cannot place pattern on the maze {self.width}/{self.height}")
|
|
34
|
+
exit(0)
|
|
35
|
+
scale_x = self.width // pattern_width
|
|
36
|
+
scale_y = self.height // pattern_height
|
|
37
|
+
scale = max(1, min(scale_x, scale_y) // 25)
|
|
38
|
+
pattern_height = pattern_height * scale
|
|
39
|
+
pattern_width = pattern_width * scale
|
|
40
|
+
middle_x = (self.width - pattern_width) // 2
|
|
41
|
+
middle_y = (self.height - pattern_height) // 2
|
|
42
|
+
scaled_pattern = set()
|
|
43
|
+
for bx, by in base_pattern:
|
|
44
|
+
for sx in range(scale):
|
|
45
|
+
for sy in range(scale):
|
|
46
|
+
x = bx * scale + sx + middle_x
|
|
47
|
+
y = by * scale + sy + middle_y
|
|
48
|
+
scaled_pattern.add((x, y))
|
|
49
|
+
return scaled_pattern
|
|
50
|
+
|
|
51
|
+
def iterative_backtracker(
|
|
52
|
+
self,
|
|
53
|
+
maze: n.ndarray,
|
|
54
|
+
compass: List[str],
|
|
55
|
+
record: bool = False
|
|
56
|
+
) -> Union[n.ndarray, Tuple[n.ndarray, List[Dict[str, Any]]]]:
|
|
57
|
+
direction_map = {
|
|
58
|
+
"North": (0, -1, 1, 4),
|
|
59
|
+
"South": (0, 1, 4, 1),
|
|
60
|
+
"West": (-1, 0, 8, 2),
|
|
61
|
+
"East": (1, 0, 2, 8),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
actions = [] if record else None
|
|
65
|
+
pattern_coords = self.pattern_42()
|
|
66
|
+
enx, eny = self.entry
|
|
67
|
+
random.shuffle(compass)
|
|
68
|
+
stack_simulation = [(enx, eny, compass.copy())]
|
|
69
|
+
visited_cells = {(enx, eny)}
|
|
70
|
+
|
|
71
|
+
if record:
|
|
72
|
+
actions.append({'type': 'visit', 'cell': (enx, eny), 'stack_size': 1})
|
|
73
|
+
|
|
74
|
+
for cord in pattern_coords:
|
|
75
|
+
if cord == self.entry or cord == self.exit:
|
|
76
|
+
print(f"Entry/Exit spotted on pattern")
|
|
77
|
+
exit(0)
|
|
78
|
+
visited_cells.add(cord)
|
|
79
|
+
x, y = cord
|
|
80
|
+
maze[y, x] = 0xF
|
|
81
|
+
if record:
|
|
82
|
+
actions.append({'type': 'pattern', 'cell': (x, y)})
|
|
83
|
+
|
|
84
|
+
while stack_simulation:
|
|
85
|
+
x, y, cell_compass = stack_simulation[-1]
|
|
86
|
+
moved = False
|
|
87
|
+
|
|
88
|
+
while cell_compass:
|
|
89
|
+
move = cell_compass.pop(0)
|
|
90
|
+
dx, dy, wall, neighbor_wall = direction_map[move]
|
|
91
|
+
nx, ny = x + dx, y + dy
|
|
92
|
+
|
|
93
|
+
if (0 <= nx < self.width and 0 <= ny < self.height
|
|
94
|
+
and (nx, ny) not in visited_cells):
|
|
95
|
+
maze[y, x] -= wall
|
|
96
|
+
maze[ny, nx] -= neighbor_wall
|
|
97
|
+
visited_cells.add((nx, ny))
|
|
98
|
+
|
|
99
|
+
if record:
|
|
100
|
+
actions.append({
|
|
101
|
+
'type': 'carve',
|
|
102
|
+
'from_cell': (x, y),
|
|
103
|
+
'to_cell': (nx, ny),
|
|
104
|
+
'direction': move[0],
|
|
105
|
+
'stack_size': len(stack_simulation)
|
|
106
|
+
})
|
|
107
|
+
actions.append({
|
|
108
|
+
'type': 'visit',
|
|
109
|
+
'cell': (nx, ny),
|
|
110
|
+
'stack_size': len(stack_simulation) + 1
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
fresh_compass = compass.copy()
|
|
114
|
+
random.shuffle(fresh_compass)
|
|
115
|
+
stack_simulation.append((nx, ny, fresh_compass))
|
|
116
|
+
moved = True
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
if not moved:
|
|
120
|
+
if record:
|
|
121
|
+
actions.append({
|
|
122
|
+
'type': 'backtrack',
|
|
123
|
+
'from_cell': (x, y),
|
|
124
|
+
'stack_size': len(stack_simulation) - 1
|
|
125
|
+
})
|
|
126
|
+
stack_simulation.pop()
|
|
127
|
+
|
|
128
|
+
return (maze, actions) if record else maze
|
|
129
|
+
|
|
130
|
+
def bfs(self, maze: n.ndarray) -> str:
|
|
131
|
+
directions = {0: (0, -1, 'N'), 1: (1, 0, 'E'), 2: (0, 1, 'S'), 3: (-1, 0, 'W')}
|
|
132
|
+
enx, eny = self.entry
|
|
133
|
+
exx, exy = self.exit
|
|
134
|
+
queue = [(enx, eny, "")]
|
|
135
|
+
visited = {(enx, eny)}
|
|
136
|
+
height, width = maze.shape
|
|
137
|
+
while queue:
|
|
138
|
+
x, y, path = queue.pop(0)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
for direction, (dx, dy, compass) in directions.items():
|
|
142
|
+
if (x, y) == (exx, exy):
|
|
143
|
+
return path + compass
|
|
144
|
+
if not bool(maze[y, x] & (0x1 << direction)):
|
|
145
|
+
nx, ny = x + dx, y + dy
|
|
146
|
+
|
|
147
|
+
if (0 <= nx < width and
|
|
148
|
+
0 <= ny < height and
|
|
149
|
+
(nx, ny) not in visited):
|
|
150
|
+
|
|
151
|
+
visited.add((nx, ny))
|
|
152
|
+
queue.append((nx, ny, path + compass))
|
|
153
|
+
|
|
154
|
+
return "No path found"
|
|
155
|
+
def imperfection_helper(self, maze: n.ndarray) -> bool:
|
|
156
|
+
for y in range(self.height - 2):
|
|
157
|
+
for x in range(self.width - 2):
|
|
158
|
+
open_check = True
|
|
159
|
+
for y1 in range (y, y + 3):
|
|
160
|
+
for x1 in range (x, x + 3):
|
|
161
|
+
if maze[y1, x1] & (1 << 1) or maze[y1, x1] & (1 << 2):
|
|
162
|
+
open_check = False
|
|
163
|
+
break
|
|
164
|
+
if open_check:
|
|
165
|
+
return open_check
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
def imperfect_maze(self, maze: n.ndarray, record: bool = False) -> n.ndarray:
|
|
169
|
+
actions = [] if record else None
|
|
170
|
+
pattern_coords = self.pattern_42()
|
|
171
|
+
directions = [(0, -1, 0, 2), (1, 0, 1, 3), (0, 1, 2, 0), (-1, 0, 3, 1)]
|
|
172
|
+
removed_walls = set()
|
|
173
|
+
|
|
174
|
+
total_walls = int((self.width * self.height) * 0.25)
|
|
175
|
+
max_tries = self.width * self.height
|
|
176
|
+
walls = 0
|
|
177
|
+
tries = 0
|
|
178
|
+
while tries < max_tries and walls < total_walls:
|
|
179
|
+
tries += 1
|
|
180
|
+
x , y = random.randint(0, self.width - 1), random.randint(0, self.height - 1)
|
|
181
|
+
dx, dy , wall, neighbor_wall = random.choice(directions)
|
|
182
|
+
nx = dx + x
|
|
183
|
+
ny = dy + y
|
|
184
|
+
if not (0 <= nx < self.width and 0 <= ny < self.height):
|
|
185
|
+
continue
|
|
186
|
+
if (nx, ny) in pattern_coords or (x, y) in pattern_coords:
|
|
187
|
+
continue
|
|
188
|
+
if bool(maze[y, x] & (1 << wall)):
|
|
189
|
+
maze[y,x] &= ~(1 << wall)
|
|
190
|
+
maze[ny,nx] &= ~(1 << neighbor_wall)
|
|
191
|
+
walls += 1
|
|
192
|
+
removed_walls.add((x, y))
|
|
193
|
+
if self.imperfection_helper(maze):
|
|
194
|
+
maze[y,x] |= (1 << wall)
|
|
195
|
+
maze[ny,nx] |= (1 << neighbor_wall)
|
|
196
|
+
walls -= 1
|
|
197
|
+
removed_walls.remove((x, y))
|
|
198
|
+
if record:
|
|
199
|
+
actions.append({
|
|
200
|
+
'type': 'imperfect_carve',
|
|
201
|
+
'cell': (x, y),
|
|
202
|
+
'neighbor': (nx, ny),
|
|
203
|
+
'wall': wall,
|
|
204
|
+
'neighbor_wall': neighbor_wall
|
|
205
|
+
})
|
|
206
|
+
if record:
|
|
207
|
+
return maze, actions
|
|
208
|
+
return maze
|
|
209
|
+
def get_maze(self) -> n.ndarray:
|
|
210
|
+
compass = ["North", "East", "South", "West"]
|
|
211
|
+
maze = n.full((self.height, self.width), 0xF,dtype=n.uint8)
|
|
212
|
+
self.generated_maze = self.iterative_backtracker(maze, compass, True)
|
|
213
|
+
if self.perfect == False:
|
|
214
|
+
self.generated_maze = self.imperfect_maze(self.generated_maze)
|
|
215
|
+
return self.generated_maze
|
|
216
|
+
def get_solution_path(self, generated_maze: n.ndarray) -> str:
|
|
217
|
+
self.path = self.bfs(generated_maze)
|
|
218
|
+
return self.path
|
|
219
|
+
def get_entry(self) -> Tuple[int, int]:
|
|
220
|
+
return self.entry
|
|
221
|
+
def get_exit(self) -> Tuple[int, int]:
|
|
222
|
+
return self.exit
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "mazeengine"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A maze generator stand-alone module that will generate a maze based on config"
|
|
5
|
+
authors = ["aymel-ha", "iamessag"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
package-mode = true
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = ">=3.10.12"
|
|
11
|
+
numpy = ">=1.26.4"
|
|
12
|
+
|
|
13
|
+
[build-system]
|
|
14
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
15
|
+
build-backend = "poetry.core.masonry.api"
|