Описание задачи

Разработать гибкую, расширяемую программу для:

Загрузки лабиринта из текстового файла

Поиска пути от старта до выхода с возможностью выбора алгоритма (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. **Программа корректно определяет отсутствие пути.** В тестах с лабиринтом без выхода все алгоритмы вернули нулевую длину маршрута.
