From 3c644d29b06a65cb255c03c8f867a325b0e0fce5 Mon Sep 17 00:00:00 2001 From: kirill Date: Sun, 24 May 2026 21:31:36 +0300 Subject: [PATCH] maze.py added --- svetlakovkyu/02/codes/maze.py | 239 ++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 svetlakovkyu/02/codes/maze.py diff --git a/svetlakovkyu/02/codes/maze.py b/svetlakovkyu/02/codes/maze.py new file mode 100644 index 0000000..d436474 --- /dev/null +++ b/svetlakovkyu/02/codes/maze.py @@ -0,0 +1,239 @@ +import heapq +import time +from abc import ABC, abstractmethod +from collections import deque +from dataclasses import dataclass, field +from typing import List, Optional + + +class Cell: + def __init__(self, x, y, is_wall=False, is_start=False, is_exit=False): + self.x = x + self.y = y + self.is_wall = is_wall + self.is_start = is_start + self.is_exit = is_exit + + def is_passable(self): + return not self.is_wall + + def __eq__(self, other): + return isinstance(other, Cell) and self.x == other.x and self.y == other.y + + def __hash__(self): + return hash((self.x, self.y)) + + def __repr__(self): + return f"Cell({self.x},{self.y})" + + +class Maze: + def __init__(self, cells, width, height, start, exit_cell): + self.cells = cells + self.width = width + self.height = height + self.start = start + self.exit = exit_cell + + def get_cell(self, x, y): + if 0 <= x < self.width and 0 <= y < self.height: + return self.cells[y][x] + return None + + def get_neighbors(self, cell): + result = [] + for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]: + n = self.get_cell(cell.x + dx, cell.y + dy) + if n and n.is_passable(): + result.append(n) + return result + + def render(self, path=None): + path_set = set(path) if path else set() + lines = [] + for row in self.cells: + line = "" + for cell in row: + if cell.is_start: + line += " S" + elif cell.is_exit: + line += " E" + elif cell.is_wall: + line += "##" + elif cell in path_set: + line += " ." + else: + line += " " + lines.append(line) + return "\n".join(lines) + + +class MazeBuilder(ABC): + @abstractmethod + def build_from_file(self, filename) -> Maze: + pass + + +class TextFileMazeBuilder(MazeBuilder): + def build_from_file(self, filename) -> Maze: + with open(filename, encoding="utf-8") as f: + lines = [l.rstrip("\n") for l in f] + + height = len(lines) + width = max(len(l) for l in lines) + cells = [] + start = exit_cell = None + + for y, line in enumerate(lines): + row = [] + for x in range(width): + ch = line[x] if x < len(line) else " " + is_wall = ch == "#" + is_start = ch == "S" + is_exit = ch == "E" + c = Cell(x, y, is_wall, is_start, is_exit) + if is_start: + start = c + if is_exit: + exit_cell = c + row.append(c) + cells.append(row) + + if not start or not exit_cell: + raise ValueError("Maze must have S and E") + return Maze(cells, width, height, start, exit_cell) + + +@dataclass +class SearchStats: + strategy: str + time_ms: float + visited: int + path_length: int + path: List[Cell] = field(default_factory=list) + + +class PathFindingStrategy(ABC): + _visited = 0 + + @property + def name(self): + return self.__class__.__name__ + + @abstractmethod + def find_path(self, maze: Maze, start: Cell, end: Cell) -> List[Cell]: + pass + + @staticmethod + def _build_path(parent, start, end): + path, cur = [], end + while cur: + path.append(cur) + cur = parent.get(cur) + path.reverse() + return path if path and path[0] == start else [] + + +class BFSStrategy(PathFindingStrategy): + @property + def name(self): + return "BFS" + + def find_path(self, maze, start, end): + queue = deque([start]) + parent = {start: None} + visited = 0 + while queue: + cur = queue.popleft() + visited += 1 + if cur == end: + self._visited = visited + return self._build_path(parent, start, end) + for nb in maze.get_neighbors(cur): + if nb not in parent: + parent[nb] = cur + queue.append(nb) + self._visited = visited + return [] + + +class DFSStrategy(PathFindingStrategy): + @property + def name(self): + return "DFS" + + def find_path(self, maze, start, end): + stack = [start] + parent = {start: None} + visited = 0 + while stack: + cur = stack.pop() + visited += 1 + if cur == end: + self._visited = visited + return self._build_path(parent, start, end) + for nb in maze.get_neighbors(cur): + if nb not in parent: + parent[nb] = cur + stack.append(nb) + self._visited = visited + return [] + + +class AStarStrategy(PathFindingStrategy): + @property + def name(self): + return "A*" + + @staticmethod + def _h(a, b): + return abs(a.x - b.x) + abs(a.y - b.y) + + def find_path(self, maze, start, end): + counter = 0 + heap = [(0, counter, start)] + parent = {start: None} + g = {start: 0} + closed = set() + visited = 0 + while heap: + _, _, cur = heapq.heappop(heap) + if cur in closed: + continue + closed.add(cur) + visited += 1 + if cur == end: + self._visited = visited + return self._build_path(parent, start, end) + for nb in maze.get_neighbors(cur): + if nb in closed: + continue + ng = g[cur] + 1 + if ng < g.get(nb, float("inf")): + g[nb] = ng + counter += 1 + heapq.heappush(heap, (ng + self._h(nb, end), counter, nb)) + parent[nb] = cur + self._visited = visited + return [] + + +class MazeSolver: + def __init__(self, maze: Maze, strategy: PathFindingStrategy): + self.maze = maze + self.strategy = strategy + + def set_strategy(self, strategy: PathFindingStrategy): + self.strategy = strategy + + def solve(self) -> SearchStats: + t0 = time.perf_counter() + path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) + t1 = time.perf_counter() + return SearchStats( + strategy=self.strategy.name, + time_ms=(t1 - t0) * 1000, + visited=self.strategy._visited, + path_length=len(path), + path=path, + )