# Этап 5. Визуализация и пошаговое управление

В пятом этапе реализованы два паттерна: **Observer** для отображения событий и **Command** для пошагового управления игроком.

## 5.1. Паттерн Observer

### Идея

`MazeSolver` и игровой цикл не знают как именно отображать происходящее — они просто генерируют события. Наблюдатели подписываются на эти события и реагируют по своему усмотрению. Это позволяет в будущем добавить, например, `FileLogger` или графический интерфейс без изменения основного кода.

### Класс `Event`

Оформлен через `@dataclass`. Хранит тип события строкой и словарь `payload` с дополнительными данными. Поддерживаются четыре типа событий:

| Тип           | Когда генерируется         |
|---------------|----------------------------|
| `maze_loaded` | Лабиринт загружен из файла |
| `path_found`  | Алгоритм нашёл путь        |
| `no_path`     | Путь не найден             |
| `move`        | Игрок сделал ход           |

### Класс `Observer`

Абстрактный базовый класс с единственным методом `update(event)`. Любой наблюдатель обязан его реализовать.

### Класс `ConsoleView`

Конкретная реализация наблюдателя. Обрабатывает события через `match/case` и вызывает `render()` для перерисовки лабиринта.

Метод `render()` принимает лабиринт, опциональную позицию игрока и опциональный путь. Путь преобразуется в `set` для быстрой проверки принадлежности клетки — это O(1) вместо O(n) при каждом обходе:

```python
path_set = set(path) if path else set()
```

Лабиринт обрамляется рамкой из `+` и `─` для читаемости в консоли. Символы игрока и пути вынесены в константы класса — легко поменять без правки логики:

```python
PLAYER_SYMBOL = "P"
PATH_SYMBOL   = "·"
```

## 5.2. Паттерн Command

### Идея

Каждое перемещение игрока оборачивается в объект `MoveCommand`. Это позволяет сохранить предыдущее состояние и отменить ход — реализация `undo` становится тривиальной.

### Класс `Player`

Простой контейнер для текущей клетки игрока. Намеренно минималистичный — вся логика перемещения и проверок находится в команде, а не в игроке.

### Класс `Command`

Абстрактный интерфейс с двумя методами: `execute()` и `undo()`. `execute()` возвращает `bool` — это отличие от классического варианта паттерна, где команды не возвращают значений. Возврат `False` нужен чтобы не добавлять неуспешный ход в историю.

### Класс `MoveCommand`

Хранит ссылку на игрока, направление и лабиринт. При `execute()` проверяет проходимость целевой клетки, сохраняет текущую в `_prev_cell` и перемещает игрока. При `undo()` восстанавливает `_prev_cell`.

Направления вынесены в словарь `DIRECTIONS` на уровне модуля:

```python
DIRECTIONS = {
    "w": (0, -1),   # вверх
    "s": (0,  1),   # вниз
    "a": (-1, 0),   # влево
    "d": (1,  0),   # вправо
}
```

### Класс `CommandHistory`

Стек выполненных команд. Хранит только успешные ходы — неуспешные (`execute()` вернул `False`) в историю не добавляются. `undo()` снимает последнюю команду со стека и вызывает её `undo()`.

Пример игрового цикла:

```python
cmd = MoveCommand(player, 'd', maze)
if cmd.execute():
    history.push(cmd)   # добавляем только успешный ход

history.undo()          # отмена последнего хода
```
