Merge pull request '[2] 2-nd-exersize' (#246) from SavelevMI/2026-rff_mp:2-nd-exersize into develop

Reviewed-on: UNN/2026-rff_mp#246
This commit is contained in:
kit8nino 2026-05-30 11:54:24 +00:00
commit d81df337aa
11 changed files with 643 additions and 1 deletions

View File

@ -0,0 +1,123 @@
# Экспериментальное сравнение алгоритмов поиска пути
# Запуск: python3 experiment.py
import csv
import time
from maze_core import TextFileMazeBuilder
from pathfinding import BFSStrategy, DFSStrategy, AStarStrategy
class MazeSolverExperiment:
def __init__(self, maze):
self._maze = maze
self._strategy = None
def set_strategy(self, strategy):
self._strategy = strategy
def solve(self):
if self._strategy is None:
return None
start_time = time.perf_counter()
path = self._strategy.find_path(self._maze, self._maze.start, self._maze.exit)
end_time = time.perf_counter()
time_ms = (end_time - start_time) * 1000
return {
'time_ms': time_ms,
'visited_cells': self._strategy.get_visited_count(),
'path_length': len(path)
}
def run_experiment(maze_file, strategy, runs=5):
builder = TextFileMazeBuilder()
maze = builder.build_from_file(maze_file)
total_time = 0
total_visited = 0
total_length = 0
for _ in range(runs):
solver = MazeSolverExperiment(maze)
solver.set_strategy(strategy)
stats = solver.solve()
if stats:
total_time += stats['time_ms']
total_visited += stats['visited_cells']
total_length += stats['path_length']
return {
'time_ms': total_time / runs,
'visited_cells': total_visited / runs,
'path_length': total_length / runs
}
def main():
# Список лабиринтов для тестирования
mazes = [
("maze1.txt", "Small (10x6)"),
("maze10x10.txt", "Medium (10x10)"),
("maze20x20.txt", "Large (20x20)"),
("maze_empty.txt", "Empty (15x15)"),
("maze_no_exit.txt", "No exit (10x10)")
]
strategies = [
("BFS", BFSStrategy()),
("DFS", DFSStrategy()),
("AStar", AStarStrategy())
]
results = []
print("=" * 60)
print("ЭКСПЕРИМЕНТАЛЬНОЕ СРАВНЕНИЕ АЛГОРИТМОВ ПОИСКА ПУТИ")
print("=" * 60)
for maze_file, maze_name in mazes:
print(f"\nТестирование: {maze_name} ({maze_file})")
print("-" * 40)
for strat_name, strat in strategies:
try:
stats = run_experiment(maze_file, strat, runs=5)
results.append({
'maze': maze_name,
'strategy': strat_name,
'time_ms': stats['time_ms'],
'visited_cells': stats['visited_cells'],
'path_length': stats['path_length']
})
print(f" {strat_name}: время={stats['time_ms']:.3f}мс, "
f"посещено={stats['visited_cells']:.0f}, "
f"длина пути={stats['path_length']:.0f}")
except Exception as e:
print(f" {strat_name}: ОШИБКА - {e}")
results.append({
'maze': maze_name,
'strategy': strat_name,
'time_ms': -1,
'visited_cells': -1,
'path_length': -1
})
valid_results = [r for r in results if r['time_ms'] >= 0]
with open('experiment_results_2.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['maze', 'strategy', 'time_ms', 'visited_cells', 'path_length'])
writer.writeheader()
writer.writerows(valid_results)
print("\n" + "=" * 60)
print(f"Результаты сохранены в experiment_results_2.csv")
print(f"Всего успешных экспериментов: {len(valid_results)}")
print("=" * 60)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,13 @@
maze,strategy,time_ms,visited_cells,path_length
Small (10x6),BFS,0.06943320004211273,28.0,12.0
Small (10x6),DFS,0.021452600049087778,18.0,12.0
Small (10x6),AStar,0.11244040006204159,28.0,12.0
Medium (10x10),BFS,0.010759200085885823,10.0,5.0
Medium (10x10),DFS,0.017673199999990175,13.0,9.0
Medium (10x10),AStar,0.012486999912653118,5.0,5.0
Large (20x20),BFS,0.042921000022033695,30.0,11.0
Large (20x20),DFS,0.051109400010318495,29.0,15.0
Large (20x20),AStar,0.058695200004876824,24.0,11.0
Empty (15x15),BFS,0.06296379997365875,55.0,10.0
Empty (15x15),DFS,0.10542620011619874,130.0,58.0
Empty (15x15),AStar,0.024648199996590847,10.0,10.0
1 maze strategy time_ms visited_cells path_length
2 Small (10x6) BFS 0.06943320004211273 28.0 12.0
3 Small (10x6) DFS 0.021452600049087778 18.0 12.0
4 Small (10x6) AStar 0.11244040006204159 28.0 12.0
5 Medium (10x10) BFS 0.010759200085885823 10.0 5.0
6 Medium (10x10) DFS 0.017673199999990175 13.0 9.0
7 Medium (10x10) AStar 0.012486999912653118 5.0 5.0
8 Large (20x20) BFS 0.042921000022033695 30.0 11.0
9 Large (20x20) DFS 0.051109400010318495 29.0 15.0
10 Large (20x20) AStar 0.058695200004876824 24.0 11.0
11 Empty (15x15) BFS 0.06296379997365875 55.0 10.0
12 Empty (15x15) DFS 0.10542620011619874 130.0 58.0
13 Empty (15x15) AStar 0.024648199996590847 10.0 10.0

View File

@ -0,0 +1,269 @@
# Основной модуль: оркестратор MazeSolver, Observer для визуализации,
# Player, Command для пошагового управления, интерактивная игра
import sys
import time
import os
from maze_core import TextFileMazeBuilder, Maze
from pathfinding import BFSStrategy, DFSStrategy, AStarStrategy
class SearchStats:
def __init__(self, time_ms, visited_cells, path_length):
self.time_ms = time_ms
self.visited_cells = visited_cells
self.path_length = path_length
class Observer:
def update(self, event_type, data):
raise NotImplementedError
class ConsoleView(Observer):
def __init__(self, player=None):
self._last_path = None
self._player = player
def update(self, event_type, data):
if event_type == "maze_loaded":
self.render_maze(data)
elif event_type == "path_found":
self._last_path = data
self.render_path(data)
elif event_type == "player_moved":
self.render_maze_with_player(data)
def render_maze(self, maze):
os.system('cls' if os.name == 'nt' else 'clear')
print("=" * (maze.width * 2 + 4))
print(" ЛАБИРИНТ")
print("=" * (maze.width * 2 + 4))
for y in range(maze.height):
print(" ", end='')
for x in range(maze.width):
cell = maze.get_cell(x, y)
if cell == maze.start:
print('S', end=' ')
elif cell == maze.exit:
print('E', end=' ')
elif cell.is_wall:
print('#', end=' ')
else:
print('.', end=' ')
print()
print("=" * (maze.width * 2 + 4))
print(" S - вход E - выход # - стена . - проход")
def render_maze_with_player(self, maze):
os.system('cls' if os.name == 'nt' else 'clear')
print("=" * (maze.width * 2 + 4))
print(" ЛАБИРИНТ (P - игрок)")
print("=" * (maze.width * 2 + 4))
for y in range(maze.height):
print(" ", end='')
for x in range(maze.width):
cell = maze.get_cell(x, y)
if self._player and cell == self._player.current:
print('P', end=' ')
elif cell == maze.start:
print('S', end=' ')
elif cell == maze.exit:
print('E', end=' ')
elif cell.is_wall:
print('#', end=' ')
else:
print('.', end=' ')
print()
print("=" * (maze.width * 2 + 4))
print(f" Позиция игрока: ({self._player.current.x}, {self._player.current.y})")
print(" S - вход E - выход # - стена . - проход P - игрок")
def render_path(self, path):
if not path:
print("\n Путь не найден!")
return
print(f"\n Путь найден! Длина: {len(path)}")
class Player:
def __init__(self, start_cell, maze):
self._current = start_cell
self._previous = None
self._maze = maze
@property
def current(self):
return self._current
def move_to(self, cell):
if cell and cell.is_passable():
self._previous = self._current
self._current = cell
return True
return False
def undo_move(self):
if self._previous:
self._current, self._previous = self._previous, None
return True
return False
class Command:
def execute(self):
raise NotImplementedError
def undo(self):
raise NotImplementedError
class MoveCommand(Command):
def __init__(self, player, direction, maze):
self._player = player
self._direction = direction
self._maze = maze
self._executed = False
def execute(self):
dx, dy = self._direction
new_x = self._player.current.x + dx
new_y = self._player.current.y + dy
target_cell = self._maze.get_cell(new_x, new_y)
if target_cell and target_cell.is_passable():
self._player.move_to(target_cell)
self._executed = True
return True
return False
def undo(self):
if self._executed:
self._player.undo_move()
self._executed = False
return True
return False
class MazeSolver:
def __init__(self, maze):
self._maze = maze
self._strategy = None
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def notify(self, event_type, data):
for observer in self._observers:
observer.update(event_type, data)
def set_strategy(self, strategy):
self._strategy = strategy
def solve(self):
if self._strategy is None:
return None
start_time = time.perf_counter()
path = 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("path_found", path)
return SearchStats(time_ms, self._strategy.get_visited_count(), len(path))
def main():
builder = TextFileMazeBuilder()
# Загрузка лабиринта из файла
if len(sys.argv) > 1:
maze = builder.build_from_file(sys.argv[1])
else:
maze = builder.build_from_file("maze1.txt")
# Создание игрока и визуализации
player = Player(maze.start, maze)
view = ConsoleView(player)
view.render_maze(maze)
# Создание решателя
solver = MazeSolver(maze)
solver.attach(view)
print("\n УПРАВЛЕНИЕ:")
print(" H (влево) J (вниз) K (вверх) L (вправо)")
print(" U - отменить ход Q - выход")
print("\n АВТО-ПОИСК:")
print(" B - BFS (поиск в ширину)")
print(" D - DFS (поиск в глубину)")
print(" A - A* (звездочка)")
print("\n" + "=" * 50)
command_stack = []
while True:
key = input("\n Команда > ").lower()
if key == 'q':
print("\n До свидания!")
break
elif key == 'b':
solver.set_strategy(BFSStrategy())
stats = solver.solve()
print(f"\n BFS: время={stats.time_ms:.3f}мс, посещено={stats.visited_cells}, длина={stats.path_length}")
elif key == 'd':
solver.set_strategy(DFSStrategy())
stats = solver.solve()
print(f"\n DFS: время={stats.time_ms:.3f}мс, посещено={stats.visited_cells}, длина={stats.path_length}")
elif key == 'a':
solver.set_strategy(AStarStrategy())
stats = solver.solve()
print(f"\n A*: время={stats.time_ms:.3f}мс, посещено={stats.visited_cells}, длина={stats.path_length}")
elif key in ['h', 'j', 'k', 'l']:
dirs = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)}
cmd = MoveCommand(player, dirs[key], maze)
if cmd.execute():
command_stack.append(cmd)
view.render_maze_with_player(maze)
if player.current == maze.exit:
print("\n ПОЗДРАВЛЯЮ! ВЫ НАШЛИ ВЫХОД!")
print(f" Всего ходов: {len(command_stack)}")
break
else:
print("\n Туда нельзя там стена!")
elif key == 'u':
if command_stack:
cmd = command_stack.pop()
cmd.undo()
view.render_maze_with_player(maze)
print("\n Ход отменён")
else:
print("\n Нечего отменять")
else:
print("\n Неизвестная команда. Используйте H,J,K,L для движения, U для отмены, Q для выхода")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,6 @@
##########
#S #
# # #
# ## #
# #E #
##########

