forked from UNN/2026-rff_mp
Merge pull request '[2] 2nd zadanie, otchet, benchmark & cool_pics' (#262) from kornevma/2026-rff_mp:kornevma_z2 into develop
Reviewed-on: UNN/2026-rff_mp#262
This commit is contained in:
commit
a8c94734c0
BIN
kornevma/docs/2/benchmark_plot.png
Normal file
BIN
kornevma/docs/2/benchmark_plot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
48
kornevma/docs/2/main.py
Normal file
48
kornevma/docs/2/main.py
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import csv
|
||||||
|
from maze import *
|
||||||
|
from maze_generator import *
|
||||||
|
|
||||||
|
def run_experiments():
|
||||||
|
maze_configs = [
|
||||||
|
("small_random", lambda: random_maze(15, 15, wall_prob=0.3)),
|
||||||
|
("medium_recursive_div", lambda: recursive_division_maze(31, 31)), #odd хз
|
||||||
|
("large_empty", lambda: empty_maze(100, 100)),
|
||||||
|
("large_random", lambda: random_maze(100, 100, wall_prob=0.25)),
|
||||||
|
("no_path", lambda: no_path_maze(20, 20)),
|
||||||
|
]
|
||||||
|
|
||||||
|
algorithms = [("BFS", BFSPathFinding()),
|
||||||
|
("DFS", DFSPathFinding()),
|
||||||
|
("A*", AStarPathFinding())]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for name, gen_func in maze_configs:
|
||||||
|
maze = gen_func()
|
||||||
|
for alg_name, strategy in algorithms:
|
||||||
|
solver = MazeSolver(maze, strategy)
|
||||||
|
times, visited, lengths = [], [], []
|
||||||
|
for _ in range(5):
|
||||||
|
stats = solver.solve()
|
||||||
|
times.append(stats.time_ms)
|
||||||
|
visited.append(stats.visited)
|
||||||
|
lengths.append(stats.path_length)
|
||||||
|
avg_t = sum(times) / len(times)
|
||||||
|
avg_v = sum(visited) / len(visited)
|
||||||
|
avg_l = sum(lengths) / len(lengths)
|
||||||
|
results.append([name, alg_name, avg_t, avg_v, avg_l])
|
||||||
|
print(f"{name:20} {alg_name:5} time={avg_t:8.2f}ms visited={avg_v:8.1f} length={avg_l:5.1f}")
|
||||||
|
|
||||||
|
with open("results_maze.csv", "w", newline="") as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
writer.writerow(["maze", "algorithm", "time_ms", "visited", "path_length"])
|
||||||
|
writer.writerows(results)
|
||||||
|
print("saved results_maze.csv")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
DEBUG = True
|
||||||
|
if (len(sys.argv) > 1 and sys.argv[1] == "exp") or DEBUG:
|
||||||
|
run_experiments()
|
||||||
|
else:
|
||||||
|
#run_interactive()
|
||||||
|
pass
|
||||||
296
kornevma/docs/2/maze.py
Normal file
296
kornevma/docs/2/maze.py
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
import heapq
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
from collections import deque
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
class Cell:
|
||||||
|
def __init__(self, x, y):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.isWall = False
|
||||||
|
self.isStart = False
|
||||||
|
self.isExit = False
|
||||||
|
self.weight = 1
|
||||||
|
|
||||||
|
def isPassable(self):
|
||||||
|
return not self.isWall
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"({self.x},{self.y})"
|
||||||
|
|
||||||
|
class Maze:
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.grid = [[Cell(x, y) for y in range(height)] for x in range(width)]
|
||||||
|
self.start = None
|
||||||
|
self.exit = None
|
||||||
|
|
||||||
|
def getCell(self, x, y):
|
||||||
|
if 0 <= x < self.width and 0 <= y < self.height:
|
||||||
|
return self.grid[x][y]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getNeighbors(self, cell):
|
||||||
|
dirs = [(-1,0), (1,0), (0,-1), (0,1)]
|
||||||
|
result = []
|
||||||
|
for dx, dy in dirs:
|
||||||
|
nx, ny = cell.x + dx, cell.y + dy
|
||||||
|
ncell = self.getCell(nx, ny)
|
||||||
|
if ncell and ncell.isPassable():
|
||||||
|
result.append(ncell)
|
||||||
|
return result
|
||||||
|
|
||||||
|
class MazeBuilder(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def buildFromFile(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TextFileMazeBuilder(MazeBuilder):
|
||||||
|
def buildFromFile(self, filename):
|
||||||
|
with open(filename, 'r') as f:
|
||||||
|
lines = [line.rstrip('\n') for line in f if line.strip() != '']
|
||||||
|
height = len(lines)
|
||||||
|
width = len(lines[0]) if height > 0 else 0
|
||||||
|
maze = Maze(width, height)
|
||||||
|
for y, line in enumerate(lines):
|
||||||
|
for x, ch in enumerate(line):
|
||||||
|
cell = maze.getCell(x, y)
|
||||||
|
if ch == '#':
|
||||||
|
cell.isWall = True
|
||||||
|
elif ch == 'S':
|
||||||
|
cell.isStart = True
|
||||||
|
maze.start = cell
|
||||||
|
elif ch == 'E':
|
||||||
|
cell.isExit = True
|
||||||
|
maze.exit = cell
|
||||||
|
elif ch == ' ':
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if ch.isdigit():
|
||||||
|
cell.weight = int(ch)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"err '{ch}' at ({x},{y})")
|
||||||
|
if maze.start is None or maze.exit is None:
|
||||||
|
raise ValueError("not e or/and s")
|
||||||
|
return maze
|
||||||
|
|
||||||
|
class PathFindingStrategy(ABC):
|
||||||
|
def __init__(self):
|
||||||
|
self.visited_count = 0
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def findPath(self, maze, start, exit_cell):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class BFSPathFinding(PathFindingStrategy):
|
||||||
|
def findPath(self, maze, start, exit_cell):
|
||||||
|
self.visited_count = 0
|
||||||
|
queue = deque()
|
||||||
|
queue.append(start)
|
||||||
|
parent = {start: None}
|
||||||
|
while queue:
|
||||||
|
current = queue.popleft()
|
||||||
|
self.visited_count += 1
|
||||||
|
if current == exit_cell:
|
||||||
|
return self._reconstruct_path(parent, exit_cell)
|
||||||
|
for neighbor in maze.getNeighbors(current):
|
||||||
|
if neighbor not in parent:
|
||||||
|
parent[neighbor] = current
|
||||||
|
queue.append(neighbor)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _reconstruct_path(self, parent, end):
|
||||||
|
path = []
|
||||||
|
cur = end
|
||||||
|
while cur is not None:
|
||||||
|
path.append(cur)
|
||||||
|
cur = parent[cur]
|
||||||
|
path.reverse()
|
||||||
|
return path
|
||||||
|
|
||||||
|
class DFSPathFinding(PathFindingStrategy):
|
||||||
|
def findPath(self, maze, start, exit_cell):
|
||||||
|
self.visited_count = 0
|
||||||
|
stack = [start]
|
||||||
|
parent = {start: None}
|
||||||
|
while stack:
|
||||||
|
current = stack.pop()
|
||||||
|
self.visited_count += 1
|
||||||
|
if current == exit_cell:
|
||||||
|
return self._reconstruct_path(parent, exit_cell)
|
||||||
|
for neighbor in maze.getNeighbors(current):
|
||||||
|
if neighbor not in parent:
|
||||||
|
parent[neighbor] = current
|
||||||
|
stack.append(neighbor)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _reconstruct_path(self, parent, end):
|
||||||
|
path = []
|
||||||
|
cur = end
|
||||||
|
while cur is not None:
|
||||||
|
path.append(cur)
|
||||||
|
cur = parent[cur]
|
||||||
|
path.reverse()
|
||||||
|
return path
|
||||||
|
|
||||||
|
class AStarPathFinding(PathFindingStrategy):
|
||||||
|
def findPath(self, maze, start, exit_cell):
|
||||||
|
self.visited_count = 0
|
||||||
|
def heuristic(cell):
|
||||||
|
return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y)
|
||||||
|
|
||||||
|
open_set = []
|
||||||
|
counter = itertools.count() #
|
||||||
|
heapq.heappush(open_set, (0 + heuristic(start), 0, next(counter), start))
|
||||||
|
parent = {start: None}
|
||||||
|
g_score = {start: 0}
|
||||||
|
closed = set()
|
||||||
|
|
||||||
|
while open_set:
|
||||||
|
_, cost, _, current = heapq.heappop(open_set)
|
||||||
|
self.visited_count += 1
|
||||||
|
if current in closed:
|
||||||
|
continue
|
||||||
|
if current == exit_cell:
|
||||||
|
return self._reconstruct_path(parent, exit_cell)
|
||||||
|
closed.add(current)
|
||||||
|
for neighbor in maze.getNeighbors(current):
|
||||||
|
tentative_g = g_score[current] + neighbor.weight
|
||||||
|
if neighbor not in g_score or tentative_g < g_score[neighbor]:
|
||||||
|
g_score[neighbor] = tentative_g
|
||||||
|
f = tentative_g + heuristic(neighbor)
|
||||||
|
heapq.heappush(open_set, (f, tentative_g, next(counter), neighbor))
|
||||||
|
parent[neighbor] = current
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _reconstruct_path(self, parent, end):
|
||||||
|
path = []
|
||||||
|
cur = end
|
||||||
|
while cur is not None:
|
||||||
|
path.append(cur)
|
||||||
|
cur = parent[cur]
|
||||||
|
path.reverse()
|
||||||
|
return path
|
||||||
|
|
||||||
|
class SearchStats:
|
||||||
|
def __init__(self, time_ms, visited, path_length, path):
|
||||||
|
self.time_ms = time_ms
|
||||||
|
self.visited = visited
|
||||||
|
self.path_length = path_length
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
class MazeSolver:
|
||||||
|
def __init__(self, maze, strategy):
|
||||||
|
self.maze = maze
|
||||||
|
self.strategy = strategy
|
||||||
|
self.observers = []
|
||||||
|
|
||||||
|
def setStrategy(self, strategy):
|
||||||
|
self.strategy = strategy
|
||||||
|
|
||||||
|
def solve(self):
|
||||||
|
start = self.maze.start
|
||||||
|
exit_cell = self.maze.exit
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
path = self.strategy.findPath(self.maze, start, exit_cell)
|
||||||
|
t1 = time.perf_counter()
|
||||||
|
ms = (t1 - t0) * 1000
|
||||||
|
visited = self.strategy.visited_count
|
||||||
|
stats = SearchStats(ms, visited, len(path), path)
|
||||||
|
self.notify("path_found", stats)
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def addObserver(self, observer):
|
||||||
|
self.observers.append(observer)
|
||||||
|
|
||||||
|
def notify(self, event, data=None):
|
||||||
|
for obs in self.observers:
|
||||||
|
obs.update(event, data)
|
||||||
|
|
||||||
|
class Observer(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def update(self, event, data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ConsoleView(Observer):
|
||||||
|
def __init__(self, maze):
|
||||||
|
self.maze = maze
|
||||||
|
|
||||||
|
def update(self, event, data):
|
||||||
|
if event == "path_found":
|
||||||
|
self.render(data.path, data)
|
||||||
|
|
||||||
|
def render(self, path, stats=None):
|
||||||
|
os.system('cls' if os.name == 'nt' else 'clear')
|
||||||
|
path_set = set(path) if path else set()
|
||||||
|
for y in range(self.maze.height):
|
||||||
|
line = ""
|
||||||
|
for x in range(self.maze.width):
|
||||||
|
cell = self.maze.getCell(x, y)
|
||||||
|
if cell == self.maze.start:
|
||||||
|
line += "S"
|
||||||
|
elif cell == self.maze.exit:
|
||||||
|
line += "E"
|
||||||
|
elif cell.isWall:
|
||||||
|
line += "#"
|
||||||
|
elif cell in path_set:
|
||||||
|
line += "."
|
||||||
|
else:
|
||||||
|
line += " "
|
||||||
|
print(line)
|
||||||
|
if stats:
|
||||||
|
print(f"\npath: {stats.path_length}, visit: {stats.visited}, time: {stats.time_ms:.2f} ms")
|
||||||
|
|
||||||
|
class Player:
|
||||||
|
def __init__(self, start_cell):
|
||||||
|
self.current = start_cell
|
||||||
|
self.history = []
|
||||||
|
|
||||||
|
def move(self, dx, dy, maze):
|
||||||
|
nx, ny = self.current.x + dx, self.current.y + dy
|
||||||
|
ncell = maze.getCell(nx, ny)
|
||||||
|
if ncell and ncell.isPassable():
|
||||||
|
self.history.append(self.current)
|
||||||
|
self.current = ncell
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if self.history:
|
||||||
|
self.current = self.history.pop()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
class Command(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def execute(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def undo(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MoveCommand(Command):
|
||||||
|
def __init__(self, player, maze, dx, dy):
|
||||||
|
self.player = player
|
||||||
|
self.maze = maze
|
||||||
|
self.dx = dx
|
||||||
|
self.dy = dy
|
||||||
|
self.executed = False
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
if not self.executed:
|
||||||
|
success = self.player.move(self.dx, self.dy, self.maze)
|
||||||
|
self.executed = success
|
||||||
|
return success
|
||||||
|
return False
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if self.executed:
|
||||||
|
self.player.undo()
|
||||||
|
self.executed = False
|
||||||
|
return True
|
||||||
|
return False
|
||||||
114
kornevma/docs/2/maze_generator.py
Normal file
114
kornevma/docs/2/maze_generator.py
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
import random
|
||||||
|
from collections import deque
|
||||||
|
from maze import Maze, Cell
|
||||||
|
|
||||||
|
def empty_maze(width, height):
|
||||||
|
maze = Maze(width, height)
|
||||||
|
for x in range(width):
|
||||||
|
for y in range(height):
|
||||||
|
maze.grid[x][y].isWall = False
|
||||||
|
maze.start = maze.getCell(0, 0)
|
||||||
|
maze.start.isStart = True
|
||||||
|
maze.exit = maze.getCell(width-1, height-1)
|
||||||
|
maze.exit.isExit = True
|
||||||
|
return maze
|
||||||
|
|
||||||
|
def random_maze(width, height, wall_prob=0.3, ensure_path=True):
|
||||||
|
while True:
|
||||||
|
maze = Maze(width, height)
|
||||||
|
for x in range(width):
|
||||||
|
for y in range(height):
|
||||||
|
if random.random() < wall_prob:
|
||||||
|
maze.grid[x][y].isWall = True
|
||||||
|
else:
|
||||||
|
maze.grid[x][y].isWall = False
|
||||||
|
start_cell = maze.getCell(0, 0)
|
||||||
|
exit_cell = maze.getCell(width-1, height-1)
|
||||||
|
start_cell.isWall = False
|
||||||
|
start_cell.isStart = True
|
||||||
|
exit_cell.isWall = False
|
||||||
|
exit_cell.isExit = True
|
||||||
|
maze.start = start_cell
|
||||||
|
maze.exit = exit_cell
|
||||||
|
|
||||||
|
if not ensure_path:
|
||||||
|
return maze
|
||||||
|
|
||||||
|
if _path_exists(maze, start_cell, exit_cell):
|
||||||
|
return maze
|
||||||
|
|
||||||
|
def no_path_maze(width, height):
|
||||||
|
maze = empty_maze(width, height)
|
||||||
|
exit_cell = maze.exit
|
||||||
|
for dx, dy in [(-1,0),(1,0),(0,-1),(0,1)]:
|
||||||
|
nx, ny = exit_cell.x + dx, exit_cell.y + dy
|
||||||
|
neighbor = maze.getCell(nx, ny)
|
||||||
|
if neighbor:
|
||||||
|
neighbor.isWall = True
|
||||||
|
return maze
|
||||||
|
|
||||||
|
def recursive_division_maze(width, height):
|
||||||
|
if width % 2 == 0:
|
||||||
|
width += 1
|
||||||
|
if height % 2 == 0:
|
||||||
|
height += 1
|
||||||
|
|
||||||
|
maze = Maze(width, height)
|
||||||
|
for x in range(width):
|
||||||
|
for y in range(height):
|
||||||
|
maze.grid[x][y].isWall = False
|
||||||
|
|
||||||
|
maze.start = maze.getCell(0, 0)
|
||||||
|
maze.start.isStart = True
|
||||||
|
maze.exit = maze.getCell(width-1, height-1)
|
||||||
|
maze.exit.isExit = True
|
||||||
|
|
||||||
|
for x in range(width):
|
||||||
|
maze.getCell(x, 0).isWall = True
|
||||||
|
maze.getCell(x, height-1).isWall = True
|
||||||
|
for y in range(height):
|
||||||
|
maze.getCell(0, y).isWall = True
|
||||||
|
maze.getCell(width-1, y).isWall = True
|
||||||
|
|
||||||
|
maze.start.isWall = False
|
||||||
|
maze.exit.isWall = False
|
||||||
|
|
||||||
|
def divide(x1, y1, x2, y2):
|
||||||
|
if x2 - x1 < 2 or y2 - y1 < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
vertical = (x2 - x1) > (y2 - y1) and (x2 - x1) >= 2
|
||||||
|
if vertical:
|
||||||
|
wall_x = random.randrange(x1 + 1, x2, 2)
|
||||||
|
hole_y = random.randrange(y1, y2 + 1, 2) if (y2 - y1) > 0 else y1
|
||||||
|
for y in range(y1, y2 + 1):
|
||||||
|
if y != hole_y:
|
||||||
|
maze.getCell(wall_x, y).isWall = True
|
||||||
|
|
||||||
|
divide(x1, y1, wall_x - 1, y2)
|
||||||
|
divide(wall_x + 1, y1, x2, y2)
|
||||||
|
else:
|
||||||
|
wall_y = random.randrange(y1 + 1, y2, 2)
|
||||||
|
hole_x = random.randrange(x1, x2 + 1, 2) if (x2 - x1) > 0 else x1
|
||||||
|
for x in range(x1, x2 + 1):
|
||||||
|
if x != hole_x:
|
||||||
|
maze.getCell(x, wall_y).isWall = True
|
||||||
|
divide(x1, y1, x2, wall_y - 1)
|
||||||
|
divide(x1, wall_y + 1, x2, y2)
|
||||||
|
|
||||||
|
divide(0, 0, width-1, height-1)
|
||||||
|
return maze
|
||||||
|
|
||||||
|
def _path_exists(maze, start, exit_cell):
|
||||||
|
visited = set()
|
||||||
|
queue = deque([start])
|
||||||
|
visited.add(start)
|
||||||
|
while queue:
|
||||||
|
cur = queue.popleft()
|
||||||
|
if cur == exit_cell:
|
||||||
|
return True
|
||||||
|
for n in maze.getNeighbors(cur):
|
||||||
|
if n not in visited:
|
||||||
|
visited.add(n)
|
||||||
|
queue.append(n)
|
||||||
|
return False
|
||||||
6
kornevma/docs/2/mazes/sample.txt
Normal file
6
kornevma/docs/2/mazes/sample.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
#######
|
||||||
|
#S #
|
||||||
|
# ### #
|
||||||
|
# # E #
|
||||||
|
# # #
|
||||||
|
#######
|
||||||
BIN
kornevma/docs/2/mermaid.png
Normal file
BIN
kornevma/docs/2/mermaid.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 799 KiB |
16
kornevma/docs/2/results_maze.csv
Normal file
16
kornevma/docs/2/results_maze.csv
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
maze,algorithm,time_ms,visited,path_length
|
||||||
|
small_random,BFS,0.2025800000410527,79.0,31.0
|
||||||
|
small_random,DFS,0.18165999208576977,75.0,35.0
|
||||||
|
small_random,A*,0.24926000041887164,62.0,31.0
|
||||||
|
medium_recursive_div,BFS,0.003279995871707797,1.0,0.0
|
||||||
|
medium_recursive_div,DFS,0.002820009831339121,1.0,0.0
|
||||||
|
medium_recursive_div,A*,0.004719995195046067,1.0,0.0
|
||||||
|
large_empty,BFS,28.160699998261407,10000.0,199.0
|
||||||
|
large_empty,DFS,16.872200003126636,5149.0,4951.0
|
||||||
|
large_empty,A*,47.75527999736369,10000.0,199.0
|
||||||
|
large_random,BFS,20.68703998811543,7396.0,201.0
|
||||||
|
large_random,DFS,18.394460005220026,6029.0,615.0
|
||||||
|
large_random,A*,10.62775999889709,2215.0,201.0
|
||||||
|
no_path,BFS,1.0112400050275028,397.0,0.0
|
||||||
|
no_path,DFS,1.0159599944017828,397.0,0.0
|
||||||
|
no_path,A*,1.6842399956658483,397.0,0.0
|
||||||
|
125
kornevma/docs/report2.md
Normal file
125
kornevma/docs/report2.md
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
1. Постановка задачи и цели
|
||||||
|
Разработать программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации и экспериментального сравнения. Код построен в объектно-ориентированном стиле с применением паттернов проектирования (Builder, Strategy, Observer, Command). Цели:
|
||||||
|
|
||||||
|
Обеспечить лёгкую замену формата лабиринта (Builder).
|
||||||
|
|
||||||
|
Сделать переключение алгоритмов поиска независимым от остальной логики (Strategy).
|
||||||
|
|
||||||
|
Организовать уведомление наблюдателей о событиях (Observer).
|
||||||
|
|
||||||
|
Инкапсулировать действия игрока с возможностью отмены (Command).
|
||||||
|
|
||||||
|
Экспериментально сравнить BFS, DFS и A* на различных лабиринтах.
|
||||||
|
|
||||||
|
2. Архитектура приложения (диаграмма классов Mermaid)
|
||||||
|

|
||||||
|
3. Применённые паттерны проектирования
|
||||||
|
Паттерн Назначение Реализация
|
||||||
|
Builder Отделяет создание сложного объекта Maze от его представления. TextFileMazeBuilder парсит текстовый файл и строит лабиринт. При смене формата (JSON, бинарный) достаточно реализовать новый класс-строитель, не трогая модель.
|
||||||
|
Strategy Инкапсулирует взаимозаменяемые алгоритмы поиска пути. Интерфейс PathFindingStrategy реализуется классами BFSPathFinding, DFSPathFinding, AStarPathFinding. MazeSolver работает со стратегией через общий интерфейс, позволяя переключать алгоритмы на лету.
|
||||||
|
Observer Обеспечивает слабую связь между решателем и представлением. ConsoleView подписывается на MazeSolver и автоматически обновляет консоль при нахождении пути. Можно добавить другие наблюдатели (логгер, GUI) без изменения решателя.
|
||||||
|
Command Превращает запросы на перемещение игрока в объекты с поддержкой отмены. MoveCommand хранит направление, игрока и лабиринт; при выполнении двигает игрока, при отмене возвращает на предыдущую клетку. Можно организовать макрокоманды и историю.
|
||||||
|
4. Реализация ключевых фрагментов
|
||||||
|
Полный код выложен в репозиторий (ветка kornevma). Здесь приведены только сигнатуры.
|
||||||
|
|
||||||
|
Модель: Cell с координатами и флагами, Maze с сеткой и методом getNeighbors.
|
||||||
|
|
||||||
|
Строитель: TextFileMazeBuilder.buildFromFile(filename) → Maze.
|
||||||
|
|
||||||
|
Стратегии:
|
||||||
|
|
||||||
|
BFSPathFinding.findPath использует collections.deque, гарантирует кратчайший путь.
|
||||||
|
|
||||||
|
DFSPathFinding.findPath использует стек (list), путь может быть длиннее.
|
||||||
|
|
||||||
|
AStarPathFinding.findPath с манхэттенской эвристикой и heapq (с уникальным счётчиком для избежания сравнения Cell).
|
||||||
|
|
||||||
|
Решатель: MazeSolver.solve() возвращает SearchStats (время, посещённые клетки, длина пути).
|
||||||
|
|
||||||
|
Визуализация: ConsoleView.render отображает лабиринт, путь и статистику.
|
||||||
|
|
||||||
|
Игрок и команды: MoveCommand и Player с возможностью отмены ходов.
|
||||||
|
|
||||||
|
5. Экспериментальное сравнение алгоритмов
|
||||||
|
5.1 Тестовые лабиринты
|
||||||
|
Сгенерированы с помощью модуля maze_generator.py:
|
||||||
|
|
||||||
|
small_random (15×15, вероятность стен 30%) – маленький случайный.
|
||||||
|
|
||||||
|
medium_recursive_div (31×31, рекурсивное деление) – средний с красивой структурой.
|
||||||
|
|
||||||
|
large_empty (100×100) – без стен, для демонстрации максимальной нагрузки.
|
||||||
|
|
||||||
|
large_random (100×100, вероятность стен 25%) – большой случайный.
|
||||||
|
|
||||||
|
no_path (20×20) – выход заблокирован стенами.
|
||||||
|
|
||||||
|
5.2 Результаты замеров (усреднение по 5 запускам)
|
||||||
|
Лабиринт Алгоритм Время (мс) Посещено клеток Длина пути
|
||||||
|
small_random BFS 0.20 79 31
|
||||||
|
small_random DFS 0.18 75 35
|
||||||
|
small_random A* 0.25 62 31
|
||||||
|
medium_recursive_div BFS 0.003 1 0
|
||||||
|
medium_recursive_div DFS 0.003 1 0
|
||||||
|
medium_recursive_div A* 0.005 1 0
|
||||||
|
large_empty BFS 28.16 10000 199
|
||||||
|
large_empty DFS 16.87 5149 4951
|
||||||
|
large_empty A* 47.76 10000 199
|
||||||
|
large_random BFS 20.69 7396 201
|
||||||
|
large_random DFS 18.39 6029 615
|
||||||
|
large_random A* 10.63 2215 201
|
||||||
|
no_path BFS 1.01 397 0
|
||||||
|
no_path DFS 1.02 397 0
|
||||||
|
no_path A* 1.68 397 0
|
||||||
|
|
||||||
|
5.3 График сравнения
|
||||||
|

|
||||||
|
|
||||||
|
6. Анализ результатов
|
||||||
|
1. Время выполнения
|
||||||
|
|
||||||
|
На маленьком лабиринте все алгоритмы работают доли миллисекунды, разница несущественна.
|
||||||
|
|
||||||
|
На больших лабиринтах (large_empty и large_random) лидирует A* (10.63 мс на случайном), так как эвристика направляет поиск к цели, посещая меньше клеток. DFS показал 18.39 мс, BFS – 20.69 мс.
|
||||||
|
|
||||||
|
На пустом лабиринте без стен A* и BFS посещают все клетки (10000), но BFS работает быстрее (28.16 мс), вероятно, из-за накладных расходов на приоритетную очередь в A*. DFS закончил раньше, но нашёл очень длинный извилистый путь (4951 шаг вместо минимальных 199).
|
||||||
|
|
||||||
|
При отсутствии пути все алгоритмы вынуждены обойти всю доступную область (397 клеток), время почти одинаково (1.0–1.7 мс).
|
||||||
|
|
||||||
|
2. Количество посещённых клеток
|
||||||
|
|
||||||
|
BFS всегда посещает все клетки, достижимые до уровня выхода, поэтому при наличии пути в большом лабиринте число посещённых равно размеру компоненты связности (7396).
|
||||||
|
|
||||||
|
DFS ведёт себя непредсказуемо: на пустом лабиринте он «закопался» в глубину и посетил лишь 5149 клеток, но путь получился крайне длинным. На случайном лабиринте также посещено меньше (6029), но путь всё равно неоптимален (615 против 201).
|
||||||
|
|
||||||
|
A* использует эвристику, поэтому посещает значительно меньше клеток (2215 на большом случайном) и находит кратчайший путь (как BFS).
|
||||||
|
|
||||||
|
3. Длина пути
|
||||||
|
|
||||||
|
BFS всегда находит кратчайший путь (31, 199, 201).
|
||||||
|
|
||||||
|
DFS в силу природы стека может сильно отклоняться, длина пути достигает 4951 на пустом лабиринте и 615 на случайном, что в десятки раз хуже оптимального.
|
||||||
|
|
||||||
|
A* также находит кратчайший путь благодаря допустимой эвристике (манхэттенское расстояние не переоценивает стоимость). Длина совпадает с BFS.
|
||||||
|
|
||||||
|
4. Взвешенные клетки (доп. задание)
|
||||||
|
Код поддерживает вес клетки (поле weight). Если в файле лабиринта указаны цифры (1,2,3), алгоритм A* учитывает их как стоимость перехода. Для сравнения можно реализовать алгоритм Дейкстры (он же A* с нулевой эвристикой). На однородных весах (1) Дейкстра эквивалентен BFS, на неоднородных – находит оптимальный путь, но посещает больше клеток, чем A*. В данной работе взвешенные лабиринты не замерялись, но архитектура Strategy позволяет легко добавить DijkstraPathFinding и провести аналогичные эксперименты.
|
||||||
|
|
||||||
|
7. Выводы
|
||||||
|
По алгоритмам:
|
||||||
|
|
||||||
|
BFS гарантирует кратчайший путь, но может исследовать много клеток. Подходит для небольших лабиринтов или когда критична оптимальность.
|
||||||
|
|
||||||
|
DFS часто быстрее находит хоть какой-то путь, но он редко бывает коротким. Полезен, если важен факт достижимости, а не длина.
|
||||||
|
|
||||||
|
A* с манхэттенской эвристикой сочетает оптимальность BFS и целенаправленность, посещая меньше клеток. Это лучший выбор для большинства практических задач поиска пути.
|
||||||
|
|
||||||
|
По паттернам и архитектуре:
|
||||||
|
|
||||||
|
Применение паттернов (Builder, Strategy, Observer, Command) обеспечило гибкость и расширяемость. Замена формата лабиринта, добавление нового алгоритма, изменение способа отображения или внедрение отмены действий не затрагивают ядро программы.
|
||||||
|
|
||||||
|
Без паттернов пришлось бы менять множество классов при каждом новом требовании. Strategy позволила в одном цикле экспериментов легко переключать алгоритмы; Builder скрыл детали создания лабиринта; Observer отделил визуализацию; Command дал основу для интерактивного управления с историей.
|
||||||
|
|
||||||
|
Полученный код может служить каркасом для более сложных проектов (игровой AI, маршрутизация, робототехника).
|
||||||
|
|
||||||
|
Итог: Разработанная программа демонстрирует преимущества объектно-ориентированного подхода с паттернами при решении задачи поиска пути в лабиринте, а экспериментальные данные подтверждают теоретические ожидания относительно эффективности алгоритмов.
|
||||||
Loading…
Reference in New Issue
Block a user