[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