мучаюсь с файлами
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
|
@ -2,7 +2,7 @@ import random
|
||||||
import time
|
import time
|
||||||
import csv
|
import csv
|
||||||
import os
|
import os
|
||||||
from phonebook import *
|
from lab1.phonebook import *
|
||||||
|
|
||||||
def generate_test_data(n=10000):
|
def generate_test_data(n=10000):
|
||||||
|
|
||||||
|
|
@ -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",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\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
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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 - выход")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||