From 9be996f8cdcc95cc87b2ae541f20a52b2e2eb99a Mon Sep 17 00:00:00 2001 From: konnovaea Date: Fri, 22 May 2026 18:49:41 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BC=D1=83=D1=87=D0=B0=D1=8E=D1=81=D1=8C=20?= =?UTF-8?q?=D1=81=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ => lab1}/docs/data/graph_delete.png | Bin .../{ => lab1}/docs/data/graph_insert.png | Bin .../{ => lab1}/docs/data/graph_search.png | Bin konnovaea/{ => lab1}/docs/data/results.csv | 0 .../{ => lab1}/docs/data/table_results.png | Bin konnovaea/{ => lab1}/docs/отчет.ipynb | 0 konnovaea/{ => lab1}/experiments.py | 2 +- konnovaea/{ => lab1}/make_graphs.py | 0 konnovaea/{ => lab1}/make_tables.py | 0 konnovaea/{ => lab1}/phonebook.py | 0 konnovaea/lab2_report.ipynb | 187 --------- konnovaea/make_lab2_plots.py | 93 ----- konnovaea/maze_experiments.py | 154 -------- konnovaea/maze_solver.py | 367 ------------------ 14 files changed, 1 insertion(+), 802 deletions(-) rename konnovaea/{ => lab1}/docs/data/graph_delete.png (100%) rename konnovaea/{ => lab1}/docs/data/graph_insert.png (100%) rename konnovaea/{ => lab1}/docs/data/graph_search.png (100%) rename konnovaea/{ => lab1}/docs/data/results.csv (100%) rename konnovaea/{ => lab1}/docs/data/table_results.png (100%) rename konnovaea/{ => lab1}/docs/отчет.ipynb (100%) rename konnovaea/{ => lab1}/experiments.py (99%) rename konnovaea/{ => lab1}/make_graphs.py (100%) rename konnovaea/{ => lab1}/make_tables.py (100%) rename konnovaea/{ => lab1}/phonebook.py (100%) delete mode 100644 konnovaea/lab2_report.ipynb delete mode 100644 konnovaea/make_lab2_plots.py delete mode 100644 konnovaea/maze_experiments.py delete mode 100644 konnovaea/maze_solver.py diff --git a/konnovaea/docs/data/graph_delete.png b/konnovaea/lab1/docs/data/graph_delete.png similarity index 100% rename from konnovaea/docs/data/graph_delete.png rename to konnovaea/lab1/docs/data/graph_delete.png diff --git a/konnovaea/docs/data/graph_insert.png b/konnovaea/lab1/docs/data/graph_insert.png similarity index 100% rename from konnovaea/docs/data/graph_insert.png rename to konnovaea/lab1/docs/data/graph_insert.png diff --git a/konnovaea/docs/data/graph_search.png b/konnovaea/lab1/docs/data/graph_search.png similarity index 100% rename from konnovaea/docs/data/graph_search.png rename to konnovaea/lab1/docs/data/graph_search.png diff --git a/konnovaea/docs/data/results.csv b/konnovaea/lab1/docs/data/results.csv similarity index 100% rename from konnovaea/docs/data/results.csv rename to konnovaea/lab1/docs/data/results.csv diff --git a/konnovaea/docs/data/table_results.png b/konnovaea/lab1/docs/data/table_results.png similarity index 100% rename from konnovaea/docs/data/table_results.png rename to konnovaea/lab1/docs/data/table_results.png diff --git a/konnovaea/docs/отчет.ipynb b/konnovaea/lab1/docs/отчет.ipynb similarity index 100% rename from konnovaea/docs/отчет.ipynb rename to konnovaea/lab1/docs/отчет.ipynb diff --git a/konnovaea/experiments.py b/konnovaea/lab1/experiments.py similarity index 99% rename from konnovaea/experiments.py rename to konnovaea/lab1/experiments.py index 349e57b..72e83c0 100644 --- a/konnovaea/experiments.py +++ b/konnovaea/lab1/experiments.py @@ -2,7 +2,7 @@ import random import time import csv import os -from phonebook import * +from lab1.phonebook import * def generate_test_data(n=10000): diff --git a/konnovaea/make_graphs.py b/konnovaea/lab1/make_graphs.py similarity index 100% rename from konnovaea/make_graphs.py rename to konnovaea/lab1/make_graphs.py diff --git a/konnovaea/make_tables.py b/konnovaea/lab1/make_tables.py similarity index 100% rename from konnovaea/make_tables.py rename to konnovaea/lab1/make_tables.py diff --git a/konnovaea/phonebook.py b/konnovaea/lab1/phonebook.py similarity index 100% rename from konnovaea/phonebook.py rename to konnovaea/lab1/phonebook.py diff --git a/konnovaea/lab2_report.ipynb b/konnovaea/lab2_report.ipynb deleted file mode 100644 index 9154f71..0000000 --- a/konnovaea/lab2_report.ipynb +++ /dev/null @@ -1,187 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "bdef001e", - "metadata": {}, - "source": [ - "# Отчёт \n", - "## Поиск выхода из лабиринта: применение паттернов проектирования\n", - "\n", - "**Студент:** Коннова Е.А.\n", - "**Группа:** 429\n", - "**Дата:** 21.05.2026" - ] - }, - { - "cell_type": "markdown", - "id": "21f948a4", - "metadata": {}, - "source": [ - "## Введение\n", - "\n", - "### О чём это работа\n", - "В данной работе реализуется программа для поиска выхода из лабиринта с применением паттернов проектирования. Поддерживаются три алгоритма поиска пути: BFS, DFS и A*.\n", - "\n", - "### Цель работы\n", - "Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. Применить минимум 3 паттерна проектирования.\n", - "\n", - "### Задачи\n", - "1. Реализовать модель лабиринта (классы Cell, Maze)\n", - "2. Реализовать загрузку лабиринта из файла (паттерн Builder)\n", - "3. Реализовать алгоритмы поиска пути (паттерн Strategy): BFS, DFS, A*\n", - "4. Реализовать класс-оркестратор MazeSolver со сбором статистики\n", - "5. Реализовать визуализацию (паттерн Observer) и пошаговое управление (паттерн Command)\n", - "6. Провести эксперименты на лабиринтах разной сложности\n", - "7. Сравнить результаты и сделать выводы\n" - ] - }, - { - "cell_type": "markdown", - "id": "cf1dc2ba", - "metadata": {}, - "source": [ - "## Часть 1. Паттерны проектирования\n", - "\n", - "### Использованные паттерны\n", - "\n", - "| Паттерн | Назначение | Реализация |\n", - "|---------|------------|------------|\n", - "| Builder | Создание лабиринта из файла | TextFileMazeBuilder |\n", - "| Strategy | Семейство алгоритмов поиска | BFSStrategy, DFSStrategy, AStarStrategy |\n", - "| Observer | Уведомление о событиях | ConsoleView |\n", - "| Command | Отмена ходов | MoveCommand |\n" - ] - }, - { - "cell_type": "markdown", - "id": "55cef4b9", - "metadata": {}, - "source": [ - "## Часть 2. Реализация\n", - "\n", - "### 2.1 Модель лабиринта\n", - "\n", - "**Класс Cell** - клетка лабиринта\n", - "- Поля: x, y, is_wall, is_start, is_exit\n", - "- Метод: is_passable() - возвращает True, если не стена\n", - "\n", - "**Класс Maze** - лабиринт\n", - "- Поля: width, height, cells[][], start, exit\n", - "- Методы: get_cell(x, y), get_neighbors(cell)\n", - "\n", - "### 2.2 Загрузка лабиринта (Builder)\n", - "\n", - "**TextFileMazeBuilder**\n", - "- Читает файл с символами (# - стена, пробел - проход, S - старт, E - выход)\n", - "- Создаёт клетки с нужными флагами\n", - "- Возвращает готовый Maze\n", - "\n", - "### 2.3 Алгоритмы поиска (Strategy)\n", - "\n", - "**Интерфейс PathFindingStrategy**\n", - "- Метод: find_path(maze, start, exit) возвращает (путь, количество_посещённых)\n", - "\n", - "**BFSStrategy** - поиск в ширину (очередь)\n", - "- Гарантирует кратчайший путь\n", - "\n", - "**DFSStrategy** - поиск в глубину (стек)\n", - "- Быстрый, но не гарантирует кратчайший путь\n", - "\n", - "**AStarStrategy** - A* (приоритетная очередь)\n", - "- Использует эвристику (манхэттенское расстояние)\n", - "\n", - "### 2.4 Оркестратор\n", - "\n", - "**MazeSolver**\n", - "- Поля: maze, strategy\n", - "- Методы: set_strategy(), solve() → SearchStats\n", - "\n", - "**SearchStats**\n", - "- Поля: path, time_ms, visited_count, path_length" - ] - }, - { - "cell_type": "markdown", - "id": "5c9bd0d2", - "metadata": {}, - "source": [ - "## Часть 3. Эксперименты\n", - "\n", - "### 3.1 Условия\n", - "\n", - "| Параметр | Значение |\n", - "|----------|----------|\n", - "| Повторений | 5 |\n", - "| Алгоритмы | BFS, DFS, A* |\n", - "| Лабиринты | Простой (10x10), С тупиками (50x50), Пустой (100x100), Без выхода |\n", - "\n", - "### 3.2 Результаты\n", - "\n", - "| Лабиринт | Стратегия | Время (мс) | Посещено | Длина пути |\n", - "|----------|-----------|------------|----------|------------|\n", - "| Простой | BFS | 0.037 | 11 | 6 |\n", - "| Простой | DFS | 0.016 | 9 | 8 |\n", - "| Простой | A* | 0.027 | 9 | 6 |\n", - "\n", - "### 3.3 Графики\n", - "\n", - "![Время](data/maze_time_graph.png)\n", - "![Посещения](data/maze_visited_graph.png)\n", - "![Длина пути](data/maze_path_graph.png)\n", - "\n", - "### 3.4 Анализ\n", - "\n", - "| Алгоритм | Кратчайший путь | Скорость | Память |\n", - "|----------|-----------------|----------|--------|\n", - "| BFS | Да | Средняя | Много |\n", - "| DFS | Нет | Быстрая | Мало |\n", - "| A* | Да | Быстрая | Средне |\n", - "\n", - "**Выводы:**\n", - "- BFS и A* нашли кратчайший путь (6 шагов)\n", - "- DFS нашёл более длинный путь (8 шагов), но быстрее всех\n", - "- A* - лучший компромисс между скоростью и оптимальностью\n" - ] - }, - { - "cell_type": "markdown", - "id": "1036c160", - "metadata": {}, - "source": [ - "## Заключение\n", - "\n", - "### Рекомендации по выбору алгоритма\n", - "\n", - "| Сценарий | Алгоритм | Причина |\n", - "|----------|----------|---------|\n", - "| Нужен кратчайший путь | BFS | Гарантирует оптимальность |\n", - "| Важна скорость | DFS | Самый быстрый |\n", - "| Большой лабиринт | A* | Эвристика ускоряет поиск |\n", - "\n", - "### Как паттерны помогли\n", - "\n", - "| Изменение | Без паттернов | С паттернами |\n", - "|-----------|---------------|--------------|\n", - "| Добавить JSON лабиринт | Изменить весь код | Создать JSONBuilder |\n", - "| Добавить алгоритм | Изменить MazeSolver | Создать новую стратегию |\n", - "| Сменить визуализацию | Переписать MazeSolver | Добавить новый Observer |\n", - "\n", - "**Итог:** Паттерны сделали код гибким, расширяемым и тестируемым." - ] - }, - { - "cell_type": "markdown", - "id": "cb24b904", - "metadata": {}, - "source": [] - } - ], - "metadata": { - "language_info": { - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/konnovaea/make_lab2_plots.py b/konnovaea/make_lab2_plots.py deleted file mode 100644 index 08a2574..0000000 --- a/konnovaea/make_lab2_plots.py +++ /dev/null @@ -1,93 +0,0 @@ -import matplotlib.pyplot as plt -import os - -os.makedirs('laba2/docs/data', exist_ok=True) - - -table_data = [ - ['Лабиринт', 'Стратегия', 'Время (мс)', 'Посещено', 'Длина пути'], - ['Простой (10x10)', 'BFS', '0.037', '11', '6'], - ['Простой (10x10)', 'DFS', '0.016', '9', '8'], - ['Простой (10x10)', 'A*', '0.027', '9', '6'], - ['С тупиками (50x50)', 'BFS', '-', '-', '-'], - ['С тупиками (50x50)', 'DFS', '-', '-', '-'], - ['С тупиками (50x50)', 'A*', '-', '-', '-'], - ['Пустой (100x100)', 'BFS', '-', '-', '-'], - ['Пустой (100x100)', 'DFS', '-', '-', '-'], - ['Пустой (100x100)', 'A*', '-', '-', '-'], - ['Без выхода (20x20)', 'BFS', '-', '-', 'нет пути'], - ['Без выхода (20x20)', 'DFS', '-', '-', 'нет пути'], - ['Без выхода (20x20)', 'A*', '-', '-', 'нет пути'], -] - - -fig, ax = plt.subplots(figsize=(12, 5)) -ax.axis('off') - -table = ax.table(cellText=table_data, loc='center', cellLoc='center', colWidths=[0.2, 0.13, 0.13, 0.13, 0.13]) - -table.auto_set_font_size(False) -table.set_fontsize(10) -table.scale(1, 1.8) - - -for i in range(5): - table[(0, i)].set_facecolor('#4472C4') - table[(0, i)].set_text_props(weight='bold', color='white') - - -for i in range(1, len(table_data)): - if i % 2 == 1: - for j in range(5): - table[(i, j)].set_facecolor('#E8F0FE') - else: - for j in range(5): - table[(i, j)].set_facecolor('#FFFFFF') - -plt.title('Результаты экспериментов по поиску пути в лабиринте', fontsize=14, fontweight='bold', pad=30) -plt.savefig('laba2/docs/data/maze_table_results.png', dpi=200, bbox_inches='tight', facecolor='white') -plt.close() - - - -fig, ax = plt.subplots(figsize=(8, 5)) -algorithms = ['BFS', 'DFS', 'A*'] -time_data = [0.037, 0.016, 0.027] -bars = ax.bar(algorithms, time_data, color=['#3498db', '#e74c3c', '#2ecc71']) -ax.set_ylabel('Время (мс)') -ax.set_title('Время выполнения алгоритмов (простой лабиринт 10x10)') -for bar, val in zip(bars, time_data): - ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.001, f'{val:.3f}', ha='center', va='bottom') -plt.savefig('laba2/docs/data/maze_time_graph.png', dpi=150, bbox_inches='tight') -plt.close() - - - -fig, ax = plt.subplots(figsize=(8, 5)) -visited_data = [11, 9, 9] -bars = ax.bar(algorithms, visited_data, color=['#3498db', '#e74c3c', '#2ecc71']) -ax.set_ylabel('Количество клеток') -ax.set_title('Посещённые клетки при поиске') -for bar, val in zip(bars, visited_data): - ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3, str(val), ha='center', va='bottom') -plt.savefig('laba2/docs/data/maze_visited_graph.png', dpi=150, bbox_inches='tight') -plt.close() - - - -fig, ax = plt.subplots(figsize=(8, 5)) -path_data = [6, 8, 6] -bars = ax.bar(algorithms, path_data, color=['#3498db', '#e74c3c', '#2ecc71']) -ax.set_ylabel('Длина пути (шагов)') -ax.set_title('Длина найденного пути') -for bar, val in zip(bars, path_data): - ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3, str(val), ha='center', va='bottom') -plt.savefig('laba2/docs/data/maze_path_graph.png', dpi=150, bbox_inches='tight') -plt.close() - - - - - - - diff --git a/konnovaea/maze_experiments.py b/konnovaea/maze_experiments.py deleted file mode 100644 index d31a268..0000000 --- a/konnovaea/maze_experiments.py +++ /dev/null @@ -1,154 +0,0 @@ -import time -import csv -import os -from maze_solver import TextFileMazeBuilder, BFSStrategy, DFSStrategy, AStarStrategy, MazeSolver - -def save_maze_to_file(maze, filename): - with open(filename, 'w') as f: - for row in maze: - f.write(''.join(row) + '\n') - -def run_test(maze_file, strategy_class): - builder = TextFileMazeBuilder() - maze = builder.build_from_file(maze_file) - solver = MazeSolver(maze, strategy_class) - - times = [] - visited = [] - path_len = [] - - for i in range(5): - stats = solver.solve() - times.append(stats.time_ms) - visited.append(stats.visited_count) - path_len.append(stats.path_length) - - return { - 'time': sum(times) / 5, - 'visited': sum(visited) / 5, - 'path': sum(path_len) / 5, - 'path_found': max(path_len) > 0 - } - -def main(): - - print("Эксперименты по поиску пути в лабиринте") - - - results = [] - - - print("\n1. Простой лабиринт (10x10)") - - - - simple = [ - "#######", - "#S #", - "# ### #", - "# E #", - "#######" - ] - with open('simple.txt', 'w') as f: - for line in simple: - f.write(line + '\n') - - for name, strategy in [('BFS', BFSStrategy()), ('DFS', DFSStrategy()), ('A*', AStarStrategy())]: - res = run_test('simple.txt', strategy) - print(f"{name}: время={res['time']:.3f}мс, посещено={res['visited']:.0f}, путь={res['path']:.0f}") - results.append(['Простой', name, round(res['time'], 3), round(res['visited'], 0), round(res['path'], 0)]) - - - print("\n2. Лабиринт с тупиками (20x20)") - - dead = [] - for y in range(20): - row = [] - for x in range(20): - if x == 0 or y == 0 or x == 19 or y == 19: - row.append('#') - elif (x == 5 and y > 5 and y < 15) or (y == 5 and x > 5 and x < 15): - row.append('#') - else: - row.append(' ') - dead.append(row) - dead[1][1] = 'S' - dead[18][18] = 'E' - - with open('dead.txt', 'w') as f: - for row in dead: - f.write(''.join(row) + '\n') - - for name, strategy in [('BFS', BFSStrategy()), ('DFS', DFSStrategy()), ('A*', AStarStrategy())]: - res = run_test('dead.txt', strategy) - print(f"{name}: время={res['time']:.3f}мс, посещено={res['visited']:.0f}, путь={res['path']:.0f}") - results.append(['С тупиками', name, round(res['time'], 3), round(res['visited'], 0), round(res['path'], 0)]) - - - print("\n3. Пустой лабиринт (50x50)") - - - empty = [] - for y in range(50): - row = [] - for x in range(50): - if x == 0 or y == 0 or x == 49 or y == 49: - row.append('#') - else: - row.append(' ') - empty.append(row) - empty[1][1] = 'S' - empty[48][48] = 'E' - - with open('empty.txt', 'w') as f: - for row in empty: - f.write(''.join(row) + '\n') - - for name, strategy in [('BFS', BFSStrategy()), ('DFS', DFSStrategy()), ('A*', AStarStrategy())]: - res = run_test('empty.txt', strategy) - print(f"{name}: время={res['time']:.3f}мс, посещено={res['visited']:.0f}, путь={res['path']:.0f}") - results.append(['Пустой', name, round(res['time'], 3), round(res['visited'], 0), round(res['path'], 0)]) - - - print("\n4. Лабиринт без выхода (10x10)") - - - noexit = [] - for y in range(10): - row = [] - for x in range(10): - if x == 0 or y == 0 or x == 9 or y == 9: - row.append('#') - else: - row.append('#') - noexit.append(row) - noexit[1][1] = 'S' - noexit[8][8] = 'E' - - with open('noexit.txt', 'w') as f: - for row in noexit: - f.write(''.join(row) + '\n') - - for name, strategy in [('BFS', BFSStrategy()), ('DFS', DFSStrategy()), ('A*', AStarStrategy())]: - try: - res = run_test('noexit.txt', strategy) - if res['path_found']: - print(f"{name}: путь найден! длина={res['path']:.0f}") - results.append(['Без выхода', name, round(res['time'], 3), round(res['visited'], 0), round(res['path'], 0)]) - else: - print(f"{name}: путь не найден (корректно)") - results.append(['Без выхода', name, round(res['time'], 3), round(res['visited'], 0), 'нет пути']) - except Exception as e: - print(f"{name}: ошибка - {e}") - results.append(['Без выхода', name, 0, 0, 'ошибка']) - - - os.makedirs('docs/data', exist_ok=True) - with open('docs/data/maze_experiments.csv', 'w', newline='', encoding='utf-8') as f: - writer = csv.writer(f) - writer.writerow(['Лабиринт', 'Стратегия', 'Время(мс)', 'Посещено клеток', 'Длина пути']) - writer.writerows(results) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/konnovaea/maze_solver.py b/konnovaea/maze_solver.py deleted file mode 100644 index c22b789..0000000 --- a/konnovaea/maze_solver.py +++ /dev/null @@ -1,367 +0,0 @@ -from abc import ABC, abstractmethod -from collections import deque -import heapq -import time -import os - -class Cell: - def __init__(self, x, y): - self.x = x - self.y = y - self.is_wall = False - self.is_start = False - self.is_exit = False - - def is_passable(self): - return not self.is_wall - - def __repr__(self): - return f"Cell({self.x},{self.y})" - - -class Maze: - def __init__(self, width, height): - self.width = width - self.height = height - self.cells = [] - self.start = None - self.exit = None - - for y in range(height): - row = [] - for x in range(width): - row.append(Cell(x, y)) - self.cells.append(row) - - def get_cell(self, x, y): - if 0 <= x < self.width and 0 <= y < self.height: - return self.cells[y][x] - return None - - def get_neighbors(self, cell): - neighbors = [] - for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]: - nx, ny = cell.x + dx, cell.y + dy - neighbor = self.get_cell(nx, ny) - if neighbor and neighbor.is_passable(): - neighbors.append(neighbor) - return neighbors - - -class TextFileMazeBuilder: - def build_from_file(self, filename): - with open(filename, 'r') as f: - lines = [line.rstrip() for line in f.readlines()] - - height = len(lines) - width = len(lines[0]) - maze = Maze(width, height) - - for y, line in enumerate(lines): - for x, ch in enumerate(line): - cell = maze.get_cell(x, y) - if ch == '#': - cell.is_wall = True - elif ch == 'S': - maze.start = cell - cell.is_start = True - elif ch == 'E': - maze.exit = cell - cell.is_exit = True - - return maze - - -class PathFindingStrategy(ABC): - @abstractmethod - def find_path(self, maze, start, exit): - pass - - -class BFSStrategy(PathFindingStrategy): - def find_path(self, maze, start, exit): - if not start or not exit: - return [], 0 - - queue = deque([(start, [start])]) - visited = {start} - - while queue: - current, path = queue.popleft() - if current == exit: - return path, len(visited) - - for neighbor in maze.get_neighbors(current): - if neighbor not in visited: - visited.add(neighbor) - queue.append((neighbor, path + [neighbor])) - - return [], len(visited) - - -class DFSStrategy(PathFindingStrategy): - def find_path(self, maze, start, exit): - if not start or not exit: - return [], 0 - - stack = [(start, [start])] - visited = {start} - - while stack: - current, path = stack.pop() - if current == exit: - return path, len(visited) - - for neighbor in maze.get_neighbors(current): - if neighbor not in visited: - visited.add(neighbor) - stack.append((neighbor, path + [neighbor])) - - return [], len(visited) - - -class AStarStrategy(PathFindingStrategy): - def _heuristic(self, a, b): - return abs(a.x - b.x) + abs(a.y - b.y) - - def find_path(self, maze, start, exit): - if not start or not exit: - return [], 0 - - heap = [(self._heuristic(start, exit), 0, start, [start])] - g_score = {start: 0} - visited = set() - counter = 1 - - while heap: - _, _, current, path = heapq.heappop(heap) - - if current in visited: - continue - - visited.add(current) - - if current == exit: - return path, len(visited) - - for neighbor in maze.get_neighbors(current): - tentative_g = g_score[current] + 1 - if neighbor not in g_score or tentative_g < g_score[neighbor]: - g_score[neighbor] = tentative_g - f = tentative_g + self._heuristic(neighbor, exit) - heapq.heappush(heap, (f, counter, neighbor, path + [neighbor])) - counter += 1 - - return [], len(visited) - - -class SearchStats: - def __init__(self, path, time_ms, visited_count): - self.path = path - self.time_ms = time_ms - self.visited_count = visited_count - self.path_length = len(path) if path else 0 - - -class MazeSolver: - def __init__(self, maze, strategy=None): - self.maze = maze - self.strategy = strategy - self.observers = [] - - def attach(self, observer): - self.observers.append(observer) - - def detach(self, observer): - self.observers.remove(observer) - - def notify(self, event, data=None): - for observer in self.observers: - observer.update(event, data) - - def set_strategy(self, strategy): - self.strategy = strategy - - def solve(self): - if self.strategy is None: - raise ValueError("Стратегия не установлена") - self.notify("search_started") - - start_time = time.perf_counter() - 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 - - self.notify("search_finished", time_ms) - self.notify("path_found", path) - - return SearchStats(path, time_ms, visited_count) - -class Observer(ABC): - - @abstractmethod - def update(self, event, data=None): - pass - -class ConsoleView(Observer): - - def __init__(self): - self.events = [] - - def update(self, event, data=None): - self.events.append((event, data)) - - if event == "maze_loaded": - print("[Событие] Лфбирин загружен") - elif event == "path_found": - print(f"[Событие] Путь найден! Длина: {len(data) if data else 0}") - elif event == "search_started": - print(f"[Событие] Поиск завершён. Время: {data:.3f}мс" if data else "[Событие] Поиск завершён") - elif event == "mpve": - print(f"[Событие] Игрок переместился в {data}") - elif event == "undo": - print("[Событие] Отмена последнего хода") - - def render(self,maze, player=None, path=None): - - os.system('cls' if os.name == 'nt' else 'clear') - - print("Лабиринт") - - for y in range(maze.height): - row = "" - for x in range(maze.width): - cell = maze.get_cell(x,y) - - if player and cell == player.current_cell: - row += "p " #игрок - elif path and cell in path: - row += "* " #путь - elif cell.is_wall: - row += "# " #стена - elif cell.is_start: - row += "S " #старт - elif cell.is_exit: - row += "E " #выход - else: - row += ". " #прозод - print(row) - - print("Управление: W/A/S/D - движение, U - отмена, Q - выход") - -class Command(ABC): - - @abstractmethod - def execute(self): - pass - - @abstractmethod - def undo(self): - pass - -class Player: - - def __init__(self, start_cell): - self.current_cell = start_cell - self.start_cell = start_cell - - def move_to(self, cell): - self.current_cell = cell - - def resent(self): - self.current_cell = self.start_cell - - def __repr__(self): - return f"Player at ({self.current_cell.x}, {self.current_cell.y})" - -class MoveCommand(Command): - - def __init__(self, player, new_cell, view): - self.player = player - self.new_cell = new_cell - self.old_cell = player.current_cell - self.view = view - - def execute(self): - self.player.move_to(self.new_cell) - self.view.update("undo", None) - - def undo(self): - self.player.move_to(self.old_cell) - self.view.update("undo",None) - -class GameController: - - def __init__(self, maze, view): - self.maze = maze - self.view = view - self.player = Player(maze.start) - self.command_history = [] - - def get_cell_in_direction(self, direction): - - x, y = self.player.current_cell.x, self.player.current_cell.y - - if direction == 'w': - y -= 1 - elif direction == 's': - y += 1 - elif direction == 'a': - x -= 1 - elif direction == 'd': - x += 1 - else: - return None - - return self.maze.get_cell(x, y) - - def try_move(self, direction): - - new_cell = self.get_cell_in_direction(direction) - - if new_cell and new_cell.is_passable(): - command = MoveCommand(self.player, new_cell, self.view) - command.execute() - self.command_history.append(command) - - if new_cell.is_exit: - self.view.update("path_found", []) - print("Вы нашли выход.") - return True - else: - print("Невозможно пройти - стена") - return False - - def undo(self): - - if self.command_history: - command = self.command_history.pop() - command.undo() - else: - print("Нечего отменять") - - def visualize_path(self, path): - self.view.render(self.maze, self.player, path) - - def run_manual_mode(self): - - while True: - self.view.render(self.maze, self.player) - - command = input("Введите команду: ").lower().strip() - - if command in ['w', 'a', 's', 'd']: - self.try_move(command) - elif command == 'u': - self.undo() - elif command == 'q': - print('Выход из игры') - break - else: - print("Неизвестная команда. Используйте: W/A/S/D - движение, U - отмена, Q - выход") - - - - - -