# Отчёт по лабораторной работе №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)

```python
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


