2026-rff_mp/ShulpinIN/maze_lab2/README.md

28 KiB
Raw Blame History

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

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

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

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

Графики

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