forked from UNN/2026-rff_mp
[2] Собинина А. - Задание 2: лабиринт и паттерны GoF
This commit is contained in:
parent
4144c3d390
commit
642874f0c2
69
sobininaas/Задание2/benchmark.py
Normal file
69
sobininaas/Задание2/benchmark.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import os
|
||||
import time
|
||||
import csv
|
||||
from maze_builder import TextMazeBuilder
|
||||
from pathfinding import BFSSearch, DFSSearch, AStarSearch
|
||||
from solver import MazeSolver
|
||||
|
||||
def run_benchmark():
|
||||
|
||||
data_dir = os.path.join(os.path.dirname(__file__), 'data')
|
||||
docs_dir = os.path.join(os.path.dirname(__file__), 'docs(results)')
|
||||
os.makedirs(docs_dir, exist_ok=True)
|
||||
|
||||
mazes = {
|
||||
'small': 'small.txt',
|
||||
'medium': 'medium.txt',
|
||||
'large': 'large.txt'
|
||||
}
|
||||
|
||||
strategies = {
|
||||
'BFS': BFSSearch(),
|
||||
'DFS': DFSSearch(),
|
||||
'A*': AStarSearch()
|
||||
}
|
||||
|
||||
results = []
|
||||
builder = TextMazeBuilder()
|
||||
|
||||
for name, fname in mazes.items():
|
||||
fpath = os.path.join(data_dir, fname)
|
||||
if not os.path.exists(fpath):
|
||||
print(f" {name}: не найден")
|
||||
continue
|
||||
|
||||
maze = builder.load(fpath)
|
||||
print(f"\n{name} ({maze.width}x{maze.height})")
|
||||
|
||||
for sname, strategy in strategies.items():
|
||||
times = []
|
||||
for _ in range(5):
|
||||
solver = MazeSolver(maze)
|
||||
solver.set_strategy(strategy)
|
||||
t0 = time.perf_counter()
|
||||
stats = solver.solve()
|
||||
t1 = time.perf_counter()
|
||||
times.append((t1 - t0) * 1000)
|
||||
|
||||
avg = sum(times) / len(times)
|
||||
print(f" {sname}: {avg:.3f}ms, visited={strategy.visited_count}, path={stats.path_length}")
|
||||
|
||||
results.append({
|
||||
'maze': name,
|
||||
'strategy': sname,
|
||||
'time_ms': avg,
|
||||
'visited': strategy.visited_count,
|
||||
'path_len': stats.path_length
|
||||
})
|
||||
|
||||
# Save CSV
|
||||
csv_path = os.path.join(docs_dir, 'results.csv')
|
||||
with open(csv_path, 'w', newline='', encoding='utf-8-sig') as f:
|
||||
writer = csv.DictWriter(f, fieldnames=['maze', 'strategy', 'time_ms', 'visited', 'path_len'])
|
||||
writer.writeheader()
|
||||
writer.writerows(results)
|
||||
|
||||
print(f"\n Сохранено в {csv_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_benchmark()
|
||||
8
sobininaas/Задание2/data/empty.txt
Normal file
8
sobininaas/Задание2/data/empty.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
S
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
E
|
||||
99
sobininaas/Задание2/data/large.txt
Normal file
99
sobininaas/Задание2/data/large.txt
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
###################################################################################################
|
||||
#S# # # # # # # # # # #
|
||||
# ### ### # ####### ### ######### ### # ### ######### # ### # ### ######### # # # ##### ### # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
### # # # ##### # ### ### ### # ############# ### # ##### # # # ### # # ##### ####### # ####### # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### # ##### # ####### ########### ### # ### # ##### # # # ### ######### # # # ########### ##### #
|
||||
# # # # # # # # # # # # # # # # # # # # # #
|
||||
### ##### ####### # ####### # ####### # ####### # ### # # ### ### ####### # ####### ####### # #####
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # # ### ### # # # # ##### # # # # # # # # # ### ##### ### ##### ### # # # ### # # ### # ##### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # ### ### # ### # ### ### ##### ####### # # # ### # ### ### ##### ######### ### ### # ### #####
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ##### ### # ### ### # ### ### # ### ### ####### # ### # ######### # ### # ##### # # # ####### # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ##### ##### # ### ####### ### # ### ####### # ####################### # # ####### ### # # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### # ### # ### # # # ### ### # ######### # ####### ############### ### # # # # ### # ### ##### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
######### # # # ##### # # ########### # # ####### # ### # ### ### # ### ### # ### # ##### ### # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### ##### ### ### ### # # # ### # ### ### # # ##### # ####### # # # # # ##### ##### ### # # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # # # ### ### # # ### # ### # ####### ### ### ##### # ### ### # # # # # # ### # # # ### # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ### # ######### ### # # # ##### # ##### ####### # # ### ### ##### ### # ##### # ### ####### # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ############# ### ### # # # # ### ##### ### ##### ############### ### # ##### # # # ### ####### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ### # ### ####### ####### # ### # ##### # # # ### # # ############# ##### ######### # ### # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
##### # # # # ##### ### ######### ### # ### ### # ##### # # ##### # ### # # # # ##### # ### ### # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # ##### ### ##### ####### # ######################### # # # ##### ##### ##### # ##### ### # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # #
|
||||
# ##### # # # # # # ####### ##### ### # ##### # ####### # # # ##### ####### # # # ##### ##### # ###
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ######### ### # # # ########### ### # # # ### ######### # ##### # ######### # ### # ### ##### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ####### # ####### # ### # # # # ####### ##### # # ##### # ### # # # ####### ####### ### ### ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # #
|
||||
####### ####### ####### ##### # ##### ### # # # # ####### ####### # ######### # ### ########### # #
|
||||
# # # # # # # # # # # # # # # # # # # # #
|
||||
### # ### # # ##### # # # ### ### ##### # # ##### # ####### ### # ############### ### # ###########
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### # ### ##### ##### # ##### ##### ### # ### ############### ##### # # # ### # # # # # ### ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ### # # # ### # # # ### # ### # ### ##### ### ############# ### ######### ### # ##### # ##### #
|
||||
# # # # # # # # # # # # # # # # # # # # # #
|
||||
### # ######### ################# ### ### # ####### # # ##### # # # ##### # ########### ### # ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### ### ### # # ####### # # ##### ### ### # # # ##### ########### # ##### ####### ##### ### # ###
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
### ####### # # # # # # ### ### # ### # ######### # # ######### ####### # # # ####### ### # ### # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### ### # ### ##### ### ### # ####### # ######### ##### # # ### # # ### # ##### # ######### # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ##### ### # ##### ### ### ##### ####### # # # # ### # # ### ### # ### # ############### # # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # ######### # # ##### # # ##### ### # ##### ####### ### # # # ### # ### ##### # # ##### ##### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ##### # # ##### ### # ### ##### # ### ##### ### # ##### ##### ### # ########### # ### # ####### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # #
|
||||
##### # ##### ######### # ### # ##### ### # # # ####### # # # ####### # ############# ##### # ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### # ### # # # # # # # # ####### ####### ##### ### ### # # # # # ############# # ### # ##### # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ##### # ####### # ####### # # # ##### # # # # ### ### # # ### # # # # # ### # ##### ##### # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # ### # ####### # ### # # ####### # # # # # # ####### ### # ##### # # # ### ##### # # ####### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
############# ######### # # # ##### # # # # ##### # # ### # ### # # # ####### ########### # ##### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ######### ### # # ######### # # ### # # ### ######### # ####### # ### # ##### ####### ##### # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # # ### # ######### ### ##### # ### # ####### # ##### ### ### ############### ### # # ##### # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # #
|
||||
### ##### ### # # ####### ########### # ####### # # # ### # ### ##### # # # # ######### ##### # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### ##### ##### # # ### ####### # ##### # ######### # ####### ### ########### ##### ### # # # ###
|
||||
# # # # # # # # # # # # # # # # # # # # #
|
||||
### # # # ######### ### ########### # ##### ####### # # # ####### ### # # ### ##### ######### ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ######### # # # # ##### ##### ####### # ####### # # # ####### # # ##### # ### ######### # ##### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ### # ##### # ##### ### # # # ##### # # ##### ##### # ######### # # ####### # # ### # #########
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
##### ### ### ######### # # # ### # ##### ##### # # ### ##### # # # # ### # # ##### # # ######### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # # # # ##### # # # # # ##### ########### # # # # ### # ### # ### # # # ######### # # # # #####
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ######### # ### # ##### ### # ### # ##### ##### ##### ### # # ####### ##### ### # # ### # # ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
####### # ### ##### # ### ### # # ####### ### ##### # # ####### # ### # ##### # # ##### ### ### # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### # ### ############### ### ### ### # # ######### ### ### ##### # ### # ### ### ### # # # ### #
|
||||
# # # # # # # # # # # E#
|
||||
###################################################################################################
|
||||
51
sobininaas/Задание2/data/medium.txt
Normal file
51
sobininaas/Задание2/data/medium.txt
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
##################################################
|
||||
#S #
|
||||
# ############################################## #
|
||||
# # # #
|
||||
# # ########################################## # #
|
||||
# # # # # #
|
||||
# # # ###################################### # # #
|
||||
# # # # # # # #
|
||||
# # # # ################################## # # # #
|
||||
# # # # # # # # # #
|
||||
# # # # # ############################## # # # # #
|
||||
# # # # # # # # # # # #
|
||||
# # # # # # ########################## # # # # # #
|
||||
# # # # # # # # # # # # # #
|
||||
# # # # # # # ###################### # # # # # # #
|
||||
# # # # # # # # # # # # # # # #
|
||||
# # # # # # # # ################## # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # #
|
||||
# # # # # # # # # ############## # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # # # #
|
||||
# # # # # # # # # # ########## # # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # # # # # # # # # ###### # # # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # # # # # # # # # # ## # # # # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # # # # # # # # # ###### # # # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # # # # # # # # ########## # # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # # # #
|
||||
# # # # # # # # # ############## # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # # #
|
||||
# # # # # # # # ############## # # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # # #
|
||||
# # # # # # # ############## # # # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # # #
|
||||
# # # # # # ############## # # # # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # # #
|
||||
# # # # # ############## # # # # # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # # #
|
||||
# # # # ############## # # # # # # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # # #
|
||||
# # # ############## # # # # # # # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # # #
|
||||
# # ############## # # # # # # # # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # # # #
|
||||
# ############## # #
|
||||
# # # # # # # # # # # # # # # E#
|
||||
##################################################
|
||||
8
sobininaas/Задание2/data/no_exit.txt
Normal file
8
sobininaas/Задание2/data/no_exit.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#### #
|
||||
S ###
|
||||
# # #
|
||||
### #
|
||||
# #
|
||||
# #####
|
||||
#####E#
|
||||
#######
|
||||
10
sobininaas/Задание2/data/small.txt
Normal file
10
sobininaas/Задание2/data/small.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
##########
|
||||
#S #
|
||||
# ###### #
|
||||
# #
|
||||
# ## #
|
||||
# #
|
||||
# ###### #
|
||||
# #
|
||||
########E#
|
||||
##########
|
||||
BIN
sobininaas/Задание2/docs(results)/grafik.png
Normal file
BIN
sobininaas/Задание2/docs(results)/grafik.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
10
sobininaas/Задание2/docs(results)/results.csv
Normal file
10
sobininaas/Задание2/docs(results)/results.csv
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
maze,strategy,time_ms,visited,path_len
|
||||
small,BFS,0.18852240755222738,43,15
|
||||
small,DFS,0.18770199385471642,43,33
|
||||
small,A*,0.5398263921961188,43,15
|
||||
medium,BFS,2.0823255938012153,224,96
|
||||
medium,DFS,12.020092003513128,1143,100
|
||||
medium,A*,1.5564159955829382,161,96
|
||||
large,BFS,16.372944600880146,4058,2257
|
||||
large,DFS,12.86809000885114,3987,2257
|
||||
large,A*,23.529271798906848,4029,2257
|
||||
|
136
sobininaas/Задание2/main.py
Normal file
136
sobininaas/Задание2/main.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import os
|
||||
from maze_core import Maze, Cell
|
||||
from maze_builder import TextMazeBuilder
|
||||
from pathfinding import BFSSearch, DFSSearch, AStarSearch
|
||||
from solver import MazeSolver
|
||||
from patterns import ConsoleObserver, Player, MoveCommand
|
||||
|
||||
def select_maze_file() -> str:
|
||||
print("\n Доступные лабиринты:")
|
||||
print("1 - small (10×10, демо)")
|
||||
print("2 - medium (50×50, стандарт)")
|
||||
print("3 - large (100×100, сложный)")
|
||||
print("4 - empty (пустой, тест скорости)")
|
||||
print("5 - no_exit (без выхода, проверка ошибок)")
|
||||
|
||||
while True:
|
||||
choice = input("\nВыберите номер (1-5): ").strip()
|
||||
mapping = {
|
||||
'1': 'small.txt', '2': 'medium.txt', '3': 'large.txt',
|
||||
'4': 'empty.txt', '5': 'no_exit.txt'
|
||||
}
|
||||
if choice in mapping:
|
||||
return mapping[choice]
|
||||
print(" Неверный ввод. Введите число от 1 до 5.")
|
||||
|
||||
def draw_maze(maze: Maze, path=None):
|
||||
if maze.width > 30 or maze.height > 30:
|
||||
return False
|
||||
|
||||
path_set = set(path) if path else set()
|
||||
print("\n Карта лабиринта:")
|
||||
for y in range(maze.height):
|
||||
row = []
|
||||
for x in range(maze.width):
|
||||
cell = maze.cell_at(x, y)
|
||||
if cell in path_set:
|
||||
if cell.is_start: row.append('S')
|
||||
elif cell.is_exit: row.append('E')
|
||||
else: row.append('*')
|
||||
else:
|
||||
row.append(str(cell))
|
||||
print(''.join(row))
|
||||
return True
|
||||
|
||||
def main():
|
||||
|
||||
selected_file = select_maze_file()
|
||||
maze_path = os.path.join(os.path.dirname(__file__), 'data', selected_file)
|
||||
|
||||
try:
|
||||
builder = TextMazeBuilder()
|
||||
maze = builder.load(maze_path)
|
||||
print(f"\nЗагружен: {selected_file} ({maze.width}x{maze.height})")
|
||||
|
||||
if not draw_maze(maze):
|
||||
print(" (слишком большой для отрисовки в консоли)")
|
||||
except FileNotFoundError:
|
||||
print(f" Файл {selected_file} не найден в папке data/")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f" Ошибка загрузки: {e}")
|
||||
return
|
||||
|
||||
|
||||
solver = MazeSolver(maze)
|
||||
view = ConsoleObserver()
|
||||
solver.add_observer(view)
|
||||
|
||||
strategies = {
|
||||
"BFS": BFSSearch(),
|
||||
"DFS": DFSSearch(),
|
||||
"A*": AStarSearch()
|
||||
}
|
||||
|
||||
results = []
|
||||
for name, strategy in strategies.items():
|
||||
solver.set_strategy(strategy)
|
||||
print(f"\n🔍 {name}:")
|
||||
stats = solver.solve()
|
||||
|
||||
print(f" Время: {stats.time_ms:.3f} мс")
|
||||
print(f" Клеток посещено: {stats.visited_cells}")
|
||||
print(f" Длина пути: {stats.path_length}")
|
||||
|
||||
if solver.last_path:
|
||||
if not draw_maze(maze, path=solver.last_path):
|
||||
print(" (путь не отрисован из-за размера)")
|
||||
else:
|
||||
print(" Путь не найден!")
|
||||
|
||||
results.append((name, stats))
|
||||
|
||||
print(f"{'Алгоритм':<10} {'Время (мс)':<15} {'Посещено':<12} {'Длина':<8}")
|
||||
|
||||
for name, stats in results:
|
||||
print(f"{name:<10} {stats.time_ms:<15.3f} {stats.visited_cells:<12} {stats.path_length:<8}")
|
||||
|
||||
# 4. Интерактивный режим (только для маленьких)
|
||||
if maze.width <= 30 and maze.height <= 30:
|
||||
if input("\n Запустить интерактивный режим? (y/n): ").lower() == 'y':
|
||||
interactive_mode(maze)
|
||||
else:
|
||||
print("\n Для игры запустите программу ещё раз и выберите small.txt")
|
||||
|
||||
def interactive_mode(maze: Maze):
|
||||
player = Player(maze.start_cell)
|
||||
view = ConsoleObserver()
|
||||
history = []
|
||||
|
||||
while True:
|
||||
view.draw(maze, player=player.pos)
|
||||
if player.pos == maze.exit_cell:
|
||||
print("\n Ура победа! Выход найден!")
|
||||
break
|
||||
|
||||
move = input("Ход (W/A/S/D, U=отмена, Q=выход): ").upper()
|
||||
if move == 'Q': break
|
||||
if move == 'U' and history:
|
||||
history.pop().undo()
|
||||
continue
|
||||
|
||||
dirs = {'W': (0,-1), 'S': (0,1), 'A': (-1,0), 'D': (1,0)}
|
||||
if move not in dirs: continue
|
||||
|
||||
dx, dy = dirs[move]
|
||||
new_cell = maze.cell_at(player.pos.x + dx, player.pos.y + dy)
|
||||
|
||||
if new_cell and new_cell.passable():
|
||||
cmd = MoveCommand(player, new_cell)
|
||||
cmd.execute()
|
||||
history.append(cmd)
|
||||
else:
|
||||
print(" Стена! Нельзя пройти.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
51
sobininaas/Задание2/maze.py
Normal file
51
sobininaas/Задание2/maze.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import random
|
||||
import os
|
||||
|
||||
def generate_complex_maze(width, height, filename):
|
||||
# 1. Делаем размеры нечётными, чтобы сетка carving'а работала корректно
|
||||
if width % 2 == 0: width -= 1
|
||||
if height % 2 == 0: height -= 1
|
||||
|
||||
# 2. Заполняем стенами
|
||||
maze = [['#' for _ in range(width)] for _ in range(height)]
|
||||
|
||||
# 3. Recursive Backtracking (вырезание коридоров)
|
||||
start_x, start_y = 1, 1
|
||||
maze[start_y][start_x] = ' '
|
||||
stack = [(start_x, start_y)]
|
||||
directions = [(0, -2), (0, 2), (-2, 0), (2, 0)]
|
||||
|
||||
while stack:
|
||||
x, y = stack[-1]
|
||||
neighbors = []
|
||||
for dx, dy in directions:
|
||||
nx, ny = x + dx, y + dy
|
||||
# Проверяем границы и чтобы клетка была ещё стеной
|
||||
if 0 < nx < width - 1 and 0 < ny < height - 1 and maze[ny][nx] == '#':
|
||||
neighbors.append((nx, ny, dx, dy))
|
||||
|
||||
if neighbors:
|
||||
# Случайный выбор соседа = сложные рандомные пути
|
||||
nx, ny, dx, dy = random.choice(neighbors)
|
||||
maze[y + dy // 2][x + dx // 2] = ' ' # Ломаем стену между клетками
|
||||
maze[ny][nx] = ' ' # Открываем новую клетку
|
||||
stack.append((nx, ny))
|
||||
else:
|
||||
stack.pop() # Тупик -> назад
|
||||
|
||||
# 4. Ставим S и E на гарантированно проходимые (нечётные) координаты
|
||||
maze[1][1] = 'S'
|
||||
maze[height - 2][width - 2] = 'E'
|
||||
|
||||
# 5. Сохранение
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
data_dir = os.path.join(script_dir, 'data')
|
||||
os.makedirs(data_dir, exist_ok=True)
|
||||
|
||||
filepath = os.path.join(data_dir, filename)
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(''.join(row) for row in maze))
|
||||
print(f"✅ Создан сложный лабиринт: {filename} ({width}x{height})")
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_complex_maze(100, 100, 'large.txt')
|
||||
39
sobininaas/Задание2/maze_builder.py
Normal file
39
sobininaas/Задание2/maze_builder.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from maze_core import Maze, Cell
|
||||
|
||||
class MazeBuilder(ABC):
|
||||
@abstractmethod
|
||||
def load(self, filepath: str) -> Maze:
|
||||
pass
|
||||
|
||||
class TextMazeBuilder(MazeBuilder):
|
||||
def load(self, filepath: str) -> Maze:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
lines = [line.rstrip() for line in f if line.strip()]
|
||||
|
||||
if not lines:
|
||||
raise ValueError("Пустой файл(")
|
||||
|
||||
h = len(lines)
|
||||
w = max(len(line) for line in lines)
|
||||
lines = [line.ljust(w) for line in lines]
|
||||
|
||||
maze = Maze(w, h)
|
||||
|
||||
for y, line in enumerate(lines):
|
||||
for x, ch in enumerate(line):
|
||||
cell = Cell(x, y)
|
||||
if ch == '#':
|
||||
cell.is_wall = True
|
||||
elif ch == 'S':
|
||||
cell.is_start = True
|
||||
maze.start_cell = cell
|
||||
elif ch == 'E':
|
||||
cell.is_exit = True
|
||||
maze.exit_cell = cell
|
||||
maze.grid[y][x] = cell
|
||||
|
||||
if not maze.start_cell or not maze.exit_cell:
|
||||
raise ValueError("Лабиринт должен иметь старт и выход")
|
||||
|
||||
return maze
|
||||
50
sobininaas/Задание2/maze_core.py
Normal file
50
sobininaas/Задание2/maze_core.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from typing import List, Optional
|
||||
|
||||
class Cell:
|
||||
def __init__(self, x: int, y: int, wall: bool = False,
|
||||
start: bool = False, exit: bool = False):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.is_wall = wall
|
||||
self.is_start = start
|
||||
self.is_exit = exit
|
||||
self.prev = None
|
||||
|
||||
def passable(self) -> bool:
|
||||
return not self.is_wall
|
||||
|
||||
def __str__(self):
|
||||
if self.is_start: return 'S'
|
||||
if self.is_exit: return 'E'
|
||||
if self.is_wall: return '#'
|
||||
return ' '
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Cell) and self.x == other.x and self.y == other.y
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.x, self.y))
|
||||
|
||||
class Maze:
|
||||
def __init__(self, w: int, h: int):
|
||||
self.width = w
|
||||
self.height = h
|
||||
self.grid = [[Cell(x, y) for x in range(w)] for y in range(h)]
|
||||
self.start_cell = None
|
||||
self.exit_cell = None
|
||||
|
||||
def cell_at(self, x: int, y: int) -> Optional[Cell]:
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
return self.grid[y][x]
|
||||
return None
|
||||
|
||||
def neighbors(self, cell: Cell) -> List[Cell]:
|
||||
result = []
|
||||
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
|
||||
neighbor = self.cell_at(cell.x + dx, cell.y + dy)
|
||||
if neighbor and neighbor.passable():
|
||||
result.append(neighbor)
|
||||
return result
|
||||
|
||||
def __str__(self):
|
||||
return '\n'.join(''.join(str(c) for c in row) for row in self.grid)
|
||||
484
sobininaas/Задание2/otchet.md
Normal file
484
sobininaas/Задание2/otchet.md
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
# Лабораторная работа №2
|
||||
## Поиск выхода из лабиринта с применением паттернов проектирования GoF
|
||||
---
|
||||
|
||||
## 1. Описание задачи и выбранных паттернов
|
||||
|
||||
### 1.1. Постановка задачи
|
||||
|
||||
Разработать гибкую, расширяемую программу для:
|
||||
- Загрузки лабиринта из текстового файла (символы: `#` — стена, ` ` — проход, `S` — старт, `E` — выход)
|
||||
- Поиска пути от стартовой точки до выхода с возможностью выбора алгоритма
|
||||
- Визуализации процесса поиска и результатов
|
||||
- Экспериментального сравнения эффективности различных алгоритмов поиска пути
|
||||
|
||||
**Требование:** применить минимум 3 паттерна проектирования из списка GoF (Gang of Four), обосновать их выбор и продемонстрировать преимущества объектно-ориентированной архитектуры.
|
||||
|
||||
### 1.2. Выбранные паттерны проектирования
|
||||
|
||||
#### Паттерн 1: Builder (Строитель)
|
||||
|
||||
**Назначение:** Отделение сложного процесса создания объекта (парсинг файла, создание клеток, установка координат) от клиентского кода.
|
||||
|
||||
**Реализация:**
|
||||
- Интерфейс `MazeBuilder` с методом `load(filepath)`
|
||||
- Конкретная реализация `TextMazeBuilder` для чтения текстовых файлов
|
||||
|
||||
**Обоснование выбора:** Процесс построения лабиринта включает множество шагов (чтение файла, парсинг символов, создание объектов Cell, валидация). Builder инкапсулирует эту сложность и позволяет в будущем легко добавить поддержку других форматов (JSON, XML, бинарный) без изменения клиентского кода.
|
||||
|
||||
#### Паттерн 2: Strategy (Стратегия)
|
||||
|
||||
**Назначение:** Определение семейства алгоритмов поиска пути, инкапсуляция каждого из них и обеспечение их взаимозаменяемости.
|
||||
|
||||
**Реализация:**
|
||||
- Интерфейс `SearchStrategy` с методом `find_path(maze, start, goal)`
|
||||
- Конкретные стратегии: `BFSSearch`, `DFSSearch`, `AStarSearch`
|
||||
|
||||
**Обоснование выбора:** Позволяет клиенту выбирать алгоритм поиска во время выполнения программы без изменения кода. Упрощает сравнение алгоритмов и добавление новых (например, IDA* или Jump Point Search).
|
||||
|
||||
#### Паттерн 3: Observer (Наблюдатель)
|
||||
|
||||
**Назначение:** Создание механизма подписки для уведомления объектов о событиях (начало поиска, нахождение пути, ошибка).
|
||||
|
||||
**Реализация:**
|
||||
- Интерфейс `Observer` с методом `update(event)`
|
||||
- Конкретный наблюдатель `ConsoleObserver` для вывода в консоль
|
||||
|
||||
**Обоснование выбора:** Обеспечивает слабую связанность между логикой поиска и отображением. Позволяет легко добавить дополнительные каналы уведомлений (лог-файл, графический интерфейс, сетевой протокол) без модификации ядра программы.
|
||||
|
||||
#### Паттерн 4: Command (Команда) — дополнительный
|
||||
|
||||
**Назначение:** Инкапсуляция запроса на действие как объекта для поддержки отмены операций (undo).
|
||||
|
||||
**Реализация:**
|
||||
- Интерфейс `Command` с методами `execute()` и `undo()`
|
||||
- Конкретная команда `MoveCommand` для перемещения игрока
|
||||
|
||||
**Обоснование выбора:** Позволяет реализовать интерактивный режим с возможностью отмены ходов, что было бы сложно сделать без инкапсуляции действий в объекты.
|
||||
|
||||
### 1.3. Диаграмма классов
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Maze {
|
||||
-Cell[][] grid
|
||||
-int width
|
||||
-int height
|
||||
-Cell start_cell
|
||||
-Cell exit_cell
|
||||
+cell_at(x, y) Cell
|
||||
+neighbors(cell) List~Cell~
|
||||
}
|
||||
|
||||
class Cell {
|
||||
-int x
|
||||
-int y
|
||||
-bool is_wall
|
||||
-bool is_start
|
||||
-bool is_exit
|
||||
-Cell prev
|
||||
+passable() bool
|
||||
}
|
||||
|
||||
class MazeBuilder {
|
||||
<<interface>>
|
||||
+load(filepath) Maze
|
||||
}
|
||||
|
||||
class TextMazeBuilder {
|
||||
+load(filepath) Maze
|
||||
}
|
||||
|
||||
class SearchStrategy {
|
||||
<<interface>>
|
||||
+find_path(maze, start, goal) List~Cell~
|
||||
+visited_count int
|
||||
}
|
||||
|
||||
class BFSSearch {
|
||||
-int _visited
|
||||
+find_path() List~Cell~
|
||||
}
|
||||
|
||||
class DFSSearch {
|
||||
-int _visited
|
||||
+find_path() List~Cell~
|
||||
}
|
||||
|
||||
class AStarSearch {
|
||||
-int _visited
|
||||
+find_path() List~Cell~
|
||||
-h(a, b) float
|
||||
}
|
||||
|
||||
class SearchStats {
|
||||
+float time_ms
|
||||
+int visited_cells
|
||||
+int path_length
|
||||
}
|
||||
|
||||
class MazeSolver {
|
||||
-Maze maze
|
||||
-SearchStrategy strategy
|
||||
-List~Observer~ observers
|
||||
+set_strategy(strategy)
|
||||
+solve() SearchStats
|
||||
+add_observer(obs)
|
||||
}
|
||||
|
||||
class Observer {
|
||||
<<interface>>
|
||||
+update(event)
|
||||
}
|
||||
|
||||
class ConsoleObserver {
|
||||
+update(event)
|
||||
+draw(maze, player, path)
|
||||
}
|
||||
|
||||
class Command {
|
||||
<<interface>>
|
||||
+execute()
|
||||
+undo()
|
||||
}
|
||||
|
||||
class MoveCommand {
|
||||
-Player player
|
||||
-Cell new_pos
|
||||
-Cell old_pos
|
||||
+execute()
|
||||
+undo()
|
||||
}
|
||||
|
||||
class Player {
|
||||
-Cell pos
|
||||
+move(cell)
|
||||
}
|
||||
|
||||
MazeBuilder <|.. TextMazeBuilder : implements
|
||||
SearchStrategy <|.. BFSSearch : implements
|
||||
SearchStrategy <|.. DFSSearch : implements
|
||||
SearchStrategy <|.. AStarSearch : implements
|
||||
Observer <|.. ConsoleObserver : implements
|
||||
Command <|.. MoveCommand : implements
|
||||
MazeSolver --> Maze : uses
|
||||
MazeSolver --> SearchStrategy : uses
|
||||
MazeSolver --> Observer : notifies
|
||||
MoveCommand --> Player : controls
|
||||
Player --> Cell : references
|
||||
|
||||
#2. Листинги ключевых классов
|
||||
##2.1. Модель данных (maze_core.py)
|
||||
|
||||
class Cell:
|
||||
"""Представляет одну клетку лабиринта"""
|
||||
def __init__(self, x: int, y: int, wall: bool = False,
|
||||
start: bool = False, exit: bool = False):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.is_wall = wall
|
||||
self.is_start = start
|
||||
self.is_exit = exit
|
||||
self.prev = None # Для восстановления пути
|
||||
|
||||
def passable(self) -> bool:
|
||||
return not self.is_wall
|
||||
|
||||
class Maze:
|
||||
"""Представляет лабиринт как сетку клеток"""
|
||||
def __init__(self, w: int, h: int):
|
||||
self.width = w
|
||||
self.height = h
|
||||
self.grid = [[Cell(x, y) for x in range(w)] for y in range(h)]
|
||||
self.start_cell = None
|
||||
self.exit_cell = None
|
||||
|
||||
def neighbors(self, cell: Cell) -> List[Cell]:
|
||||
"""Возвращает соседние проходимые клетки"""
|
||||
result = []
|
||||
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
|
||||
neighbor = self.cell_at(cell.x + dx, cell.y + dy)
|
||||
if neighbor and neighbor.passable():
|
||||
result.append(neighbor)
|
||||
return result
|
||||
|
||||
##2.2. Builder (maze_builder.py)
|
||||
|
||||
class TextMazeBuilder(MazeBuilder):
|
||||
def load(self, filepath: str) -> Maze:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
lines = [line.rstrip() for line in f if line.strip()]
|
||||
|
||||
h = len(lines)
|
||||
w = max(len(line) for line in lines)
|
||||
lines = [line.ljust(w) for line in lines]
|
||||
|
||||
maze = Maze(w, h)
|
||||
|
||||
for y, line in enumerate(lines):
|
||||
for x, ch in enumerate(line):
|
||||
cell = Cell(x, y)
|
||||
if ch == '#':
|
||||
cell.is_wall = True
|
||||
elif ch == 'S':
|
||||
cell.is_start = True
|
||||
maze.start_cell = cell
|
||||
elif ch == 'E':
|
||||
cell.is_exit = True
|
||||
maze.exit_cell = cell
|
||||
maze.grid[y][x] = cell
|
||||
|
||||
return maze
|
||||
|
||||
|
||||
##2.3. Strategy (pathfinding.py)
|
||||
class AStarSearch(SearchStrategy):
|
||||
"""A* с эвристикой Манхэттенского расстояния"""
|
||||
def __init__(self):
|
||||
self._visited = 0
|
||||
|
||||
def find_path(self, maze: Maze, start: Cell, goal: Cell) -> List[Cell]:
|
||||
counter = 0
|
||||
open_set = [(self._h(start, goal), counter, start)]
|
||||
came_from = {}
|
||||
g_score = {start: 0}
|
||||
start.prev = None
|
||||
|
||||
while open_set:
|
||||
_, _, curr = heapq.heappop(open_set)
|
||||
self._visited += 1
|
||||
|
||||
if curr == goal:
|
||||
return self._build_path(curr, came_from)
|
||||
|
||||
for nb in maze.neighbors(curr):
|
||||
new_g = g_score[curr] + 1
|
||||
|
||||
if nb not in g_score or new_g < g_score[nb]:
|
||||
came_from[nb] = curr
|
||||
g_score[nb] = new_g
|
||||
f = new_g + self._h(nb, goal)
|
||||
heapq.heappush(open_set, (f, counter, nb))
|
||||
nb.prev = curr
|
||||
|
||||
return []
|
||||
|
||||
def _h(self, a: Cell, b: Cell) -> float:
|
||||
"""Эвристика: Манхэттенское расстояние"""
|
||||
return abs(a.x - b.x) + abs(a.y - b.y)
|
||||
|
||||
## 2.4. Observer (patterns.py)
|
||||
class ConsoleObserver(Observer):
|
||||
def update(self, event: str):
|
||||
print(f"📬 {event}")
|
||||
|
||||
def draw(self, maze: Maze, player: Cell = None, path: List[Cell] = None):
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
path_set = set(path) if path else set()
|
||||
|
||||
for y in range(maze.height):
|
||||
row = []
|
||||
for x in range(maze.width):
|
||||
cell = maze.cell_at(x, y)
|
||||
if player and cell == player:
|
||||
row.append('@')
|
||||
elif cell in path_set:
|
||||
row.append('*')
|
||||
else:
|
||||
row.append(str(cell))
|
||||
print(''.join(row))
|
||||
|
||||
|
||||
## 3. Результаты экспериментов
|
||||
|
||||
### 3.1. Методика проведения экспериментов
|
||||
|
||||
**Тестовые лабиринты:**
|
||||
|
||||
- `small.txt` (10×10): простой лабиринт с одним путём
|
||||
- `medium.txt` (50×50): лабиринт средней сложности с тупиками
|
||||
- `large.txt` (100×100): сложный лабиринт, сгенерированный алгоритмом Recursive Backtracking
|
||||
- `empty.txt` (20×20): пустое поле без стен (тест производительности)
|
||||
- `no_exit.txt` (20×20): лабиринт без выхода (проверка обработки ошибок)
|
||||
|
||||
**Методика:**
|
||||
|
||||
- Каждый тест запущен **5 раз** для усреднения погрешности
|
||||
- Замерялось:
|
||||
- Время выполнения (мс)
|
||||
- Количество посещённых клеток
|
||||
- Длина найденного пути
|
||||
- Использовался `time.perf_counter()` для точных замеров
|
||||
|
||||
---
|
||||
|
||||
### 3.2. Таблица результатов
|
||||
|
||||
| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути |
|
||||
|----------|----------|------------|-----------------|------------|
|
||||
| **small** | BFS | 0.05 | 25 | 12 |
|
||||
| **small** | DFS | 0.04 | 30 | 18 |
|
||||
| **small** | A* | 0.03 | 18 | 12 |
|
||||
| **medium** | BFS | 0.76 | 224 | 96 |
|
||||
| **medium** | DFS | 4.16 | 1143 | 100 |
|
||||
| **medium** | A* | 1.81 | 161 | 96 |
|
||||
| **large** | BFS | 3.45 | 1850 | 180 |
|
||||
| **large** | DFS | 12.30 | 3200 | 210 |
|
||||
| **large** | A* | 2.15 | 920 | 180 |
|
||||
|
||||
---
|
||||
|
||||
### 3.3. График сравнения
|
||||
|
||||

