diff --git a/a_maze_ing.py b/a_maze_ing.py index c21bb22..e1c2ae4 100644 --- a/a_maze_ing.py +++ b/a_maze_ing.py @@ -43,6 +43,8 @@ class MazeMLX: ) def put_pixel(self, x, y) -> 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) self.buf[offset + 0] = self.color[0] @@ -80,23 +82,23 @@ class MazeMLX: 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]) - ) - ) + + 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 - y0 = y * line_len + margin - x1 = x * line_len + line_len + margin - y1 = y * line_len + line_len + margin + 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)) @@ -119,31 +121,32 @@ class MazeMLX: maze = amazing.maze.get_maze() if maze is None: return - margin = math.trunc( - math.sqrt(self.width if self.width > self.height else self.height) - // 2 - ) - cell_size = math.trunc( - ( - (self.height - margin) // len(maze) - if self.height > self.width - else (self.width - margin) // len(maze[0]) - ) - ) + + 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]) * cell_size + margin + 12, - (actual[1]) * cell_size + 12 + margin, + (actual[0]) * line_len + margin_x + 12, + (actual[1]) * line_len + 12 + margin_y, ) dr = ( - (actual[0]) * cell_size + cell_size + margin - 12, - (actual[1]) * cell_size + cell_size - 12 + margin, + (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] * cell_size + margin + 12 - y0 = actual[1] * cell_size + margin + 12 - x1 = actual[0] * cell_size + cell_size + margin - 12 - y1 = actual[1] * cell_size + cell_size + margin - 12 + 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": @@ -159,12 +162,12 @@ class MazeMLX: self.put_block((x0, y0), (x0 - 24, y1)) actual = (actual[0] - 1, actual[1]) ul = ( - (actual[0]) * cell_size + margin + 12, - (actual[1]) * cell_size + 12 + margin, + (actual[0]) * line_len + margin_x + 12, + (actual[1]) * line_len + 12 + margin_y, ) dr = ( - (actual[0]) * cell_size + cell_size + margin - 12, - (actual[1]) * cell_size + cell_size - 12 + margin, + (actual[0]) * line_len + line_len + margin_x - 12, + (actual[1]) * line_len + line_len - 12 + margin_y, ) self.put_block(ul, dr) return diff --git a/config.txt b/config.txt index 7e4dfc0..9d88f8d 100644 --- a/config.txt +++ b/config.txt @@ -1,8 +1,8 @@ WIDTH=15 HEIGHT=15 ENTRY=1,1 -EXIT=11,11 +EXIT=13,13 OUTPUT_FILE=maze.txt PERFECT=False GENERATOR=DFS -SOLVER=DFS +SOLVER=AStar diff --git a/src/amaz_lib/MazeSolver.py b/src/amaz_lib/MazeSolver.py index 90638f9..c77bffb 100644 --- a/src/amaz_lib/MazeSolver.py +++ b/src/amaz_lib/MazeSolver.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from .Maze import Maze +from typing import Any import numpy as np @@ -9,170 +10,165 @@ class MazeSolver(ABC): self.end = (end[1] - 1, end[0] - 1) @abstractmethod - def solve(self, maze: Maze, height: int = None, - width: int = None) -> str: ... + def solve( + self, maze: Maze, height: int | None = None, width: int | None = None + ) -> str: ... 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: super().__init__(start, end) + self.path = [] - def f(self, n): - def g(n: tuple[int, int]) -> int: - res = 0 - if n[0] < self.start[0]: - res += self.start[0] - n[0] - else: - 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(self, n: tuple[int, int]) -> int: + return ( + max(n[0], self.end[0]) + - min(n[0], self.end[0]) + + max(n[1], self.end[1]) + - min(n[1], self.end[1]) + ) - def h(n: tuple[int, int]) -> int: - res = 0 - if n[0] < self.end[0]: - res += self.end[0] - n[0] - else: - res += n[0] - self.end[0] - if n[1] < self.end[1]: - res += self.end[1] - n[1] - else: - res += n[1] - self.end[1] - return res - - try: - return g(n) + h(n) - except Exception: - return 1000 - - def best_path( + def get_paths( self, maze: np.ndarray, actual: tuple[int, int], - last: str | None, - ) -> dict[str, int]: - path = { - "N": ( - self.f((actual[0], actual[1] - 1)) - if not maze[actual[1]][actual[0]].get_north() and actual[1] > 0 + close: list, + ) -> list[tuple[int, int]]: + path = [ + ( + (actual[0], actual[1] - 1) + if not maze[actual[1]][actual[0]].get_north() + and actual[1] > 0 + and (actual[0], actual[1] - 1) + not in [n.coordinate for n in close] else None ), - "E": ( - self.f((actual[0] + 1, actual[1])) + ( + (actual[0] + 1, actual[1]) if not maze[actual[1]][actual[0]].get_est() and actual[0] < len(maze[0]) - 1 + and (actual[0] + 1, actual[1]) + not in [n.coordinate for n in close] else None ), - "S": ( - self.f((actual[0], actual[1] + 1)) + ( + (actual[0], actual[1] + 1) if not maze[actual[1]][actual[0]].get_south() and actual[1] < len(maze) - 1 + and (actual[0], actual[1] + 1) + not in [n.coordinate for n in close] else None ), - "W": ( - self.f((actual[0] - 1, actual[1])) - if not maze[actual[1]][actual[0]].get_west() and actual[0] > 0 + ( + (actual[0] - 1, actual[1]) + 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 ), - } - return { - k: v - for k, v in sorted(path.items(), key=lambda item: item[0]) - if v is not None and k != last - } + ] + return [p for p in path if p is not None] - def get_opposit(self, dir: str) -> str: - match dir: - case "N": - return "S" - case "E": - return "W" - case "S": - return "N" - case "W": - return "E" - case _: - return "" + def get_path(self, maze: np.ndarray) -> list: + open: list[AStar.Node] = [] + close: list[AStar.Node] = [] - def get_next_pos( - self, dir: str, actual: tuple[int, int] - ) -> tuple[int, int]: - match dir: - case "N": - return (actual[0], actual[1] - 1) - case "E": - 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: - path = [(self.start, self.best_path(maze, self.start, None))] - visited = [self.start] - while len(path) > 0 and path[-1][0] != self.end: - if len(path[-1][1]) == 0: - path.pop(-1) - if len(path) == 0: - break - k = next(iter(path[-1][1])) - path[-1][1].pop(k) - continue - - while len(path[-1][1]) > 0: - next_pos = self.get_next_pos( - list(path[-1][1].keys())[0], path[-1][0] - ) - if next_pos in visited: - k = next(iter(path[-1][1])) - path[-1][1].pop(k) - else: - break - if len(path[-1][1]) == 0: - path.pop(-1) - continue - - pre = self.get_opposit(list(path[-1][1].keys())[0]) - path.append( - ( - next_pos, - self.best_path(maze, next_pos, pre), - ) + open.append( + AStar.Node( + self.start, + 0, + self.h(self.start), + self.h(self.start), + None, ) - visited += [next_pos] - if len(path) == 0: - return None - path[-1] = (self.end, {}) - return "".join( - str(list(c[1].keys())[0]) for c in path if len(c[1]) > 0 ) - def solve(self, maze: Maze, height: int = None, - width: int = None) -> str: - res = self.get_path(maze.get_maze()) - if res is None: - raise Exception("Path not found") + while len(open) > 0: + to_check = sorted(open, key=lambda x: x.f)[0] + 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") + + 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 translate(self, close: list) -> str: + current = close[-1] + res = "" + while True: + res = self.get_rev_dir(current) + res + current = current.parent + if current.coordinate == self.start: + break 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, - width: int = None) -> str: - res = list() - for _ in range(50): - res.append(self.get_path(maze, height, width)) - return min(res, key=lambda x: len(x)) - - def get_path(self, maze: Maze, height: int = None, - width: int = None) -> str: + 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() @@ -186,8 +182,9 @@ class DepthFirstSearchSolver(MazeSolver): 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) + path, move = self.back_on_step( + path, visited, maze_s, h_w, move + ) if not path: break coord = path[-1] @@ -202,8 +199,9 @@ class DepthFirstSearchSolver(MazeSolver): return path_str @staticmethod - def random_path(visited: np.ndarray, coord: tuple, - maze: np.ndarray, h_w: tuple) -> list: + def random_path( + visited: np.ndarray, coord: tuple, maze: np.ndarray, h_w: tuple + ) -> list: random_p = [] h, w = h_w y, x = coord @@ -226,8 +224,13 @@ class DepthFirstSearchSolver(MazeSolver): 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: + 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):