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"