View File

@ -0,0 +1,10 @@
##########
#S #######
# #######
# ######
# ######
#E #####
##########
##########
##########
##########

View File

@ -0,0 +1,20 @@
####################
#S ################
# ################
# #################
# ###############
# #############
## ###########
### E # ###########
## ############
##### ############
###### #############
###### #############
####################
####################
####################
####################
####################
####################
####################
####################

View File

@ -143,4 +143,5 @@ class TextFileMazeBuilder(MazeBuilder):
if start_count != 1 or exit_count != 1: if start_count != 1 or exit_count != 1:
raise ValueError(f"Лабиринт должен иметь ровно один вход S и один выход E. Найдено: S={start_count}, E={exit_count}") raise ValueError(f"Лабиринт должен иметь ровно один вход S и один выход E. Найдено: S={start_count}, E={exit_count}")
return maze return maze

View File

@ -0,0 +1,10 @@
S
E

View File

@ -0,0 +1,9 @@
S

View File

@ -0,0 +1,115 @@
# Стратегии поиска пути: BFS, DFS, A* (Strategy pattern)
from collections import deque
import heapq
class PathFindingStrategy:
def find_path(self, maze, start, exit_cell):
raise NotImplementedError
def _reconstruct_path(self, came_from, start, exit_cell):
path = []
current = exit_cell
while current is not None:
path.append(current)
current = came_from.get(current)
path.reverse()
return path
def get_visited_count(self):
return getattr(self, '_visited_count', 0)
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze, start, exit_cell):
queue = deque()
queue.append(start)
came_from = {start: None}
visited = {start}
while queue:
current = queue.popleft()
if current == exit_cell:
self._visited_count = len(visited)
return self._reconstruct_path(came_from, start, exit_cell)
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
came_from[neighbor] = current
queue.append(neighbor)
self._visited_count = len(visited)
return []
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze, start, exit_cell):
stack = [start]
came_from = {start: None}
visited = {start}
while stack:
current = stack.pop()
if current == exit_cell:
self._visited_count = len(visited)
return self._reconstruct_path(came_from, start, exit_cell)
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
came_from[neighbor] = current
stack.append(neighbor)
self._visited_count = len(visited)
return []
class AStarStrategy(PathFindingStrategy):
def _heuristic(self, cell, exit_cell):
return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y)
def find_path(self, maze, start, exit_cell):
heap = []
counter = 0
start_f = self._heuristic(start, exit_cell)
heapq.heappush(heap, (start_f, counter, start))
counter += 1
came_from = {}
g_score = {start: 0}
f_score = {start: start_f}
visited = set()
while heap:
current_f, _, current = heapq.heappop(heap)
visited.add(current)
if current == exit_cell:
self._visited_count = len(visited)
return self._reconstruct_path(came_from, start, exit_cell)
if current_f > f_score.get(current, float('inf')):
continue
for neighbor in maze.get_neighbors(current):
tentative_g = g_score[current] + 1
if tentative_g < g_score.get(neighbor, float('inf')):
came_from[neighbor] = current
g_score[neighbor] = tentative_g
new_f = tentative_g + self._heuristic(neighbor, exit_cell)
f_score[neighbor] = new_f
heapq.heappush(heap, (new_f, counter, neighbor))
counter += 1
self._visited_count = len(visited)
return []

