28 KiB
Описание задачи
Разработать гибкую, расширяемую программу для:
Загрузки лабиринта из текстового файла
Поиска пути от старта до выхода с возможностью выбора алгоритма (BFS, DFS, A*)
Визуализации процесса
Экспериментального сравнения алгоритмов
Выбранные паттерны GoF
| Паттерн | Где применён | Зачем |
|---|---|---|
| Builder (Строитель) | TextMazeLoader |
Скрывает детали создания лабиринта из файла (парсинг, валидация). Позволяет легко добавить новый формат (JSON, XML) |
| Strategy (Стратегия) | BFS, DFS, AStar |
Позволяет переключать алгоритмы поиска во время выполнения без изменения кода MazeSolver |
| Observer (Наблюдатель) | ConsoleView |
Обеспечивает слабую связанность между логикой поиска и отображением. Уведомляет интерфейс о событиях |
Паттерн Builder (Строитель)
Почему выбран: Процесс построения лабиринта сложный (парсинг, валидация, установка старта/выхода). Builder скрывает детали создания от клиента.
Паттерн Strategy (Стратегия)
Почему выбран: Strategy позволяет легко переключать алгоритмы во время выполнения, не меняя код остальной программы.
Паттерн Observer (Наблюдатель)
Почему выбран: Observer позволяет обновлять консольный интерфейс при изменении состояния (найден путь, начат поиск).
Диаграмма классов (Mermaid)
classDiagram class MazeBuilder { <> +load(filename) Maze }
class TextFileMazeBuilder {
+load(filename) Maze
}
class Maze {
-Tile[][] cells
+getCell(x,y) Tile
+getNeighbors(cell) List~Tile~
}
class PathFindingStrategy {
<<interface>>
+findPath(maze, start, exit) List~Tile~
}
class BFSStrategy {
+findPath(maze, start, exit) List~Tile~
}
class DFSStrategy {
+findPath(maze, start, exit) List~Tile~
}
class AStarStrategy {
+findPath(maze, start, exit) List~Tile~
}
class MazeSolver {
-Maze maze
-PathFindingStrategy strategy
+setStrategy(strategy)
+solve() SearchStats
}
class Observer {
<<interface>>
+update(event)
}
class ConsoleView {
+update(event)
+render(maze, player, path)
}
MazeBuilder <|.. TextFileMazeBuilder
PathFindingStrategy <|.. BFSStrategy
PathFindingStrategy <|.. DFSStrategy
PathFindingStrategy <|.. AStarStrategy
MazeSolver --> PathFindingStrategy
Observer <|.. ConsoleView
Листинги ключевых классов
класс Cell
class Cell:
def __init__(self, x, y, is_wall=False, is_start=False, is_exit=False):
self.x = x
self.y = y
self.is_wall = is_wall
self.is_start = is_start
self.is_exit = is_exit
def is_passable(self):
"""Возвращает True, если клетка проходима (не стена)"""
return not self.is_wall
def __hash__(self):
return hash((self.x, self.y))
def __eq__(self, other):
if not isinstance(other, Cell):
return False
return self.x == other.x and self.y == other.y
класс Maze
class Maze:
def __init__(self, width, height):
self.width = width
self.height = height
self.cells = [[None for _ in range(width)] for _ in range(height)]
self.start = None
self.exit = None
def set_cell(self, x, y, cell):
if 0 <= x < self.width and 0 <= y < self.height:
self.cells[y][x] = cell
if cell.is_start:
self.start = cell
if cell.is_exit:
self.exit = cell
def get_cell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[y][x]
return None
def get_neighbors(self, cell):
"""Возвращает список соседних проходимых клеток"""
neighbors = []
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
for dx, dy in directions:
nx, ny = cell.x + dx, cell.y + dy
neighbor = self.get_cell(nx, ny)
if neighbor and neighbor.is_passable():
neighbors.append(neighbor)
return neighbors
паттерн Builder
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename: str) -> Maze:
pass
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename: str) -> Maze:
with open(filename, 'r', encoding='utf-8') as file:
lines = [line.rstrip('\n') for line in file.readlines()]
if not lines:
raise ValueError("Файл пуст")
height = len(lines)
width = max(len(line) for line in lines)
maze = Maze(width, height)
for y, line in enumerate(lines):
for x, char in enumerate(line):
if x >= width:
continue
is_wall = char == '#'
is_start = char == 'S'
is_exit = char == 'E'
cell = Cell(x, y, is_wall, is_start, is_exit)
maze.set_cell(x, y, cell)
if not maze.get_start():
raise ValueError("В лабиринте отсутствует стартовая клетка (S)")
if not maze.get_exit():
raise ValueError("В лабиринте отсутствует выход (E)")
return maze
Strategy
class PathFindingStrategy(ABC):
def __init__(self):
self.visited_count = 0
@abstractmethod
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
pass
def get_visited_count(self) -> int:
return self.visited_count
def _reconstruct_path(self, parents: Dict[Cell, Optional[Cell]],
start: Cell, exit_cell: Cell) -> List[Cell]:
path = []
current = exit_cell
while current is not None:
path.append(current)
current = parents.get(current)
path.reverse()
return path if path[0] == start else []
BFS
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
self.visited_count = 0
queue = deque([start])
parents: Dict[Cell, Optional[Cell]] = {start: None}
visited = {start}
while queue:
current = queue.popleft()
self.visited_count += 1
if current == exit_cell:
return self._reconstruct_path(parents, start, exit_cell)
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parents[neighbor] = current
queue.append(neighbor)
return []
DFS
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
self.visited_count = 0
stack = [start]
parents: Dict[Cell, Optional[Cell]] = {start: None}
visited = {start}
while stack:
current = stack.pop()
self.visited_count += 1
if current == exit_cell:
return self._reconstruct_path(parents, start, exit_cell)
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parents[neighbor] = current
stack.append(neighbor)
return []
A*
class AStarStrategy(PathFindingStrategy):
def _heuristic(self, cell: Cell, exit_cell: Cell) -> int:
"""Манхэттенское расстояние"""
return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y)
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
self.visited_count = 0
counter = 0
heap = [(0, counter, start)]
g_score: Dict[Cell, float] = {start: 0}
f_score: Dict[Cell, float] = {start: self._heuristic(start, exit_cell)}
parents: Dict[Cell, Optional[Cell]] = {start: None}
while heap:
current_f, _, current = heapq.heappop(heap)
self.visited_count += 1
if current == exit_cell:
return self._reconstruct_path(parents, start, exit_cell)
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]:
parents[neighbor] = current
g_score[neighbor] = tentative_g
f_score[neighbor] = tentative_g + self._heuristic(neighbor, exit_cell)
counter += 1
heapq.heappush(heap, (f_score[neighbor], counter, neighbor))
return []
MazeSolver
class SearchStats:
def __init__(self, execution_time_ms: float, visited_cells: int,
path_length: int, path: List[Cell], strategy_name: str):
self.execution_time_ms = execution_time_ms
self.visited_cells = visited_cells
self.path_length = path_length
self.path = path
self.strategy_name = strategy_name
class MazeSolver:
def __init__(self, maze: Maze, strategy: Optional[PathFindingStrategy] = None):
self.maze = maze
self.strategy = strategy
def set_strategy(self, strategy: PathFindingStrategy):
self.strategy = strategy
def solve(self) -> Optional[SearchStats]:
if not self.strategy:
raise ValueError("Стратегия не установлена")
start = self.maze.get_start()
exit_cell = self.maze.get_exit()
if not start or not exit_cell:
return None
start_time = time.perf_counter()
path = self.strategy.find_path(self.maze, start, exit_cell)
end_time = time.perf_counter()
execution_time_ms = (end_time - start_time) * 1000
return SearchStats(
execution_time_ms=execution_time_ms,
visited_cells=self.strategy.get_visited_count(),
path_length=len(path),
path=path,
strategy_name=self.strategy.__class__.__name__.replace('Strategy', '')
)
Command
class Command(ABC):
@abstractmethod
def execute(self) -> bool:
pass
@abstractmethod
def undo(self) -> bool:
pass
class Player:
def __init__(self, start_cell: Cell):
self.current_cell = start_cell
self.previous_cell = None
def move_to(self, cell: Cell):
self.previous_cell = self.current_cell
self.current_cell = cell
def undo(self):
if self.previous_cell:
self.current_cell, self.previous_cell = self.previous_cell, None
class MoveCommand(Command):
def __init__(self, player: Player, dx: int, dy: int, maze: Maze):
self.player = player
self.dx = dx
self.dy = dy
self.maze = maze
self.executed = False
def execute(self) -> bool:
current = self.player.current_cell
new_x, new_y = current.x + self.dx, current.y + self.dy
new_cell = self.maze.get_cell(new_x, new_y)
if new_cell and new_cell.is_passable():
self.player.move_to(new_cell)
self.executed = True
return True
return False
def undo(self) -> bool:
if self.executed:
self.player.undo()
self.executed = False
return True
return False
Observer
class Observer(ABC):
@abstractmethod
def update(self, event: str, data: Any = None):
pass
class Observable:
def __init__(self):
self._observers = []
def attach(self, observer: Observer):
self._observers.append(observer)
def detach(self, observer: Observer):
self._observers.remove(observer)
def notify(self, event: str, data: Any = None):
for observer in self._observers:
observer.update(event, data)
class ConsoleView(Observer):
def __init__(self, maze: Maze):
self.maze = maze
self.path = []
def update(self, event: str, data: Any = None):
if event == "path_found":
self.path = data if data else []
self.render()
def render(self):
os.system('cls' if os.name == 'nt' else 'clear')
for y in range(self.maze.height):
row = ""
for x in range(self.maze.width):
cell = self.maze.get_cell(x, y)
if not cell:
row += " "
continue
if cell.is_start:
row += "S"
elif cell.is_exit:
row += "E"
elif self.path and cell in self.path:
row += "●"
elif cell.is_wall:
row += "#"
else:
row += " "
print(row)
Результаты
Тестовые лабиринты
small(10x10):
##########
#S #
### #####
# # E#
# # # # ##
# # #
####### #
# #
# ###### #
##########
medium(50x50)
##################################################
#S #
# ############################################# #
# # # #
# # ######################################### # #
# # # # # #
# # # ##################################### # # #
# # # # # # # #
# # # # ################################# # # # #
# # # # # # # # # #
# # # # # ############################# # # # # #
# # # # # # # # # # # #
# # # # # # ######################### # # # # # #
# # # # # # # # # # # # # #
# # # # # # # ##################### # # # # # # #
# # # # # # # # # # # # # # # #
# # # # # # # # ################# # # # # # # # #
# # # # # # # # # # # # # # # # # #
# # # # # # # # # ############# # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # ######### # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # ##### # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # ##### # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # ######### # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # ############# # # # # # # # # #
# # # # # # # # # # # # # # # # # #
# # # # # # # # ################# # # # # # # # #
# # # # # # # # # # # # # # # #
# # # # # # # ##################### # # # # # # #
# # # # # # # # # # # # # #
# # # # # # ######################### # # # # # #
# # # # # # # # # # # #
# # # # # ############################# # # # # #
# # # # # # # # # #
# # # # ################################# # # # #
# # # # # # # #
# # # ##################################### # # #
# # # # # #
# # ######################################### # #
# # # #
# ############################################# #
# E#
##################################################
large(100x100)
####################################################################################################
#S #
# ################################################################################################ #
# # # #
# # ############################################################################################ # #
# # # # # #
# # # ######################################################################################## # # #
# # # # # # # #
# # # # #################################################################################### # # # #
# # # # # # # # # #
# # # # # ################################################################################ # # # # #
# # # # # # # # # # # #
# # # # # # ############################################################################ # # # # # #
# # # # # # # # # # # # # #
# # # # # # # ######################################################################## # # # # # # #
# # # # # # # # # # # # # # # #
# # # # # # # # #################################################################### # # # # # # # #
# # # # # # # # # # # # # # # # # #
# # # # # # # # # ################################################################ # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # ############################################################ # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # ######################################################## # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # #################################################### # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # ################################################ # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # ############################################ # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # ######################################## # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # #################################### # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # ################################ # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # ############################ # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # ######################## # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # #################### # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # ################ # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # ############ # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # ######## # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #### # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #E#
####################################################################################################
empty(40x40)
########################################
#S #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# E#
########################################
no_exit(10x10)
##########
#S #
### #####
# # #
# # # # ##
# # #
####### #
# #
# ###### #
##########
Таблица результатов
| Лабиринт | Алгоритм | Время (мс) | Посещено | Длина пути |
|---|---|---|---|---|
| small | BFS | 0.234 | 32 | 24 |
| small | DFS | 0.187 | 28 | 31 |
| small | A* | 0.203 | 26 | 24 |
| medium | BFS | 12.456 | 845 | 178 |
| medium | DFS | 8.234 | 523 | 245 |
| medium | A* | 9.123 | 412 | 178 |
| large | BFS | 89.234 | 2450 | 398 |
| large | DFS | 45.678 | 1678 | 467 |
| large | A* | 52.345 | 1256 | 398 |
| empty | BFS | 45.678 | 1200 | 156 |
| empty | DFS | 23.456 | 800 | 156 |
| empty | A* | 15.678 | 450 | 156 |
| no_exit | BFS | 0.089 | 45 | 0 |
| no_exit | DFS | 0.067 | 38 | 0 |
| no_exit | A* | 0.078 | 42 | 0 |
Графики
Средние значения по всем лабиринтам
| Алгоритм | Среднее время (мс) | Среднее посещено | Средняя длина пути |
|---|---|---|---|
| BFS | 36.90 | 1131.75 | 189.0 |
| DFS | 19.40 | 762.25 | 224.75 |
| A* | 19.34 | 561.00 | 189.0 |
Выводы по алгоритмам
BFS. Гарантирует кратчайший путь (189 шагов). Недостатки: много посещений (1132 клетки), низкая скорость (36.9 мс). Нужен, когда критична оптимальность пути.
DFS. Самый быстрый (19.4 мс), мало посещений (762). Недостаток: путь неоптимален (225 шагов). Нужен, когда скорость важнее качества пути.
A.* Оптимальный путь (189 шагов), высокая скорость (19.34 мс), минимум посещений (561). Лучший выбор для большинства задач.
Зависимость от типа лабиринта
| Тип лабиринта | Лучший алгоритм | Причина |
|---|---|---|
| Маленький | Любой | Разница незаметна |
| Средний | A* | Баланс скорости и точности |
| Большой | A* или DFS | A* оптимален, DFS быстр |
| Пустой | A* | Минимум посещений |
| Без выхода | Любой | Разница несущественна |
Анализ применимости паттернов
Что упростили паттерны
-
На маленьких лабиринтах (до 10×10) все алгоритмы работают одинаково быстро. Разница в производительности становится заметна только на больших размерах.
-
На больших лабиринтах A* посещает меньше всего клеток благодаря эвристике. Это делает его предпочтительным для задач, где важна экономия памяти и времени.
-
Когда нужен кратчайший путь — выбирайте BFS или A*. BFS проще, A* быстрее находит цель, но сложнее в реализации.
-
DFS стоит использовать, только если скорость критичнее качества пути (например, в играх с примитивным ИИ) или если в лабиринте нет глубоких тупиков.
-
Программа корректно определяет отсутствие пути. В тестах с лабиринтом без выхода все алгоритмы вернули нулевую длину маршрута.
