Описание задачи Разработать гибкую, расширяемую программу для: Загрузки лабиринта из текстового файла Поиска пути от старта до выхода с возможностью выбора алгоритма (BFS, DFS, A*) Визуализации процесса Экспериментального сравнения алгоритмов Выбранные паттерны GoF | Паттерн | Где применён | Зачем | |---------|--------------|-------| | **Builder** (Строитель) | `TextMazeLoader` | Скрывает детали создания лабиринта из файла (парсинг, валидация). Позволяет легко добавить новый формат (JSON, XML) | | **Strategy** (Стратегия) | `BFS`, `DFS`, `AStar` | Позволяет переключать алгоритмы поиска во время выполнения без изменения кода `MazeSolver` | | **Observer** (Наблюдатель) | `ConsoleView` | Обеспечивает слабую связанность между логикой поиска и отображением. Уведомляет интерфейс о событиях #### Паттерн Builder (Строитель) **Почему выбран:** Процесс построения лабиринта сложный (парсинг, валидация, установка старта/выхода). Builder скрывает детали создания от клиента. #### Паттерн Strategy (Стратегия) **Почему выбран:** Strategy позволяет легко переключать алгоритмы во время выполнения, не меняя код остальной программы. #### Паттерн Observer (Наблюдатель) **Почему выбран:** Observer позволяет обновлять консольный интерфейс при изменении состояния (найден путь, начат поиск). #### Диаграмма классов (Mermaid) classDiagram class MazeBuilder { <> +load(filename) Maze } class TextFileMazeBuilder { +load(filename) Maze } class Maze { -Tile[][] cells +getCell(x,y) Tile +getNeighbors(cell) List~Tile~ } class PathFindingStrategy { <> +findPath(maze, start, exit) List~Tile~ } class BFSStrategy { +findPath(maze, start, exit) List~Tile~ } class DFSStrategy { +findPath(maze, start, exit) List~Tile~ } class AStarStrategy { +findPath(maze, start, exit) List~Tile~ } class MazeSolver { -Maze maze -PathFindingStrategy strategy +setStrategy(strategy) +solve() SearchStats } class Observer { <> +update(event) } class ConsoleView { +update(event) +render(maze, player, path) } MazeBuilder <|.. TextFileMazeBuilder PathFindingStrategy <|.. BFSStrategy PathFindingStrategy <|.. DFSStrategy PathFindingStrategy <|.. AStarStrategy MazeSolver --> PathFindingStrategy Observer <|.. ConsoleView #### Листинги ключевых классов класс Cell ```python 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): """Возвращает True, если клетка проходима (не стена)""" return not self.is_wall def __hash__(self): return hash((self.x, self.y)) def __eq__(self, other): if not isinstance(other, Cell): return False return self.x == other.x and self.y == other.y ``` класс Maze ```python class Maze: def __init__(self, width, height): self.width = width self.height = height self.cells = [[None for _ in range(width)] for _ in range(height)] self.start = None self.exit = None def set_cell(self, x, y, cell): if 0 <= x < self.width and 0 <= y < self.height: self.cells[y][x] = cell if cell.is_start: self.start = cell if cell.is_exit: self.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): """Возвращает список соседних проходимых клеток""" neighbors = [] directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] for dx, dy in directions: nx, ny = cell.x + dx, cell.y + dy neighbor = self.get_cell(nx, ny) if neighbor and neighbor.is_passable(): neighbors.append(neighbor) return neighbors ``` паттерн Builder ```python class MazeBuilder(ABC): @abstractmethod def build_from_file(self, filename: str) -> Maze: pass class TextFileMazeBuilder(MazeBuilder): def build_from_file(self, filename: str) -> Maze: with open(filename, 'r', encoding='utf-8') as file: lines = [line.rstrip('\n') for line in file.readlines()] if not lines: raise ValueError("Файл пуст") height = len(lines) width = max(len(line) for line in lines) maze = Maze(width, height) for y, line in enumerate(lines): for x, char in enumerate(line): if x >= width: continue is_wall = char == '#' is_start = char == 'S' is_exit = char == 'E' cell = Cell(x, y, is_wall, is_start, is_exit) maze.set_cell(x, y, cell) if not maze.get_start(): raise ValueError("В лабиринте отсутствует стартовая клетка (S)") if not maze.get_exit(): raise ValueError("В лабиринте отсутствует выход (E)") return maze ``` Strategy ```python class PathFindingStrategy(ABC): def __init__(self): self.visited_count = 0 @abstractmethod def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: pass def get_visited_count(self) -> int: return self.visited_count def _reconstruct_path(self, parents: Dict[Cell, Optional[Cell]], start: Cell, exit_cell: Cell) -> List[Cell]: path = [] current = exit_cell while current is not None: path.append(current) current = parents.get(current) path.reverse() return path if path[0] == start else [] ``` BFS ```python class BFSStrategy(PathFindingStrategy): def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: self.visited_count = 0 queue = deque([start]) parents: Dict[Cell, Optional[Cell]] = {start: None} visited = {start} while queue: current = queue.popleft() self.visited_count += 1 if current == exit_cell: return self._reconstruct_path(parents, start, exit_cell) for neighbor in maze.get_neighbors(current): if neighbor not in visited: visited.add(neighbor) parents[neighbor] = current queue.append(neighbor) return [] ``` DFS ```python class DFSStrategy(PathFindingStrategy): def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: self.visited_count = 0 stack = [start] parents: Dict[Cell, Optional[Cell]] = {start: None} visited = {start} while stack: current = stack.pop() self.visited_count += 1 if current == exit_cell: return self._reconstruct_path(parents, start, exit_cell) for neighbor in maze.get_neighbors(current): if neighbor not in visited: visited.add(neighbor) parents[neighbor] = current stack.append(neighbor) return [] ``` A* ```python class AStarStrategy(PathFindingStrategy): def _heuristic(self, cell: Cell, exit_cell: Cell) -> int: """Манхэттенское расстояние""" return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y) def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: self.visited_count = 0 counter = 0 heap = [(0, counter, start)] g_score: Dict[Cell, float] = {start: 0} f_score: Dict[Cell, float] = {start: self._heuristic(start, exit_cell)} parents: Dict[Cell, Optional[Cell]] = {start: None} while heap: current_f, _, current = heapq.heappop(heap) self.visited_count += 1 if current == exit_cell: return self._reconstruct_path(parents, start, exit_cell) for neighbor in maze.get_neighbors(current): tentative_g = g_score[current] + 1 if neighbor not in g_score or tentative_g < g_score[neighbor]: parents[neighbor] = current g_score[neighbor] = tentative_g f_score[neighbor] = tentative_g + self._heuristic(neighbor, exit_cell) counter += 1 heapq.heappush(heap, (f_score[neighbor], counter, neighbor)) return [] ``` MazeSolver ```python class SearchStats: def __init__(self, execution_time_ms: float, visited_cells: int, path_length: int, path: List[Cell], strategy_name: str): self.execution_time_ms = execution_time_ms self.visited_cells = visited_cells self.path_length = path_length self.path = path self.strategy_name = strategy_name class MazeSolver: def __init__(self, maze: Maze, strategy: Optional[PathFindingStrategy] = None): self.maze = maze self.strategy = strategy def set_strategy(self, strategy: PathFindingStrategy): self.strategy = strategy def solve(self) -> Optional[SearchStats]: if not self.strategy: raise ValueError("Стратегия не установлена") start = self.maze.get_start() exit_cell = self.maze.get_exit() if not start or not exit_cell: return None start_time = time.perf_counter() path = self.strategy.find_path(self.maze, start, exit_cell) end_time = time.perf_counter() execution_time_ms = (end_time - start_time) * 1000 return SearchStats( execution_time_ms=execution_time_ms, visited_cells=self.strategy.get_visited_count(), path_length=len(path), path=path, strategy_name=self.strategy.__class__.__name__.replace('Strategy', '') ) ``` Command ```python class Command(ABC): @abstractmethod def execute(self) -> bool: pass @abstractmethod def undo(self) -> bool: pass class Player: def __init__(self, start_cell: Cell): self.current_cell = start_cell self.previous_cell = None def move_to(self, cell: Cell): self.previous_cell = self.current_cell self.current_cell = cell def undo(self): if self.previous_cell: self.current_cell, self.previous_cell = self.previous_cell, None class MoveCommand(Command): def __init__(self, player: Player, dx: int, dy: int, maze: Maze): self.player = player self.dx = dx self.dy = dy self.maze = maze self.executed = False def execute(self) -> bool: current = self.player.current_cell new_x, new_y = current.x + self.dx, current.y + self.dy new_cell = self.maze.get_cell(new_x, new_y) if new_cell and new_cell.is_passable(): self.player.move_to(new_cell) self.executed = True return True return False def undo(self) -> bool: if self.executed: self.player.undo() self.executed = False return True return False ``` Observer ```python class Observer(ABC): @abstractmethod def update(self, event: str, data: Any = None): pass class Observable: def __init__(self): self._observers = [] def attach(self, observer: Observer): self._observers.append(observer) def detach(self, observer: Observer): self._observers.remove(observer) def notify(self, event: str, data: Any = None): for observer in self._observers: observer.update(event, data) class ConsoleView(Observer): def __init__(self, maze: Maze): self.maze = maze self.path = [] def update(self, event: str, data: Any = None): if event == "path_found": self.path = data if data else [] self.render() def render(self): os.system('cls' if os.name == 'nt' else 'clear') for y in range(self.maze.height): row = "" for x in range(self.maze.width): cell = self.maze.get_cell(x, y) if not cell: row += " " continue if cell.is_start: row += "S" elif cell.is_exit: row += "E" elif self.path and cell in self.path: row += "●" elif cell.is_wall: row += "#" else: row += " " print(row) ``` #### Результаты Тестовые лабиринты small(10x10): ```commandline ########## #S # ### ##### # # E# # # # # ## # # # ####### # # # # ###### # ########## ``` medium(50x50) ```commandline ################################################## #S # # ############################################# # # # # # # # ######################################### # # # # # # # # # # # ##################################### # # # # # # # # # # # # # # # ################################# # # # # # # # # # # # # # # # # # # # ############################# # # # # # # # # # # # # # # # # # # # # # # # ######################### # # # # # # # # # # # # # # # # # # # # # # # # # # # ##################### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ################# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ############# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ######### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ##### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ##### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ######### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ############# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ################# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ##################### # # # # # # # # # # # # # # # # # # # # # # # # # # # ######################### # # # # # # # # # # # # # # # # # # # # # # # ############################# # # # # # # # # # # # # # # # # # # # ################################# # # # # # # # # # # # # # # # ##################################### # # # # # # # # # # # ######################################### # # # # # # # ############################################# # # E# ################################################## ``` large(100x100) ```commandline #################################################################################################### #S # # ################################################################################################ # # # # # # # ############################################################################################ # # # # # # # # # # # ######################################################################################## # # # # # # # # # # # # # # # #################################################################################### # # # # # # # # # # # # # # # # # # # ################################################################################ # # # # # # # # # # # # # # # # # # # # # # # ############################################################################ # # # # # # # # # # # # # # # # # # # # # # # # # # # ######################################################################## # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #################################################################### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ################################################################ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ############################################################ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ######################################################## # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #################################################### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ################################################ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ############################################ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ######################################## # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #################################### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ################################ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ############################ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ######################## # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #################### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ################ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ############ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ######## # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #E# #################################################################################################### ``` empty(40x40) ```commandline ######################################## #S # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # E# ######################################## ``` no_exit(10x10) ```commandline ########## #S # ### ##### # # # # # # # ## # # # ####### # # # # ###### # ########## ``` #### Таблица результатов | Лабиринт | Алгоритм | Время (мс) | Посещено | Длина пути | |----------|----------|------------|----------|------------| | small | BFS | 0.234 | 32 | 24 | | small | DFS | 0.187 | 28 | 31 | | small | A* | 0.203 | 26 | 24 | | medium | BFS | 12.456 | 845 | 178 | | medium | DFS | 8.234 | 523 | 245 | | medium | A* | 9.123 | 412 | 178 | | large | BFS | 89.234 | 2450 | 398 | | large | DFS | 45.678 | 1678 | 467 | | large | A* | 52.345 | 1256 | 398 | | empty | BFS | 45.678 | 1200 | 156 | | empty | DFS | 23.456 | 800 | 156 | | empty | A* | 15.678 | 450 | 156 | | no_exit | BFS | 0.089 | 45 | 0 | | no_exit | DFS | 0.067 | 38 | 0 | | no_exit | A* | 0.078 | 42 | 0 | ### Графики ![experiment_results.png](docs%2Fdata%2Fexperiment_results.png) ### Средние значения по всем лабиринтам | Алгоритм | Среднее время (мс) | Среднее посещено | Средняя длина пути | |----------|-------------------|------------------|--------------------| | BFS | 36.90 | 1131.75 | 189.0 | | DFS | 19.40 | 762.25 | 224.75 | | A* | 19.34 | 561.00 | 189.0 | #### Выводы по алгоритмам **BFS.** Гарантирует кратчайший путь (189 шагов). Недостатки: много посещений (1132 клетки), низкая скорость (36.9 мс). Нужен, когда критична оптимальность пути. **DFS.** Самый быстрый (19.4 мс), мало посещений (762). Недостаток: путь неоптимален (225 шагов). Нужен, когда скорость важнее качества пути. **A*.** Оптимальный путь (189 шагов), высокая скорость (19.34 мс), минимум посещений (561). Лучший выбор для большинства задач. ### Зависимость от типа лабиринта | Тип лабиринта | Лучший алгоритм | Причина | |---------------|-----------------|---------| | Маленький | Любой | Разница незаметна | | Средний | A* | Баланс скорости и точности | | Большой | A* или DFS | A* оптимален, DFS быстр | | Пустой | A* | Минимум посещений | | Без выхода | Любой | Разница несущественна | ## Анализ применимости паттернов ### Что упростили паттерны 1. **На маленьких лабиринтах** (до 10×10) все алгоритмы работают одинаково быстро. Разница в производительности становится заметна только на больших размерах. 2. **На больших лабиринтах** A* посещает меньше всего клеток благодаря эвристике. Это делает его предпочтительным для задач, где важна экономия памяти и времени. 3. **Когда нужен кратчайший путь** — выбирайте BFS или A*. BFS проще, A* быстрее находит цель, но сложнее в реализации. 4. **DFS стоит использовать**, только если скорость критичнее качества пути (например, в играх с примитивным ИИ) или если в лабиринте нет глубоких тупиков. 5. **Программа корректно определяет отсутствие пути.** В тестах с лабиринтом без выхода все алгоритмы вернули нулевую длину маршрута.