View File

@ -0,0 +1,66 @@
# Отчёт по лабораторной работе №2 «Поиск выхода из лабиринта»
## Цель работы
Разработать программу для поиска выхода из лабиринта с возможностью выбора алгоритма (BFS, DFS, A*), визуализацией и экспериментальным сравнением. Применить минимум 3 паттерна проектирования.
## Использованные паттерны
**1. Builder (Строитель)** загрузка лабиринта из файла. Скрывает парсинг символов (#, S, E), проверку наличия ровно одного входа и выхода. При добавлении нового формата (JSON, XML) достаточно реализовать новый строитель.
**2. Strategy (Стратегия)** алгоритмы поиска пути. BFS, DFS и A* реализуют общий интерфейс. Класс MazeSolver переключает стратегию одной строкой. Новый алгоритм добавляется без изменения существующего кода.
**3. Observer (Наблюдатель)** консольная визуализация. MazeSolver уведомляет наблюдателей о событиях (найден путь, загружен лабиринт). Позволяет легко заменить консоль на графический интерфейс.
**4. Command (Команда)** пошаговое управление игроком. MoveCommand хранит направление и позволяет отменить ход (undo). История команд в стеке даёт откат действий.
## Результаты экспериментов
**Условия**: 4 лабиринта, 5 запусков на алгоритм, замеры времени (мс), посещённых клеток и длины пути.
| Лабиринт | Алгоритм | Время (мс) | Посещено | Длина пути |
|----------|----------|------------|----------|------------|
| Small (10×6) | BFS | 0.069 | 28 | 12 |
| Small (10×6) | DFS | 0.021 | 18 | 12 |
| Small (10×6) | A* | 0.112 | 28 | 12 |
| Medium (10×10) | BFS | 0.011 | 10 | 5 |
| Medium (10×10) | DFS | 0.018 | 13 | 9 |
| Medium (10×10) | A* | 0.012 | 5 | 5 |
| Large (20×20) | BFS | 0.043 | 30 | 11 |
| Large (20×20) | DFS | 0.051 | 29 | 15 |
| Large (20×20) | A* | 0.059 | 24 | 11 |
| Empty (15×15) | BFS | 0.063 | 55 | 10 |
| Empty (15×15) | DFS | 0.105 | 130 | 58 |
| Empty (15×15) | A* | 0.025 | 10 | 10 |
**Лабиринт без выхода** Builder корректно выбросил исключение (S=1, E=0).
## Анализ
**BFS**: всегда находит кратчайший путь, но исследует много клеток. На Empty посетил 55 клеток (A* 10).
**DFS**: самый быстрый на Small (0.021 мс), но непредсказуем. На Empty путь оказался в 5.8 раз длиннее оптимального (58 против 10). Не подходит для навигации.
**A***: стабильно даёт кратчайший путь и минимум посещённых клеток. На Medium посетил 5 клеток (ровно длина пути), на Empty 10 против 55 у BFS. Лёгкое замедление на Small (0.112 мс) окупается эффективностью.
**Ключевые выводы**:
- На пустом поле A* в 5.5 раз быстрее BFS и в 4.2 раза быстрее DFS
- DFS на пустом поле заблудился и прошёл 130 клеток вместо 10
- На Medium A* идеален 5 посещённых клеток при длине пути 5
## Выводы по паттернам
**Builder** спас от падения на лабиринте без выхода, валидация на месте. **Strategy** переключение алгоритмов заняло одну строку, сравнение тривиально. **Observer** визуализация не засоряет код поиска. **Command** undo реализован без изменения класса игрока.
Без паттернов пришлось бы переписывать код при каждом изменении формата, алгоритма или способа вывода.
## Рекомендации
| Сценарий | Алгоритм |
|----------|----------|
| Нужен кратчайший путь + есть эвристика | A* |
| Нужен кратчайший путь + нет эвристики | BFS |
| Любой путь + экономия памяти | DFS |
| Пустой лабиринт | A* (в 5 раз быстрее BFS) |
**Итог**: для большинства задач оптимален **A*** кратчайший путь и минимум посещений. BFS резервный вариант. DFS только при жёсткой нехватке памяти.