Merge pull request '[2] 2-nd-exersize' (#246) from SavelevMI/2026-rff_mp:2-nd-exersize into develop
Reviewed-on: #246
This commit is contained in:
commit
d81df337aa
123
SavelevMI/docs/data/2-nd-exersize/experiment.py
Normal file
123
SavelevMI/docs/data/2-nd-exersize/experiment.py
Normal 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()
|
||||||
13
SavelevMI/docs/data/2-nd-exersize/experiment_results_2.csv
Normal file
13
SavelevMI/docs/data/2-nd-exersize/experiment_results_2.csv
Normal 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
|
||||||
|
269
SavelevMI/docs/data/2-nd-exersize/main.py
Normal file
269
SavelevMI/docs/data/2-nd-exersize/main.py
Normal 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()
|
||||||
6
SavelevMI/docs/data/2-nd-exersize/maze1.txt
Normal file
6
SavelevMI/docs/data/2-nd-exersize/maze1.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
##########
|
||||||
|
#S #
|
||||||
|
# # #
|
||||||
|
# ## #
|
||||||
|
# #E #
|
||||||
|
##########
|
||||||
10
SavelevMI/docs/data/2-nd-exersize/maze10x10.txt
Normal file
10
SavelevMI/docs/data/2-nd-exersize/maze10x10.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
##########
|
||||||
|
#S #######
|
||||||
|
# #######
|
||||||
|
# ######
|
||||||
|
# ######
|
||||||
|
#E #####
|
||||||
|
##########
|
||||||
|
##########
|
||||||
|
##########
|
||||||
|
##########
|
||||||
20
SavelevMI/docs/data/2-nd-exersize/maze20x20.txt
Normal file
20
SavelevMI/docs/data/2-nd-exersize/maze20x20.txt
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
####################
|
||||||
|
#S ################
|
||||||
|
# ################
|
||||||
|
# #################
|
||||||
|
# ###############
|
||||||
|
# #############
|
||||||
|
## ###########
|
||||||
|
### E # ###########
|
||||||
|
## ############
|
||||||
|
##### ############
|
||||||
|
###### #############
|
||||||
|
###### #############
|
||||||
|
####################
|
||||||
|
####################
|
||||||
|
####################
|
||||||
|
####################
|
||||||
|
####################
|
||||||
|
####################
|
||||||
|
####################
|
||||||
|
####################
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
10
SavelevMI/docs/data/2-nd-exersize/maze_empty.txt
Normal file
10
SavelevMI/docs/data/2-nd-exersize/maze_empty.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
S
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
E
|
||||||
9
SavelevMI/docs/data/2-nd-exersize/maze_no_exit.txt
Normal file
9
SavelevMI/docs/data/2-nd-exersize/maze_no_exit.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
S
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
115
SavelevMI/docs/data/2-nd-exersize/pathfinding.py
Normal file
115
SavelevMI/docs/data/2-nd-exersize/pathfinding.py
Normal 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 []
|
||||||
66
SavelevMI/docs/report-2.md
Normal file
66
SavelevMI/docs/report-2.md
Normal 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 – только при жёсткой нехватке памяти.
|
||||||
Loading…
Reference in New Issue
Block a user