2026-rff_mp/ShulpinIN/maze_lab2/README.md

724 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Описание задачи
Разработать гибкую, расширяемую программу для:
Загрузки лабиринта из текстового файла
Поиска пути от старта до выхода с возможностью выбора алгоритма (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 {
<<interface>>
+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
```python
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
```python
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
```python
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
```python
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
```python
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
```python
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*
```python
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
```python
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
```python
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
```python
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):
```commandline
##########
#S #
### #####
# # E#
# # # # ##
# # #
####### #
# #
# ###### #
##########
```
medium(50x50)
```commandline
##################################################
#S #
# ############################################# #
# # # #
# # ######################################### # #
# # # # # #
# # # ##################################### # # #
# # # # # # # #
# # # # ################################# # # # #
# # # # # # # # # #
# # # # # ############################# # # # # #
# # # # # # # # # # # #
# # # # # # ######################### # # # # # #
# # # # # # # # # # # # # #
# # # # # # # ##################### # # # # # # #
# # # # # # # # # # # # # # # #
# # # # # # # # ################# # # # # # # # #
# # # # # # # # # # # # # # # # # #
# # # # # # # # # ############# # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # ######### # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # ##### # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # ##### # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # ######### # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # ############# # # # # # # # # #
# # # # # # # # # # # # # # # # # #
# # # # # # # # ################# # # # # # # # #
# # # # # # # # # # # # # # # #
# # # # # # # ##################### # # # # # # #
# # # # # # # # # # # # # #
# # # # # # ######################### # # # # # #
# # # # # # # # # # # #
# # # # # ############################# # # # # #
# # # # # # # # # #
# # # # ################################# # # # #
# # # # # # # #
# # # ##################################### # # #
# # # # # #
# # ######################################### # #
# # # #
# ############################################# #
# E#
##################################################
```
large(100x100)
```commandline
####################################################################################################
#S #
# ################################################################################################ #
# # # #
# # ############################################################################################ # #
# # # # # #
# # # ######################################################################################## # # #
# # # # # # # #
# # # # #################################################################################### # # # #
# # # # # # # # # #
# # # # # ################################################################################ # # # # #
# # # # # # # # # # # #
# # # # # # ############################################################################ # # # # # #
# # # # # # # # # # # # # #
# # # # # # # ######################################################################## # # # # # # #
# # # # # # # # # # # # # # # #
# # # # # # # # #################################################################### # # # # # # # #
# # # # # # # # # # # # # # # # # #
# # # # # # # # # ################################################################ # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # ############################################################ # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # ######################################################## # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # #################################################### # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # ################################################ # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # ############################################ # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # ######################################## # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # #################################### # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # ################################ # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # ############################ # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # ######################## # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # #################### # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # ################ # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # ############ # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # ######## # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #### # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #E#
####################################################################################################
```
empty(40x40)
```commandline
########################################
#S #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# E#
########################################
```
no_exit(10x10)
```commandline
##########
#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 |
### Графики
![experiment_results.png](docs%2Fdata%2Fexperiment_results.png)
### Средние значения по всем лабиринтам
| Алгоритм | Среднее время (мс) | Среднее посещено | Средняя длина пути |
|----------|-------------------|------------------|--------------------|
| 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* | Минимум посещений |
| Без выхода | Любой | Разница несущественна |
## Анализ применимости паттернов
### Что упростили паттерны
1. **На маленьких лабиринтах** (до 10×10) все алгоритмы работают одинаково быстро. Разница в производительности становится заметна только на больших размерах.
2. **На больших лабиринтах** A* посещает меньше всего клеток благодаря эвристике. Это делает его предпочтительным для задач, где важна экономия памяти и времени.
3. **Когда нужен кратчайший путь** — выбирайте BFS или A*. BFS проще, A* быстрее находит цель, но сложнее в реализации.
4. **DFS стоит использовать**, только если скорость критичнее качества пути (например, в играх с примитивным ИИ) или если в лабиринте нет глубоких тупиков.
5. **Программа корректно определяет отсутствие пути.** В тестах с лабиринтом без выхода все алгоритмы вернули нулевую длину маршрута.