[2] Добавление этапа 5

This commit is contained in:
SerKin0 2026-05-24 18:00:14 +03:00
parent 86b31473f3
commit 27c2f99467
4 changed files with 334 additions and 0 deletions

View File

@ -0,0 +1,13 @@
from source.view.observer import Observer, ConsoleView, Event
from source.view.command import Player, Command, MoveCommand, CommandHistory, DIRECTIONS
__all__ = [
"Observer",
"ConsoleView",
"Event",
"Player",
"Command",
"MoveCommand",
"CommandHistory",
"DIRECTIONS",
]

View File

@ -0,0 +1,153 @@
from abc import ABC, abstractmethod
from typing import Optional
from source.models.base import Maze, Cell
# ---------------------------------------------------------------------------- #
# Игрок #
# ---------------------------------------------------------------------------- #
class Player:
"""Хранит текущее положение игрока в лабиринте.
Attributes:
cell: Текущая клетка игрока.
"""
def __init__(self, cell: Cell) -> None:
"""Инициализирует игрока на заданной клетке.
Args:
cell: Начальная клетка игрока.
"""
self.cell = cell
def __repr__(self) -> str:
return f"Player(x={self.cell.x}, y={self.cell.y})"
# ---------------------------------------------------------------------------- #
# Интерфейс команды #
# ---------------------------------------------------------------------------- #
class Command(ABC):
"""Интерфейс команды с поддержкой отмены."""
@abstractmethod
def execute(self) -> bool:
"""Выполняет команду.
Returns:
True если команда выполнена успешно, False иначе.
"""
@abstractmethod
def undo(self) -> None:
"""Отменяет команду, восстанавливая предыдущее состояние."""
# ---------------------------------------------------------------------------- #
# Команда перемещения #
# ---------------------------------------------------------------------------- #
DIRECTIONS = {
"w": (0, -1),
"s": (0, 1),
"a": (-1, 0),
"d": (1, 0),
}
class MoveCommand(Command):
"""Перемещает игрока в заданном направлении.
Сохраняет предыдущую клетку для возможности отмены хода.
"""
def __init__(self, player: Player, direction: str, maze: Maze) -> None:
"""Инициализирует команду перемещения.
Args:
player: Объект игрока.
direction: Направление ('w', 'a', 's', 'd').
maze: Объект лабиринта для проверки проходимости.
Raises:
ValueError: Если направление не распознано.
"""
if direction not in DIRECTIONS:
raise ValueError(f"Неизвестное направление '{direction}'. Используй: w/a/s/d")
self._player = player
self._direction = direction
self._maze = maze
self._prev_cell: Optional[Cell] = None
def execute(self) -> bool:
"""Перемещает игрока если целевая клетка проходима.
Returns:
True если перемещение выполнено, False если клетка непроходима.
"""
dx, dy = DIRECTIONS[self._direction]
target = self._maze.get_cell(
self._player.cell.x + dx,
self._player.cell.y + dy,
)
if target is None or not target.is_possible():
return False
self._prev_cell = self._player.cell
self._player.cell = target
return True
def undo(self) -> None:
"""Возвращает игрока на предыдущую клетку."""
if self._prev_cell is not None:
self._player.cell = self._prev_cell
# ---------------------------------------------------------------------------- #
# История команд #
# ---------------------------------------------------------------------------- #
class CommandHistory:
"""Хранит историю выполненных команд и позволяет отменять их.
Example:
history = CommandHistory()
cmd = MoveCommand(player, 'w', maze)
if cmd.execute():
history.push(cmd)
history.undo() # отменяет последний успешный ход
"""
def __init__(self) -> None:
self._history: list[Command] = []
def push(self, command: Command) -> None:
"""Добавляет выполненную команду в историю.
Args:
command: Успешно выполненная команда.
"""
self._history.append(command)
def undo(self) -> bool:
"""Отменяет последнюю команду из истории.
Returns:
True если отмена выполнена, False если история пуста.
"""
if not self._history:
print("Нечего отменять.")
return False
self._history.pop().undo()
return True
def clear(self) -> None:
"""Очищает историю команд."""
self._history.clear()

