{
 "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
}
