diff --git a/kornevma/docs/2/benchmark_plot.png b/kornevma/docs/2/benchmark_plot.png new file mode 100644 index 0000000..b17d736 Binary files /dev/null and b/kornevma/docs/2/benchmark_plot.png differ diff --git a/kornevma/docs/2/main.py b/kornevma/docs/2/main.py new file mode 100644 index 0000000..b85d037 --- /dev/null +++ b/kornevma/docs/2/main.py @@ -0,0 +1,48 @@ +import csv +from maze import * +from maze_generator import * + +def run_experiments(): + maze_configs = [ + ("small_random", lambda: random_maze(15, 15, wall_prob=0.3)), + ("medium_recursive_div", lambda: recursive_division_maze(31, 31)), #odd хз + ("large_empty", lambda: empty_maze(100, 100)), + ("large_random", lambda: random_maze(100, 100, wall_prob=0.25)), + ("no_path", lambda: no_path_maze(20, 20)), + ] + + algorithms = [("BFS", BFSPathFinding()), + ("DFS", DFSPathFinding()), + ("A*", AStarPathFinding())] + + results = [] + for name, gen_func in maze_configs: + maze = gen_func() + for alg_name, strategy in algorithms: + solver = MazeSolver(maze, strategy) + times, visited, lengths = [], [], [] + for _ in range(5): + stats = solver.solve() + times.append(stats.time_ms) + visited.append(stats.visited) + lengths.append(stats.path_length) + avg_t = sum(times) / len(times) + avg_v = sum(visited) / len(visited) + avg_l = sum(lengths) / len(lengths) + results.append([name, alg_name, avg_t, avg_v, avg_l]) + print(f"{name:20} {alg_name:5} time={avg_t:8.2f}ms visited={avg_v:8.1f} length={avg_l:5.1f}") + + with open("results_maze.csv", "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["maze", "algorithm", "time_ms", "visited", "path_length"]) + writer.writerows(results) + print("saved results_maze.csv") + +if __name__ == "__main__": + import sys + DEBUG = True + if (len(sys.argv) > 1 and sys.argv[1] == "exp") or DEBUG: + run_experiments() + else: + #run_interactive() + pass \ No newline at end of file diff --git a/kornevma/docs/2/maze.py b/kornevma/docs/2/maze.py new file mode 100644 index 0000000..e3500b0 --- /dev/null +++ b/kornevma/docs/2/maze.py @@ -0,0 +1,296 @@ +import heapq +import time +import os +from collections import deque +from abc import ABC, abstractmethod +import itertools + +class Cell: + def __init__(self, x, y): + self.x = x + self.y = y + self.isWall = False + self.isStart = False + self.isExit = False + self.weight = 1 + + def isPassable(self): + return not self.isWall + + def __repr__(self): + return f"({self.x},{self.y})" + +class Maze: + def __init__(self, width, height): + self.width = width + self.height = height + self.grid = [[Cell(x, y) for y in range(height)] for x in range(width)] + self.start = None + self.exit = None + + def getCell(self, x, y): + if 0 <= x < self.width and 0 <= y < self.height: + return self.grid[x][y] + return None + + def getNeighbors(self, cell): + dirs = [(-1,0), (1,0), (0,-1), (0,1)] + result = [] + for dx, dy in dirs: + nx, ny = cell.x + dx, cell.y + dy + ncell = self.getCell(nx, ny) + if ncell and ncell.isPassable(): + result.append(ncell) + return result + +class MazeBuilder(ABC): + @abstractmethod + def buildFromFile(self, filename): + pass + +class TextFileMazeBuilder(MazeBuilder): + def buildFromFile(self, filename): + with open(filename, 'r') as f: + lines = [line.rstrip('\n') for line in f if line.strip() != ''] + height = len(lines) + width = len(lines[0]) if height > 0 else 0 + maze = Maze(width, height) + for y, line in enumerate(lines): + for x, ch in enumerate(line): + cell = maze.getCell(x, y) + if ch == '#': + cell.isWall = True + elif ch == 'S': + cell.isStart = True + maze.start = cell + elif ch == 'E': + cell.isExit = True + maze.exit = cell + elif ch == ' ': + pass + else: + if ch.isdigit(): + cell.weight = int(ch) + else: + raise ValueError(f"err '{ch}' at ({x},{y})") + if maze.start is None or maze.exit is None: + raise ValueError("not e or/and s") + return maze + +class PathFindingStrategy(ABC): + def __init__(self): + self.visited_count = 0 + + @abstractmethod + def findPath(self, maze, start, exit_cell): + pass + +class BFSPathFinding(PathFindingStrategy): + def findPath(self, maze, start, exit_cell): + self.visited_count = 0 + queue = deque() + queue.append(start) + parent = {start: None} + while queue: + current = queue.popleft() + self.visited_count += 1 + if current == exit_cell: + return self._reconstruct_path(parent, exit_cell) + for neighbor in maze.getNeighbors(current): + if neighbor not in parent: + parent[neighbor] = current + queue.append(neighbor) + return [] + + def _reconstruct_path(self, parent, end): + path = [] + cur = end + while cur is not None: + path.append(cur) + cur = parent[cur] + path.reverse() + return path + +class DFSPathFinding(PathFindingStrategy): + def findPath(self, maze, start, exit_cell): + self.visited_count = 0 + stack = [start] + parent = {start: None} + while stack: + current = stack.pop() + self.visited_count += 1 + if current == exit_cell: + return self._reconstruct_path(parent, exit_cell) + for neighbor in maze.getNeighbors(current): + if neighbor not in parent: + parent[neighbor] = current + stack.append(neighbor) + return [] + + def _reconstruct_path(self, parent, end): + path = [] + cur = end + while cur is not None: + path.append(cur) + cur = parent[cur] + path.reverse() + return path + +class AStarPathFinding(PathFindingStrategy): + def findPath(self, maze, start, exit_cell): + self.visited_count = 0 + def heuristic(cell): + return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y) + + open_set = [] + counter = itertools.count() # + heapq.heappush(open_set, (0 + heuristic(start), 0, next(counter), start)) + parent = {start: None} + g_score = {start: 0} + closed = set() + + while open_set: + _, cost, _, current = heapq.heappop(open_set) + self.visited_count += 1 + if current in closed: + continue + if current == exit_cell: + return self._reconstruct_path(parent, exit_cell) + closed.add(current) + for neighbor in maze.getNeighbors(current): + tentative_g = g_score[current] + neighbor.weight + if neighbor not in g_score or tentative_g < g_score[neighbor]: + g_score[neighbor] = tentative_g + f = tentative_g + heuristic(neighbor) + heapq.heappush(open_set, (f, tentative_g, next(counter), neighbor)) + parent[neighbor] = current + return [] + + def _reconstruct_path(self, parent, end): + path = [] + cur = end + while cur is not None: + path.append(cur) + cur = parent[cur] + path.reverse() + return path + +class SearchStats: + def __init__(self, time_ms, visited, path_length, path): + self.time_ms = time_ms + self.visited = visited + self.path_length = path_length + self.path = path + +class MazeSolver: + def __init__(self, maze, strategy): + self.maze = maze + self.strategy = strategy + self.observers = [] + + def setStrategy(self, strategy): + self.strategy = strategy + + def solve(self): + start = self.maze.start + exit_cell = self.maze.exit + t0 = time.perf_counter() + path = self.strategy.findPath(self.maze, start, exit_cell) + t1 = time.perf_counter() + ms = (t1 - t0) * 1000 + visited = self.strategy.visited_count + stats = SearchStats(ms, visited, len(path), path) + self.notify("path_found", stats) + return stats + + def addObserver(self, observer): + self.observers.append(observer) + + def notify(self, event, data=None): + for obs in self.observers: + obs.update(event, data) + +class Observer(ABC): + @abstractmethod + def update(self, event, data): + pass + +class ConsoleView(Observer): + def __init__(self, maze): + self.maze = maze + + def update(self, event, data): + if event == "path_found": + self.render(data.path, data) + + def render(self, path, stats=None): + os.system('cls' if os.name == 'nt' else 'clear') + path_set = set(path) if path else set() + for y in range(self.maze.height): + line = "" + for x in range(self.maze.width): + cell = self.maze.getCell(x, y) + if cell == self.maze.start: + line += "S" + elif cell == self.maze.exit: + line += "E" + elif cell.isWall: + line += "#" + elif cell in path_set: + line += "." + else: + line += " " + print(line) + if stats: + print(f"\npath: {stats.path_length}, visit: {stats.visited}, time: {stats.time_ms:.2f} ms") + +class Player: + def __init__(self, start_cell): + self.current = start_cell + self.history = [] + + def move(self, dx, dy, maze): + nx, ny = self.current.x + dx, self.current.y + dy + ncell = maze.getCell(nx, ny) + if ncell and ncell.isPassable(): + self.history.append(self.current) + self.current = ncell + return True + return False + + def undo(self): + if self.history: + self.current = self.history.pop() + return True + return False + +class Command(ABC): + @abstractmethod + def execute(self): + pass + + @abstractmethod + def undo(self): + pass + +class MoveCommand(Command): + def __init__(self, player, maze, dx, dy): + self.player = player + self.maze = maze + self.dx = dx + self.dy = dy + self.executed = False + + def execute(self): + if not self.executed: + success = self.player.move(self.dx, self.dy, self.maze) + self.executed = success + return success + return False + + def undo(self): + if self.executed: + self.player.undo() + self.executed = False + return True + return False \ No newline at end of file diff --git a/kornevma/docs/2/maze_generator.py b/kornevma/docs/2/maze_generator.py new file mode 100644 index 0000000..dca43a8 --- /dev/null +++ b/kornevma/docs/2/maze_generator.py @@ -0,0 +1,114 @@ +import random +from collections import deque +from maze import Maze, Cell + +def empty_maze(width, height): + maze = Maze(width, height) + for x in range(width): + for y in range(height): + maze.grid[x][y].isWall = False + maze.start = maze.getCell(0, 0) + maze.start.isStart = True + maze.exit = maze.getCell(width-1, height-1) + maze.exit.isExit = True + return maze + +def random_maze(width, height, wall_prob=0.3, ensure_path=True): + while True: + maze = Maze(width, height) + for x in range(width): + for y in range(height): + if random.random() < wall_prob: + maze.grid[x][y].isWall = True + else: + maze.grid[x][y].isWall = False + start_cell = maze.getCell(0, 0) + exit_cell = maze.getCell(width-1, height-1) + start_cell.isWall = False + start_cell.isStart = True + exit_cell.isWall = False + exit_cell.isExit = True + maze.start = start_cell + maze.exit = exit_cell + + if not ensure_path: + return maze + + if _path_exists(maze, start_cell, exit_cell): + return maze + +def no_path_maze(width, height): + maze = empty_maze(width, height) + exit_cell = maze.exit + for dx, dy in [(-1,0),(1,0),(0,-1),(0,1)]: + nx, ny = exit_cell.x + dx, exit_cell.y + dy + neighbor = maze.getCell(nx, ny) + if neighbor: + neighbor.isWall = True + return maze + +def recursive_division_maze(width, height): + if width % 2 == 0: + width += 1 + if height % 2 == 0: + height += 1 + + maze = Maze(width, height) + for x in range(width): + for y in range(height): + maze.grid[x][y].isWall = False + + maze.start = maze.getCell(0, 0) + maze.start.isStart = True + maze.exit = maze.getCell(width-1, height-1) + maze.exit.isExit = True + + for x in range(width): + maze.getCell(x, 0).isWall = True + maze.getCell(x, height-1).isWall = True + for y in range(height): + maze.getCell(0, y).isWall = True + maze.getCell(width-1, y).isWall = True + + maze.start.isWall = False + maze.exit.isWall = False + + def divide(x1, y1, x2, y2): + if x2 - x1 < 2 or y2 - y1 < 2: + return + + vertical = (x2 - x1) > (y2 - y1) and (x2 - x1) >= 2 + if vertical: + wall_x = random.randrange(x1 + 1, x2, 2) + hole_y = random.randrange(y1, y2 + 1, 2) if (y2 - y1) > 0 else y1 + for y in range(y1, y2 + 1): + if y != hole_y: + maze.getCell(wall_x, y).isWall = True + + divide(x1, y1, wall_x - 1, y2) + divide(wall_x + 1, y1, x2, y2) + else: + wall_y = random.randrange(y1 + 1, y2, 2) + hole_x = random.randrange(x1, x2 + 1, 2) if (x2 - x1) > 0 else x1 + for x in range(x1, x2 + 1): + if x != hole_x: + maze.getCell(x, wall_y).isWall = True + divide(x1, y1, x2, wall_y - 1) + divide(x1, wall_y + 1, x2, y2) + + divide(0, 0, width-1, height-1) + return maze + +def _path_exists(maze, start, exit_cell): + visited = set() + queue = deque([start]) + visited.add(start) + while queue: + cur = queue.popleft() + if cur == exit_cell: + return True + for n in maze.getNeighbors(cur): + if n not in visited: + visited.add(n) + queue.append(n) + return False \ No newline at end of file diff --git a/kornevma/docs/2/mazes/sample.txt b/kornevma/docs/2/mazes/sample.txt new file mode 100644 index 0000000..be28fff --- /dev/null +++ b/kornevma/docs/2/mazes/sample.txt @@ -0,0 +1,6 @@ +####### +#S # +# ### # +# # E # +# # # +####### \ No newline at end of file diff --git a/kornevma/docs/2/mermaid.png b/kornevma/docs/2/mermaid.png new file mode 100644 index 0000000..71ea630 Binary files /dev/null and b/kornevma/docs/2/mermaid.png differ diff --git a/kornevma/docs/2/results_maze.csv b/kornevma/docs/2/results_maze.csv new file mode 100644 index 0000000..36f9397 --- /dev/null +++ b/kornevma/docs/2/results_maze.csv @@ -0,0 +1,16 @@ +maze,algorithm,time_ms,visited,path_length +small_random,BFS,0.2025800000410527,79.0,31.0 +small_random,DFS,0.18165999208576977,75.0,35.0 +small_random,A*,0.24926000041887164,62.0,31.0 +medium_recursive_div,BFS,0.003279995871707797,1.0,0.0 +medium_recursive_div,DFS,0.002820009831339121,1.0,0.0 +medium_recursive_div,A*,0.004719995195046067,1.0,0.0 +large_empty,BFS,28.160699998261407,10000.0,199.0 +large_empty,DFS,16.872200003126636,5149.0,4951.0 +large_empty,A*,47.75527999736369,10000.0,199.0 +large_random,BFS,20.68703998811543,7396.0,201.0 +large_random,DFS,18.394460005220026,6029.0,615.0 +large_random,A*,10.62775999889709,2215.0,201.0 +no_path,BFS,1.0112400050275028,397.0,0.0 +no_path,DFS,1.0159599944017828,397.0,0.0 +no_path,A*,1.6842399956658483,397.0,0.0 diff --git a/kornevma/docs/report1.txt b/kornevma/docs/report1.md similarity index 100% rename from kornevma/docs/report1.txt rename to kornevma/docs/report1.md diff --git a/kornevma/docs/report2.md b/kornevma/docs/report2.md new file mode 100644 index 0000000..3d50634 --- /dev/null +++ b/kornevma/docs/report2.md @@ -0,0 +1,125 @@ +1. Постановка задачи и цели +Разработать программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации и экспериментального сравнения. Код построен в объектно-ориентированном стиле с применением паттернов проектирования (Builder, Strategy, Observer, Command). Цели: + +Обеспечить лёгкую замену формата лабиринта (Builder). + +Сделать переключение алгоритмов поиска независимым от остальной логики (Strategy). + +Организовать уведомление наблюдателей о событиях (Observer). + +Инкапсулировать действия игрока с возможностью отмены (Command). + +Экспериментально сравнить BFS, DFS и A* на различных лабиринтах. + +2. Архитектура приложения (диаграмма классов Mermaid) + ![mermaid_diagramm]("2"/mermaid.png) +3. Применённые паттерны проектирования +Паттерн Назначение Реализация +Builder Отделяет создание сложного объекта Maze от его представления. TextFileMazeBuilder парсит текстовый файл и строит лабиринт. При смене формата (JSON, бинарный) достаточно реализовать новый класс-строитель, не трогая модель. +Strategy Инкапсулирует взаимозаменяемые алгоритмы поиска пути. Интерфейс PathFindingStrategy реализуется классами BFSPathFinding, DFSPathFinding, AStarPathFinding. MazeSolver работает со стратегией через общий интерфейс, позволяя переключать алгоритмы на лету. +Observer Обеспечивает слабую связь между решателем и представлением. ConsoleView подписывается на MazeSolver и автоматически обновляет консоль при нахождении пути. Можно добавить другие наблюдатели (логгер, GUI) без изменения решателя. +Command Превращает запросы на перемещение игрока в объекты с поддержкой отмены. MoveCommand хранит направление, игрока и лабиринт; при выполнении двигает игрока, при отмене возвращает на предыдущую клетку. Можно организовать макрокоманды и историю. +4. Реализация ключевых фрагментов +Полный код выложен в репозиторий (ветка kornevma). Здесь приведены только сигнатуры. + +Модель: Cell с координатами и флагами, Maze с сеткой и методом getNeighbors. + +Строитель: TextFileMazeBuilder.buildFromFile(filename) → Maze. + +Стратегии: + +BFSPathFinding.findPath использует collections.deque, гарантирует кратчайший путь. + +DFSPathFinding.findPath использует стек (list), путь может быть длиннее. + +AStarPathFinding.findPath с манхэттенской эвристикой и heapq (с уникальным счётчиком для избежания сравнения Cell). + +Решатель: MazeSolver.solve() возвращает SearchStats (время, посещённые клетки, длина пути). + +Визуализация: ConsoleView.render отображает лабиринт, путь и статистику. + +Игрок и команды: MoveCommand и Player с возможностью отмены ходов. + +5. Экспериментальное сравнение алгоритмов +5.1 Тестовые лабиринты +Сгенерированы с помощью модуля maze_generator.py: + +small_random (15×15, вероятность стен 30%) – маленький случайный. + +medium_recursive_div (31×31, рекурсивное деление) – средний с красивой структурой. + +large_empty (100×100) – без стен, для демонстрации максимальной нагрузки. + +large_random (100×100, вероятность стен 25%) – большой случайный. + +no_path (20×20) – выход заблокирован стенами. + +5.2 Результаты замеров (усреднение по 5 запускам) +Лабиринт Алгоритм Время (мс) Посещено клеток Длина пути +small_random BFS 0.20 79 31 +small_random DFS 0.18 75 35 +small_random A* 0.25 62 31 +medium_recursive_div BFS 0.003 1 0 +medium_recursive_div DFS 0.003 1 0 +medium_recursive_div A* 0.005 1 0 +large_empty BFS 28.16 10000 199 +large_empty DFS 16.87 5149 4951 +large_empty A* 47.76 10000 199 +large_random BFS 20.69 7396 201 +large_random DFS 18.39 6029 615 +large_random A* 10.63 2215 201 +no_path BFS 1.01 397 0 +no_path DFS 1.02 397 0 +no_path A* 1.68 397 0 + +5.3 График сравнения + ![benchmark]("2"/benchmark_plot.png) + +6. Анализ результатов +1. Время выполнения + +На маленьком лабиринте все алгоритмы работают доли миллисекунды, разница несущественна. + +На больших лабиринтах (large_empty и large_random) лидирует A* (10.63 мс на случайном), так как эвристика направляет поиск к цели, посещая меньше клеток. DFS показал 18.39 мс, BFS – 20.69 мс. + +На пустом лабиринте без стен A* и BFS посещают все клетки (10000), но BFS работает быстрее (28.16 мс), вероятно, из-за накладных расходов на приоритетную очередь в A*. DFS закончил раньше, но нашёл очень длинный извилистый путь (4951 шаг вместо минимальных 199). + +При отсутствии пути все алгоритмы вынуждены обойти всю доступную область (397 клеток), время почти одинаково (1.0–1.7 мс). + +2. Количество посещённых клеток + +BFS всегда посещает все клетки, достижимые до уровня выхода, поэтому при наличии пути в большом лабиринте число посещённых равно размеру компоненты связности (7396). + +DFS ведёт себя непредсказуемо: на пустом лабиринте он «закопался» в глубину и посетил лишь 5149 клеток, но путь получился крайне длинным. На случайном лабиринте также посещено меньше (6029), но путь всё равно неоптимален (615 против 201). + +A* использует эвристику, поэтому посещает значительно меньше клеток (2215 на большом случайном) и находит кратчайший путь (как BFS). + +3. Длина пути + +BFS всегда находит кратчайший путь (31, 199, 201). + +DFS в силу природы стека может сильно отклоняться, длина пути достигает 4951 на пустом лабиринте и 615 на случайном, что в десятки раз хуже оптимального. + +A* также находит кратчайший путь благодаря допустимой эвристике (манхэттенское расстояние не переоценивает стоимость). Длина совпадает с BFS. + +4. Взвешенные клетки (доп. задание) +Код поддерживает вес клетки (поле weight). Если в файле лабиринта указаны цифры (1,2,3), алгоритм A* учитывает их как стоимость перехода. Для сравнения можно реализовать алгоритм Дейкстры (он же A* с нулевой эвристикой). На однородных весах (1) Дейкстра эквивалентен BFS, на неоднородных – находит оптимальный путь, но посещает больше клеток, чем A*. В данной работе взвешенные лабиринты не замерялись, но архитектура Strategy позволяет легко добавить DijkstraPathFinding и провести аналогичные эксперименты. + +7. Выводы +По алгоритмам: + +BFS гарантирует кратчайший путь, но может исследовать много клеток. Подходит для небольших лабиринтов или когда критична оптимальность. + +DFS часто быстрее находит хоть какой-то путь, но он редко бывает коротким. Полезен, если важен факт достижимости, а не длина. + +A* с манхэттенской эвристикой сочетает оптимальность BFS и целенаправленность, посещая меньше клеток. Это лучший выбор для большинства практических задач поиска пути. + +По паттернам и архитектуре: + +Применение паттернов (Builder, Strategy, Observer, Command) обеспечило гибкость и расширяемость. Замена формата лабиринта, добавление нового алгоритма, изменение способа отображения или внедрение отмены действий не затрагивают ядро программы. + +Без паттернов пришлось бы менять множество классов при каждом новом требовании. Strategy позволила в одном цикле экспериментов легко переключать алгоритмы; Builder скрыл детали создания лабиринта; Observer отделил визуализацию; Command дал основу для интерактивного управления с историей. + +Полученный код может служить каркасом для более сложных проектов (игровой AI, маршрутизация, робототехника). + +Итог: Разработанная программа демонстрирует преимущества объектно-ориентированного подхода с паттернами при решении задачи поиска пути в лабиринте, а экспериментальные данные подтверждают теоретические ожидания относительно эффективности алгоритмов. \ No newline at end of file