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
+
+
+ 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