247 lines
7.8 KiB
Python
247 lines
7.8 KiB
Python
"""Модель лабиринта и алгоритмы поиска с паттернами Builder, Strategy, Observer."""
|
||
|
||
from abc import ABC, abstractmethod
|
||
from collections import deque
|
||
from dataclasses import dataclass, field
|
||
from heapq import heappop, heappush
|
||
from itertools import count
|
||
from pathlib import Path
|
||
from time import perf_counter
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class Cell:
|
||
x: int
|
||
y: int
|
||
is_wall: bool = False
|
||
is_start: bool = False
|
||
is_exit: bool = False
|
||
|
||
def is_passable(self):
|
||
return not self.is_wall
|
||
|
||
|
||
class Maze:
|
||
def __init__(self, cells):
|
||
if not cells or not cells[0]:
|
||
raise ValueError("Лабиринт не может быть пустым")
|
||
self.cells = cells
|
||
self.height = len(cells)
|
||
self.width = len(cells[0])
|
||
self.start = next((cell for row in cells for cell in row if cell.is_start), None)
|
||
self.exit = next((cell for row in cells for cell in row if cell.is_exit), None)
|
||
|
||
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):
|
||
neighbors = []
|
||
for dx, dy in ((0, -1), (1, 0), (0, 1), (-1, 0)):
|
||
neighbor = self.get_cell(cell.x + dx, cell.y + dy)
|
||
if neighbor is not None and neighbor.is_passable():
|
||
neighbors.append(neighbor)
|
||
return neighbors
|
||
|
||
|
||
class MazeBuilder(ABC):
|
||
@abstractmethod
|
||
def build_from_file(self, filename):
|
||
pass
|
||
|
||
|
||
class TextFileMazeBuilder(MazeBuilder):
|
||
SYMBOLS = {"#", " ", "S", "E"}
|
||
|
||
def build_from_file(self, filename):
|
||
lines = Path(filename).read_text(encoding="utf-8").splitlines()
|
||
if not lines or not lines[0] or any(len(line) != len(lines[0]) for line in lines):
|
||
raise ValueError("Строки лабиринта должны иметь одинаковую ненулевую длину")
|
||
unknown = {character for line in lines for character in line} - self.SYMBOLS
|
||
if unknown:
|
||
raise ValueError(f"Недопустимые символы: {sorted(unknown)}")
|
||
if sum(line.count("S") for line in lines) != 1 or sum(line.count("E") for line in lines) != 1:
|
||
raise ValueError("Лабиринт должен содержать ровно один старт S и один выход E")
|
||
|
||
cells = []
|
||
for y, line in enumerate(lines):
|
||
cells.append([
|
||
Cell(x, y, symbol == "#", symbol == "S", symbol == "E")
|
||
for x, symbol in enumerate(line)
|
||
])
|
||
return Maze(cells)
|
||
|
||
|
||
class PathFindingStrategy(ABC):
|
||
name = "Неизвестно"
|
||
|
||
def __init__(self):
|
||
self.visited_count = 0
|
||
|
||
@abstractmethod
|
||
def find_path(self, maze, start, exit_cell):
|
||
pass
|
||
|
||
@staticmethod
|
||
def restore_path(parent, start, exit_cell):
|
||
if exit_cell not in parent:
|
||
return []
|
||
path, current = [], exit_cell
|
||
while current is not None:
|
||
path.append(current)
|
||
current = parent[current]
|
||
path.reverse()
|
||
return path if path and path[0] == start else []
|
||
|
||
|
||
class BFSStrategy(PathFindingStrategy):
|
||
name = "BFS"
|
||
|
||
def find_path(self, maze, start, exit_cell):
|
||
queue = deque([start])
|
||
parent = {start: None}
|
||
visited = 0
|
||
while queue:
|
||
current = queue.popleft()
|
||
visited += 1
|
||
if current == exit_cell:
|
||
break
|
||
for neighbor in maze.get_neighbors(current):
|
||
if neighbor not in parent:
|
||
parent[neighbor] = current
|
||
queue.append(neighbor)
|
||
self.visited_count = visited
|
||
return self.restore_path(parent, start, exit_cell)
|
||
|
||
|
||
class DFSStrategy(PathFindingStrategy):
|
||
name = "DFS"
|
||
|
||
def find_path(self, maze, start, exit_cell):
|
||
stack = [start]
|
||
parent = {start: None}
|
||
visited = 0
|
||
while stack:
|
||
current = stack.pop()
|
||
visited += 1
|
||
if current == exit_cell:
|
||
break
|
||
for neighbor in maze.get_neighbors(current):
|
||
if neighbor not in parent:
|
||
parent[neighbor] = current
|
||
stack.append(neighbor)
|
||
self.visited_count = visited
|
||
return self.restore_path(parent, start, exit_cell)
|
||
|
||
|
||
class AStarStrategy(PathFindingStrategy):
|
||
name = "A*"
|
||
|
||
@staticmethod
|
||
def heuristic(first, second):
|
||
return abs(first.x - second.x) + abs(first.y - second.y)
|
||
|
||
def find_path(self, maze, start, exit_cell):
|
||
order = count()
|
||
queue = [(self.heuristic(start, exit_cell), next(order), start)]
|
||
distance = {start: 0}
|
||
parent = {start: None}
|
||
closed = set()
|
||
while queue:
|
||
_, _, current = heappop(queue)
|
||
if current in closed:
|
||
continue
|
||
closed.add(current)
|
||
if current == exit_cell:
|
||
break
|
||
for neighbor in maze.get_neighbors(current):
|
||
new_distance = distance[current] + 1
|
||
if new_distance < distance.get(neighbor, float("inf")):
|
||
distance[neighbor] = new_distance
|
||
parent[neighbor] = current
|
||
priority = new_distance + self.heuristic(neighbor, exit_cell)
|
||
heappush(queue, (priority, next(order), neighbor))
|
||
self.visited_count = len(closed)
|
||
return self.restore_path(parent, start, exit_cell)
|
||
|
||
|
||
@dataclass
|
||
class SearchStats:
|
||
strategy: str
|
||
time_ms: float
|
||
visited_cells: int
|
||
path_length: int
|
||
path: list = field(repr=False)
|
||
|
||
|
||
class Observer(ABC):
|
||
@abstractmethod
|
||
def update(self, event):
|
||
pass
|
||
|
||
|
||
class ConsoleView(Observer):
|
||
def __init__(self, verbose=True):
|
||
self.verbose = verbose
|
||
self.events = []
|
||
|
||
def update(self, event):
|
||
self.events.append(event)
|
||
if self.verbose:
|
||
print(event["message"])
|
||
|
||
def render(self, maze, path=None):
|
||
path_cells = set(path or [])
|
||
rows = []
|
||
for row in maze.cells:
|
||
symbols = []
|
||
for cell in row:
|
||
if cell.is_start:
|
||
symbols.append("S")
|
||
elif cell.is_exit:
|
||
symbols.append("E")
|
||
elif cell.is_wall:
|
||
symbols.append("#")
|
||
elif cell in path_cells:
|
||
symbols.append(".")
|
||
else:
|
||
symbols.append(" ")
|
||
rows.append("".join(symbols))
|
||
return "\n".join(rows)
|
||
|
||
|
||
class MazeSolver:
|
||
def __init__(self, maze, strategy):
|
||
self.maze = maze
|
||
self.strategy = strategy
|
||
self.observers = []
|
||
|
||
def set_strategy(self, strategy):
|
||
self.strategy = strategy
|
||
|
||
def attach(self, observer):
|
||
if observer not in self.observers:
|
||
self.observers.append(observer)
|
||
|
||
def notify(self, event):
|
||
for observer in self.observers:
|
||
observer.update(event)
|
||
|
||
def solve(self):
|
||
self.notify({"type": "search_started", "message": f"Запущен алгоритм {self.strategy.name}"})
|
||
started = perf_counter()
|
||
path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
|
||
elapsed_ms = max((perf_counter() - started) * 1000, 1e-9)
|
||
stats = SearchStats(
|
||
strategy=self.strategy.name,
|
||
time_ms=elapsed_ms,
|
||
visited_cells=self.strategy.visited_count,
|
||
path_length=max(len(path) - 1, 0),
|
||
path=path,
|
||
)
|
||
event_type = "path_found" if path else "path_not_found"
|
||
message = f"{self.strategy.name}: путь длиной {stats.path_length}" if path else f"{self.strategy.name}: путь не найден"
|
||
self.notify({"type": event_type, "message": message, "stats": stats})
|
||
return stats
|