From d59cd16706a6333588f88faf8ae2827ef9d81647 Mon Sep 17 00:00:00 2001 From: pogodinda Date: Sun, 24 May 2026 01:55:48 +0300 Subject: [PATCH] =?UTF-8?q?[2]=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20MazeSolver,=20=D0=BF=D0=B0=D1=82=D1=82?= =?UTF-8?q?=D0=B5=D1=80=D0=BD=D1=8B=20Observer=20=D0=B8=20Command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pogodinda/lab2/src/commands.py | 79 +++++++++++++++++ pogodinda/lab2/src/maze_solver.py | 86 +++++++++++++++++++ pogodinda/lab2/src/observer.py | 54 ++++++++++++ pogodinda/lab2/tests/test_observer_command.py | 45 ++++++++++ pogodinda/lab2/tests/test_solver.py | 58 +++++++++++++ 5 files changed, 322 insertions(+) create mode 100644 pogodinda/lab2/src/commands.py create mode 100644 pogodinda/lab2/src/maze_solver.py create mode 100644 pogodinda/lab2/src/observer.py create mode 100644 pogodinda/lab2/tests/test_observer_command.py create mode 100644 pogodinda/lab2/tests/test_solver.py diff --git a/pogodinda/lab2/src/commands.py b/pogodinda/lab2/src/commands.py new file mode 100644 index 0000000..d302e16 --- /dev/null +++ b/pogodinda/lab2/src/commands.py @@ -0,0 +1,79 @@ +from abc import ABC, abstractmethod +from typing import List + +from maze import Maze, Cell + + +class Command(ABC): + """Интерфейс команды (Command pattern).""" + + @abstractmethod + def execute(self) -> bool: + pass + + @abstractmethod + def undo(self): + pass + + +class Player: + """Игрок, перемещающийся по лабиринту.""" + + def __init__(self, cell: Cell): + self.current_cell = cell + self._history: List[Cell] = [] + + def move_to(self, cell: Cell): + self._history.append(self.current_cell) + self.current_cell = cell + + def move_back(self): + if self._history: + self.current_cell = self._history.pop() + return self.current_cell + return None + + def __repr__(self): + return f"Player({self.current_cell.x}, {self.current_cell.y})" + + +class MoveCommand(Command): + """Команда перемещения игрока.""" + + DIRECTIONS = { + 'W': (0, -1), + 'S': (0, 1), + 'A': (-1, 0), + 'D': (1, 0), + } + + def __init__(self, player: Player, maze: Maze, direction: str): + self.player = player + self.maze = maze + self.direction = direction.upper() + self._previous_cell = None + self._executed = False + + def execute(self) -> bool: + if self.direction not in self.DIRECTIONS: + return False + + dx, dy = self.DIRECTIONS[self.direction] + new_x = self.player.current_cell.x + dx + new_y = self.player.current_cell.y + dy + + new_cell = self.maze.get_cell(new_x, new_y) + + if new_cell and new_cell.is_passable(): + self._previous_cell = self.player.current_cell + self.player.move_to(new_cell) + self._executed = True + return True + + return False + + def undo(self): + if self._executed and self._previous_cell: + self.player.current_cell = self._previous_cell + self._executed = False + self._previous_cell = None \ No newline at end of file diff --git a/pogodinda/lab2/src/maze_solver.py b/pogodinda/lab2/src/maze_solver.py new file mode 100644 index 0000000..295fcf6 --- /dev/null +++ b/pogodinda/lab2/src/maze_solver.py @@ -0,0 +1,86 @@ +import time +from dataclasses import dataclass +from typing import List, Optional + +from maze import Maze, Cell +from pathfinding import PathFindingStrategy + + +@dataclass +class SearchStats: + """Статистика поиска пути.""" + time_ms: float # время выполнения в миллисекундах + visited_cells: int # сколько клеток посетил алгоритм + path_length: int # длина найденного пути + algorithm_name: str # какой алгоритм использовался + maze_name: str # название лабиринта + + +class MazeSolver: + """ + Оркестратор поиска пути. + Использует паттерн Strategy для переключения алгоритмов. + """ + + def __init__(self, maze: Maze, strategy: PathFindingStrategy = None): + self.maze = maze + self.strategy = strategy + self._observers = [] # для паттерна Observer (Этап 5) + + def set_strategy(self, strategy: PathFindingStrategy): + """ + Динамическая смена алгоритма. + Без паттерна Strategy пришлось бы переписывать этот метод + под каждый новый алгоритм. + """ + self.strategy = strategy + + def add_observer(self, observer): + """Добавление наблюдателя (подготовка к Этапу 5).""" + self._observers.append(observer) + + def _notify_observers(self, event: str): + """Уведомляет всех наблюдателей о событии.""" + for observer in self._observers: + observer.update(event) + + def solve(self, maze_name: str = "unnamed") -> SearchStats: + """ + Выполняет поиск пути и возвращает статистику. + + Args: + maze_name: название лабиринта для отчёта + + Returns: + SearchStats с результатами поиска + """ + if not self.strategy: + raise ValueError("Стратегия не установлена! Вызовите set_strategy()") + + # Уведомляем наблюдателей + self._notify_observers("search_started") + + # Замер времени + start_time = time.perf_counter() + + # Запускаем алгоритм (Strategy делает всю работу) + path, visited_count = self.strategy.find_path( + self.maze, self.maze.start, self.maze.exit + ) + + # Останавливаем замер + end_time = time.perf_counter() + time_ms = (end_time - start_time) * 1000 + + # Уведомляем о результате + event = "path_found" if path else "no_path" + self._notify_observers(event) + + # Формируем статистику + return SearchStats( + time_ms=time_ms, + visited_cells=visited_count, + path_length=len(path), + algorithm_name=self.strategy.get_name(), + maze_name=maze_name + ) \ No newline at end of file diff --git a/pogodinda/lab2/src/observer.py b/pogodinda/lab2/src/observer.py new file mode 100644 index 0000000..5b3ecec --- /dev/null +++ b/pogodinda/lab2/src/observer.py @@ -0,0 +1,54 @@ +from abc import ABC, abstractmethod +from typing import List, Optional + +from maze import Maze, Cell + + +class Observer(ABC): + """Интерфейс наблюдателя (Observer pattern).""" + + @abstractmethod + def update(self, event: str): + pass + + +class ConsoleView(Observer): + """ + Консольное представление лабиринта. + """ + + def __init__(self): + self.events: List[str] = [] + + def update(self, event: str): + """Получаем уведомление о событии.""" + self.events.append(event) + print(f"[Observer] Событие: {event}") + + def render(self, maze: Maze, player_position: Cell = None, path: List[Cell] = None): + """ + Отрисовка лабиринта в консоли. + """ + path_set = set(path) if path else set() + + for y in range(maze.height): + row = [] + for x in range(maze.width): + cell = maze.get_cell(x, y) + + if player_position and cell == player_position: + row.append('P') + elif cell in path_set: + row.append('*') + else: + row.append(str(cell)) + + print(''.join(row)) + print() + + def render_stats(self, stats): + """Отрисовка статистики поиска.""" + print(f"Алгоритм: {stats.algorithm_name}") + print(f"Время: {stats.time_ms:.4f} мс") + print(f"Посещено клеток: {stats.visited_cells}") + print(f"Длина пути: {stats.path_length}") \ No newline at end of file diff --git a/pogodinda/lab2/tests/test_observer_command.py b/pogodinda/lab2/tests/test_observer_command.py new file mode 100644 index 0000000..d88636e --- /dev/null +++ b/pogodinda/lab2/tests/test_observer_command.py @@ -0,0 +1,45 @@ +from src.maze_builder import TextFileMazeBuilder +from src.maze_solver import MazeSolver +from src.pathfinding import AStarStrategy +from src.observer import ConsoleView +from src.commands import Player, MoveCommand + +# Загружаем лабиринт +builder = TextFileMazeBuilder() +maze = builder.build_from_file("demo_maze.txt") + +print("=" * 50) +print("ТЕСТ OBSERVER") +print("=" * 50) + +solver = MazeSolver(maze, AStarStrategy()) +console = ConsoleView() +solver.add_observer(console) + +stats = solver.solve("demo_maze") +console.render_stats(stats) + +print(f"\nСобытия: {console.events}") + +print("\n" + "=" * 50) +print("ТЕСТ COMMAND") +print("=" * 50) + +player = Player(maze.start) +print(f"Начальная позиция: {player}") +console.render(maze, player.current_cell) + +cmd1 = MoveCommand(player, maze, 'S') +success = cmd1.execute() +print(f"Движение S: {'успешно' if success else 'не удалось'} → {player}") + +cmd2 = MoveCommand(player, maze, 'S') +success = cmd2.execute() +print(f"Движение S: {'успешно' if success else 'не удалось'} → {player}") + +console.render(maze, player.current_cell) + +print("\nОтмена последнего хода:") +cmd2.undo() +print(f"После undo: {player}") +console.render(maze, player.current_cell) \ No newline at end of file diff --git a/pogodinda/lab2/tests/test_solver.py b/pogodinda/lab2/tests/test_solver.py new file mode 100644 index 0000000..667d498 --- /dev/null +++ b/pogodinda/lab2/tests/test_solver.py @@ -0,0 +1,58 @@ +from src.maze_builder import TextFileMazeBuilder +from src.pathfinding import BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy +from src.maze_solver import MazeSolver + +# Загружаем лабиринт +builder = TextFileMazeBuilder() +maze = builder.build_from_file("demo_maze.txt") + +print("Лабиринт:") +print(maze) +print(f"\nСтарт: ({maze.start.x}, {maze.start.y})") +print(f"Выход: ({maze.exit.x}, {maze.exit.y})") + +# Создаём solver без стратегии +solver = MazeSolver(maze) + +# Тест 1: BFS +print(f"\n{'='*50}") +print("ТЕСТ 1: BFS") +solver.set_strategy(BFSStrategy()) +stats = solver.solve("demo_maze") +print(f"Время: {stats.time_ms:.4f} мс") +print(f"Посещено: {stats.visited_cells}") +print(f"Длина пути: {stats.path_length}") + +# Тест 2: DFS +print(f"\n{'='*50}") +print("ТЕСТ 2: DFS") +solver.set_strategy(DFSStrategy()) +stats = solver.solve("demo_maze") +print(f"Время: {stats.time_ms:.4f} мс") +print(f"Посещено: {stats.visited_cells}") +print(f"Длина пути: {stats.path_length}") + +# Тест 3: A* +print(f"\n{'='*50}") +print("ТЕСТ 3: A*") +solver.set_strategy(AStarStrategy()) +stats = solver.solve("demo_maze") +print(f"Время: {stats.time_ms:.4f} мс") +print(f"Посещено: {stats.visited_cells}") +print(f"Длина пути: {stats.path_length}") + +# Тест 4: Дейкстра +print(f"\n{'='*50}") +print("ТЕСТ 4: Дейкстра") +solver.set_strategy(DijkstraStrategy()) +stats = solver.solve("demo_maze") +print(f"Время: {stats.time_ms:.4f} мс") +print(f"Посещено: {stats.visited_cells}") +print(f"Длина пути: {stats.path_length}") + +# Тест 5: Динамическая смена алгоритма +print(f"\n{'='*50}") +print("ТЕСТ 5: Смена алгоритма на лету") +print("Было:", solver.strategy.get_name()) +solver.set_strategy(BFSStrategy()) +print("Стало:", solver.strategy.get_name()) \ No newline at end of file