forked from UNN/2026-rff_mp
[2] добавлены MazeSolver, паттерны Observer и Command
This commit is contained in:
parent
0c93c3a3b0
commit
d59cd16706
79
pogodinda/lab2/src/commands.py
Normal file
79
pogodinda/lab2/src/commands.py
Normal 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
|
||||||
86
pogodinda/lab2/src/maze_solver.py
Normal file
86
pogodinda/lab2/src/maze_solver.py
Normal 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
|
||||||
|
)
|
||||||
54
pogodinda/lab2/src/observer.py
Normal file
54
pogodinda/lab2/src/observer.py
Normal 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}")
|
||||||
45
pogodinda/lab2/tests/test_observer_command.py
Normal file
45
pogodinda/lab2/tests/test_observer_command.py
Normal 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)
|
||||||
58
pogodinda/lab2/tests/test_solver.py
Normal file
58
pogodinda/lab2/tests/test_solver.py
Normal 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())
|
||||||
Loading…
Reference in New Issue
Block a user