View File

@ -0,0 +1,111 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
from source.models.base import Maze, Cell
from source.settings import cell_mapping
# ---------------------------------------------------------------------------- #
# События #
# ---------------------------------------------------------------------------- #
@dataclass
class Event:
"""Событие, передаваемое наблюдателям.
Attributes:
type: Тип события ('maze_loaded', 'path_found', 'move', 'no_path').
payload: Дополнительные данные события.
"""
type: str
payload: dict = None
# ---------------------------------------------------------------------------- #
# Интерфейс наблюдателя #
# ---------------------------------------------------------------------------- #
class Observer(ABC):
"""Интерфейс наблюдателя за событиями лабиринта."""
@abstractmethod
def update(self, event: Event) -> None:
"""Обрабатывает входящее событие.
Args:
event: Объект события с типом и данными.
"""
# ---------------------------------------------------------------------------
# Консольный наблюдатель
# ---------------------------------------------------------------------------
class ConsoleView(Observer):
"""Отображает состояние лабиринта и события в консоли."""
# Символ игрока на карте
PLAYER_SYMBOL = "P"
PATH_SYMBOL = "·"
def update(self, event: Event) -> None:
"""Реагирует на события и выводит информацию в консоль.
Args:
event: Объект события.
"""
match event.type:
case "maze_loaded":
print("Лабиринт загружен.")
self.render(event.payload["maze"])
case "path_found":
print(f"Путь найден! Длина: {event.payload['length']} шагов.")
self.render(
event.payload["maze"],
path=event.payload["path"],
)
case "no_path":
print("Путь не найден.")
case "move":
print(f"Ход: {event.payload['direction']}")
self.render(
event.payload["maze"],
player=event.payload["player_cell"],
path=event.payload.get("path"),
)
case _:
print(f"[событие] {event.type}")
def render(
self,
maze: Maze,
player: Optional[Cell] = None,
path: Optional[list[Cell]] = None,
) -> None:
"""Рисует лабиринт в консоли.
Путь отмечается символом '·', позиция игрока 'P'.
Args:
maze: Объект лабиринта.
player: Текущая клетка игрока (опционально).
path: Список клеток найденного пути (опционально).
"""
path_set = set(path) if path else set()
rows, cols = maze.shape
print("+" + "" * cols + "+")
for y in range(rows):
row_str = "|"
for x in range(cols):
cell = maze[y, x]
if player and cell is player:
row_str += self.PLAYER_SYMBOL
elif cell in path_set:
row_str += self.PATH_SYMBOL
else:
row_str += str(cell)
row_str += "|"
print(row_str)
print("+" + "" * cols + "+")

View File

@ -0,0 +1,57 @@
from source.build.builder import TextFileBuilder
from source.models.base import Maze
from source.view.observer import ConsoleView, Event
from source.view.command import Player, MoveCommand, CommandHistory
maze: Maze = TextFileBuilder().build_from_file("source/templates/10x10_path_v1.txt")
rows, cols = maze.shape
start = None
for y in range(rows):
for x in range(cols):
if maze[y, x].is_start:
start = maze[y, x]
break
if start is None:
print("Стартовая клетка не найдена!")
exit()
player = Player(start)
history = CommandHistory()
view = ConsoleView()
view.update(Event("maze_loaded", {"maze": maze}))
print("Управление: w/a/s/d — движение, z — отмена, q — выход\n")
while True:
key = input(">>> ").strip().lower()
if key == 'q':
print("Выход.")
break
elif key == 'z':
if history.undo():
print("Ход отменён.")
view.render(maze, player=player.cell)
elif key in ('w', 'a', 's', 'd'):
cmd = MoveCommand(player, key, maze)
if cmd.execute():
history.push(cmd)
view.update(Event("move", {
"maze": maze,
"player_cell": player.cell,
"direction": key,
}))
if player.cell.is_exit:
print("Выход найден! Победа!")
break
else:
print("Туда нельзя — стена или граница.")
else:
print("Неизвестная команда. Используй: w/a/s/d, z, q")