43 Commits

Author SHA1 Message Date
da7e b2aa93e04d add color to put block 2026-03-30 15:47:39 +02:00
da7e 56ebb2823a code refactor(AmazMLX) 2026-03-30 15:45:15 +02:00
da7e 150eaedc94 Merge branch 'main' of github.com:maoakeEnterprise/amazing 2026-03-30 15:41:35 +02:00
da7e 6f4699c29f wip(entry exit) 2026-03-30 15:37:45 +02:00
Maoake Teriierooiterai 5913f5267d trying to get the blink on the 42 2026-03-30 15:36:52 +02:00
Maoake Teriierooiterai d4251dc8b7 fixing the conflict 2026-03-30 14:47:16 +02:00
Maoake Teriierooiterai 282fbd6867 poop the conflict 2026-03-30 14:39:05 +02:00
da7e 0f77e0c6e4 fix buffer overflow in put pixel + margin calculation 2026-03-30 14:37:33 +02:00
Maoake Teriierooiterai cfac4bed25 need to add the color 2026-03-30 13:53:14 +02:00
Maoake Teriierooiterai cd3c75fb1e set up the path print with the button 2026-03-30 12:01:23 +02:00
Maoake Teriierooiterai 628bb8a94b put the functions color and need to refactor the code 2026-03-30 08:26:53 +02:00
mteriier dc19b526fa testing colors on the project cause we need to test it out 2026-03-29 23:35:42 +02:00
Maoake Teriierooiterai 68d710e313 color 42 2026-03-29 18:47:29 +02:00
da7e 92c6237f06 fix(astar): the actual astar wasn't the real astar algoritm 2026-03-29 15:38:40 +02:00
Maoake Teriierooiterai b682274102 opti path 2026-03-29 14:31:04 +02:00
mteriier d534993f4c starting my branch need to rush this 2026-03-28 23:01:42 +01:00
da7e fa38f7a311 Merge branch 'mlx' 2026-03-27 21:53:06 +01:00
da7e 16d97e9912 fix(astar): function f() miscalculate the best path 2026-03-27 21:51:49 +01:00
da7e b317f7a3a0 FIX(path render): path render was called twice 2026-03-27 21:42:14 +01:00
da7e 2fc67683d8 add key handling without color management (not implemented) 2026-03-27 20:58:28 +01:00
da7e cb19cf1413 ADD(mlx path animation) 2026-03-27 19:47:21 +01:00
da7e 6ec617848f Merge branch 'main' of github.com:maoakeEnterprise/amazing into mlx 2026-03-27 18:29:39 +01:00
da7e 349e58ce41 ifpjefp 2026-03-27 18:29:09 +01:00
Maoake Teriierooiterai b078241359 fix something on the solver 2026-03-27 18:05:05 +01:00
Maoake Teriierooiterai 3c072de0f4 finish the animation generator maze 2026-03-27 16:49:13 +01:00
Maoake Teriierooiterai a3bbce861d fix conflict 2026-03-27 14:39:06 +01:00
Maoake Teriierooiterai ca9444778e need to test the mlx 2026-03-27 14:35:04 +01:00
da7e 135e13deff WIP(iterative display): display change for every generation doesn't work 2026-03-27 13:33:45 +01:00
da7e 2828e37853 maze is display once 2026-03-26 18:22:45 +01:00
Maoake Teriierooiterai ef030f70a7 finish to synchronize the maze generator and the solver 2026-03-26 14:19:43 +01:00
Maoake Teriierooiterai 170de8813a update the gitignore 2026-03-26 13:11:36 +01:00
Maoake TERIIEROOITERAI 5aec319f7b need to fix the unperfect maze and add the function in the kruskal generator 2026-03-26 00:58:07 +01:00
Maoake TERIIEROOITERAI 24748c47ad merge main to the branch solver_dfs and fixing some conflict 2026-03-26 00:14:39 +01:00
Maoake TERIIEROOITERAI e4c91dc4a1 finish the imperfect maze 2026-03-26 00:10:10 +01:00
Maoake Teriierooiterai 6b3fd9a6b7 need to finish the unperfect maze 2026-03-25 18:45:30 +01:00
da7e e33d0a8e29 ADD(main): expected output in file test.txt 2026-03-25 17:48:18 +01:00
da7e a408004bd7 fix(parsing): make output work for AMazeIng class __init__
Basic main to display ascii print
2026-03-25 17:40:13 +01:00
Maoake Teriierooiterai b8631d5b89 doing some test 2026-03-25 17:24:32 +01:00
Maoake Teriierooiterai 5f1ffcc01c finish the solver 2026-03-25 17:00:14 +01:00
da7e e717bf52e9 fix 42 logo adapt with size 2026-03-25 15:50:08 +01:00
da7e 3fa0d3204e add ft logo to maze 2026-03-25 15:27:39 +01:00
da7e cc6f2eb147 Merge branch 'fix_aster' 2026-03-25 14:52:10 +01:00
da7e c6242eeec0 fix astar algorithm work 2026-03-25 14:51:12 +01:00
13 changed files with 774 additions and 198 deletions
+1
View File
@@ -214,4 +214,5 @@ __marimo__/
# Streamlit # Streamlit
.streamlit/secrets.toml .streamlit/secrets.toml
test.txt
+6
View File
@@ -1,9 +1,13 @@
install: install:
uv sync uv sync
uv pip install mlx-2.2-py3-none-any.whl
run: install run: install
uv run python3 a_maze_ing.py config.txt uv run python3 a_maze_ing.py config.txt
run_windows:
.venv\Scripts\python -m a_maze_ing config.txt
debug: debug:
uv pdb python3 a_maze_ing.py config.txt uv pdb python3 a_maze_ing.py config.txt
@@ -28,3 +32,5 @@ run_test_maze_gen:
PYTHONPATH=src uv run pytest tests/test_MazeGenerator.py PYTHONPATH=src uv run pytest tests/test_MazeGenerator.py
run_test: run_test:
uv run pytest uv run pytest
mlx:
uv run python3 test.py
+345 -16
View File
@@ -1,23 +1,352 @@
import os from typing import Any
from src.amaz_lib import Maze from src.AMazeIng import AMazeIng
from src.amaz_lib import MazeGenerator from src.parsing import Parsing
import src.amaz_lib as g from mlx import Mlx
import numpy as np
import math
import time
def main(maze_gen: MazeGenerator) -> None: class MazeMLX:
# try: def __init__(self, height: int, width: int) -> None:
maze = Maze(maze=None) self.mlx = Mlx()
for alg in maze_gen.generator(10, 10): self.height = height
maze.set_maze(alg) self.width = width
os.system("clear") self.print_path = False
maze.ascii_print() self.color = [0x00, 0x00, 0xFF, 0xFF]
# solver = AStar((1, 1), (14, 18)) self.mlx_ptr = self.mlx.mlx_init()
# print(solver.solve(maze)) self.win_ptr = self.mlx.mlx_new_window(
self.mlx_ptr, width, height + 200, "A-Maze-Ing"
)
self.img_ptr = self.mlx.mlx_new_image(self.mlx_ptr, width, height)
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:
self.mlx.mlx_destroy_image(self.mlx_ptr, self.img_ptr)
def close_loop(self, _: Any):
self.mlx.mlx_loop_exit(self.mlx_ptr)
def clear_image(self) -> None:
self.buf[:] = b"\x00" * len(self.buf)
def redraw_image(self) -> None:
self.mlx.mlx_clear_window(self.mlx_ptr, self.win_ptr)
self.mlx.mlx_put_image_to_window(
self.mlx_ptr, self.win_ptr, self.img_ptr, 0, 0
)
self.mlx.mlx_string_put(
self.mlx_ptr,
self.win_ptr,
self.width // 3,
self.height + 100,
0xFFFFFF,
"1: regen; 2: path; 3: color; 4: quit;",
)
def put_pixel(self, x, y, color: list | None = None) -> None:
if x < 0 or y < 0 or x >= self.width or y >= self.height:
return
offset = y * self.size_line + x * (self.bpp // 8)
if color:
self.buf[offset + 0] = color[0]
self.buf[offset + 1] = color[1]
self.buf[offset + 2] = color[2]
if self.bpp >= 32:
self.buf[offset + 3] = color[3]
else:
self.buf[offset + 0] = self.color[0]
self.buf[offset + 1] = self.color[1]
self.buf[offset + 2] = self.color[2]
if self.bpp >= 32:
self.buf[offset + 3] = self.color[3]
def put_line(
self,
start: tuple[int, int],
end: tuple[int, int],
color: list | None = None,
) -> None:
sx, sy = start
ex, ey = end
if sy == ey:
for x in range(min(sx, ex), max(sx, ex) + 1):
self.put_pixel(x, sy, color)
if sx == ex:
for y in range(min(sy, ey), max(sy, ey) + 1):
self.put_pixel(sx, y, color)
def put_block(
self,
ul: tuple[int, int],
dr: tuple[int, int],
color: list | None = None,
) -> None:
for y in range(min(ul[1], dr[1]), max(dr[1], ul[1])):
self.put_line(
(min(ul[0], dr[0]), y), (max(ul[0], dr[0]), y), color
)
@staticmethod
def random_color_ft() -> Any:
colors = [
[0xFF, 0xBF, 0x00, 0xFF], # blue
[0xFF, 0x00, 0x80, 0xFF], # purple
[0xFF, 0x00, 0xFF, 0xFF], # rose
]
while True:
for color in colors:
yield color
@staticmethod
def random_color() -> Any:
colors = [
[0x00, 0x00, 0xFF, 0xFF], # red
[0x00, 0xFF, 0xFF, 0xFF], # yellow
[0x00, 0xFF, 0x40, 0xFF], # green
[0xFF, 0xBF, 0x00, 0xFF], # blue
[0xFF, 0x00, 0x80, 0xFF], # purple
[0xFF, 0x00, 0xFF, 0xFF], # pink
]
while True:
for color in colors:
yield color
def update_maze(self, maze: np.ndarray) -> None:
self.clear_image()
rows = len(maze)
cols = len(maze[0])
line_len = min(self.width // cols, self.height // rows)
maze_width = cols * line_len
maze_height = rows * line_len
margin_x = (self.width - maze_width) // 2
margin_y = (self.height - maze_height) // 2
for y in range(len(maze)):
for x in range(len(maze[0])):
x0 = x * line_len + margin_x
y0 = y * line_len + margin_y
x1 = x * line_len + line_len + margin_x
y1 = y * line_len + line_len + margin_y
if maze[y][x].get_north():
self.put_line((x0, y0), (x1, y0))
if maze[y][x].get_est():
self.put_line((x1, y0), (x1, y1))
if maze[y][x].get_south():
self.put_line((x0, y1), (x1, y1))
if maze[y][x].get_west():
self.put_line((x0, y0), (x0, y1))
def put_path(self, amazing: AMazeIng) -> Any:
path = amazing.solve_path()
print(path)
actual = amazing.entry
actual = (actual[0] - 1, actual[1] - 1)
maze = amazing.maze.get_maze()
if maze is None:
return
rows = len(maze)
cols = len(maze[0])
line_len = min(self.width // cols, self.height // rows)
maze_width = cols * line_len
maze_height = rows * line_len
margin_x = (self.width - maze_width) // 2
margin_y = (self.height - maze_height) // 2
for i in range(len(path)):
ul = (
(actual[0]) * line_len + margin_x + 12,
(actual[1]) * line_len + 12 + margin_y,
)
dr = (
(actual[0]) * line_len + line_len + margin_x - 12,
(actual[1]) * line_len + line_len - 12 + margin_y,
)
self.put_block(ul, dr)
x0 = actual[0] * line_len + margin_x + 12
y0 = actual[1] * line_len + margin_y + 12
x1 = actual[0] * line_len + line_len + margin_x - 12
y1 = actual[1] * line_len + line_len + margin_y - 12
yield
match path[i]:
case "N":
self.put_block((x0, y0), (x1, y0 - 24))
actual = (actual[0], actual[1] - 1)
case "E":
self.put_block((x1, y0), (x1 + 24, y1))
actual = (actual[0] + 1, actual[1])
case "S":
self.put_block((x0, y1), (x1, y1 + 24))
actual = (actual[0], actual[1] + 1)
case "W":
self.put_block((x0, y0), (x0 - 24, y1))
actual = (actual[0] - 1, actual[1])
ul = (
(actual[0]) * line_len + margin_x + 12,
(actual[1]) * line_len + 12 + margin_y,
)
dr = (
(actual[0]) * line_len + line_len + margin_x - 12,
(actual[1]) * line_len + line_len - 12 + margin_y,
)
self.put_block(ul, dr)
return
def put_start_end(self, amazing: AMazeIng):
entry = amazing.entry
exit = amazing.exit
maze = amazing.maze.get_maze()
if maze is None:
return
rows = len(maze)
cols = len(maze[0])
line_len = min(self.width // cols, self.height // rows)
maze_width = cols * line_len
maze_height = rows * line_len
margin_x = (self.width - maze_width) // 2
margin_y = (self.height - maze_height) // 2
ul = (
(entry[0] - 1) * line_len + margin_x + 3,
(entry[1] - 1) * line_len + 3 + margin_y,
)
dr = (
(entry[0] - 1) * line_len + line_len + margin_x - 3,
(entry[1] - 1) * line_len + line_len - 3 + margin_y,
)
print(f"ul: {ul}; dr: {dr}")
self.put_block(ul, dr)
self.redraw_image()
def draw_ft(self, maze: np.ndarray, color: list | None = None):
self.clear_image()
margin = math.trunc(
math.sqrt(self.width if self.width > self.height else self.height)
// 2
)
line_len = math.trunc(
(
(self.height - margin) // len(maze)
if self.height > self.width
else (self.width - margin) // len(maze[0])
)
)
for y in range(len(maze)):
for x in range(len(maze[0])):
x0 = x * line_len + margin
y0 = y * line_len + margin
x1 = x * line_len + line_len + margin
y1 = y * line_len + line_len + margin
if maze[y][x].get_north():
self.put_line((x0, y0), (x1, y0))
if maze[y][x].get_est():
self.put_line((x1, y0), (x1, y1))
if maze[y][x].get_south():
self.put_line((x0, y1), (x1, y1))
if maze[y][x].get_west():
self.put_line((x0, y0), (x0, y1))
if maze[y][x].value == 15:
self.put_block((x0, y0), (x1, y1))
def draw_image(self, amazing: AMazeIng) -> None:
if self.render_maze(amazing):
if self.path_printer and self.print_path:
if self.render_path():
color = next(self.color_gen_ft)
color
else:
self.draw_ft(amazing.maze.get_maze())
self.redraw_image()
def shift_color(self):
self.color_gen = self.random_color()
def shift_color_ft(self):
self.color_gen_ft = self.random_color_ft()
def restart_maze(self, amazing: AMazeIng) -> None:
self.generator = amazing.generate()
def restart_path(self, amazing: AMazeIng) -> None:
self.path_printer = self.put_path(amazing)
def render_path(self) -> bool:
try:
next(self.path_printer)
time.sleep(0.03)
return False
except StopIteration:
pass
return True
def render_maze(self, amazing: AMazeIng) -> bool:
try:
next(self.generator)
self.update_maze(amazing.maze.get_maze())
return False
except StopIteration:
self.put_start_end(amazing)
pass
return True
def handle_key_press(self, keycode: int, amazing: AMazeIng) -> None:
if keycode == 49:
self.restart_maze(amazing)
self.print_path = False
if keycode == 50:
self.restart_path(amazing)
self.print_path = True if self.print_path is False else False
if keycode == 51:
self.print_path = False
self.color = next(self.color_gen)
if keycode == 52:
self.close_loop(None)
def start(self, amazing: AMazeIng) -> None:
self.restart_maze(amazing)
self.shift_color()
self.shift_color_ft()
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.mlx.mlx_loop(self.mlx_ptr)
# except Exception as err: def main() -> None:
# print(err) mlx = None
try:
mlx = MazeMLX(1000, 1000)
config = Parsing.DataMaze.get_data_maze("config.txt")
amazing = AMazeIng(**config)
mlx.start(amazing)
with open("test.txt", "w") as output:
output.write(amazing.__str__())
except Exception as err:
print(err)
finally:
if mlx is not None:
mlx.close()
if __name__ == "__main__": if __name__ == "__main__":
main(g.DepthFirstSearch()) main()
+7 -5
View File
@@ -1,6 +1,8 @@
WIDTH=200 WIDTH=11
HEIGHT=100 HEIGHT=11
ENTRY=0,0 ENTRY=1,1
EXIT=19,14 EXIT=11,11
OUTPUT_FILE=maze.txt OUTPUT_FILE=maze.txt
PERFECT=True PERFECT=False
GENERATOR=DFS
SOLVER=AStar
Binary file not shown.
+11 -12
View File
@@ -1,30 +1,28 @@
from dataclasses import field
from os import eventfd_read
from typing import Generator from typing import Generator
import numpy
from typing_extensions import Self from typing_extensions import Self
from pydantic import AfterValidator, BaseModel, Field, model_validator from pydantic import BaseModel, Field, model_validator, ConfigDict
from amaz_lib import Maze, MazeGenerator, MazeSolver from src.amaz_lib import Maze, MazeGenerator, MazeSolver
from amaz_lib.Cell import Cell
class AMazeIng(BaseModel): class AMazeIng(BaseModel):
width: int = Field(ge=3) model_config = ConfigDict(arbitrary_types_allowed=True)
height: int = Field(ge=3)
width: int = Field(ge=4)
height: int = Field(ge=4)
entry: tuple[int, int] entry: tuple[int, int]
exit: tuple[int, int] exit: tuple[int, int]
output_file: str = Field(min_length=3) output_file: str = Field(min_length=3)
perfect: bool = Field(default=True) perfect: bool = Field(default=True)
maze: Maze = Field(default=Maze(maze=numpy.array([]))) maze: Maze = Field(default=Maze(None))
generator: MazeGenerator generator: MazeGenerator
solver: MazeSolver solver: MazeSolver
@model_validator(mode="after") @model_validator(mode="after")
def check_entry_exit(self) -> Self: def check_entry_exit(self) -> Self:
if self.entry[0] >= self.width or self.entry[1] >= self.height: if self.entry[0] > self.width or self.entry[1] > self.height:
raise ValueError("Entry coordinates exceed the maze size") raise ValueError("Entry coordinates exceed the maze size")
if self.exit[0] >= self.width or self.exit[1] >= self.height: if self.exit[0] > self.width or self.exit[1] > self.height:
raise ValueError("Exit coordinates exceed the maze size") raise ValueError("Exit coordinates exceed the maze size")
return self return self
@@ -32,9 +30,10 @@ class AMazeIng(BaseModel):
for array in self.generator.generator(self.height, self.width): for array in self.generator.generator(self.height, self.width):
self.maze.set_maze(array) self.maze.set_maze(array)
yield self.maze yield self.maze
return
def solve_path(self) -> str: def solve_path(self) -> str:
return self.solver.solve(self.maze) return self.solver.solve(self.maze, self.height, self.width)
def __str__(self) -> str: def __str__(self) -> str:
res = self.maze.__str__() res = self.maze.__str__()
-2
View File
@@ -1,8 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
import numpy import numpy
from .Cell import Cell
from .MazeGenerator import MazeGenerator
@dataclass @dataclass
+117 -12
View File
@@ -6,9 +6,14 @@ import math
class MazeGenerator(ABC): class MazeGenerator(ABC):
def __init__(self, start: tuple, end: tuple, perfect: bool) -> None:
self.start = (start[0] - 1, start[1] - 1)
self.end = (end[0] - 1, end[1] - 1)
self.perfect = perfect
@abstractmethod @abstractmethod
def generator( def generator(
self, height: int, width: int, seed: int = None self, height: int, width: int, seed: int | None = None
) -> Generator[np.ndarray, None, np.ndarray]: ... ) -> Generator[np.ndarray, None, np.ndarray]: ...
@staticmethod @staticmethod
@@ -35,8 +40,60 @@ class MazeGenerator(ABC):
forty_two.add((y + 2, x + 3)) forty_two.add((y + 2, x + 3))
return forty_two return forty_two
@staticmethod
def unperfect_maze(width: int, height: int,
maze: np.ndarray, forty_two: set | None,
prob: float = 0.1
) -> Generator[np.ndarray, None, np.ndarray]:
directions = {
"N": (0, -1),
"S": (0, 1),
"W": (-1, 0),
"E": (1, 0)
}
reverse = {
"N": "S",
"S": "N",
"W": "E",
"E": "W"
}
min_break = 2
while True:
count = 0
for y in range(height):
for x in range(width):
if forty_two and (x, y) in forty_two:
continue
for direc, (dx, dy) in directions.items():
nx, ny = x + dx, y + dy
if forty_two and (
(y, x) in forty_two
or (ny, nx) in forty_two
):
continue
if not (0 <= nx < width and 0 < ny < height):
continue
if direc in ["S", "E"]:
continue
if np.random.random() < prob:
count += 1
cell = maze[y][x]
cell_n = maze[ny][nx]
cell = DepthFirstSearch.broken_wall(cell, direc)
cell_n = DepthFirstSearch.broken_wall(cell_n,
reverse[
direc])
maze[y][x] = cell
maze[ny][nx] = cell_n
yield maze
if count > min_break:
break
return maze
class Kruskal(MazeGenerator): class Kruskal(MazeGenerator):
class Set: class Set:
def __init__(self, cells: list[int]) -> None: def __init__(self, cells: list[int]) -> None:
self.cells: list[int] = cells self.cells: list[int] = cells
@@ -100,9 +157,27 @@ class Kruskal(MazeGenerator):
return return
raise Exception("two sets not found") raise Exception("two sets not found")
@staticmethod
def touch_ft(
width: int,
wall: tuple[int, int],
cells_ft: None | set[tuple[int, int]],
) -> bool:
if cells_ft is None:
return False
s1 = (math.trunc(wall[0] / width), wall[0] % width)
s2 = (math.trunc(wall[1] / width), wall[1] % width)
return s1 in cells_ft or s2 in cells_ft
def generator( def generator(
self, height: int, width: int, seed: int = None self, height: int, width: int, seed: int | None = None
) -> Generator[np.ndarray, None, np.ndarray]: ) -> Generator[np.ndarray, None, np.ndarray]:
cells_ft = None
if height > 10 and width > 10:
cells_ft = self.get_cell_ft(width, height)
if cells_ft and (self.start in cells_ft or self.end in cells_ft):
cells_ft = None
if seed is not None: if seed is not None:
np.random.seed(seed) np.random.seed(seed)
sets = self.Sets([self.Set([i]) for i in range(height * width)]) sets = self.Sets([self.Set([i]) for i in range(height * width)])
@@ -113,23 +188,40 @@ class Kruskal(MazeGenerator):
for h in range(height - 1): for h in range(height - 1):
for w in range(width): for w in range(width):
walls += [(w + (width * h), w + (width * (h + 1)))] walls += [(w + (width * h), w + (width * (h + 1)))]
print(walls)
np.random.shuffle(walls) np.random.shuffle(walls)
yield self.walls_to_maze(walls, height, width) yield self.walls_to_maze(walls, height, width)
while len(sets.sets) > 1: while (len(sets.sets) != 1 and cells_ft is None) or (
len(sets.sets) != 19 and cells_ft is not None
):
for wall in walls: for wall in walls:
if not self.is_in_same_set(sets, wall): if not self.is_in_same_set(sets, wall) and not self.touch_ft(
width, wall, cells_ft
):
self.merge_sets(sets, wall) self.merge_sets(sets, wall)
walls.remove(wall) walls.remove(wall)
yield self.walls_to_maze(walls, height, width) yield self.walls_to_maze(walls, height, width)
if len(sets.sets) == 1: if (len(sets.sets) == 1 and cells_ft is None) or (
len(sets.sets) == 19 and cells_ft is not None
):
break break
print(f"nb sets: {len(sets.sets)}") print(f"nb sets: {len(sets.sets)}")
return self.walls_to_maze(walls, height, width) maze = self.walls_to_maze(walls, height, width)
if self.perfect is False:
gen = Kruskal.unperfect_maze(width, height, maze,
cells_ft)
for res in gen:
maze = res
yield maze
return maze
class DepthFirstSearch(MazeGenerator): class DepthFirstSearch(MazeGenerator):
def __init__(self, start: bool, end: bool, perfect: bool) -> None:
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
def generator( def generator(
self, height: int, width: int, seed: int = None self, height: int, width: int, seed: int = None
@@ -137,9 +229,15 @@ class DepthFirstSearch(MazeGenerator):
if seed is not None: if seed is not None:
np.random.seed(seed) np.random.seed(seed)
maze = self.init_maze(width, height) maze = self.init_maze(width, height)
forty_two = self.get_cell_ft(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 = np.zeros((height, width), dtype=bool)
visited = self.lock_cell_ft(visited, forty_two) 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()
w_h = (width, height) w_h = (width, height)
coord = (0, 0) coord = (0, 0)
@@ -170,6 +268,12 @@ class DepthFirstSearch(MazeGenerator):
x, y = coord x, y = coord
maze[y][x] = self.broken_wall(maze[y][x], wall_r) maze[y][x] = self.broken_wall(maze[y][x], wall_r)
yield maze yield maze
if self.perfect is False:
gen = DepthFirstSearch.unperfect_maze(width, height, maze,
self.forty_two)
for res in gen:
maze = res
yield maze
return maze return maze
@staticmethod @staticmethod
@@ -230,7 +334,7 @@ class DepthFirstSearch(MazeGenerator):
return {"N": "S", "S": "N", "W": "E", "E": "W"}[direction] return {"N": "S", "S": "N", "W": "E", "E": "W"}[direction]
@staticmethod @staticmethod
def back_on_step(path: list, w_h: tuple, visited: np.array) -> list: def back_on_step(path: list, w_h: tuple, visited: np.ndarray) -> list:
while path: while path:
last = path[-1] last = path[-1]
if DepthFirstSearch.random_cells(visited, last, w_h): if DepthFirstSearch.random_cells(visited, last, w_h):
@@ -239,8 +343,9 @@ class DepthFirstSearch(MazeGenerator):
return path return path
@staticmethod @staticmethod
def lock_cell_ft(visited: np.ndarray, forty_two: set[tuple[int]] def lock_cell_ft(
) -> np.ndarray: visited: np.ndarray, forty_two: set[tuple[int]]
) -> np.ndarray:
tab = [cell for cell in forty_two] tab = [cell for cell in forty_two]
for cell in tab: for cell in tab:
visited[cell] = True visited[cell] = True
+214 -88
View File
@@ -1,121 +1,247 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from .Maze import Maze from .Maze import Maze
from typing import Any
import numpy as np import numpy as np
class MazeSolver(ABC): class MazeSolver(ABC):
def __init__(self, start: tuple[int, int], end: tuple[int, int]) -> None: def __init__(self, start: tuple[int, int], end: tuple[int, int]) -> None:
self.start = (start[0] - 1, start[1] - 1) self.start = (start[1] - 1, start[0] - 1)
self.end = (end[0] - 1, end[1] - 1) self.end = (end[1] - 1, end[0] - 1)
@abstractmethod @abstractmethod
def solve(self, maze: Maze) -> str: ... def solve(
self, maze: Maze, height: int | None = None, width: int | None = None
) -> str: ...
class AStar(MazeSolver): class AStar(MazeSolver):
class Node:
def __init__(
self,
coordinate: tuple[int, int],
g: int,
h: int,
f: int,
parent: Any,
) -> None:
self.coordinate = coordinate
self.g = g
self.h = h
self.f = f
self.parent = parent
def __eq__(self, value: object, /) -> bool:
return value == self.coordinate
def __init__(self, start: tuple[int, int], end: tuple[int, int]) -> None: def __init__(self, start: tuple[int, int], end: tuple[int, int]) -> None:
super().__init__(start, end) super().__init__(start, end)
self.path = []
def f(self, n): def h(self, n: tuple[int, int]) -> int:
def g(n: tuple[int, int]) -> int: return (
res = 0 max(n[0], self.end[0])
if n[0] < self.start[0]: - min(n[0], self.end[0])
res += self.start[0] - n[0] + max(n[1], self.end[1])
else: - min(n[1], self.end[1])
res += n[0] - self.start[0] )
if n[1] < self.start[1]:
res += self.start[1] - n[1]
else:
res += n[1] - self.start[1]
return res
def h(n: tuple[int, int]) -> int: def get_paths(
res = 0 self,
if n[0] < self.end[0]: maze: np.ndarray,
res += self.end[0] - n[0] actual: tuple[int, int],
else: close: list,
res += n[0] - self.end[0] ) -> list[tuple[int, int]]:
if n[1] < self.end[1]: path = [
res += self.end[1] - n[1] (
else: (actual[0], actual[1] - 1)
res += n[1] - self.end[1] if not maze[actual[1]][actual[0]].get_north()
return res and actual[1] > 0
and (actual[0], actual[1] - 1)
try: not in [n.coordinate for n in close]
return g(n) + h(n)
except Exception:
return 1000
def best_path(
self, maze: np.ndarray, actual: tuple[int, int]
) -> dict[str, int | None]:
print(actual)
path = {
"N": (
self.f((actual[1] - 1, actual[0]))
if not maze[actual[1]][actual[0]].get_north() and actual[0] > 0
else None else None
), ),
"E": ( (
self.f((actual[1], actual[0] + 1)) (actual[0] + 1, actual[1])
if not maze[actual[1]][actual[0]].get_est() if not maze[actual[1]][actual[0]].get_est()
and actual[1] < len(maze) - 1 and actual[0] < len(maze[0]) - 1
and (actual[0] + 1, actual[1])
not in [n.coordinate for n in close]
else None else None
), ),
"S": ( (
self.f((actual[1] + 1, actual[0])) (actual[0], actual[1] + 1)
if not maze[actual[1]][actual[0]].get_south() if not maze[actual[1]][actual[0]].get_south()
and actual[0] < len(maze) - 1 and actual[1] < len(maze) - 1
and (actual[0], actual[1] + 1)
not in [n.coordinate for n in close]
else None else None
), ),
"W": ( (
self.f((actual[1], actual[0] - 1)) (actual[0] - 1, actual[1])
if not maze[actual[1]][actual[0]].get_west() and actual[1] > 0 if not maze[actual[1]][actual[0]].get_west()
and actual[0] > 0
and (actual[0] - 1, actual[1])
not in [n.coordinate for n in close]
else None else None
), ),
} ]
return { return [p for p in path if p is not None]
k: v for k, v in sorted(path.items(), key=lambda item: item[0])
}
def get_opposit(self, dir: str) -> str: def get_path(self, maze: np.ndarray) -> list:
match dir: open: list[AStar.Node] = []
case "N": close: list[AStar.Node] = []
return "S"
case "E":
return "W"
case "S":
return "N"
case "W":
return "E"
case _:
return ""
def get_next_pos( open.append(
self, dir: str, actual: tuple[int, int] AStar.Node(
) -> tuple[int, int]: self.start,
match dir: 0,
case "N": self.h(self.start),
return (actual[0], actual[1] - 1) self.h(self.start),
case "E": None,
return (actual[0] + 1, actual[1]) )
case "S": )
return (actual[0], actual[1] + 1)
case "W":
return (actual[0] - 1, actual[1])
case _:
return actual
def get_path(self, maze: np.ndarray) -> str | None: while len(open) > 0:
actual = self.start to_check = sorted(open, key=lambda x: x.f)[0]
path = "" open.remove(to_check)
close.append(to_check)
if to_check.coordinate == self.end:
return close
paths = self.get_paths(maze, to_check.coordinate, close)
for path in paths:
open.append(
self.Node(
path,
to_check.g + 1,
self.h(path),
self.h(path) + to_check.g + 1,
to_check,
)
)
raise Exception("Path not found")
return None def get_rev_dir(self, current: Node) -> str:
if current.parent.coordinate == (
current.coordinate[0],
current.coordinate[1] - 1,
):
return "S"
elif current.parent.coordinate == (
current.coordinate[0] + 1,
current.coordinate[1],
):
return "W"
elif current.parent.coordinate == (
current.coordinate[0],
current.coordinate[1] + 1,
):
return "N"
elif current.parent.coordinate == (
current.coordinate[0] - 1,
current.coordinate[1],
):
return "E"
else:
raise Exception("Translate error: AStar path not found")
def solve(self, maze: Maze) -> str: def translate(self, close: list) -> str:
print(maze) current = close[-1]
res = self.get_path(self.start, maze.get_maze(), None) res = ""
if res is None: while True:
raise Exception("Path not found") res = self.get_rev_dir(current) + res
current = current.parent
if current.coordinate == self.start:
break
return res return res
def solve(
self, maze: Maze, height: int | None = None, width: int | None = None
) -> str:
path = self.get_path(maze.get_maze())
return self.translate(path)
class DepthFirstSearchSolver(MazeSolver):
def __init__(self, start, end):
super().__init__(start, end)
def solve(
self, maze: Maze, height: int | None = None, width: int | None = None
) -> str:
path_str = ""
visited = np.zeros((height, width), dtype=bool)
path = list()
move = list()
maze_s = maze.get_maze()
coord = self.start
h_w = (height, width)
while coord != self.end:
visited[coord] = True
path.append(coord)
rand_p = self.random_path(visited, coord, maze_s, h_w)
if not rand_p:
path, move = self.back_on_step(
path, visited, maze_s, h_w, move
)
if not path:
break
coord = path[-1]
rand_p = self.random_path(visited, coord, maze_s, h_w)
next = self.next_path(rand_p)
move.append(next)
coord = self.next_cell(coord, next)
for m in move:
path_str += m
if not path:
raise Exception("Path not found")
return path_str
@staticmethod
def random_path(
visited: np.ndarray, coord: tuple, maze: np.ndarray, h_w: tuple
) -> list:
random_p = []
h, w = h_w
y, x = coord
if y - 1 >= 0 and not maze[y][x].get_north() and not visited[y - 1][x]:
random_p.append("N")
if y + 1 < h and not maze[y][x].get_south() and not visited[y + 1][x]:
random_p.append("S")
if x - 1 >= 0 and not maze[y][x].get_west() and not visited[y][x - 1]:
random_p.append("W")
if x + 1 < w and not maze[y][x].get_est() and not visited[y][x + 1]:
random_p.append("E")
return random_p
@staticmethod
def next_path(rand_path: list) -> str:
return np.random.choice(rand_path)
@staticmethod
def back_on_step(
path: list,
visited: np.ndarray,
maze: np.ndarray,
h_w: tuple,
move: list,
) -> list:
while path:
last = path[-1]
if DepthFirstSearchSolver.random_path(visited, last, maze, h_w):
break
path.pop()
move.pop()
return path, move
@staticmethod
def next_cell(coord: tuple, next: str) -> tuple:
y, x = coord
next_step = {"N": (-1, 0), "S": (1, 0), "W": (0, -1), "E": (0, 1)}
add_y, add_x = next_step[next]
return (y + add_y, x + add_x)
+2 -2
View File
@@ -2,9 +2,9 @@ from .Cell import Cell
from .Maze import Maze from .Maze import Maze
from .MazeGenerator import MazeGenerator, DepthFirstSearch from .MazeGenerator import MazeGenerator, DepthFirstSearch
from .MazeGenerator import Kruskal from .MazeGenerator import Kruskal
from .MazeSolver import MazeSolver, AStar from .MazeSolver import MazeSolver, AStar, DepthFirstSearchSolver
__version__ = "1.0.0" __version__ = "1.0.0"
__author__ = "us" __author__ = "us"
__all__ = ["Cell", "Maze", "MazeGenerator", __all__ = ["Cell", "Maze", "MazeGenerator", "DepthFirstSearchSolver",
"MazeSolver", "AStar", "Kruskal", "DepthFirstSearch"] "MazeSolver", "AStar", "Kruskal", "DepthFirstSearch"]
+64 -35
View File
@@ -1,3 +1,7 @@
from src.amaz_lib.MazeGenerator import DepthFirstSearch, Kruskal
from src.amaz_lib.MazeSolver import AStar, DepthFirstSearchSolver
class DataMaze: class DataMaze:
@staticmethod @staticmethod
@@ -11,28 +15,30 @@ class DataMaze:
@staticmethod @staticmethod
def transform_data(data: str) -> dict: def transform_data(data: str) -> dict:
tmp = data.split("\n") tmp = data.split("\n")
tmp2 = [ tmp2 = [value.split("=", 1) for value in tmp if "=" in value]
value.split("=", 1) for value in tmp data_t = {value[0]: value[1] for value in tmp2}
]
data_t = {
value[0]: value[1] for value in tmp2
}
return data_t return data_t
@staticmethod @staticmethod
def verif_key_data(data: dict) -> None: def verif_key_data(data: dict) -> None:
key_test = { key_test = {
"WIDTH", "HEIGHT", "ENTRY", "EXIT", "OUTPUT_FILE", "PERFECT" "WIDTH",
} "HEIGHT",
set_key = { "ENTRY",
key for key in data.keys() "EXIT",
"OUTPUT_FILE",
"PERFECT",
"GENERATOR",
"SOLVER",
} }
set_key = {key for key in data.keys()}
if len(set_key) != len(key_test): if len(set_key) != len(key_test):
raise KeyError("Missing some data the len do not correspond") raise KeyError("Missing some data the len do not correspond")
res_key = {key for key in set_key if key not in key_test} res_key = {key for key in set_key if key not in key_test}
if len(res_key) != 0: if len(res_key) != 0:
raise KeyError("Some Key " raise KeyError(
f"do not correspond the keys: {res_key}") "Some Key " f"do not correspond the keys: {res_key}"
)
@staticmethod @staticmethod
def convert_values(data: dict): def convert_values(data: dict):
@@ -47,8 +53,48 @@ class DataMaze:
for key in key_bool: for key in key_bool:
res.update({key: DataMaze.convert_bool(data[key])}) res.update({key: DataMaze.convert_bool(data[key])})
res.update({"OUTPUT_FILE": data["OUTPUT_FILE"]}) res.update({"OUTPUT_FILE": data["OUTPUT_FILE"]})
res.update(
DataMaze.get_solver_generator(data, res["ENTRY"], res["EXIT"],
res["PERFECT"])
)
return res return res
@staticmethod
def get_solver_generator(data: dict, entry: tuple, exit: tuple,
perfect: bool) -> dict:
available_generator = {
"Kruskal": Kruskal,
"DFS": DepthFirstSearch,
}
available_solver = {
"AStar": AStar,
"DFS": DepthFirstSearchSolver
}
res = {}
res["GENERATOR"] = available_generator[data["GENERATOR"]](entry, exit,
perfect)
res["SOLVER"] = available_solver[data["SOLVER"]](entry, exit)
return res
@staticmethod
def convert_tuple(data: str) -> tuple:
data_t = data.split(",")
if len(data_t) != 2:
raise ValueError(
"There is too much " "argument in the coordinate given"
)
x, y = data_t
tup = (int(x), int(y))
return tup
@staticmethod
def convert_bool(data: str) -> bool:
if data != "True" and data != "False":
raise ValueError("This is not True or False")
if data == "True":
return True
return False
@staticmethod @staticmethod
def get_data_maze(name_file: str) -> dict: def get_data_maze(name_file: str) -> dict:
try: try:
@@ -56,7 +102,7 @@ class DataMaze:
data_dict = DataMaze.transform_data(data_str) data_dict = DataMaze.transform_data(data_str)
DataMaze.verif_key_data(data_dict) DataMaze.verif_key_data(data_dict)
data_maze = DataMaze.convert_values(data_dict) data_maze = DataMaze.convert_values(data_dict)
return data_maze return {k.lower(): v for k, v in data_maze.items()}
except FileNotFoundError: except FileNotFoundError:
print("The file do not exist") print("The file do not exist")
exit() exit()
@@ -70,28 +116,11 @@ class DataMaze:
print(f"Error on the key in the file: {e}") print(f"Error on the key in the file: {e}")
exit() exit()
except IndexError as e: except IndexError as e:
print("In the function transform Data some data cannot " print(
f"be splited by '=' because '=' was not present: {e}") "In the function transform Data some data cannot "
f"be splited by '=' because '=' was not present: {e}"
)
exit() exit()
except AttributeError as e: except AttributeError as e:
print("Error on the " print("Error on the " f"funciton get_data_maze : {e}")
f"funciton get_data_maze : {e}")
exit() exit()
@staticmethod
def convert_tuple(data: str) -> tuple:
data_t = data.split(",")
if len(data_t) != 2:
raise ValueError("There is too much "
"argument in the coordinate given")
x, y = data_t
tup = (int(x), int(y))
return tup
@staticmethod
def convert_bool(data: str) -> bool:
if data != "True" and data != "False":
raise ValueError("This is not True or False")
if data == "True":
return True
return False
-23
View File
@@ -1,23 +0,0 @@
7D53BFD3D57951517D1D
3D12C3903BD03AD4178D
2BAEBEEEAA92EED547C9
2287ED17AAAC5393FFF0
6C6951292A87D2AEBD30
37D43E8686E93AABAB8C
21516D2D47FEE8284049
6C7857C3FB9116C696D8
751453D6D2AAC57BE970
3BA952D17EA83BD05470
22AAD2907BAE86967B74
2AA83C2EFC69696FBC35
686EE96FD7D4783FAD21
7ED17ED3D57D3EC52FA0
7B943D16FB7BABD3AFC8
7407C5297EB82EB84174
392D53C6912EE9447E9D
62A952BBAAC13EFD7B89
3AAC3EC6EABAAD557824
66C7C7D7D6C6C7D556CD
1,1
16,15
+7 -3
View File
@@ -1,14 +1,18 @@
import numpy import numpy
from amaz_lib.MazeGenerator import DepthFirstSearch from amaz_lib.MazeGenerator import DepthFirstSearch, MazeGenerator
class TestMazeGenerator: class TestMazeGenerator:
def test_generator(self) -> None: def test_generator(self) -> None:
w_h = (300, 300) w_h = (10, 10)
maze = numpy.array([]) maze = numpy.array([])
generator = DepthFirstSearch().generator(*w_h) generator = DepthFirstSearch((1, 1), (2, 2), True).generator(*w_h)
for output in generator: for output in generator:
maze = output maze = output
assert maze.shape == w_h assert maze.shape == w_h
def test_gen_broken(self) -> None:
test = MazeGenerator.gen_broken_set(50, 50)
assert len(test) > 0