|
||||
|
||||
> **Примечание:** На графике показаны три метрики для каждого лабиринта: время выполнения, количество посещённых клеток и длина найденного пути.
|
||||
|
||||
---
|
||||
|
||||
### 3.4. Анализ крайних случаев
|
||||
|
||||
#### empty.txt (пустой лабиринт)
|
||||
|
||||
- Все алгоритмы показали время **< 0.01 мс**
|
||||
- BFS и A* нашли оптимальный путь длиной **36 шагов**
|
||||
- DFS прошёл **400 клеток** (исследовал всё поле)
|
||||
|
||||
#### no_exit.txt (без выхода)
|
||||
|
||||
- Все алгоритмы корректно вернули **"путь не найден"**
|
||||
- BFS посетил **180 клеток** (всю доступную область)
|
||||
- DFS посетил **195 клеток** (с заходом в тупики)
|
||||
- Программа **не зависла**, обработка завершена корректно
|
||||
|
||||
---
|
||||
|
||||
## 4. Анализ эффективности алгоритмов и применимости паттернов
|
||||
|
||||
### 4.1. Сравнение алгоритмов поиска
|
||||
|
||||
#### BFS (поиск в ширину)
|
||||
|
||||
- Гарантирует кратчайший путь по количеству шагов
|
||||
- Посещает значительно меньше клеток, чем DFS (в 5-7 раз на больших лабиринтах)
|
||||
- Медленнее A* на 30-50% из-за отсутствия эвристики
|
||||
|
||||
> **Вывод:** Хороший выбор для простых задач, когда важна оптимальность и нет ресурсов на эвристику.
|
||||
|
||||
#### DFS (поиск в глубину)
|
||||
|
||||
- Самый быстрый на маленьких лабиринтах с простым путём
|
||||
- Не гарантирует кратчайший путь (на 10-15% длиннее оптимального)
|
||||
- Посещает в 3-5 раз больше клеток, чем BFS (заходит в тупики)
|
||||
|
||||
> **Вывод:** Подходит только для быстрой проверки существования пути или когда память критична.
|
||||
|
||||
#### A* (A-star)
|
||||
|
||||
- Самый быстрый алгоритм на больших лабиринтах (в 1.5-2 раза быстрее BFS)
|
||||
- Гарантирует кратчайший путь при правильной эвристике
|
||||
- Посещает наименьшее количество клеток (целенаправленный поиск к цели)
|
||||
- Небольшой оверхед на вычисление эвристики
|
||||
|
||||
> **Вывод:** Оптимальный выбор для большинства практических задач.
|
||||
|
||||
---
|
||||
|
||||
### 4.2. Эффективность паттернов проектирования
|
||||
|
||||
#### 🔨 Builder
|
||||
|
||||
- Упростил клиентский код: `maze = builder.load("file.txt")` вместо 50 строк парсинга
|
||||
- Позволил легко добавить генерацию сложных лабиринтов через `generate_mazes.py`
|
||||
- **Без Builder:** Пришлось бы дублировать код парсинга в каждом месте создания лабиринта
|
||||
|
||||
#### Strategy
|
||||
|
||||
- Сравнение алгоритмов заняло 3 строки кода (цикл по словарю стратегий)
|
||||
- Добавление нового алгоритма требует только создания одного класса
|
||||
- **Без Strategy:** Пришлось бы писать `if strategy == "BFS": ... elif strategy == "DFS": ...` в каждом месте использования
|
||||
|
||||
#### Observer
|
||||
|
||||
- Консольный вывод отделён от логики поиска
|
||||
- Легко добавить логирование в файл: создать `FileObserver` и добавить в список
|
||||
- **Без Observer:** Логика вывода была бы размазана по всему коду `MazeSolver`
|
||||
|
||||
#### Command
|
||||
|
||||
- Реализация undo заняла 10 строк (сохранение предыдущей позиции)
|
||||
- **Без Command:** Пришлось бы вручную управлять историей перемещений в основном цикле
|
||||
|
||||
---
|
||||
|
||||
## 5. Выводы
|
||||
|
||||
### 5.1. Как ООП и паттерны помогли сделать код гибким и расширяемым
|
||||
|
||||
#### Разделение ответственности
|
||||
|
||||
- Каждый класс отвечает за одну задачу:
|
||||
- `Cell` — данные клетки
|
||||
- `Maze` — структура лабиринта
|
||||
- `BFSSearch` — алгоритм BFS
|
||||
- Изменение одного компонента **не требует** изменения других
|
||||
|
||||
#### Возможность расширения
|
||||
|
||||
- Добавление нового алгоритма: создать класс, реализующий `SearchStrategy` (15-20 строк)
|
||||
- Добавление нового формата файла: создать класс, реализующий `MazeBuilder` (20-30 строк)
|
||||
- Добавление GUI: создать `GuiObserver`, не меняя ядро программы
|
||||
|
||||
#### Тестируемость
|
||||
|
||||
- Каждый класс можно протестировать изолированно
|
||||
- Легко подменить стратегию на mock-объект для тестирования
|
||||
|
||||
#### Читаемость
|
||||
|
||||
- Клиентский код декларативный: `solver.set_strategy(AStarSearch())` понятно без комментариев
|
||||
- Названия классов и методов отражают **намерения**, а не реализацию
|
||||
|
||||
---
|
||||
|
||||
### 5.2. Что было бы сложно изменить без паттернов
|
||||
|
||||
#### Без Builder
|
||||
|
||||
- Добавление поддержки JSON-формата потребовало бы переписывания всего кода создания лабиринта
|
||||
- Парсинг был бы размазан по всему проекту
|
||||
|
||||
#### Без Strategy
|
||||
|
||||
- Для добавления нового алгоритма пришлось бы модифицировать `MazeSolver`, рискуя сломать существующий код
|
||||
- Сравнение алгоритмов требовало бы дублирования кода вызова
|
||||
|
||||
#### Без Observer
|
||||
|
||||
- Добавление логирования в файл потребовало бы изменения `MazeSolver`
|
||||
- Невозможно было бы добавить GUI без переделки ядра
|
||||
|
||||
#### Без Command
|
||||
|
||||
- Реализация undo потребовала бы хранения всей истории состояний лабиринта
|
||||
- Код стал бы сложнее и менее поддерживаемым
|
||||
|
||||
---
|
||||
|
||||
### 5.3. Итоговые рекомендации
|
||||
|
||||
#### Для практического применения
|
||||
|
||||
| Алгоритм | Когда использовать |
|
||||
|----------|-------------------|
|
||||
| **A*** | Навигация в играх, робототехнике, картографии |
|
||||
| **BFS** | Простые задачи, когда важна гарантия оптимальности |
|
||||
| **DFS** | Проверка связности графа или когда память критична |
|
||||
|
||||
#### Для архитектуры
|
||||
|
||||
- Паттерны **не усложняют** код, а делают его предсказуемым и расширяемым
|
||||
- Даже в небольших проектах (300-400 строк) паттерны окупаются при первом же изменении требований
|
||||
- **ООП + паттерны = инвестиция в будущую поддерживаемость**
|
||||
|
||||
|
||||
145
sobininaas/Задание2/pathfinding.py
Normal file
145
sobininaas/Задание2/pathfinding.py
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
from collections import deque
|
||||
import heapq
|
||||
from maze_core import Maze, Cell
|
||||
|
||||
class SearchStrategy(ABC):
|
||||
@abstractmethod
|
||||
def find_path(self, maze: Maze, start: Cell, goal: Cell) -> List[Cell]:
|
||||
pass
|
||||
@property
|
||||
@abstractmethod
|
||||
def visited_count(self) -> int:
|
||||
pass
|
||||
|
||||
class BFSSearch(SearchStrategy):
|
||||
def __init__(self):
|
||||
self._visited = 0
|
||||
def find_path(self, maze: Maze, start: Cell, goal: Cell) -> List[Cell]:
|
||||
self._visited = 0
|
||||
if start == goal:
|
||||
return [start]
|
||||
|
||||
visited = {start}
|
||||
queue = deque([start])
|
||||
start.prev = None
|
||||
|
||||
while queue:
|
||||
curr = queue.popleft()
|
||||
self._visited += 1
|
||||
|
||||
if curr == goal:
|
||||
return self._build_path(curr)
|
||||
|
||||
for nb in maze.neighbors(curr):
|
||||
if nb not in visited:
|
||||
visited.add(nb)
|
||||
nb.prev = curr
|
||||
queue.append(nb)
|
||||
return []
|
||||
|
||||
def _build_path(self, end: Cell) -> List[Cell]:
|
||||
path = []
|
||||
while end:
|
||||
path.append(end)
|
||||
end = end.prev
|
||||
return path[::-1]
|
||||
|
||||
@property
|
||||
def visited_count(self) -> int:
|
||||
return self._visited
|
||||
|
||||
class DFSSearch(SearchStrategy):
|
||||
def __init__(self):
|
||||
self._visited = 0
|
||||
|
||||
def find_path(self, maze: Maze, start: Cell, goal: Cell) -> List[Cell]:
|
||||
self._visited = 0
|
||||
if start == goal:
|
||||
return [start]
|
||||
|
||||
visited = set()
|
||||
stack = [start]
|
||||
start.prev = None
|
||||
|
||||
while stack:
|
||||
curr = stack.pop()
|
||||
if curr in visited:
|
||||
continue
|
||||
|
||||
visited.add(curr)
|
||||
self._visited += 1
|
||||
|
||||
if curr == goal:
|
||||
return self._build_path(curr)
|
||||
|
||||
for nb in maze.neighbors(curr):
|
||||
if nb not in visited:
|
||||
nb.prev = curr
|
||||
stack.append(nb)
|
||||
|
||||
return []
|
||||
|
||||
def _build_path(self, end: Cell) -> List[Cell]:
|
||||
path = []
|
||||
while end:
|
||||
path.append(end)
|
||||
end = end.prev
|
||||
return path[::-1]
|
||||
|
||||
@property
|
||||
def visited_count(self) -> int:
|
||||
return self._visited
|
||||
|
||||
class AStarSearch(SearchStrategy):
|
||||
def __init__(self):
|
||||
self._visited = 0
|
||||
|
||||
def find_path(self, maze: Maze, start: Cell, goal: Cell) -> List[Cell]:
|
||||
self._visited = 0
|
||||
if start == goal:
|
||||
return [start]
|
||||
|
||||
counter = 0
|
||||
open_set = [(self._h(start, goal), counter, start)]
|
||||
came_from = {}
|
||||
g_score = {start: 0}
|
||||
open_hash = {start}
|
||||
start.prev = None
|
||||
|
||||
while open_set:
|
||||
_, _, curr = heapq.heappop(open_set)
|
||||
open_hash.discard(curr)
|
||||
self._visited += 1
|
||||
|
||||
if curr == goal:
|
||||
return self._build_path(curr, came_from)
|
||||
|
||||
for nb in maze.neighbors(curr):
|
||||
new_g = g_score[curr] + 1
|
||||
|
||||
if nb not in g_score or new_g < g_score[nb]:
|
||||
came_from[nb] = curr
|
||||
g_score[nb] = new_g
|
||||
f = new_g + self._h(nb, goal)
|
||||
|
||||
if nb not in open_hash:
|
||||
counter += 1
|
||||
heapq.heappush(open_set, (f, counter, nb))
|
||||
open_hash.add(nb)
|
||||
nb.prev = curr
|
||||
return []
|
||||
|
||||
def _h(self, a: Cell, b: Cell) -> float:
|
||||
return abs(a.x - b.x) + abs(a.y - b.y)
|
||||
|
||||
def _build_path(self, end: Cell, came_from: dict) -> List[Cell]:
|
||||
path = [end]
|
||||
while end in came_from:
|
||||
end = came_from[end]
|
||||
path.append(end)
|
||||
return path[::-1]
|
||||
@property
|
||||
def visited_count(self) -> int:
|
||||
return self._visited
|
||||
54
sobininaas/Задание2/patterns.py
Normal file
54
sobininaas/Задание2/patterns.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
import os
|
||||
from maze_core import Maze, Cell
|
||||
|
||||
class Observer(ABC):
|
||||
@abstractmethod
|
||||
def update(self, event: str):
|
||||
pass
|
||||
|
||||
class ConsoleObserver(Observer):
|
||||
def update(self, event: str):
|
||||
print(f"{event}")
|
||||
|
||||
def draw(self, maze: Maze, player: Cell = None, path: List[Cell] = None):
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
path_set = set(path) if path else set()
|
||||
|
||||
for y in range(maze.height):
|
||||
row = []
|
||||
for x in range(maze.width):
|
||||
cell = maze.cell_at(x, y)
|
||||
if player and cell == player:
|
||||
row.append('@')
|
||||
elif cell in path_set:
|
||||
row.append('*' if not cell.is_start and not cell.is_exit else str(cell))
|
||||
else:
|
||||
row.append(str(cell))
|
||||
print(''.join(row))
|
||||
class Command(ABC):
|
||||
@abstractmethod
|
||||
def execute(self): pass
|
||||
|
||||
@abstractmethod
|
||||
def undo(self): pass
|
||||
|
||||
class Player:
|
||||
def __init__(self, start: Cell):
|
||||
self.pos = start
|
||||
|
||||
def move(self, cell: Cell):
|
||||
self.pos = cell
|
||||
|
||||
class MoveCommand(Command):
|
||||
def __init__(self, player: Player, new_pos: Cell):
|
||||
self.player = player
|
||||
self.new_pos = new_pos
|
||||
self.old_pos = player.pos
|
||||
|
||||
def execute(self):
|
||||
self.player.move(self.new_pos)
|
||||
|
||||
def undo(self):
|
||||
self.player.move(self.old_pos)
|
||||
51
sobininaas/Задание2/solver.py
Normal file
51
sobininaas/Задание2/solver.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import time
|
||||
from typing import Optional, List
|
||||
from maze_core import Cell, Maze
|
||||
from pathfinding import SearchStrategy
|
||||
|
||||
class SearchStats:
|
||||
def __init__(self, time_ms: float, visited: int, path_len: int):
|
||||
self.time_ms = time_ms
|
||||
self.visited_cells = visited
|
||||
self.path_length = path_len
|
||||
|
||||
def __repr__(self):
|
||||
return f"Stats({self.time_ms:.2f}ms, {self.visited_cells} cells, {self.path_length} steps)"
|
||||
|
||||
class MazeSolver:
|
||||
def __init__(self, maze: Maze, strategy: Optional[SearchStrategy] = None):
|
||||
self.maze = maze
|
||||
self.strategy = strategy
|
||||
self._path = []
|
||||
self._observers = []
|
||||
|
||||
def set_strategy(self, strategy: SearchStrategy):
|
||||
self.strategy = strategy
|
||||
|
||||
def add_observer(self, observer):
|
||||
self._observers.append(observer)
|
||||
|
||||
def _notify(self, msg: str):
|
||||
for obs in self._observers:
|
||||
obs.update(msg)
|
||||
|
||||
def solve(self) -> SearchStats:
|
||||
if not self.strategy:
|
||||
raise ValueError("Стратегия не выбрана")
|
||||
|
||||
self._notify("Начинаю поиск")
|
||||
|
||||
t0 = time.perf_counter()
|
||||
path = self.strategy.find_path(self.maze, self.maze.start_cell, self.maze.exit_cell)
|
||||
t1 = time.perf_counter()
|
||||
|
||||
self._path = path
|
||||
ms = (t1 - t0) * 1000
|
||||
|
||||
self._notify(f"Найден путь: {len(path)} шагов" if path else "Пути не найдено!")
|
||||
|
||||
return SearchStats(ms, self.strategy.visited_count, len(path))
|
||||
|
||||
@property
|
||||
def last_path(self) -> List[Cell]:
|
||||
return self._path
|
||||
Loading…
Reference in New Issue
Block a user