diff --git a/.gitignore b/.gitignore index c855401..4090c3b 100644 --- a/.gitignore +++ b/.gitignore @@ -214,4 +214,5 @@ __marimo__/ # Streamlit .streamlit/secrets.toml +test.txt diff --git a/Makefile b/Makefile index ade098d..42a2781 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,9 @@ install: run: install uv run python3 a_maze_ing.py config.txt +run_windows: + .venv\Scripts\python -m a_maze_ing config.txt + debug: uv pdb python3 a_maze_ing.py config.txt diff --git a/src/AMazeIng.py b/src/AMazeIng.py index 64047c0..3a1e90a 100644 --- a/src/AMazeIng.py +++ b/src/AMazeIng.py @@ -8,8 +8,8 @@ from src.amaz_lib import Maze, MazeGenerator, MazeSolver class AMazeIng(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) - width: int = Field(ge=3) - height: int = Field(ge=3) + width: int = Field(ge=4) + height: int = Field(ge=4) entry: tuple[int, int] exit: tuple[int, int] output_file: str = Field(min_length=3) diff --git a/src/amaz_lib/MazeGenerator.py b/src/amaz_lib/MazeGenerator.py index 8649bc4..66a29ea 100644 --- a/src/amaz_lib/MazeGenerator.py +++ b/src/amaz_lib/MazeGenerator.py @@ -6,6 +6,11 @@ import math 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 def generator( self, height: int, width: int, seed: int | None = None @@ -35,8 +40,60 @@ class MazeGenerator(ABC): forty_two.add((y + 2, x + 3)) 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 Set: def __init__(self, cells: list[int]) -> None: self.cells: list[int] = cells @@ -118,6 +175,8 @@ class Kruskal(MazeGenerator): 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: np.random.seed(seed) @@ -146,10 +205,23 @@ class Kruskal(MazeGenerator): len(sets.sets) == 19 and cells_ft is not None ): break - return self.walls_to_maze(walls, height, width) + print(f"nb sets: {len(sets.sets)}") + 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): + 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( self, height: int, width: int, seed: int = None @@ -157,9 +229,15 @@ class DepthFirstSearch(MazeGenerator): if seed is not None: np.random.seed(seed) 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 = 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() w_h = (width, height) coord = (0, 0) @@ -190,6 +268,12 @@ class DepthFirstSearch(MazeGenerator): x, y = coord maze[y][x] = self.broken_wall(maze[y][x], wall_r) 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 @staticmethod @@ -250,7 +334,7 @@ class DepthFirstSearch(MazeGenerator): return {"N": "S", "S": "N", "W": "E", "E": "W"}[direction] @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: last = path[-1] if DepthFirstSearch.random_cells(visited, last, w_h): diff --git a/src/amaz_lib/MazeSolver.py b/src/amaz_lib/MazeSolver.py index 8fa863c..f5f14fe 100644 --- a/src/amaz_lib/MazeSolver.py +++ b/src/amaz_lib/MazeSolver.py @@ -9,7 +9,8 @@ class MazeSolver(ABC): self.end = (end[1] - 1, end[0] - 1) @abstractmethod - def solve(self, maze: Maze) -> str: ... + def solve(self, maze: Maze, height: int = None, + width: int = None) -> str: ... class AStar(MazeSolver): @@ -156,3 +157,81 @@ class AStar(MazeSolver): if res is None: raise Exception("Path not found") 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) diff --git a/src/amaz_lib/__init__.py b/src/amaz_lib/__init__.py index fda6b32..2c20d16 100644 --- a/src/amaz_lib/__init__.py +++ b/src/amaz_lib/__init__.py @@ -2,9 +2,9 @@ from .Cell import Cell from .Maze import Maze from .MazeGenerator import MazeGenerator, DepthFirstSearch from .MazeGenerator import Kruskal -from .MazeSolver import MazeSolver, AStar +from .MazeSolver import MazeSolver, AStar, DepthFirstSearchSolver __version__ = "1.0.0" __author__ = "us" -__all__ = ["Cell", "Maze", "MazeGenerator", +__all__ = ["Cell", "Maze", "MazeGenerator", "DepthFirstSearchSolver", "MazeSolver", "AStar", "Kruskal", "DepthFirstSearch"] diff --git a/src/parsing/Parsing.py b/src/parsing/Parsing.py index 97bf839..75659c7 100644 --- a/src/parsing/Parsing.py +++ b/src/parsing/Parsing.py @@ -54,12 +54,14 @@ class DataMaze: res.update({key: DataMaze.convert_bool(data[key])}) res.update({"OUTPUT_FILE": data["OUTPUT_FILE"]}) res.update( - DataMaze.get_solver_generator(data, res["ENTRY"], res["EXIT"]) + DataMaze.get_solver_generator(data, res["ENTRY"], res["EXIT"], + res["PERFECT"]) ) return res @staticmethod - def get_solver_generator(data: dict, entry: int, exit: int) -> dict: + def get_solver_generator(data: dict, entry: tuple, exit: tuple, + perfect: bool) -> dict: available_generator = { "Kruskal": Kruskal, "DFS": DepthFirstSearch, @@ -68,7 +70,8 @@ class DataMaze: "AStar": AStar, } res = {} - res["GENERATOR"] = available_generator[data["GENERATOR"]]() + res["GENERATOR"] = available_generator[data["GENERATOR"]](entry, exit, + perfect) res["SOLVER"] = available_solver[data["SOLVER"]](entry, exit) return res diff --git a/tests/test_MazeGenerator.py b/tests/test_MazeGenerator.py index 948748a..99278c0 100644 --- a/tests/test_MazeGenerator.py +++ b/tests/test_MazeGenerator.py @@ -1,14 +1,18 @@ import numpy -from amaz_lib.MazeGenerator import DepthFirstSearch +from amaz_lib.MazeGenerator import DepthFirstSearch, MazeGenerator class TestMazeGenerator: def test_generator(self) -> None: - w_h = (300, 300) + w_h = (10, 10) maze = numpy.array([]) - generator = DepthFirstSearch().generator(*w_h) + generator = DepthFirstSearch((1, 1), (2, 2), True).generator(*w_h) for output in generator: maze = output assert maze.shape == w_h + + def test_gen_broken(self) -> None: + test = MazeGenerator.gen_broken_set(50, 50) + assert len(test) > 0