10 KiB
Отчёт по лабораторной работе №2
Поиск выхода из лабиринта (объектно-ориентированная реализация с паттернами)
1. Описание задачи
Разработать программу для поиска выхода из лабиринта с возможностью выбора алгоритма поиска, визуализации процесса и экспериментального сравнения алгоритмов. Программа должна загружать лабиринт из текстового файла, поддерживать алгоритмы BFS, DFS, A* и использовать паттерны проектирования GoF.
2. Выбранные паттерны
2.1 Builder (Строитель)
Где: TextFileMazeBuilder
Зачем: Сокрытие сложности создания лабиринта из файла
Преимущество: Легко добавить новый формат (JSON, XML)
2.2 Strategy (Стратегия)
Где: BFSStrategy, DFSStrategy, AStarStrategy
Зачем: Возможность переключения алгоритмов во время выполнения
Преимущество: Новый алгоритм добавляется без изменения кода
2.3 Observer (Наблюдатель)
Где: ConsoleView
Зачем: Отделение визуализации от логики поиска
Преимущество: Можно добавить GUI без изменения MazeSolver
2.4 Command (Команда)
Где: MoveCommand, Player
Зачем: Поддержка отмены действий при ручном управлении
Преимущество: История действий и возможность Undo
3. Диаграмма классов (Mermaid)
classDiagram
class Maze {
-width, height
-_cells[][]
-start, exit
+get_cell(x,y)
+get_neighbors(cell)
}
class Cell {
-x, y
-is_wall
-is_start
-is_exit
+is_passable()
}
class MazeBuilder {
<<interface>>
+build_from_file(filename)
}
class TextFileMazeBuilder {
+build_from_file(filename)
}
class PathFindingStrategy {
<<interface>>
+find_path(maze, start, exit)
}
class BFSStrategy
class DFSStrategy
class AStarStrategy
class MazeSolver {
-maze
-strategy
+set_strategy()
+solve()
}
class Observer {
<<interface>>
+update(event, data)
}
class ConsoleView {
+render(maze, path)
+update(event, data)
}
class Command {
<<interface>>
+execute()
+undo()
}
class MoveCommand {
-player
-direction
+execute()
+undo()
}
MazeBuilder <|.. TextFileMazeBuilder
PathFindingStrategy <|.. BFSStrategy
PathFindingStrategy <|.. DFSStrategy
PathFindingStrategy <|.. AStarStrategy
MazeSolver --> PathFindingStrategy
Observer <|.. ConsoleView
Command <|.. MoveCommand
class TextFileMazeBuilder(MazeBuilder):
WALL_CHAR = '#'
START_CHAR = 'S'
EXIT_CHAR = 'E'
def build_from_file(self, filename: str) -> Maze:
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
height = len(lines)
width = max(len(line) for line in lines)
maze = Maze(width, height)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if x >= width:
continue
cell = Cell(x, y)
if ch == self.WALL_CHAR:
cell.is_wall = True
elif ch == self.START_CHAR:
cell.is_start = True
elif ch == self.EXIT_CHAR:
cell.is_exit = True
maze.set_cell(x, y, cell)
if maze.start is None:
raise ValueError("Нет стартовой клетки (S)")
if maze.exit is None:
raise ValueError("Нет выхода (E)")
return maze
class TextFileMazeBuilder(MazeBuilder):
WALL_CHAR = '#'
START_CHAR = 'S'
EXIT_CHAR = 'E'
def build_from_file(self, filename: str) -> Maze:
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
height = len(lines)
width = max(len(line) for line in lines)
maze = Maze(width, height)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if x >= width:
continue
cell = Cell(x, y)
if ch == self.WALL_CHAR:
cell.is_wall = True
elif ch == self.START_CHAR:
cell.is_start = True
elif ch == self.EXIT_CHAR:
cell.is_exit = True
maze.set_cell(x, y, cell)
if maze.start is None:
raise ValueError("Нет стартовой клетки (S)")
if maze.exit is None:
raise ValueError("Нет выхода (E)")
return maze
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze, start, exit_cell):
queue = deque([start])
visited = {start}
parent = {start: None}
while queue:
current = queue.popleft()
if current == exit_cell:
return self._reconstruct_path(parent, current)
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
queue.append(neighbor)
return []
def _reconstruct_path(self, parent, current):
path = []
while current:
path.append(current)
current = parent[current]
return list(reversed(path))
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze, start, exit_cell):
stack = [(start, [start])]
visited = {start}
while stack:
current, path = stack.pop()
if current == exit_cell:
return path
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
stack.append((neighbor, path + [neighbor]))
return []
class AStarStrategy(PathFindingStrategy):
def _heuristic(self, a, b):
return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, maze, start, exit_cell):
counter = 0
open_set = [(self._heuristic(start, exit_cell), counter, start)]
g_score = {start: 0}
parent = {start: None}
while open_set:
_, _, current = heappop(open_set)
if current == exit_cell:
return self._reconstruct_path(parent, current)
for neighbor in maze.get_neighbors(current):
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
parent[neighbor] = current
g_score[neighbor] = tentative_g
counter += 1
f = tentative_g + self._heuristic(neighbor, exit_cell)
heappush(open_set, (f, counter, neighbor))
return []
class ConsoleView(Observer):
def render(self, maze, path=None):
path_set = set(path) if path else set()
print("\n+" + "-" * maze.width + "+")
for y in range(maze.height):
row = []
for x in range(maze.width):
cell = maze.get_cell(x, y)
if cell.is_start:
row.append('S')
elif cell.is_exit:
row.append('E')
elif cell in path_set:
row.append('*')
elif cell.is_wall:
row.append('#')
else:
row.append(' ')
print("|" + ''.join(row) + "|")
print("+" + "-" * maze.width + "+")
def update(self, event, data):
if event == "maze_loaded":
self.render(data.get('maze'))
elif event == "path_found":
self.render(data.get('maze'), data.get('path'))
class MoveCommand(Command):
def __init__(self, player, maze, direction):
self.player = player
self.maze = maze
self.direction = direction
self.previous_cell = None
def execute(self):
self.previous_cell = self.player.current_cell
dx, dy = self.direction
new_cell = self.maze.get_cell(
self.player.current_cell.x + dx,
self.player.current_cell.y + dy
)
if new_cell and new_cell.is_passable():
self.player.move_to(new_cell)
return True
return False
def undo(self):
if self.previous_cell:
self.player.move_to(self.previous_cell)
class MazeSolver:
def __init__(self, maze, strategy=None):
self.maze = maze
self._strategy = strategy
def set_strategy(self, strategy):
self._strategy = strategy
def solve(self):
if not self._strategy:
raise ValueError("Стратегия не установлена")
start_time = time.perf_counter()
path = self._strategy.find_path(self.maze, self.maze.start, self.maze.exit)
end_time = time.perf_counter()
stats = SearchStats(
time_ms=(end_time - start_time) * 1000,
visited_cells=len(path) if path else 0,
path_length=len(path) if path else 0,
path_found=bool(path)
)
return path, stats