diff --git a/BolonkinNM/.idea/inspectionProfiles/profiles_settings.xml b/BolonkinNM/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/BolonkinNM/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/BolonkinNM/.idea/maze_project_submission.iml b/BolonkinNM/.idea/maze_project_submission.iml new file mode 100644 index 0000000..8e5446a --- /dev/null +++ b/BolonkinNM/.idea/maze_project_submission.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/BolonkinNM/.idea/misc.xml b/BolonkinNM/.idea/misc.xml new file mode 100644 index 0000000..0ebfc91 --- /dev/null +++ b/BolonkinNM/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/BolonkinNM/.idea/modules.xml b/BolonkinNM/.idea/modules.xml new file mode 100644 index 0000000..a636c96 --- /dev/null +++ b/BolonkinNM/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/BolonkinNM/.idea/workspace.xml b/BolonkinNM/.idea/workspace.xml new file mode 100644 index 0000000..896a098 --- /dev/null +++ b/BolonkinNM/.idea/workspace.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + 1779637417749 + + + + \ No newline at end of file diff --git a/BolonkinNM/README.md b/BolonkinNM/README.md new file mode 100644 index 0000000..2e6e63f --- /dev/null +++ b/BolonkinNM/README.md @@ -0,0 +1,24 @@ +# Maze Solver Project + +ООП-проект для поиска выхода из лабиринта с паттернами: +- Builder +- Strategy +- Observer +- Command + +## Запуск +```bash +python main.py +``` + +## Эксперименты +```bash +python experiment.py +``` + +Результаты сохраняются в папку `experiment_results/`. + +## Требования +```bash +pip install -r requirements.txt +``` diff --git a/BolonkinNM/builders/__init__.py b/BolonkinNM/builders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/BolonkinNM/builders/maze_builder.py b/BolonkinNM/builders/maze_builder.py new file mode 100644 index 0000000..b055db8 --- /dev/null +++ b/BolonkinNM/builders/maze_builder.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod + + +class MazeBuilder(ABC): + @abstractmethod + def buildFromFile(self, filename): + raise NotImplementedError diff --git a/BolonkinNM/builders/text_file_maze_builder.py b/BolonkinNM/builders/text_file_maze_builder.py new file mode 100644 index 0000000..5e9ca03 --- /dev/null +++ b/BolonkinNM/builders/text_file_maze_builder.py @@ -0,0 +1,52 @@ +from core.cell import Cell +from core.maze import Maze +from builders.maze_builder import MazeBuilder + + +class TextFileMazeBuilder(MazeBuilder): + def buildFromFile(self, filename): + with open(filename, "r", encoding="utf-8") as f: + lines = [line.rstrip("\n") for line in f] + + if not lines: + raise ValueError("Maze file is empty") + + width = max(len(line) for line in lines) + height = len(lines) + + cells = [] + startCell = None + exitCell = None + + for y, line in enumerate(lines): + row = [] + for x in range(width): + ch = line[x] if x < len(line) else "#" + + if ch == "#": + cell = Cell(x, y, isWall=True) + elif ch == "S": + if startCell is not None: + raise ValueError("Multiple start cells found") + cell = Cell(x, y, isWall=False, isStart=True) + startCell = cell + elif ch == "E": + if exitCell is not None: + raise ValueError("Multiple exit cells found") + cell = Cell(x, y, isWall=False, isExit=True) + exitCell = cell + elif ch in (" ", "."): + cell = Cell(x, y, isWall=False) + elif ch.isdigit(): + cell = Cell(x, y, isWall=False, weight=max(1, int(ch))) + else: + raise ValueError(f"Unsupported symbol '{ch}' at ({x}, {y})") + row.append(cell) + cells.append(row) + + if startCell is None: + raise ValueError("Start cell 'S' not found") + if exitCell is None: + raise ValueError("Exit cell 'E' not found") + + return Maze(cells, width, height, startCell, exitCell) diff --git a/BolonkinNM/commands/__init__.py b/BolonkinNM/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/BolonkinNM/commands/command.py b/BolonkinNM/commands/command.py new file mode 100644 index 0000000..71f2dc6 --- /dev/null +++ b/BolonkinNM/commands/command.py @@ -0,0 +1,11 @@ +from abc import ABC, abstractmethod + + +class Command(ABC): + @abstractmethod + def execute(self): + raise NotImplementedError + + @abstractmethod + def undo(self): + raise NotImplementedError diff --git a/BolonkinNM/commands/move_command.py b/BolonkinNM/commands/move_command.py new file mode 100644 index 0000000..e90b7f1 --- /dev/null +++ b/BolonkinNM/commands/move_command.py @@ -0,0 +1,37 @@ +from commands.command import Command + + +class MoveCommand(Command): + DIRECTION_TO_DELTA = { + "W": (0, -1), + "A": (-1, 0), + "S": (0, 1), + "D": (1, 0), + } + + def __init__(self, player, maze, direction): + self.player = player + self.maze = maze + self.direction = direction.upper() + self.previousCell = None + + def execute(self): + if self.direction not in self.DIRECTION_TO_DELTA: + return False + + dx, dy = self.DIRECTION_TO_DELTA[self.direction] + current = self.player.currentCell + new_cell = self.maze.getCell(current.x + dx, current.y + dy) + + if new_cell is None or not new_cell.isPassable(): + return False + + self.previousCell = current + self.player.setCell(new_cell) + return True + + def undo(self): + if self.previousCell is None: + return False + self.player.setCell(self.previousCell) + return True diff --git a/BolonkinNM/controller/__init__.py b/BolonkinNM/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/BolonkinNM/controller/game_controller.py b/BolonkinNM/controller/game_controller.py new file mode 100644 index 0000000..0a4cb39 --- /dev/null +++ b/BolonkinNM/controller/game_controller.py @@ -0,0 +1,30 @@ +from commands.move_command import MoveCommand + + +class GameController: + def __init__(self, maze, player, view): + self.maze = maze + self.player = player + self.view = view + self.history = [] + + def move(self, direction): + command = MoveCommand(self.player, self.maze, direction) + if command.execute(): + self.history.append(command) + self.view.update({"type": "move", "direction": direction}) + self.view.render(self.maze, player_position=self.player.currentCell) + return True + print("Cannot move there") + return False + + def undo(self): + if not self.history: + print("Nothing to undo") + return False + command = self.history.pop() + if command.undo(): + self.view.update({"type": "undo"}) + self.view.render(self.maze, player_position=self.player.currentCell) + return True + return False diff --git a/BolonkinNM/core/__init__.py b/BolonkinNM/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/BolonkinNM/core/cell.py b/BolonkinNM/core/cell.py new file mode 100644 index 0000000..44e2d76 --- /dev/null +++ b/BolonkinNM/core/cell.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass + + +@dataclass +class Cell: + x: int + y: int + isWall: bool = False + isStart: bool = False + isExit: bool = False + weight: int = 1 + + def isPassable(self): + return not self.isWall + + def __repr__(self): + parts = [f"Cell({self.x}, {self.y}"] + if self.isWall: + parts.append("WALL") + if self.isStart: + parts.append("START") + if self.isExit: + parts.append("EXIT") + if self.weight != 1: + parts.append(f"w={self.weight}") + return ", ".join(parts) + ")" diff --git a/BolonkinNM/core/maze.py b/BolonkinNM/core/maze.py new file mode 100644 index 0000000..59c86dd --- /dev/null +++ b/BolonkinNM/core/maze.py @@ -0,0 +1,49 @@ +class Maze: + def __init__(self, cells, width, height, startCell=None, exitCell=None): + self.cells = cells + self.width = width + self.height = height + self.startCell = startCell + self.exitCell = exitCell + + def getCell(self, x, y): + if 0 <= x < self.width and 0 <= y < self.height: + return self.cells[y][x] + return None + + def getNeighbors(self, cell): + neighbors = [] + for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)): + nx, ny = cell.x + dx, cell.y + dy + neighbor = self.getCell(nx, ny) + if neighbor is not None and neighbor.isPassable(): + neighbors.append(neighbor) + return neighbors + + def render_lines(self, player_position=None, path=None): + path_set = {(c.x, c.y) for c in path} if path else set() + player_pos = None if player_position is None else (player_position.x, player_position.y) + lines = [] + for y in range(self.height): + row = [] + for x in range(self.width): + cell = self.cells[y][x] + if player_pos == (x, y): + row.append("P") + elif cell.isStart: + row.append("S") + elif cell.isExit: + row.append("E") + elif cell.isWall: + row.append("#") + elif (x, y) in path_set: + row.append("*") + elif cell.weight > 1: + row.append(str(cell.weight)) + else: + row.append(" ") + lines.append("".join(row)) + return lines + + def render(self, player_position=None, path=None): + return "\n".join(self.render_lines(player_position=player_position, path=path)) diff --git a/BolonkinNM/core/player.py b/BolonkinNM/core/player.py new file mode 100644 index 0000000..b68a0ff --- /dev/null +++ b/BolonkinNM/core/player.py @@ -0,0 +1,6 @@ +class Player: + def __init__(self, currentCell): + self.currentCell = currentCell + + def setCell(self, cell): + self.currentCell = cell diff --git a/BolonkinNM/core/search_stats.py b/BolonkinNM/core/search_stats.py new file mode 100644 index 0000000..5548118 --- /dev/null +++ b/BolonkinNM/core/search_stats.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass, field + + +@dataclass +class SearchStats: + timeMs: float + visitedCells: int + pathLength: int + path: list = field(default_factory=list) + found: bool = False + algorithm: str = "" diff --git a/BolonkinNM/docs/README.txt b/BolonkinNM/docs/README.txt new file mode 100644 index 0000000..c760a90 --- /dev/null +++ b/BolonkinNM/docs/README.txt @@ -0,0 +1 @@ +Place report files and experiment outputs here. diff --git a/BolonkinNM/docs/report.md b/BolonkinNM/docs/report.md new file mode 100644 index 0000000..8eb21e6 --- /dev/null +++ b/BolonkinNM/docs/report.md @@ -0,0 +1,249 @@ +# Отчёт по работе «Поиск выхода из лабиринта» + +## 1. Цель работы +Разработать гибкую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В работе использованы паттерны проектирования, чтобы отделить логику представления лабиринта, его загрузки, поиска пути и вывода результатов. + +## 2. Описание задачи +Лабиринт задаётся в текстовом файле символами: +- `#` — стена; +- пробел — проход; +- `S` — старт; +- `E` — выход. + +Программа должна: +- загружать лабиринт; +- строить его внутреннюю модель; +- искать путь разными алгоритмами; +- собирать статистику поиска; +- визуализировать результат в консоли; +- сравнивать стратегии на разных типах лабиринтов. + +## 3. Выбранные паттерны проектирования + +### 3.1 Builder +Паттерн Builder используется для загрузки лабиринта из файла. Он скрывает детали парсинга и валидации, а клиент получает готовый объект `Maze`. + +Преимущества: +- легко добавить новый формат загрузки; +- клиентский код не зависит от формата файла; +- создание лабиринта можно расширять без переписывания остальной программы. + +### 3.2 Strategy +Паттерн Strategy используется для выбора алгоритма поиска пути. В программе реализованы `BFS`, `DFS`, `A*`, а при необходимости можно добавить Дейкстру или любую другую стратегию. + +Преимущества: +- алгоритм можно менять во время выполнения; +- код оркестратора не зависит от конкретного метода поиска; +- новые алгоритмы добавляются без изменения существующего кода. + +### 3.3 Observer +Паттерн Observer используется для обновления консольного интерфейса при изменении состояния программы: загрузка лабиринта, поиск пути, движение игрока. + +Преимущества: +- вывод отделён от логики; +- можно заменить консольный интерфейс на графический без изменения поискового кода; +- упрощается расширение визуализации. + +### 3.4 Command +Паттерн Command используется для пошагового перемещения игрока и отмены последнего хода. + +Преимущества: +- каждое действие оформляется как отдельный объект; +- легко реализовать undo; +- история ходов хранится отдельно от логики перемещения. + +## 4. Диаграмма классов +Ниже приведена упрощённая диаграмма классов в формате Mermaid: + +```mermaid +classDiagram + class Cell { + +int x + +int y + +bool isWall + +bool isStart + +bool isExit + +isPassable() + } + + class Maze { + +cells + +width + +height + +startCell + +exitCell + +getCell(x, y) + +getNeighbors(cell) + } + + class MazeBuilder { + <> + +buildFromFile(filename) + } + + class TextFileMazeBuilder { + +buildFromFile(filename) + } + + class PathFindingStrategy { + <> + +findPath(maze, start, exitCell) + } + + class BFSStrategy { + +findPath(maze, start, exitCell) + } + + class DFSStrategy { + +findPath(maze, start, exitCell) + } + + class AStarStrategy { + +findPath(maze, start, exitCell) + } + + class SearchStats { + +timeMs + +visitedCells + +pathLength + +path + } + + class MazeSolver { + +maze + +strategy + +setStrategy(strategy) + +solve() + } + + class Observer { + <> + +update(event) + } + + class ConsoleView { + +update(event) + +render(maze, player_position, path) + } + + class Command { + <> + +execute() + +undo() + } + + class MoveCommand { + +execute() + +undo() + } + + class Player { + +currentCell + +setCell(cell) + } + + Maze <|-- TextFileMazeBuilder : creates + MazeBuilder <|.. TextFileMazeBuilder + PathFindingStrategy <|.. BFSStrategy + PathFindingStrategy <|.. DFSStrategy + PathFindingStrategy <|.. AStarStrategy + MazeSolver --> Maze + MazeSolver --> PathFindingStrategy + MazeSolver --> SearchStats + Observer <|.. ConsoleView + Command <|.. MoveCommand + MoveCommand --> Player + MoveCommand --> Maze + ConsoleView --> Maze + Maze --> Cell +``` + +## 5. Ключевые классы и их роль + +### Cell +Хранит координаты клетки и её тип. Позволяет быстро проверять, является ли клетка проходимой. + +### Maze +Содержит двумерную карту клеток, размер лабиринта, а также ссылки на старт и выход. Даёт доступ к соседним клеткам по четырём направлениям. + +### TextFileMazeBuilder +Читает текстовый файл, создаёт объекты `Cell`, определяет старт и выход, затем возвращает готовый `Maze`. + +### BFSStrategy +Ищет кратчайший путь по числу шагов. Подходит для случая, когда все переходы одинаковой стоимости. + +### DFSStrategy +Быстро исследует пространство, но не гарантирует кратчайший путь. Полезен как сравнительный алгоритм. + +### AStarStrategy +Использует эвристику Манхэттенского расстояния. Обычно посещает меньше клеток, чем BFS, если эвристика удачно направляет поиск к цели. + +### MazeSolver +Оркестратор, который хранит лабиринт и текущую стратегию. Вызывает поиск, измеряет время и собирает статистику. + +### SearchStats +Содержит итог поиска: время выполнения, количество посещённых клеток и длину пути. + +### ConsoleView +Реализует наблюдателя и умеет выводить лабиринт и найденный путь в консоль. + +### MoveCommand +Оформляет ход игрока как объект-команду. Поддерживает отмену последнего перемещения. + +## 6. Экспериментальная часть + +### 6.1 Подготовка тестовых лабиринтов +Для сравнения стратегий использовались следующие типы лабиринтов: +- маленький 10×10 с простым путём; +- средний 50×50 с тупиками; +- большой 100×100 со сложной структурой; +- пустой лабиринт без стен; +- лабиринт без выхода. + +### 6.2 Методика измерений +Для каждой стратегии и каждого лабиринта поиск запускался несколько раз, после чего вычислялись средние значения: +- время поиска в миллисекундах; +- количество посещённых клеток; +- длина найденного пути. + +Результаты сохранялись в CSV-файл в двух вариантах: +- сырой набор измерений; +- усреднённая таблица. + +## 7. Анализ эффективности + +### BFS +BFS гарантирует кратчайший путь по числу шагов, если все переходы имеют одинаковую стоимость. На простых и пустых лабиринтах работает стабильно и предсказуемо. Минус — может посещать много клеток, особенно на больших лабиринтах. + +### DFS +DFS может быстро найти какой-то путь, но он не обязательно будет кратчайшим. На сложных лабиринтах иногда работает быстро, но на других может уйти далеко от цели и пройти лишние области. + +### A* +A* использует эвристику и обычно показывает хороший баланс между скоростью и качеством пути. На больших и запутанных лабиринтах часто посещает меньше клеток, чем BFS, потому что поиск направлен в сторону выхода. + +### Лабиринт без пути +Если пути нет, все алгоритмы вынуждены исследовать доступную область. В этом случае длина пути равна 0, а различия между алгоритмами проявляются в количестве просмотренных клеток и времени выполнения. + +### Вывод по выбору алгоритма +- BFS стоит выбирать, когда нужен гарантированно кратчайший путь и веса переходов одинаковы. +- DFS полезен как простой и быстрый по реализации вариант, но без гарантии оптимальности. +- A* подходит для практических задач, где нужно ускорить поиск и сократить число посещённых клеток. +- При взвешенных переходах лучше использовать Дейкстру или взвешенный A*. + +## 8. Роль ООП и паттернов +ООП и паттерны сделали код более гибким и расширяемым. Благодаря этому: +- можно заменить алгоритм поиска без переписывания логики программы; +- можно добавить новый формат загрузки лабиринта; +- можно поменять способ визуализации; +- можно расширить управление игроком и добавить отмену действий. + +Без паттернов пришлось бы связывать загрузку, поиск, отображение и управление в один большой блок кода. Это усложнило бы отладку и дальнейшие изменения. + +## 9. Вывод +В ходе работы была создана расширяемая программа для поиска пути в лабиринте. Использование паттернов Builder, Strategy, Observer и Command позволило разделить обязанности между классами, упростить поддержку кода и сделать архитектуру удобной для дальнейшего развития. Эксперименты показали, что выбор алгоритма сильно зависит от типа лабиринта: BFS даёт кратчайший путь, DFS иногда быстрее в реализации, а A* чаще всего наиболее практичен на больших картах. + +## 10. Приложения +- Листинги ключевых классов. +- CSV-файлы с результатами экспериментов. +- Графики сравнений. +- Файлы с тестовыми лабиринтами. diff --git a/BolonkinNM/experiment.py b/BolonkinNM/experiment.py new file mode 100644 index 0000000..588f377 --- /dev/null +++ b/BolonkinNM/experiment.py @@ -0,0 +1,225 @@ +from pathlib import Path +from statistics import mean +import csv +import random + +import matplotlib.pyplot as plt + +from core.cell import Cell +from core.maze import Maze +from solver.maze_solver import MazeSolver +from strategies.astar_strategy import AStarStrategy +from strategies.bfs_strategy import BFSStrategy +from strategies.dfs_strategy import DFSStrategy +from strategies.dijkstra_strategy import DijkstraStrategy + + +BASE_DIR = Path(__file__).resolve().parent +OUT_DIR = BASE_DIR / "experiment_results" + + +def build_maze_from_symbols(lines): + height = len(lines) + width = max(len(line) for line in lines) + cells = [] + start = None + exit_cell = None + for y, line in enumerate(lines): + row = [] + for x in range(width): + ch = line[x] if x < len(line) else "#" + if ch == "#": + cell = Cell(x, y, isWall=True) + elif ch == "S": + cell = Cell(x, y, isWall=False, isStart=True) + start = cell + elif ch == "E": + cell = Cell(x, y, isWall=False, isExit=True) + exit_cell = cell + elif ch == " " or ch == ".": + cell = Cell(x, y, isWall=False) + elif ch.isdigit(): + cell = Cell(x, y, isWall=False, weight=int(ch)) + else: + raise ValueError(f"Unknown symbol '{ch}' at {x},{y}") + row.append(cell) + cells.append(row) + return Maze(cells, width, height, start, exit_cell) + + +def generate_empty_maze(width, height): + lines = [" " * width for _ in range(height)] + lines = [list(row) for row in lines] + lines[1][1] = "S" + lines[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in lines]) + + +def generate_simple_maze(width, height): + grid = [["#" for _ in range(width)] for _ in range(height)] + for x in range(1, width - 1): + grid[1][x] = " " + for y in range(1, height - 1): + grid[y][width - 2] = " " + grid[1][1] = "S" + grid[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in grid]) + + +def generate_branching_maze(width, height, seed=42, wall_density=0.30): + rng = random.Random(seed) + grid = [["#" for _ in range(width)] for _ in range(height)] + x, y = 1, 1 + grid[y][x] = "S" + while (x, y) != (width - 2, height - 2): + candidates = [] + for dx, dy in [(1, 0), (0, 1)]: + nx, ny = x + dx, y + dy + if 1 <= nx < width - 1 and 1 <= ny < height - 1: + candidates.append((nx, ny)) + if not candidates: + break + x, y = rng.choice(candidates) + grid[y][x] = " " + grid[height - 2][width - 2] = "E" + + # carve extra corridors and dead ends + for yy in range(1, height - 1): + for xx in range(1, width - 1): + if grid[yy][xx] == "#" and rng.random() > wall_density: + grid[yy][xx] = " " + grid[1][1] = "S" + grid[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in grid]) + + +def generate_no_path_maze(width, height): + grid = [[" " for _ in range(width)] for _ in range(height)] + for x in range(width): + grid[height // 2][x] = "#" + grid[1][1] = "S" + grid[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in grid]) + + +def generate_weighted_maze(width, height, seed=123): + rng = random.Random(seed) + grid = [[" " for _ in range(width)] for _ in range(height)] + for y in range(height): + for x in range(width): + r = rng.random() + if r < 0.12: + grid[y][x] = "#" + elif r < 0.25: + grid[y][x] = "3" + elif r < 0.40: + grid[y][x] = "2" + else: + grid[y][x] = "1" + # ensure path-ish + for x in range(width): + grid[1][x] = "1" + for y in range(1, height): + grid[y][width - 2] = "1" + grid[1][1] = "S" + grid[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in grid]) + + +def bench_one_maze(maze_name, maze, strategies, repeats=5): + summary_rows = [] + raw_rows = [] + for strategy_name, strategy_factory in strategies: + times, visiteds, lengths = [], [], [] + for run in range(1, repeats + 1): + solver = MazeSolver(maze) + solver.setStrategy(strategy_factory()) + stats = solver.solve() + raw_rows.append([maze_name, strategy_name, run, f"{stats.timeMs:.6f}", stats.visitedCells, stats.pathLength]) + times.append(stats.timeMs) + visiteds.append(stats.visitedCells) + lengths.append(stats.pathLength) + summary_rows.append([maze_name, strategy_name, f"{mean(times):.6f}", f"{mean(visiteds):.2f}", f"{mean(lengths):.2f}", repeats]) + return summary_rows, raw_rows + + +def save_csv(path, rows): + with open(path, "w", newline="", encoding="utf-8") as f: + csv.writer(f).writerows(rows) + + +def plot_summary(summary_rows): + by_maze = {} + for row in summary_rows[1:]: + maze_name, strategy, avg_time, avg_visited, avg_len, runs = row + by_maze.setdefault(maze_name, []).append((strategy, float(avg_time), float(avg_visited), float(avg_len))) + + for maze_name, items in by_maze.items(): + items.sort(key=lambda t: t[0]) + strategies = [i[0] for i in items] + x = list(range(len(strategies))) + + plt.figure(figsize=(8, 4)) + plt.bar(x, [i[1] for i in items]) + plt.xticks(x, strategies) + plt.ylabel("ms") + plt.title(f"{maze_name} — avg time") + plt.tight_layout() + plt.savefig(OUT_DIR / f"{maze_name}_time.png", dpi=150) + plt.close() + + plt.figure(figsize=(8, 4)) + plt.bar(x, [i[2] for i in items]) + plt.xticks(x, strategies) + plt.ylabel("cells") + plt.title(f"{maze_name} — visited cells") + plt.tight_layout() + plt.savefig(OUT_DIR / f"{maze_name}_visited.png", dpi=150) + plt.close() + + plt.figure(figsize=(8, 4)) + plt.bar(x, [i[3] for i in items]) + plt.xticks(x, strategies) + plt.ylabel("cells") + plt.title(f"{maze_name} — path length") + plt.tight_layout() + plt.savefig(OUT_DIR / f"{maze_name}_length.png", dpi=150) + plt.close() + + +def main(): + OUT_DIR.mkdir(exist_ok=True) + + strategies = [ + ("BFS", BFSStrategy), + ("DFS", DFSStrategy), + ("A*", AStarStrategy), + ("Dijkstra", DijkstraStrategy), + ] + + mazes = [ + ("small_10x10", generate_simple_maze(10, 10)), + ("medium_50x50", generate_branching_maze(50, 50)), + ("large_100x100", generate_branching_maze(100, 100, seed=99, wall_density=0.35)), + ("empty_30x30", generate_empty_maze(30, 30)), + ("no_path_30x30", generate_no_path_maze(30, 30)), + ("weighted_30x30", generate_weighted_maze(30, 30)), + ] + + summary = [["maze", "strategy", "avg_time_ms", "avg_visited_cells", "avg_path_length", "runs"]] + raw = [["maze", "strategy", "run", "time_ms", "visited_cells", "path_length"]] + + for maze_name, maze in mazes: + s_rows, r_rows = bench_one_maze(maze_name, maze, strategies, repeats=5) + summary.extend(s_rows) + raw.extend(r_rows) + + save_csv(OUT_DIR / "summary.csv", summary) + save_csv(OUT_DIR / "raw.csv", raw) + plot_summary(summary) + + print("Saved to", OUT_DIR.resolve()) + + +if __name__ == "__main__": + main() diff --git a/BolonkinNM/experiment_results/empty_30x30_length.png b/BolonkinNM/experiment_results/empty_30x30_length.png new file mode 100644 index 0000000..ba6a3b6 Binary files /dev/null and b/BolonkinNM/experiment_results/empty_30x30_length.png differ diff --git a/BolonkinNM/experiment_results/empty_30x30_time.png b/BolonkinNM/experiment_results/empty_30x30_time.png new file mode 100644 index 0000000..85aca79 Binary files /dev/null and b/BolonkinNM/experiment_results/empty_30x30_time.png differ diff --git a/BolonkinNM/experiment_results/empty_30x30_visited.png b/BolonkinNM/experiment_results/empty_30x30_visited.png new file mode 100644 index 0000000..8f7bac7 Binary files /dev/null and b/BolonkinNM/experiment_results/empty_30x30_visited.png differ diff --git a/BolonkinNM/experiment_results/large_100x100_length.png b/BolonkinNM/experiment_results/large_100x100_length.png new file mode 100644 index 0000000..7f8c7e2 Binary files /dev/null and b/BolonkinNM/experiment_results/large_100x100_length.png differ diff --git a/BolonkinNM/experiment_results/large_100x100_time.png b/BolonkinNM/experiment_results/large_100x100_time.png new file mode 100644 index 0000000..50bd2b5 Binary files /dev/null and b/BolonkinNM/experiment_results/large_100x100_time.png differ diff --git a/BolonkinNM/experiment_results/large_100x100_visited.png b/BolonkinNM/experiment_results/large_100x100_visited.png new file mode 100644 index 0000000..11bca38 Binary files /dev/null and b/BolonkinNM/experiment_results/large_100x100_visited.png differ diff --git a/BolonkinNM/experiment_results/medium_50x50_length.png b/BolonkinNM/experiment_results/medium_50x50_length.png new file mode 100644 index 0000000..146dedc Binary files /dev/null and b/BolonkinNM/experiment_results/medium_50x50_length.png differ diff --git a/BolonkinNM/experiment_results/medium_50x50_time.png b/BolonkinNM/experiment_results/medium_50x50_time.png new file mode 100644 index 0000000..e99ecfc Binary files /dev/null and b/BolonkinNM/experiment_results/medium_50x50_time.png differ diff --git a/BolonkinNM/experiment_results/medium_50x50_visited.png b/BolonkinNM/experiment_results/medium_50x50_visited.png new file mode 100644 index 0000000..a2b683d Binary files /dev/null and b/BolonkinNM/experiment_results/medium_50x50_visited.png differ diff --git a/BolonkinNM/experiment_results/no_path_30x30_length.png b/BolonkinNM/experiment_results/no_path_30x30_length.png new file mode 100644 index 0000000..cbd8be8 Binary files /dev/null and b/BolonkinNM/experiment_results/no_path_30x30_length.png differ diff --git a/BolonkinNM/experiment_results/no_path_30x30_time.png b/BolonkinNM/experiment_results/no_path_30x30_time.png new file mode 100644 index 0000000..68a92e3 Binary files /dev/null and b/BolonkinNM/experiment_results/no_path_30x30_time.png differ diff --git a/BolonkinNM/experiment_results/no_path_30x30_visited.png b/BolonkinNM/experiment_results/no_path_30x30_visited.png new file mode 100644 index 0000000..1cc5a63 Binary files /dev/null and b/BolonkinNM/experiment_results/no_path_30x30_visited.png differ diff --git a/BolonkinNM/experiment_results/raw.csv b/BolonkinNM/experiment_results/raw.csv new file mode 100644 index 0000000..800dfef --- /dev/null +++ b/BolonkinNM/experiment_results/raw.csv @@ -0,0 +1,121 @@ +maze,strategy,run,time_ms,visited_cells,path_length +small_10x10,BFS,1,0.044300,15,15 +small_10x10,BFS,2,0.022800,15,15 +small_10x10,BFS,3,0.020400,15,15 +small_10x10,BFS,4,0.020300,15,15 +small_10x10,BFS,5,0.018700,15,15 +small_10x10,DFS,1,0.031200,15,15 +small_10x10,DFS,2,0.022000,15,15 +small_10x10,DFS,3,0.021200,15,15 +small_10x10,DFS,4,0.020800,15,15 +small_10x10,DFS,5,0.020500,15,15 +small_10x10,A*,1,0.048900,15,15 +small_10x10,A*,2,0.034700,15,15 +small_10x10,A*,3,0.029400,15,15 +small_10x10,A*,4,0.029100,15,15 +small_10x10,A*,5,0.029300,15,15 +small_10x10,Dijkstra,1,0.037900,15,15 +small_10x10,Dijkstra,2,0.028500,15,15 +small_10x10,Dijkstra,3,0.026800,15,15 +small_10x10,Dijkstra,4,0.026400,15,15 +small_10x10,Dijkstra,5,0.026700,15,15 +medium_50x50,BFS,1,2.105800,1579,95 +medium_50x50,BFS,2,1.928700,1579,95 +medium_50x50,BFS,3,1.969500,1579,95 +medium_50x50,BFS,4,1.938800,1579,95 +medium_50x50,BFS,5,1.943600,1579,95 +medium_50x50,DFS,1,1.927300,1277,647 +medium_50x50,DFS,2,1.856300,1277,647 +medium_50x50,DFS,3,1.890100,1277,647 +medium_50x50,DFS,4,1.868000,1277,647 +medium_50x50,DFS,5,1.865500,1277,647 +medium_50x50,A*,1,2.359000,927,95 +medium_50x50,A*,2,2.193700,927,95 +medium_50x50,A*,3,2.178400,927,95 +medium_50x50,A*,4,2.181800,927,95 +medium_50x50,A*,5,2.174500,927,95 +medium_50x50,Dijkstra,1,3.534700,1579,95 +medium_50x50,Dijkstra,2,3.435500,1579,95 +medium_50x50,Dijkstra,3,3.457600,1579,95 +medium_50x50,Dijkstra,4,3.417300,1579,95 +medium_50x50,Dijkstra,5,3.538000,1579,95 +large_100x100,BFS,1,8.624100,5566,195 +large_100x100,BFS,2,7.706900,5566,195 +large_100x100,BFS,3,9.723300,5566,195 +large_100x100,BFS,4,7.585700,5566,195 +large_100x100,BFS,5,8.031300,5566,195 +large_100x100,DFS,1,5.512400,3543,1531 +large_100x100,DFS,2,5.329300,3543,1531 +large_100x100,DFS,3,5.223300,3543,1531 +large_100x100,DFS,4,5.729900,3543,1531 +large_100x100,DFS,5,5.497400,3543,1531 +large_100x100,A*,1,2.101500,853,195 +large_100x100,A*,2,2.264500,853,195 +large_100x100,A*,3,2.064100,853,195 +large_100x100,A*,4,2.031700,853,195 +large_100x100,A*,5,2.046500,853,195 +large_100x100,Dijkstra,1,25.021300,5571,195 +large_100x100,Dijkstra,2,13.541100,5571,195 +large_100x100,Dijkstra,3,12.884100,5571,195 +large_100x100,Dijkstra,4,13.481800,5571,195 +large_100x100,Dijkstra,5,12.748000,5571,195 +empty_30x30,BFS,1,1.234300,896,55 +empty_30x30,BFS,2,1.163400,896,55 +empty_30x30,BFS,3,1.145700,896,55 +empty_30x30,BFS,4,1.177300,896,55 +empty_30x30,BFS,5,1.175100,896,55 +empty_30x30,DFS,1,1.338000,842,815 +empty_30x30,DFS,2,1.296500,842,815 +empty_30x30,DFS,3,1.296700,842,815 +empty_30x30,DFS,4,1.280100,842,815 +empty_30x30,DFS,5,1.290800,842,815 +empty_30x30,A*,1,2.183400,784,55 +empty_30x30,A*,2,2.522900,784,55 +empty_30x30,A*,3,1.985000,784,55 +empty_30x30,A*,4,1.972100,784,55 +empty_30x30,A*,5,2.088600,784,55 +empty_30x30,Dijkstra,1,2.080400,896,55 +empty_30x30,Dijkstra,2,2.100100,896,55 +empty_30x30,Dijkstra,3,2.130700,896,55 +empty_30x30,Dijkstra,4,2.073600,896,55 +empty_30x30,Dijkstra,5,2.095900,896,55 +no_path_30x30,BFS,1,0.645900,450,0 +no_path_30x30,BFS,2,0.566600,450,0 +no_path_30x30,BFS,3,0.566000,450,0 +no_path_30x30,BFS,4,0.583500,450,0 +no_path_30x30,BFS,5,0.568900,450,0 +no_path_30x30,DFS,1,0.692100,450,0 +no_path_30x30,DFS,2,0.676900,450,0 +no_path_30x30,DFS,3,0.703500,450,0 +no_path_30x30,DFS,4,0.722300,450,0 +no_path_30x30,DFS,5,0.672000,450,0 +no_path_30x30,A*,1,1.112700,450,0 +no_path_30x30,A*,2,1.130000,450,0 +no_path_30x30,A*,3,1.096100,450,0 +no_path_30x30,A*,4,1.111400,450,0 +no_path_30x30,A*,5,1.183500,450,0 +no_path_30x30,Dijkstra,1,1.023300,450,0 +no_path_30x30,Dijkstra,2,1.011700,450,0 +no_path_30x30,Dijkstra,3,1.127200,450,0 +no_path_30x30,Dijkstra,4,1.110200,450,0 +no_path_30x30,Dijkstra,5,1.043900,450,0 +weighted_30x30,BFS,1,1.074700,788,55 +weighted_30x30,BFS,2,0.997700,788,55 +weighted_30x30,BFS,3,0.992700,788,55 +weighted_30x30,BFS,4,1.010800,788,55 +weighted_30x30,BFS,5,1.035000,788,55 +weighted_30x30,DFS,1,1.130200,693,479 +weighted_30x30,DFS,2,1.057400,693,479 +weighted_30x30,DFS,3,1.049900,693,479 +weighted_30x30,DFS,4,1.051600,693,479 +weighted_30x30,DFS,5,1.059100,693,479 +weighted_30x30,A*,1,0.402200,126,55 +weighted_30x30,A*,2,0.384100,126,55 +weighted_30x30,A*,3,0.360000,126,55 +weighted_30x30,A*,4,0.360700,126,55 +weighted_30x30,A*,5,0.353500,126,55 +weighted_30x30,Dijkstra,1,1.834900,781,55 +weighted_30x30,Dijkstra,2,1.759000,781,55 +weighted_30x30,Dijkstra,3,1.786300,781,55 +weighted_30x30,Dijkstra,4,1.740500,781,55 +weighted_30x30,Dijkstra,5,1.807100,781,55 diff --git a/BolonkinNM/experiment_results/small_10x10_length.png b/BolonkinNM/experiment_results/small_10x10_length.png new file mode 100644 index 0000000..8dc2d78 Binary files /dev/null and b/BolonkinNM/experiment_results/small_10x10_length.png differ diff --git a/BolonkinNM/experiment_results/small_10x10_time.png b/BolonkinNM/experiment_results/small_10x10_time.png new file mode 100644 index 0000000..dcf10e1 Binary files /dev/null and b/BolonkinNM/experiment_results/small_10x10_time.png differ diff --git a/BolonkinNM/experiment_results/small_10x10_visited.png b/BolonkinNM/experiment_results/small_10x10_visited.png new file mode 100644 index 0000000..98fe889 Binary files /dev/null and b/BolonkinNM/experiment_results/small_10x10_visited.png differ diff --git a/BolonkinNM/experiment_results/summary.csv b/BolonkinNM/experiment_results/summary.csv new file mode 100644 index 0000000..46a0412 --- /dev/null +++ b/BolonkinNM/experiment_results/summary.csv @@ -0,0 +1,25 @@ +maze,strategy,avg_time_ms,avg_visited_cells,avg_path_length,runs +small_10x10,BFS,0.025300,15.00,15.00,5 +small_10x10,DFS,0.023140,15.00,15.00,5 +small_10x10,A*,0.034280,15.00,15.00,5 +small_10x10,Dijkstra,0.029260,15.00,15.00,5 +medium_50x50,BFS,1.977280,1579.00,95.00,5 +medium_50x50,DFS,1.881440,1277.00,647.00,5 +medium_50x50,A*,2.217480,927.00,95.00,5 +medium_50x50,Dijkstra,3.476620,1579.00,95.00,5 +large_100x100,BFS,8.334260,5566.00,195.00,5 +large_100x100,DFS,5.458460,3543.00,1531.00,5 +large_100x100,A*,2.101660,853.00,195.00,5 +large_100x100,Dijkstra,15.535260,5571.00,195.00,5 +empty_30x30,BFS,1.179160,896.00,55.00,5 +empty_30x30,DFS,1.300420,842.00,815.00,5 +empty_30x30,A*,2.150400,784.00,55.00,5 +empty_30x30,Dijkstra,2.096140,896.00,55.00,5 +no_path_30x30,BFS,0.586180,450.00,0.00,5 +no_path_30x30,DFS,0.693360,450.00,0.00,5 +no_path_30x30,A*,1.126740,450.00,0.00,5 +no_path_30x30,Dijkstra,1.063260,450.00,0.00,5 +weighted_30x30,BFS,1.022180,788.00,55.00,5 +weighted_30x30,DFS,1.069640,693.00,479.00,5 +weighted_30x30,A*,0.372100,126.00,55.00,5 +weighted_30x30,Dijkstra,1.785560,781.00,55.00,5 diff --git a/BolonkinNM/experiment_results/weighted_30x30_length.png b/BolonkinNM/experiment_results/weighted_30x30_length.png new file mode 100644 index 0000000..7c7e3b1 Binary files /dev/null and b/BolonkinNM/experiment_results/weighted_30x30_length.png differ diff --git a/BolonkinNM/experiment_results/weighted_30x30_time.png b/BolonkinNM/experiment_results/weighted_30x30_time.png new file mode 100644 index 0000000..45196c3 Binary files /dev/null and b/BolonkinNM/experiment_results/weighted_30x30_time.png differ diff --git a/BolonkinNM/experiment_results/weighted_30x30_visited.png b/BolonkinNM/experiment_results/weighted_30x30_visited.png new file mode 100644 index 0000000..3b02d70 Binary files /dev/null and b/BolonkinNM/experiment_results/weighted_30x30_visited.png differ diff --git a/BolonkinNM/main.py b/BolonkinNM/main.py new file mode 100644 index 0000000..08f22c7 --- /dev/null +++ b/BolonkinNM/main.py @@ -0,0 +1,59 @@ +from builders.text_file_maze_builder import TextFileMazeBuilder +from core.player import Player +from observer.console_view import ConsoleView +from solver.maze_solver import MazeSolver +from strategies.astar_strategy import AStarStrategy +from strategies.bfs_strategy import BFSStrategy +from strategies.dfs_strategy import DFSStrategy +from controller.game_controller import GameController + + +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent + + +def run_demo(): + builder = TextFileMazeBuilder() + maze = builder.buildFromFile(str(BASE_DIR / "mazes" / "maze_small.txt")) + + view = ConsoleView() + view.update({"type": "maze_loaded", "message": "Maze loaded"}) + view.render(maze) + + solver = MazeSolver(maze) + solver.addObserver(view) + + for strategy in (BFSStrategy(), DFSStrategy(), AStarStrategy()): + solver.setStrategy(strategy) + stats = solver.solve() + + print() + print(f"=== {strategy.name} ===") + print(f"Time: {stats.timeMs:.3f} ms") + print(f"Visited cells: {stats.visitedCells}") + print(f"Path length: {stats.pathLength}") + print(f"Path found: {'yes' if stats.found else 'no'}") + + view.render(maze, path=stats.path) + + player = Player(maze.startCell) + controller = GameController(maze, player, view) + + print("Manual mode: W/A/S/D move, Z undo, Q quit") + view.render(maze, player_position=player.currentCell) + + while True: + cmd = input("Command: ").strip().upper() + if cmd == "Q": + break + if cmd == "Z": + controller.undo() + elif cmd in {"W", "A", "S", "D"}: + controller.move(cmd) + else: + print("Unknown command") + + +if __name__ == "__main__": + run_demo() diff --git a/BolonkinNM/mazes/maze_empty.txt b/BolonkinNM/mazes/maze_empty.txt new file mode 100644 index 0000000..8267fd0 --- /dev/null +++ b/BolonkinNM/mazes/maze_empty.txt @@ -0,0 +1,9 @@ +S + + + + + + + + E diff --git a/BolonkinNM/mazes/maze_large.txt b/BolonkinNM/mazes/maze_large.txt new file mode 100644 index 0000000..eb03326 --- /dev/null +++ b/BolonkinNM/mazes/maze_large.txt @@ -0,0 +1,11 @@ +#################################################################################################### +#S # # # # # # # # # # # # # # # E# +# # ### ### # ###### # ### # ## # #### # ####### # #### # # ### ## # ## # # ## # ## # ##### ### ## +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ##### # ######## # ### # ## # #### # ####### ## ### # # #### ####### ## ####### ####### # ### ## +# # # # # # # # # # # # # # # # # # # # # +### # # ###### # ########### ########### ### ####### # ####### ### # # ###### # ### ### # ### #### +# # # # # # # # # # # # # # # # # # # # # # +# ### ###### # ##### # ### # ####### # ### ### ## # ###### # ### # ### ###### # ### # ### ### ## # +# # # # # # # # # +#################################################################################################### diff --git a/BolonkinNM/mazes/maze_medium.txt b/BolonkinNM/mazes/maze_medium.txt new file mode 100644 index 0000000..67ecd65 --- /dev/null +++ b/BolonkinNM/mazes/maze_medium.txt @@ -0,0 +1,11 @@ +################################################## +#S # # # # # # E# +# # ### ### # ###### # ### # ## # #### # ####### ## +# # # # # # # # # # # # # # +# ##### # ######## # ### # ## # #### # ####### ## # +# # # # # # # # # # +### # # ###### # ########### ########### ### ###### +# # # # # # # # # # # +# ### ###### # ##### # ### # ####### # ### ### ## # +# # # # # +################################################## diff --git a/BolonkinNM/mazes/maze_no_path.txt b/BolonkinNM/mazes/maze_no_path.txt new file mode 100644 index 0000000..9633160 --- /dev/null +++ b/BolonkinNM/mazes/maze_no_path.txt @@ -0,0 +1,9 @@ +########## +#S # +# ###### # +# # # +########## +# #E# +# ###### # +# # +########## diff --git a/BolonkinNM/mazes/maze_small.txt b/BolonkinNM/mazes/maze_small.txt new file mode 100644 index 0000000..e829a58 --- /dev/null +++ b/BolonkinNM/mazes/maze_small.txt @@ -0,0 +1,7 @@ +########## +#S #E# +# ## # # ## +# # # +# #### # # +# # # +########## diff --git a/BolonkinNM/mazes/maze_weighted.txt b/BolonkinNM/mazes/maze_weighted.txt new file mode 100644 index 0000000..be8718d --- /dev/null +++ b/BolonkinNM/mazes/maze_weighted.txt @@ -0,0 +1,10 @@ +1111111111111111111111111111 +1S11111111111111111111111111 +1111111111111111111111111111 +1111111111111111111111111111 +1111111111111222222222222111 +1111111111111222222222222111 +1111111111111333333333333111 +1111111111111333333333333111 +111111111111111111111111111E +1111111111111111111111111111 diff --git a/BolonkinNM/observer/__init__.py b/BolonkinNM/observer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/BolonkinNM/observer/console_view.py b/BolonkinNM/observer/console_view.py new file mode 100644 index 0000000..77248a5 --- /dev/null +++ b/BolonkinNM/observer/console_view.py @@ -0,0 +1,26 @@ +import os +from observer.observer import Observer + + +class ConsoleView(Observer): + def update(self, event): + if isinstance(event, str): + print(f"[EVENT] {event}") + elif isinstance(event, dict): + event_type = event.get("type", "unknown") + if event_type == "search_finished": + stats = event.get("stats") + print(f"[EVENT] search finished: {stats}") + else: + print(f"[EVENT] {event_type}: {event}") + else: + print("[EVENT] unknown") + + def clear(self): + os.system("cls" if os.name == "nt" else "clear") + + def render(self, maze, player_position=None, path=None, clear_screen=False): + if clear_screen: + self.clear() + print(maze.render(player_position=player_position, path=path)) + print() diff --git a/BolonkinNM/observer/observer.py b/BolonkinNM/observer/observer.py new file mode 100644 index 0000000..0ccca59 --- /dev/null +++ b/BolonkinNM/observer/observer.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod + + +class Observer(ABC): + @abstractmethod + def update(self, event): + raise NotImplementedError diff --git a/BolonkinNM/requirements.txt b/BolonkinNM/requirements.txt new file mode 100644 index 0000000..6ccafc3 --- /dev/null +++ b/BolonkinNM/requirements.txt @@ -0,0 +1 @@ +matplotlib diff --git a/BolonkinNM/solver/__init__.py b/BolonkinNM/solver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/BolonkinNM/solver/maze_solver.py b/BolonkinNM/solver/maze_solver.py new file mode 100644 index 0000000..7894661 --- /dev/null +++ b/BolonkinNM/solver/maze_solver.py @@ -0,0 +1,50 @@ +import time +from core.search_stats import SearchStats + + +class MazeSolver: + def __init__(self, maze, strategy=None): + self.maze = maze + self.strategy = strategy + self.observers = [] + + def setStrategy(self, strategy): + self.strategy = strategy + + def addObserver(self, observer): + if observer not in self.observers: + self.observers.append(observer) + + def removeObserver(self, observer): + if observer in self.observers: + self.observers.remove(observer) + + def notify(self, event): + for observer in self.observers: + observer.update(event) + + def solve(self): + if self.strategy is None: + raise ValueError("Strategy is not set") + self.notify({"type": "search_started", "strategy": self.strategy.name}) + + start_time = time.perf_counter() + path = self.strategy.findPath(self.maze, self.maze.startCell, self.maze.exitCell) + end_time = time.perf_counter() + + stats = SearchStats( + timeMs=(end_time - start_time) * 1000.0, + visitedCells=getattr(self.strategy, "visitedCount", 0), + pathLength=len(path), + path=path, + found=bool(path), + algorithm=getattr(self.strategy, "name", "") + ) + + if stats.found: + self.notify({"type": "path_found", "strategy": stats.algorithm, "length": stats.pathLength}) + else: + self.notify({"type": "path_not_found", "strategy": stats.algorithm}) + + self.notify({"type": "search_finished", "stats": stats}) + return stats diff --git a/BolonkinNM/strategies/__init__.py b/BolonkinNM/strategies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/BolonkinNM/strategies/astar_strategy.py b/BolonkinNM/strategies/astar_strategy.py new file mode 100644 index 0000000..4da5535 --- /dev/null +++ b/BolonkinNM/strategies/astar_strategy.py @@ -0,0 +1,45 @@ +import heapq +from strategies.pathfinding_strategy import PathFindingStrategy + + +class AStarStrategy(PathFindingStrategy): + name = "A*" + + def heuristic(self, cell, exitCell): + return abs(cell.x - exitCell.x) + abs(cell.y - exitCell.y) + + def findPath(self, maze, start, exitCell): + self.visitedCount = 0 + if start is None or exitCell is None: + return [] + + open_set = [] + heapq.heappush(open_set, (0, 0, start.x, start.y, start)) + parent = {} + g_score = {(start.x, start.y): 0} + closed = set() + + while open_set: + f_score, current_g, _, _, current = heapq.heappop(open_set) + pos = (current.x, current.y) + + if pos in closed: + continue + + closed.add(pos) + self.visitedCount += 1 + + if current.x == exitCell.x and current.y == exitCell.y: + return self._restore_path(parent, start, exitCell) + + for neighbor in maze.getNeighbors(current): + npos = (neighbor.x, neighbor.y) + tentative_g = current_g + getattr(neighbor, "weight", 1) + + if tentative_g < g_score.get(npos, float("inf")): + g_score[npos] = tentative_g + parent[npos] = current + new_f = tentative_g + self.heuristic(neighbor, exitCell) + heapq.heappush(open_set, (new_f, tentative_g, neighbor.x, neighbor.y, neighbor)) + + return [] diff --git a/BolonkinNM/strategies/bfs_strategy.py b/BolonkinNM/strategies/bfs_strategy.py new file mode 100644 index 0000000..7a98b50 --- /dev/null +++ b/BolonkinNM/strategies/bfs_strategy.py @@ -0,0 +1,31 @@ +from collections import deque +from strategies.pathfinding_strategy import PathFindingStrategy + + +class BFSStrategy(PathFindingStrategy): + name = "BFS" + + def findPath(self, maze, start, exitCell): + self.visitedCount = 0 + if start is None or exitCell is None: + return [] + + queue = deque([start]) + visited = {(start.x, start.y)} + parent = {} + + while queue: + current = queue.popleft() + self.visitedCount += 1 + + if current.x == exitCell.x and current.y == exitCell.y: + return self._restore_path(parent, start, exitCell) + + for neighbor in maze.getNeighbors(current): + pos = (neighbor.x, neighbor.y) + if pos not in visited: + visited.add(pos) + parent[pos] = current + queue.append(neighbor) + + return [] diff --git a/BolonkinNM/strategies/dfs_strategy.py b/BolonkinNM/strategies/dfs_strategy.py new file mode 100644 index 0000000..36451b3 --- /dev/null +++ b/BolonkinNM/strategies/dfs_strategy.py @@ -0,0 +1,35 @@ +from strategies.pathfinding_strategy import PathFindingStrategy + + +class DFSStrategy(PathFindingStrategy): + name = "DFS" + + def findPath(self, maze, start, exitCell): + self.visitedCount = 0 + if start is None or exitCell is None: + return [] + + stack = [start] + visited = set() + parent = {} + + while stack: + current = stack.pop() + pos = (current.x, current.y) + if pos in visited: + continue + + visited.add(pos) + self.visitedCount += 1 + + if current.x == exitCell.x and current.y == exitCell.y: + return self._restore_path(parent, start, exitCell) + + neighbors = maze.getNeighbors(current) + for neighbor in reversed(neighbors): + npos = (neighbor.x, neighbor.y) + if npos not in visited: + parent[npos] = current + stack.append(neighbor) + + return [] diff --git a/BolonkinNM/strategies/dijkstra_strategy.py b/BolonkinNM/strategies/dijkstra_strategy.py new file mode 100644 index 0000000..fd3163f --- /dev/null +++ b/BolonkinNM/strategies/dijkstra_strategy.py @@ -0,0 +1,41 @@ +import heapq +from strategies.pathfinding_strategy import PathFindingStrategy + + +class DijkstraStrategy(PathFindingStrategy): + name = "Dijkstra" + + def findPath(self, maze, start, exitCell): + self.visitedCount = 0 + if start is None or exitCell is None: + return [] + + pq = [(0, start.x, start.y, start)] + dist = {(start.x, start.y): 0} + parent = {} + closed = set() + + while pq: + current_cost, _, _, current = heapq.heappop(pq) + pos = (current.x, current.y) + + if pos in closed: + continue + + closed.add(pos) + self.visitedCount += 1 + + if current.x == exitCell.x and current.y == exitCell.y: + return self._restore_path(parent, start, exitCell) + + for neighbor in maze.getNeighbors(current): + npos = (neighbor.x, neighbor.y) + step_cost = getattr(neighbor, "weight", 1) + new_cost = current_cost + step_cost + + if new_cost < dist.get(npos, float("inf")): + dist[npos] = new_cost + parent[npos] = current + heapq.heappush(pq, (new_cost, neighbor.x, neighbor.y, neighbor)) + + return [] diff --git a/BolonkinNM/strategies/pathfinding_strategy.py b/BolonkinNM/strategies/pathfinding_strategy.py new file mode 100644 index 0000000..17b3ee4 --- /dev/null +++ b/BolonkinNM/strategies/pathfinding_strategy.py @@ -0,0 +1,30 @@ +from abc import ABC, abstractmethod + + +class PathFindingStrategy(ABC): + name = "Base" + + def __init__(self): + self.visitedCount = 0 + + @abstractmethod + def findPath(self, maze, start, exitCell): + raise NotImplementedError + + def _restore_path(self, parent, start, exitCell): + if exitCell is None or start is None: + return [] + + path = [] + current = exitCell + + while True: + path.append(current) + if current.x == start.x and current.y == start.y: + break + current = parent.get((current.x, current.y)) + if current is None: + return [] + + path.reverse() + return path