1 Commits

Author SHA1 Message Date
da7e 2828e37853 maze is display once 2026-03-26 18:22:45 +01:00
12 changed files with 144 additions and 209 deletions
-1
View File
@@ -214,5 +214,4 @@ __marimo__/
# Streamlit # Streamlit
.streamlit/secrets.toml .streamlit/secrets.toml
test.txt
-3
View File
@@ -4,9 +4,6 @@ install:
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
+90 -1
View File
@@ -1,16 +1,105 @@
import os import os
from typing import Any, Callable
from src.AMazeIng import AMazeIng from src.AMazeIng import AMazeIng
from src.parsing import Parsing from src.parsing import Parsing
from mlx.mlx import Mlx
import numpy as np
import math
from src.amaz_lib import Maze
class MazeMLX:
def __init__(self, height: int, width: int) -> None:
self.mlx = Mlx()
self.height = height
self.width = width
self.mlx_ptr = self.mlx.mlx_init()
self.win_ptr = self.mlx.mlx_new_window(
self.mlx_ptr, width, height, "amazing"
)
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)
)
def __del__(self) -> None:
self.mlx.mlx_destroy_image(self.mlx_ptr, self.img_ptr)
self.mlx.mlx_destroy_window(self.mlx_ptr, self.win_ptr)
def put_pixel(self, x, y) -> None:
offset = y * self.size_line + x * (self.bpp // 8)
self.buf[offset + 0] = 0xFF
self.buf[offset + 1] = 0xFF
self.buf[offset + 2] = 0xFF
if self.bpp >= 32:
self.buf[offset + 3] = 0xFF
def clear_image(self) -> None:
self.buf[:] = b"\x00" * len(self.buf)
def put_line(self, start: tuple[int, int], end: tuple[int, int]) -> 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)
if sx == ex:
for y in range(min(sy, ey), max(sy, ey) + 1):
self.put_pixel(sx, y)
def update_maze(self, maze: np.ndarray) -> 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))
self.mlx.mlx_put_image_to_window(
self.mlx_ptr, self.win_ptr, self.img_ptr, 0, 0
)
def close_loop(self, _: Any):
self.mlx.mlx_loop_exit(self.mlx_ptr)
def gen_maze(self, maze: np.ndarray) -> None:
self.mlx.mlx_loop_hook(self.mlx_ptr, self.update_maze, maze)
self.mlx.mlx_hook(self.win_ptr, 17, 0, self.close_loop, None)
self.mlx.mlx_loop(self.mlx_ptr)
def main() -> None: def main() -> None:
mlx = None
try: try:
mlx = MazeMLX(1000, 1000)
config = Parsing.DataMaze.get_data_maze("config.txt") config = Parsing.DataMaze.get_data_maze("config.txt")
print(config)
amazing = AMazeIng(**config) amazing = AMazeIng(**config)
for _ in amazing.generate(): for _ in amazing.generate():
os.system("clear") os.system("clear")
amazing.maze.ascii_print() amazing.maze.ascii_print()
mlx.gen_maze(amazing.maze.get_maze())
with open("test.txt", "w") as output: with open("test.txt", "w") as output:
output.write(amazing.__str__()) output.write(amazing.__str__())
except Exception as err: except Exception as err:
+6 -6
View File
@@ -1,8 +1,8 @@
WIDTH=11 WIDTH=30
HEIGHT=11 HEIGHT=30
ENTRY=4,3 ENTRY=1,1
EXIT=2,1 EXIT=29,29
OUTPUT_FILE=maze.txt OUTPUT_FILE=maze.txt
PERFECT=False PERFECT=True
GENERATOR=DFS GENERATOR=Kruskal
SOLVER=AStar SOLVER=AStar
+2 -2
View File
@@ -8,8 +8,8 @@ from src.amaz_lib import Maze, MazeGenerator, MazeSolver
class AMazeIng(BaseModel): class AMazeIng(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True) model_config = ConfigDict(arbitrary_types_allowed=True)
width: int = Field(ge=4) width: int = Field(ge=3)
height: int = Field(ge=4) height: int = Field(ge=3)
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)
+4 -87
View File
@@ -6,11 +6,6 @@ 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 = None self, height: int, width: int, seed: int | None = None
@@ -40,60 +35,8 @@ 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
@@ -175,8 +118,6 @@ class Kruskal(MazeGenerator):
cells_ft = None cells_ft = None
if height > 10 and width > 10: if height > 10 and width > 10:
cells_ft = self.get_cell_ft(width, height) 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)
@@ -207,22 +148,10 @@ class Kruskal(MazeGenerator):
): ):
break break
print(f"nb sets: {len(sets.sets)}") print(f"nb sets: {len(sets.sets)}")
maze = self.walls_to_maze(walls, height, width) return 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
@@ -230,15 +159,9 @@ 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)
if width > 9 and height > 9: forty_two = self.get_cell_ft(width, height)
self.forty_two = self.get_cell_ft(width, height)
visited = np.zeros((height, width), dtype=bool) visited = np.zeros((height, width), dtype=bool)
if ( visited = self.lock_cell_ft(visited, forty_two)
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)
@@ -269,12 +192,6 @@ 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
@@ -335,7 +252,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.ndarray) -> list: def back_on_step(path: list, w_h: tuple, visited: np.array) -> 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):
+1 -80
View File
@@ -9,8 +9,7 @@ class MazeSolver(ABC):
self.end = (end[1] - 1, end[0] - 1) self.end = (end[1] - 1, end[0] - 1)
@abstractmethod @abstractmethod
def solve(self, maze: Maze, height: int = None, def solve(self, maze: Maze) -> str: ...
width: int = None) -> str: ...
class AStar(MazeSolver): class AStar(MazeSolver):
@@ -157,81 +156,3 @@ class AStar(MazeSolver):
if res is None: if res is None:
raise Exception("Path not found") raise Exception("Path not found")
return res return res
class DepthFirstSearchSolver(MazeSolver):
def __init__(self, start, end):
self.start = (start[1] - 1, start[0] - 1)
self.end = (end[1] - 1, end[0] - 1)
def solve(self, maze: Maze, height: int = None,
width: int = 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, DepthFirstSearchSolver from .MazeSolver import MazeSolver, AStar
__version__ = "1.0.0" __version__ = "1.0.0"
__author__ = "us" __author__ = "us"
__all__ = ["Cell", "Maze", "MazeGenerator", "DepthFirstSearchSolver", __all__ = ["Cell", "Maze", "MazeGenerator",
"MazeSolver", "AStar", "Kruskal", "DepthFirstSearch"] "MazeSolver", "AStar", "Kruskal", "DepthFirstSearch"]
+3 -6
View File
@@ -54,14 +54,12 @@ class DataMaze:
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( res.update(
DataMaze.get_solver_generator(data, res["ENTRY"], res["EXIT"], DataMaze.get_solver_generator(data, res["ENTRY"], res["EXIT"])
res["PERFECT"])
) )
return res return res
@staticmethod @staticmethod
def get_solver_generator(data: dict, entry: tuple, exit: tuple, def get_solver_generator(data: dict, entry: int, exit: int) -> dict:
perfect: bool) -> dict:
available_generator = { available_generator = {
"Kruskal": Kruskal, "Kruskal": Kruskal,
"DFS": DepthFirstSearch, "DFS": DepthFirstSearch,
@@ -70,8 +68,7 @@ class DataMaze:
"AStar": AStar, "AStar": AStar,
} }
res = {} res = {}
res["GENERATOR"] = available_generator[data["GENERATOR"]](entry, exit, res["GENERATOR"] = available_generator[data["GENERATOR"]]()
perfect)
res["SOLVER"] = available_solver[data["SOLVER"]](entry, exit) res["SOLVER"] = available_solver[data["SOLVER"]](entry, exit)
return res return res
+33 -14
View File
@@ -1,15 +1,34 @@
BD1553D3913 BBBBBB97D397BBBB917BBD15513D17
C3AD54386AA 84684003BAA946AAA854696D3EEBC3
BAC5396C7AA C112D2AC28443D06EAB9103945507A
82956C5396E BEAED283E857C3AD52AEAEAC7D5452
A86D553AC53 87C53EAEBC393C4154012D29553956
C295552C512 A95147EBAD6AEBB87D2EC386D52853
9283B9693AA 86D4517AC17C50443B813AABD1443E
AAAAC456AAA EBD1787850513A9506AAEAAC3A93EB
AC2C553D2C2 B8543850787EEAAB87EC3EC3C2A852
83C3D3C3E96 AEBD443AD4517EC6ED3D2D3EB8687E
C454547C547 838553AC7BD2BD3BBD43EB85069457
EAC57843D2B86D4407D0106943C7BB
D01552B856C4553BC556AC783E97C2
92C7D6C29557F96AFFFD6D16850512
E83D15382D3BFC7857FD39696907EA
BEAD6D46ABC2FFFAFFF96AD2D2C57A
87ABD13BC6D293FAFD50547C3C3D3E
C168128455106EFAFFFABD17EBA907
9056AEC7B92ABBBAD15407ABD2C2C3
EAD1691786E8044296952D06B8147A
BEB87EAD053E87D6ED07812BAE8796
87AAD5412B87C7957BEBEAAC2D07EB
C3C2D55286857905169456E9438796
D07EBBD287A97EAB8507957E944507
96BD0692AD445146C7EBC55503D3AB
C16D2BEAC3D514553D52BD53AE92EA
B8552C12BE97AB952D104796C52E92
C47D016AABC16869456AD5413BAD6A
D113AED2C292D03ED53ABD52800392
D6EC457C7C6C7EC57D446D56EEEC6E
4,3 1,1
2,1 29,29
EENWWNNEEESESWSEESSEEESSSSWSWWNWNWWWNNWSSSESWWNWNNNENNNNNW SSEESSEESWSEESSSESSWSWWWWSSESSSWSEENEENESEEEESWSWSEESSSSEEEENNEESEENEEENNWNNWNNEENNENENESEESWSSESSESWSSSSWWSSSEESSESEN
+3 -7
View File
@@ -1,18 +1,14 @@
import numpy import numpy
from amaz_lib.MazeGenerator import DepthFirstSearch, MazeGenerator from amaz_lib.MazeGenerator import DepthFirstSearch
class TestMazeGenerator: class TestMazeGenerator:
def test_generator(self) -> None: def test_generator(self) -> None:
w_h = (10, 10) w_h = (300, 300)
maze = numpy.array([]) maze = numpy.array([])
generator = DepthFirstSearch((1, 1), (2, 2), True).generator(*w_h) generator = DepthFirstSearch().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