[2] добавлены MazeSolver, паттерны Observer и Command

This commit is contained in:
pogodinda 2026-05-24 01:55:48 +03:00
parent 0c93c3a3b0
commit d59cd16706
5 changed files with 322 additions and 0 deletions

View File

@ -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

View File

@ -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
)

View File

@ -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}")

View File

@ -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)

View File

@ -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())