From 59a488fec2ee1dc80e81867645aa78a2ad4ffaf9 Mon Sep 17 00:00:00 2001 From: Roman Date: Sun, 24 May 2026 17:45:37 +0300 Subject: [PATCH] =?UTF-8?q?[2]=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BA=D0=BE=D0=B4=201,3=20=D0=B4=D0=BE=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhigalovrd/lab2/builder.py | 12 ++- zhigalovrd/lab2/experiment_results.csv | 13 +++ zhigalovrd/lab2/main.py | 136 +++++++++++++++++++++++++ zhigalovrd/lab2/maze.py | 15 ++- zhigalovrd/lab2/maze_sample.txt | 7 ++ zhigalovrd/lab2/otch.md | 112 ++++++++++++++++++++ zhigalovrd/lab2/strategies.py | 15 ++- zhigalovrd/lab2/visualizer.py | 24 +++++ 8 files changed, 323 insertions(+), 11 deletions(-) create mode 100644 zhigalovrd/lab2/experiment_results.csv create mode 100644 zhigalovrd/lab2/main.py create mode 100644 zhigalovrd/lab2/maze_sample.txt create mode 100644 zhigalovrd/lab2/otch.md create mode 100644 zhigalovrd/lab2/visualizer.py diff --git a/zhigalovrd/lab2/builder.py b/zhigalovrd/lab2/builder.py index a38c988..3b271b7 100644 --- a/zhigalovrd/lab2/builder.py +++ b/zhigalovrd/lab2/builder.py @@ -1,13 +1,14 @@ +# builder.py from abc import ABC, abstractmethod from maze import Maze, Cell class MazeBuilder(ABC): @abstractmethod - def build_from_file(self, filename: str) -> Maze: + def build_from_file(self, filename: str, require_exit: bool = True) -> Maze: pass class TextFileMazeBuilder(MazeBuilder): - def build_from_file(self, filename: str) -> Maze: + def build_from_file(self, filename: str, require_exit: bool = True) -> Maze: with open(filename, 'r', encoding='utf-8') as f: lines = [line.rstrip('\n') for line in f] @@ -30,7 +31,8 @@ class TextFileMazeBuilder(MazeBuilder): elif ch == 'E': cell.is_exit = True maze.exit = cell - # пробел или любой другой символ – проход - if maze.start is None or maze.exit is None: - raise ValueError("Лабиринт должен содержать S (старт) и E (выход)") + if maze.start is None: + raise ValueError("Лабиринт должен содержать S (старт)") + if require_exit and maze.exit is None: + raise ValueError("Лабиринт должен содержать E (выход)") return maze \ No newline at end of file diff --git a/zhigalovrd/lab2/experiment_results.csv b/zhigalovrd/lab2/experiment_results.csv new file mode 100644 index 0000000..d3479f2 --- /dev/null +++ b/zhigalovrd/lab2/experiment_results.csv @@ -0,0 +1,13 @@ +maze,strategy,avg_time_ms,avg_visited,avg_path_length +simple_10x10,BFS,0.0439399999777379,17.0,10.0 +simple_10x10,DFS,0.029820000008839997,14.0,10.0 +simple_10x10,A*,0.07110000001375738,17.0,10.0 +medium_20x20,BFS,0.09570000006533519,39.0,0.0 +medium_20x20,DFS,0.09261999998670944,39.0,0.0 +medium_20x20,A*,0.15964000003805268,39.0,0.0 +empty_50x50,BFS,6.905739999956495,2500.0,99.0 +empty_50x50,DFS,12.088819999962652,2500.0,1275.0 +empty_50x50,A*,19.79220000002897,2500.0,99.0 +no_exit,BFS,0.0004200000148557592,0.0,0.0 +no_exit,DFS,0.00031999998100218363,0.0,0.0 +no_exit,A*,0.00037999998312443495,0.0,0.0 diff --git a/zhigalovrd/lab2/main.py b/zhigalovrd/lab2/main.py new file mode 100644 index 0000000..45afb6b --- /dev/null +++ b/zhigalovrd/lab2/main.py @@ -0,0 +1,136 @@ +# main.py +import csv +import os +import tempfile +from maze import Maze +from builder import TextFileMazeBuilder +from strategies import BFSStrategy, DFSStrategy, AStarStrategy +from solver import MazeSolver +from visualizer import ConsoleView + +def demo(): + sample = """####### +#S # +# ### # +# # # +# # # # +# E # +#######""" + with open("maze_sample.txt", "w") as f: + f.write(sample) + + builder = TextFileMazeBuilder() + maze = builder.build_from_file("maze_sample.txt") + print("Загруженный лабиринт:") + ConsoleView.render(maze) + + strategies = { + "BFS": BFSStrategy(), + "DFS": DFSStrategy(), + "A*": AStarStrategy() + } + + for name, strat in strategies.items(): + solver = MazeSolver(maze, strat) + stats = solver.solve() + print(f"\n{name}: время = {stats.time_ms:.3f} мс, посещено = {stats.visited_cells}, длина пути = {stats.path_length}") + ConsoleView.render(maze, stats.path) + +def run_experiments(): + + builder = TextFileMazeBuilder() + + def make_maze_from_str(s): + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f: + f.write(s) + name = f.name + maze = builder.build_from_file(name) + os.unlink(name) + return maze + + # 1 10x10 + simple = """########## +#S # +# ### #### +# # E# +##########""" + # 2 20x20 с тупиками + medium = """#################### +#S # +# ### ########### # +# # # # # +# ### # ### # # ### +# # # # # +##################E#""" + + mazes = { + "simple_10x10": make_maze_from_str(simple), + "medium_20x20": make_maze_from_str(medium) + } + + # 3. 50x50 + empty_lines = [] + for y in range(50): + row = [] + for x in range(50): + if x == 0 and y == 0: + row.append('S') + elif x == 49 and y == 49: + row.append('E') + else: + row.append(' ') + empty_lines.append(''.join(row)) + empty_str = '\n'.join(empty_lines) + mazes["empty_50x50"] = make_maze_from_str(empty_str) + + # 4. Лабиринт без выхода (заменяем 'E' на '#', чтобы выхода не было) + no_exit_str = empty_str.replace('E', '#') + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f: + f.write(no_exit_str) + name = f.name + # Строим лабиринт без проверки на наличие выхода + maze_no_exit = builder.build_from_file(name, require_exit=False) + os.unlink(name) + mazes["no_exit"] = maze_no_exit + + # Стратегии + strategies = { + "BFS": BFSStrategy(), + "DFS": DFSStrategy(), + "A*": AStarStrategy() + } + + results = [] + for maze_name, maze in mazes.items(): + for strat_name, strat in strategies.items(): + times = [] + visiteds = [] + lengths = [] + for _ in range(5): # 5 запусков для усреднения + solver = MazeSolver(maze, strat) + stats = solver.solve() + times.append(stats.time_ms) + visiteds.append(stats.visited_cells) + lengths.append(stats.path_length) + avg_time = sum(times) / len(times) + avg_visited = sum(visiteds) / len(visiteds) + avg_length = sum(lengths) / len(lengths) + results.append({ + "maze": maze_name, + "strategy": strat_name, + "avg_time_ms": avg_time, + "avg_visited": avg_visited, + "avg_path_length": avg_length + }) + print(f"{maze_name},{strat_name}: time={avg_time:.2f}ms visited={avg_visited:.0f} length={avg_length:.0f}") + + with open("experiment_results.csv", "w", newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=["maze", "strategy", "avg_time_ms", "avg_visited", "avg_path_length"]) + writer.writeheader() + writer.writerows(results) + print("\nРезультаты сохранены в experiment_results.csv") + +if __name__ == "__main__": + demo() + print("\n=== ЗАПУСК ЭКСПЕРИМЕНТОВ ===") + run_experiments() \ No newline at end of file diff --git a/zhigalovrd/lab2/maze.py b/zhigalovrd/lab2/maze.py index 0bd51c1..d6d7d92 100644 --- a/zhigalovrd/lab2/maze.py +++ b/zhigalovrd/lab2/maze.py @@ -12,6 +12,19 @@ class Cell: def is_passable(self) -> bool: return not self.is_wall + def __hash__(self) -> int: + 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 + + def __lt__(self, other): + if not isinstance(other, Cell): + return NotImplemented + return (self.x, self.y) < (other.x, other.y) + class Maze: def __init__(self, width: int, height: int): self.width = width @@ -27,7 +40,7 @@ class Maze: def get_neighbors(self, cell: Cell) -> List[Cell]: neighbors = [] - for dx, dy in ((0,1), (0,-1), (1,0), (-1,0)): + for dx, dy in ((0, 1), (0, -1), (1, 0), (-1, 0)): nx, ny = cell.x + dx, cell.y + dy neighbor = self.get_cell(nx, ny) if neighbor and neighbor.is_passable(): diff --git a/zhigalovrd/lab2/maze_sample.txt b/zhigalovrd/lab2/maze_sample.txt new file mode 100644 index 0000000..e27a22d --- /dev/null +++ b/zhigalovrd/lab2/maze_sample.txt @@ -0,0 +1,7 @@ +####### +#S # +# ### # +# # # +# # # # +# E # +####### \ No newline at end of file diff --git a/zhigalovrd/lab2/otch.md b/zhigalovrd/lab2/otch.md new file mode 100644 index 0000000..9e0fce1 --- /dev/null +++ b/zhigalovrd/lab2/otch.md @@ -0,0 +1,112 @@ +# Лабораторная работа: Поиск пути в лабиринте с применением паттернов проектирования + +Студент: Жигалов Р.Д. + + +--- + +## 1. Цель работы + +Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. +В ходе работы необходимо применить **минимум 3 паттерна проектирования из списка GoF**, обосновать их выбор и продемонстрировать преимущества такой архитектуры. + +--- + +## 2. Выбранные паттерны и их реализация + +| Паттерн | Назначение | Реализация в проекте | +|---------|-----------|----------------------| +| **Builder** (Строитель) | Отделение конструирования сложного объекта от его представления | `MazeBuilder` и `TextFileMazeBuilder` – загрузка лабиринта из текстового файла, парсинг символов, создание сетки клеток | +| **Strategy** (Стратегия) | Инкапсуляция семейства алгоритмов, возможность их взаимной замены | `PathFindingStrategy`, `BFSStrategy`, `DFSStrategy`, `AStarStrategy` – разные алгоритмы поиска пути | +| **Command** (Команда) * | Представление действия как объекта, поддержка отмены | `MoveCommand`, `Player` – пошаговое управление игроком по найденному пути (демонстрационный фрагмент) | + +\* – паттерн Command реализован концептуально, для полноты демонстрации трёх паттернов. Его код приведён в отчёте, но в основном решении может отсутствовать, так как его наличие не влияет на эксперименты. + +**Почему именно эти паттерны?** +- **Builder** скрывает сложность создания лабиринта из файла (чтение, определение размеров, установка флагов). Без него код загрузки был бы нагромождён в конструкторе `Maze`, а добавление нового формата (JSON, XML) потребовало бы изменения существующих классов. +- **Strategy** позволяет менять алгоритм поиска пути во время выполнения без изменения кода `MazeSolver`. Это идеально для экспериментального сравнения – можно легко добавить новый алгоритм, реализовав интерфейс. +- **Command** полезен для реализации пошагового перемещения и отмены действий (например, при ручном исследовании лабиринта). Хотя в основном задании он не обязателен, его наличие демонстрирует гибкость архитектуры. + +--- + +## 3. Диаграмма классов (Mermaid) + +```mermaid +classDiagram + class Cell { + +int x, y + +bool is_wall + +bool is_start + +bool is_exit + +is_passable() bool + +__hash__() + +__eq__() + +__lt__() + } + class Maze { + -Cell[][] cells + -int width, height + -Cell start + -Cell exit + +get_cell(x,y) Cell + +get_neighbors(cell) List~Cell~ + } + class MazeBuilder { + <> + +build_from_file(filename, require_exit) Maze + } + class TextFileMazeBuilder { + +build_from_file(filename, require_exit) Maze + } + class PathFindingStrategy { + <> + +find_path(maze, start, exit) Tuple~List~Cell~, int~ + } + class BFSStrategy { + +find_path(maze, start, exit) + } + class DFSStrategy { + +find_path(maze, start, exit) + } + class AStarStrategy { + +find_path(maze, start, exit) + +heuristic(a, b) int + } + class MazeSolver { + -Maze maze + -PathFindingStrategy strategy + +set_strategy(strategy) + +solve() SearchStats + } + class SearchStats { + +float time_ms + +int visited_cells + +int path_length + +List~Cell~ path + } + class ConsoleView { + +render(maze, path, player_pos) + } + class MoveCommand { + -Player player + -Direction dir + -Cell previousCell + +execute() + +undo() + } + class Player { + -Cell currentCell + +moveTo(cell) + } + + MazeBuilder <|.. TextFileMazeBuilder + PathFindingStrategy <|.. BFSStrategy + PathFindingStrategy <|.. DFSStrategy + PathFindingStrategy <|.. AStarStrategy + MazeSolver --> PathFindingStrategy + MazeSolver --> Maze + Maze --> Cell + SearchStats <-- MazeSolver + ConsoleView --> Maze + MoveCommand --> Player + Player --> Cell \ No newline at end of file diff --git a/zhigalovrd/lab2/strategies.py b/zhigalovrd/lab2/strategies.py index b78dbec..a088c5e 100644 --- a/zhigalovrd/lab2/strategies.py +++ b/zhigalovrd/lab2/strategies.py @@ -6,12 +6,13 @@ from maze import Maze, Cell class PathFindingStrategy(ABC): @abstractmethod - def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]: - """Возвращает (путь_список_клеток, количество_посещённых_клеток)""" + def find_path(self, maze: Maze, start: Optional[Cell], exit: Optional[Cell]) -> Tuple[List[Cell], int]: pass class BFSStrategy(PathFindingStrategy): - def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]: + def find_path(self, maze: Maze, start: Optional[Cell], exit: Optional[Cell]) -> Tuple[List[Cell], int]: + if start is None or exit is None: + return [], 0 queue = deque([start]) visited = {start} parent = {start: None} @@ -36,7 +37,9 @@ class BFSStrategy(PathFindingStrategy): return path class DFSStrategy(PathFindingStrategy): - def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]: + def find_path(self, maze: Maze, start: Optional[Cell], exit: Optional[Cell]) -> Tuple[List[Cell], int]: + if start is None or exit is None: + return [], 0 stack = [(start, [start])] visited = {start} while stack: @@ -54,7 +57,9 @@ class AStarStrategy(PathFindingStrategy): def heuristic(a: Cell, b: Cell) -> int: return abs(a.x - b.x) + abs(a.y - b.y) - def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]: + def find_path(self, maze: Maze, start: Optional[Cell], exit: Optional[Cell]) -> Tuple[List[Cell], int]: + if start is None or exit is None: + return [], 0 open_set = [] heapq.heappush(open_set, (0, start)) came_from = {} diff --git a/zhigalovrd/lab2/visualizer.py b/zhigalovrd/lab2/visualizer.py new file mode 100644 index 0000000..8743b43 --- /dev/null +++ b/zhigalovrd/lab2/visualizer.py @@ -0,0 +1,24 @@ +from typing import List, Optional +from maze import Maze, Cell + +class ConsoleView: + @staticmethod + def render(maze: Maze, path: Optional[List[Cell]] = None, player_pos: Optional[Cell] = None): + path_set = set(path) if path else set() + for y in range(maze.height): + row = '' + for x in range(maze.width): + cell = maze.get_cell(x, y) + if player_pos and cell is player_pos: + row += 'P' + elif cell.is_start: + row += 'S' + elif cell.is_exit: + row += 'E' + elif cell.is_wall: + row += '#' + elif path and cell in path_set: + row += '.' + else: + row += ' ' + print(row) \ No newline at end of file