2026-rff_mp/VasilevIA/lab2/codes/maze.py

240 lines
6.5 KiB
Python
Raw Normal View History

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
)