{ "cells": [ { "cell_type": "markdown", "id": "332cd3ba-eb85-47e3-85cc-736843c10214", "metadata": {}, "source": [ "# Полная реализация поиска выхода из лабиринта (ООП + паттерны)" ] }, { "cell_type": "markdown", "id": "3d027c2d-7827-4b2f-8c52-0632b97fb462", "metadata": {}, "source": [ "## Этап 1. Модель лабиринта (без паттернов)\n", "\n", "**Описание:** \n", "Создаются два класса: `Cell` (клетка) и `Maze` (лабиринт). \n", "`Cell` хранит координаты `(x, y)`, флаг `is_wall`, флаги `is_start`, `is_exit`, метод `is_passable()`. \n", "`Maze` содержит двумерный массив клеток, размеры, ссылки на старт и выход. \n", "Метод `get_neighbors(cell)` возвращает список проходимых соседей (вверх, вниз, влево, вправо). \n", "\n", "Этот этап — основа для всех последующих алгоритмов." ] }, { "cell_type": "code", "execution_count": 1, "id": "c35ca325-3402-4c1b-91d4-32f734d6d599", "metadata": {}, "outputs": [], "source": [ "import time\n", "import csv\n", "import heapq\n", "from collections import deque\n", "from abc import ABC, abstractmethod\n", "import matplotlib.pyplot as plt\n", "import pandas as pd\n", "from dataclasses import dataclass\n", "\n", "class Cell:\n", " def __init__(self, x, y, is_wall=False):\n", " self.x = x\n", " self.y = y\n", " self.is_wall = is_wall\n", " self.is_start = False\n", " self.is_exit = False\n", "\n", " def is_passable(self):\n", " return not self.is_wall\n", "\n", "\n", "class Maze:\n", " def __init__(self, width, height):\n", " self.width = width\n", " self.height = height\n", " self.cells = [[Cell(x, y) for x in range(width)] for y in range(height)]\n", " self.start = None\n", " self.exit = None\n", "\n", " def get_cell(self, x, y):\n", " if 0 <= x < self.width and 0 <= y < self.height:\n", " return self.cells[y][x]\n", " return None\n", "\n", " def get_neighbors(self, cell):\n", " neighbors = []\n", " for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:\n", " nx, ny = cell.x + dx, cell.y + dy\n", " nb = self.get_cell(nx, ny)\n", " if nb and nb.is_passable():\n", " neighbors.append(nb)\n", " return neighbors" ] }, { "cell_type": "markdown", "id": "9e10908b-e541-46e4-ad15-99555c9c5de3", "metadata": {}, "source": [ "## Этап 2. Загрузка лабиринта из файла – паттерн Builder\n", "\n", "**Описание:** \n", "Паттерн **Builder** отделяет конструирование сложного объекта (лабиринта) от его представления. \n", "Интерфейс `MazeBuilder` объявляет метод `build_from_file(filename)`. \n", "`TextFileMazeBuilder` реализует загрузку из текстового файла, где:\n", "- `#` – стена\n", "- пробел (или любой другой символ, кроме `#`, `S`, `E`) – проход\n", "- `S` – старт\n", "- `E` – выход\n", "\n", "Процесс: чтение строк, определение размеров, создание клеток, установка флагов. \n", "Builder скрывает детали парсинга и валидации." ] }, { "cell_type": "code", "execution_count": 2, "id": "eab1c38a-aef1-4de7-96b3-24df9b6becb7", "metadata": {}, "outputs": [], "source": [ "class MazeBuilder(ABC):\n", " @abstractmethod\n", " def build_from_file(self, filename):\n", " pass\n", "\n", "\n", "class TextFileMazeBuilder(MazeBuilder):\n", " def build_from_file(self, filename):\n", " with open(filename, 'r', encoding='utf-8') as f:\n", " lines = [line.rstrip('\\n') for line in f.readlines()]\n", " height = len(lines)\n", " width = max(len(line) for line in lines)\n", " maze = Maze(width, height)\n", "\n", " for y, line in enumerate(lines):\n", " for x, ch in enumerate(line):\n", " cell = maze.get_cell(x, y)\n", " if ch == '#':\n", " cell.is_wall = True\n", " elif ch == 'S':\n", " cell.is_start = True\n", " maze.start = cell\n", " elif ch == 'E':\n", " cell.is_exit = True\n", " maze.exit = cell\n", " else:\n", " cell.is_wall = False\n", " return maze" ] }, { "cell_type": "markdown", "id": "791f75f0-ea40-496d-ad38-c827e444e6ae", "metadata": {}, "source": [ "## Этап 3. Стратегии поиска пути – паттерн Strategy\n", "\n", "**Описание:** \n", "Паттерн **Strategy** определяет семейство алгоритмов, инкапсулирует каждый и делает их взаимозаменяемыми. \n", "Интерфейс `PathFindingStrategy` объявляет метод `find_path(maze, start, exit)`, возвращающий `(path, visited_count)`. \n", "\n", "Реализованы три стратегии:\n", "\n", "1. **BFS (поиск в ширину)** \n", " - Использует очередь `deque`. \n", " - Гарантирует нахождение кратчайшего пути по числу шагов. \n", " - Сложность O(V+E). \n", " - Подходит для небольших и средних лабиринтов, где важна оптимальность.\n", "\n", "2. **DFS (поиск в глубину)** \n", " - Использует стек (список). \n", " - Не гарантирует кратчайший путь, но может быть быстрее на определённых конфигурациях. \n", " - Сложность O(V+E). \n", " - Полезен, когда нужно быстро найти любой путь.\n", "\n", "3. **A\\*** (A-star) \n", " - Использует приоритетную очередь (heapq) и эвристику. \n", " - Эвристика – манхэттенское расстояние: \n", " $[\n", " h(n) = |x_n - x_{exit}| + |y_n - y_{exit}|\n", " $] \n", " - Оценка стоимости пути: \\( f(n) = g(n) + h(n) \\), где \\( g(n) \\) – реальная стоимость от старта. \n", " - Гарантирует оптимальность при допустимой эвристике. \n", " - На практике быстрее BFS за счёт целенаправленного поиска." ] }, { "cell_type": "code", "execution_count": 3, "id": "fe37e65c-7f33-458f-9ead-37838b60316a", "metadata": {}, "outputs": [], "source": [ "class PathFindingStrategy(ABC):\n", " @abstractmethod\n", " def find_path(self, maze, start, exit):\n", " pass\n", "\n", "\n", "class BFSStrategy(PathFindingStrategy):\n", " def find_path(self, maze, start, exit):\n", " visited = set()\n", " if start == exit:\n", " return [start], 1\n", " queue = deque([start])\n", " visited.add(start)\n", " parent = {start: None}\n", " while queue:\n", " current = queue.popleft()\n", " for nb in maze.get_neighbors(current):\n", " if nb not in visited:\n", " visited.add(nb)\n", " parent[nb] = current\n", " if nb == exit:\n", " path = []\n", " node = nb\n", " while node is not None:\n", " path.append(node)\n", " node = parent[node]\n", " path.reverse()\n", " return path, len(visited)\n", " queue.append(nb)\n", " return [], len(visited)\n", "\n", "\n", "class DFSStrategy(PathFindingStrategy):\n", " def find_path(self, maze, start, exit):\n", " visited = set()\n", " stack = [(start, [start])]\n", " while stack:\n", " current, path = stack.pop()\n", " if current == exit:\n", " return path, len(visited)\n", " visited.add(current)\n", " for nb in maze.get_neighbors(current):\n", " if nb not in visited:\n", " stack.append((nb, path + [nb]))\n", " return [], len(visited)\n", "\n", "\n", "class AStarStrategy(PathFindingStrategy):\n", " def heuristic(self, cell, exit):\n", " return abs(cell.x - exit.x) + abs(cell.y - exit.y)\n", "\n", " def find_path(self, maze, start, exit):\n", " open_set = []\n", " counter = 0\n", " heapq.heappush(open_set, (0, counter, start))\n", " counter += 1\n", " came_from = {}\n", " g_score = {start: 0}\n", " f_score = {start: self.heuristic(start, exit)}\n", " visited = set()\n", " while open_set:\n", " _, _, current = heapq.heappop(open_set)\n", " visited.add(current)\n", " if current == exit:\n", " path = []\n", " node = current\n", " while node in came_from:\n", " path.append(node)\n", " node = came_from[node]\n", " path.append(start)\n", " path.reverse()\n", " return path, len(visited)\n", " for nb in maze.get_neighbors(current):\n", " tentative_g = g_score[current] + 1\n", " if tentative_g < g_score.get(nb, float('inf')):\n", " came_from[nb] = current\n", " g_score[nb] = tentative_g\n", " f = tentative_g + self.heuristic(nb, exit)\n", " heapq.heappush(open_set, (f, counter, nb))\n", " counter += 1\n", " return [], len(visited)" ] }, { "cell_type": "markdown", "id": "6eeb7cd4-dade-40a0-b607-c08198b602ea", "metadata": {}, "source": [ "## Этап 4. Класс-оркестратор MazeSolver и статистика\n", "\n", "**Описание:** \n", "`MazeSolver` принимает лабиринт и стратегию. \n", "Метод `solve()` замеряет время выполнения (`time.perf_counter()`), вызывает стратегию и возвращает статистику `SearchStats`: \n", "- `time_ms` – время в миллисекундах \n", "- `visited_cells` – количество посещённых клеток \n", "- `path_length` – длина пути \n", "- `algorithm` – имя алгоритма\n", "\n", "Класс также поддерживает паттерн **Observer** (будет добавлен в следующем этапе)." ] }, { "cell_type": "code", "execution_count": 4, "id": "5177c17f-6dd2-42d7-92b0-87471b5c22c1", "metadata": {}, "outputs": [], "source": [ "@dataclass\n", "class SearchStats:\n", " time_ms: float\n", " visited_cells: int\n", " path_length: int\n", " algorithm: str\n", "\n", "\n", "class MazeSolver:\n", " def __init__(self, maze, strategy, observers=None):\n", " self.maze = maze\n", " self.strategy = strategy\n", " self.observers = observers if observers else []\n", "\n", " def attach(self, observer):\n", " self.observers.append(observer)\n", "\n", " def detach(self, observer):\n", " self.observers.remove(observer)\n", "\n", " def notify(self, event_type, data=None):\n", " for obs in self.observers:\n", " obs.update(event_type, data)\n", "\n", " def set_strategy(self, strategy):\n", " self.strategy = strategy\n", "\n", " def solve(self):\n", " if self.maze.start is None or self.maze.exit is None:\n", " raise ValueError(\"Лабиринт не имеет старта или выхода\")\n", " self.notify(\"search_start\")\n", " start_time = time.perf_counter()\n", " path, visited = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)\n", " end_time = time.perf_counter()\n", " if path:\n", " self.notify(\"path_found\", len(path))\n", " else:\n", " self.notify(\"no_path\")\n", " stats = SearchStats(\n", " time_ms=(end_time - start_time) * 1000,\n", " visited_cells=visited,\n", " path_length=len(path),\n", " algorithm=self.strategy.__class__.__name__\n", " )\n", " return path, stats" ] }, { "cell_type": "markdown", "id": "87114e56-ac0f-4227-b081-b7f84ebecaa3", "metadata": {}, "source": [ "## Этап 5. Визуализация и пошаговое управление – паттерны Observer и Command\n", "\n", "**Описание:** \n", "- **Observer** (`ConsoleLogger`) подписывается на события `MazeSolver` и выводит сообщения о начале поиска, нахождении пути или его отсутствии. \n", "- **Command** – интерфейс с методами `execute()` и `undo()`. \n", " `MoveCommand` реализует перемещение игрока на одну клетку и сохраняет предыдущую позицию для отмены. \n", " `Player` хранит текущую клетку. \n", "- Демонстрация: после нахождения пути для `tiny.txt` алгоритм BFS с наблюдателем выводит логи, затем выполняется последовательное перемещение по найденному пути с возможностью отмены последнего шага (undo)." ] }, { "cell_type": "code", "execution_count": 5, "id": "171e638f-f3ce-4278-902f-37375c33a94b", "metadata": {}, "outputs": [], "source": [ "class Observer(ABC):\n", " @abstractmethod\n", " def update(self, event_type, data=None):\n", " pass\n", "\n", "\n", "class ConsoleLogger(Observer):\n", " def update(self, event_type, data=None):\n", " if event_type == \"search_start\":\n", " print(f\"[LOG] Поиск пути начат\")\n", " elif event_type == \"path_found\":\n", " print(f\"[LOG] Путь найден! Длина: {data}\")\n", " elif event_type == \"no_path\":\n", " print(\"[LOG] Путь не найден\")\n", " elif event_type == \"step\":\n", " print(f\"[LOG] Шаг: {data}\")\n", "\n", "\n", "class Command(ABC):\n", " @abstractmethod\n", " def execute(self):\n", " pass\n", "\n", " @abstractmethod\n", " def undo(self):\n", " pass\n", "\n", "\n", "class MoveCommand(Command):\n", " def __init__(self, player, direction, maze):\n", " self.player = player\n", " self.direction = direction\n", " self.maze = maze\n", " self.prev_pos = None\n", "\n", " def execute(self):\n", " self.prev_pos = self.player.current_cell\n", " dx, dy = self.direction\n", " nx, ny = self.player.current_cell.x + dx, self.player.current_cell.y + dy\n", " new_cell = self.maze.get_cell(nx, ny)\n", " if new_cell and new_cell.is_passable():\n", " self.player.current_cell = new_cell\n", " return True\n", " return False\n", "\n", " def undo(self):\n", " if self.prev_pos:\n", " self.player.current_cell = self.prev_pos\n", " return True\n", " return False\n", "\n", "\n", "class Player:\n", " def __init__(self, start_cell):\n", " self.current_cell = start_cell\n", "\n", "\n", "def interactive_move_demo(maze, path):\n", " if not path:\n", " print(\"Путь не найден, демонстрация движения невозможна.\")\n", " return\n", " player = Player(maze.start)\n", " command_history = []\n", " print(\"\\n=== Интерактивное движение по найденному пути ===\")\n", " print(\"Текущая позиция: старт\")\n", " for step, cell in enumerate(path):\n", " if cell == maze.start:\n", " continue\n", " prev = path[step-1]\n", " dx = cell.x - prev.x\n", " dy = cell.y - prev.y\n", " cmd = MoveCommand(player, (dx, dy), maze)\n", " cmd.execute()\n", " command_history.append(cmd)\n", " print(f\"Шаг {step}: перемещение на ({dx},{dy}), позиция ({player.current_cell.x},{player.current_cell.y})\")\n", " if cell == maze.exit:\n", " print(\"Достигнут выход!\")\n", " break\n", " if command_history:\n", " print(\"\\n=== Демонстрация отмены последнего шага (undo) ===\")\n", " cmd = command_history[-1]\n", " cmd.undo()\n", " print(f\"Отменён последний шаг, позиция: ({player.current_cell.x},{player.current_cell.y})\")" ] }, { "cell_type": "markdown", "id": "1d67bc06-60f8-4b1d-9018-0b6cb8a8df74", "metadata": {}, "source": [ "## Этап 6. Экспериментальная часть\n", "\n", "**Описание:** \n", "Подготавливаются 5 лабиринтов разной сложности (файлы `tiny.txt`, `medium.txt`, `large.txt`, `empty.txt`, `no_exit.txt`). \n", "Для каждого лабиринта и каждой стратегии выполняется 5 запусков `solve()`, усредняются: \n", "- время выполнения (мс) \n", "- количество посещённых клеток \n", "- длина найденного пути \n", "\n", "Результаты сохраняются в `all_results.csv`. \n", "Строятся столбчатые диаграммы для каждого лабиринта и общий график сравнения алгоритмов. \n", "\n", "Код также демонстрирует паттерны Observer (логирование) и Command (движение) на лабиринте `tiny.txt`." ] }, { "cell_type": "code", "execution_count": 1, "id": "996a2948-28ca-4f04-8571-b8ce694fe2a4", "metadata": {}, "outputs": [ { "ename": "NameError", "evalue": "name 'BFSStrategy' is not defined", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)", "Cell \u001b[1;32mIn[1], line 32\u001b[0m\n\u001b[0;32m 24\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;18m__name__\u001b[39m \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m__main__\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m 25\u001b[0m maze_files \u001b[38;5;241m=\u001b[39m [\n\u001b[0;32m 26\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtiny.txt\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 27\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmedium.txt\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 30\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mno_exit.txt\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 31\u001b[0m ]\n\u001b[1;32m---> 32\u001b[0m strategies \u001b[38;5;241m=\u001b[39m [BFSStrategy(), DFSStrategy(), AStarStrategy()]\n\u001b[0;32m 33\u001b[0m all_results \u001b[38;5;241m=\u001b[39m []\n\u001b[0;32m 34\u001b[0m logger \u001b[38;5;241m=\u001b[39m ConsoleLogger()\n", "\u001b[1;31mNameError\u001b[0m: name 'BFSStrategy' is not defined" ] } ], "source": [ "def test_single_maze(filename, strategies, repeats=5):\n", " builder = TextFileMazeBuilder()\n", " maze = builder.build_from_file(filename)\n", " results = []\n", " for strategy in strategies:\n", " solver = MazeSolver(maze, strategy)\n", " times = []\n", " visits = []\n", " lengths = []\n", " for _ in range(repeats):\n", " _, stats = solver.solve()\n", " times.append(stats.time_ms)\n", " visits.append(stats.visited_cells)\n", " lengths.append(stats.path_length)\n", " results.append({\n", " 'algorithm': strategy.__class__.__name__,\n", " 'avg_time_ms': sum(times) / repeats,\n", " 'avg_visited': sum(visits) / repeats,\n", " 'avg_path_len': sum(lengths) / repeats\n", " })\n", " return results\n", "\n", "\n", "if __name__ == \"__main__\":\n", " maze_files = [\n", " \"tiny.txt\",\n", " \"medium.txt\",\n", " \"large.txt\",\n", " \"empty.txt\",\n", " \"no_exit.txt\"\n", " ]\n", " strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy()]\n", " all_results = []\n", " logger = ConsoleLogger()\n", "\n", " for maze_file in maze_files:\n", " print(f\"Загрузка лабиринта из {maze_file}...\")\n", " try:\n", " builder = TextFileMazeBuilder()\n", " maze = builder.build_from_file(maze_file)\n", " # Демонстрация Observer и Command для tiny.txt\n", " if maze_file == \"tiny.txt\":\n", " solver_with_observer = MazeSolver(maze, strategies[0], observers=[logger])\n", " path, _ = solver_with_observer.solve()\n", " interactive_move_demo(maze, path)\n", " results = test_single_maze(maze_file, strategies)\n", " for r in results:\n", " r['maze'] = maze_file\n", " all_results.append(r)\n", " print(f\"Результаты для {maze_file}:\")\n", " for r in results:\n", " print(f\" {r['algorithm']}: время = {r['avg_time_ms']:.3f} мс, \"\n", " f\"посещено = {r['avg_visited']:.1f}, длина пути = {r['avg_path_len']:.1f}\")\n", " except Exception as e:\n", " print(f\"Ошибка при обработке {maze_file}: {e}\")\n", "\n", " if all_results:\n", " with open('all_results.csv', 'w', newline='', encoding='utf-8') as f:\n", " writer = csv.DictWriter(f, fieldnames=['maze', 'algorithm', 'avg_time_ms', 'avg_visited', 'avg_path_len'])\n", " writer.writeheader()\n", " writer.writerows(all_results)\n", "\n", " df = pd.DataFrame(all_results)\n", " for maze in df['maze'].unique():\n", " subset = df[df['maze'] == maze]\n", " plt.figure()\n", " plt.bar(subset['algorithm'], subset['avg_time_ms'])\n", " plt.title(f'Сравнение алгоритмов на лабиринте {maze}')\n", " plt.ylabel('Среднее время (мс)')\n", " plt.savefig(f'plot_{maze}.png')\n", " plt.close()\n", "\n", " plt.figure(figsize=(10, 6))\n", " for alg in df['algorithm'].unique():\n", " subset = df[df['algorithm'] == alg]\n", " plt.plot(subset['maze'], subset['avg_time_ms'], marker='o', label=alg)\n", " plt.xlabel('Лабиринт')\n", " plt.ylabel('Среднее время (мс)')\n", " plt.title('Сравнение эффективности алгоритмов на разных лабиринтах')\n", " plt.legend()\n", " plt.grid(True)\n", " plt.savefig('summary_comparison.png')\n", " plt.show()\n", " else:\n", " print(\"Нет данных для построения графиков. Проверьте файлы лабиринтов.\")\n", "\n", " print(\"\\nЭксперимент завершён. Результаты сохранены в all_results.csv и графиках.\")" ] }, { "cell_type": "markdown", "id": "937ab5f6-e884-46f6-8d45-b152ca61e7b7", "metadata": {}, "source": [ "## Заключение\n", "\n", "В работе реализованы:\n", "- Классы `Cell` и `Maze` для моделирования лабиринта.\n", "- Паттерн **Builder** для загрузки лабиринтов из текстовых файлов.\n", "- Паттерн **Strategy** для трёх алгоритмов поиска: BFS, DFS, A*.\n", "- Паттерны **Observer** (логирование) и **Command** (управление с отменой) для визуализации и интерактивности.\n", "- Экспериментальная часть с замером времени, посещённых клеток, длины пути, сохранением результатов в CSV и построением графиков.\n", "\n", "Код полностью соответствует заданию и готов к использованию." ] }, { "cell_type": "code", "execution_count": null, "id": "1ddcb647-eb50-40aa-bc9a-cb76f8a14f23", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": null, "id": "a560e7bf-6b18-4018-9912-ea8da341e8a7", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": null, "id": "25793d80-f546-4270-ae7c-86c4898d6c32", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.5" } }, "nbformat": 4, "nbformat_minor": 5 }