diff --git a/BolonkinNM/.idea/.gitignore b/BolonkinNM/.idea/.gitignore
deleted file mode 100644
index 26d3352..0000000
--- a/BolonkinNM/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
diff --git a/BolonkinNM/.idea/ds_project_archive.iml b/BolonkinNM/.idea/maze_project_submission.iml
similarity index 100%
rename from BolonkinNM/.idea/ds_project_archive.iml
rename to BolonkinNM/.idea/maze_project_submission.iml
diff --git a/BolonkinNM/.idea/misc.xml b/BolonkinNM/.idea/misc.xml
index b56db6e..0ebfc91 100644
--- a/BolonkinNM/.idea/misc.xml
+++ b/BolonkinNM/.idea/misc.xml
@@ -1,4 +1,4 @@
-
+
\ No newline at end of file
diff --git a/BolonkinNM/.idea/modules.xml b/BolonkinNM/.idea/modules.xml
index 699d578..a636c96 100644
--- a/BolonkinNM/.idea/modules.xml
+++ b/BolonkinNM/.idea/modules.xml
@@ -2,7 +2,7 @@
-
+
\ 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
index 7e15dad..2e6e63f 100644
--- a/BolonkinNM/README.md
+++ b/BolonkinNM/README.md
@@ -1,18 +1,24 @@
-# Задание 1 — структуры данных
+# Maze Solver Project
-Процедурная реализация:
-- linked_list.py
-- hash_table.py
-- bst.py
+ООП-проект для поиска выхода из лабиринта с паттернами:
+- Builder
+- Strategy
+- Observer
+- Command
-Эксперименты и отчёты:
-- experiments.py
-- plot_results.py
-- results.csv
-- docs/report.md
-- docs/data/*.png
-
-Запуск:
+## Запуск
```bash
python main.py
```
+
+## Эксперименты
+```bash
+python experiment.py
+```
+
+Результаты сохраняются в папку `experiment_results/`.
+
+## Требования
+```bash
+pip install -r requirements.txt
+```
diff --git a/BolonkinNM/bst.py b/BolonkinNM/bst.py
deleted file mode 100644
index 221fd67..0000000
--- a/BolonkinNM/bst.py
+++ /dev/null
@@ -1,118 +0,0 @@
-from typing import Any, Dict, List, Optional
-
-
-Node = Dict[str, Any]
-
-
-def _make_node(name: str, phone: str) -> Node:
- return {"name": name, "phone": phone, "left": None, "right": None}
-
-
-def bst_insert(root: Optional[Node], name: str, phone: str) -> Node:
- new_node = _make_node(name, phone)
-
- if root is None:
- return new_node
-
- current = root
- parent = None
-
- while current is not None:
- parent = current
- if name < current["name"]:
- current = current["left"]
- elif name > current["name"]:
- current = current["right"]
- else:
- current["phone"] = phone
- return root
-
- if name < parent["name"]:
- parent["left"] = new_node
- else:
- parent["right"] = new_node
-
- return root
-
-
-def bst_find(root: Optional[Node], name: str) -> Optional[str]:
- current = root
- while current is not None:
- if name < current["name"]:
- current = current["left"]
- elif name > current["name"]:
- current = current["right"]
- else:
- return current["phone"]
- return None
-
-
-def _find_min_node(node: Node) -> Node:
- current = node
- while current["left"] is not None:
- current = current["left"]
- return current
-
-
-def bst_delete(root: Optional[Node], name: str) -> Optional[Node]:
- if root is None:
- return None
-
- parent = None
- current = root
-
- while current is not None and current["name"] != name:
- parent = current
- if name < current["name"]:
- current = current["left"]
- else:
- current = current["right"]
-
- if current is None:
- return root
-
- if current["left"] is None or current["right"] is None:
- child = current["left"] if current["left"] is not None else current["right"]
-
- if parent is None:
- return child
-
- if parent["left"] is current:
- parent["left"] = child
- else:
- parent["right"] = child
- return root
-
- succ_parent = current
- successor = current["right"]
- while successor["left"] is not None:
- succ_parent = successor
- successor = successor["left"]
-
- current["name"] = successor["name"]
- current["phone"] = successor["phone"]
-
- successor_child = successor["right"]
- if succ_parent["left"] is successor:
- succ_parent["left"] = successor_child
- else:
- succ_parent["right"] = successor_child
-
- return root
-
-
-def bst_list_all(root: Optional[Node]) -> List[Dict[str, str]]:
- result: List[Dict[str, str]] = []
- stack: List[Node] = []
- current = root
-
- while current is not None or stack:
- while current is not None:
- stack.append(current)
- current = current["left"]
-
- current = stack.pop()
- result.append({"name": current["name"], "phone": current["phone"]})
- current = current["right"]
-
- return result
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/data/delete.png b/BolonkinNM/docs/data/delete.png
deleted file mode 100644
index b5307a7..0000000
Binary files a/BolonkinNM/docs/data/delete.png and /dev/null differ
diff --git a/BolonkinNM/docs/data/find.png b/BolonkinNM/docs/data/find.png
deleted file mode 100644
index d0d4e81..0000000
Binary files a/BolonkinNM/docs/data/find.png and /dev/null differ
diff --git a/BolonkinNM/docs/data/insert.png b/BolonkinNM/docs/data/insert.png
deleted file mode 100644
index b2832b9..0000000
Binary files a/BolonkinNM/docs/data/insert.png and /dev/null differ
diff --git a/BolonkinNM/docs/data/results.csv b/BolonkinNM/docs/data/results.csv
deleted file mode 100644
index 4ccfb69..0000000
--- a/BolonkinNM/docs/data/results.csv
+++ /dev/null
@@ -1,109 +0,0 @@
-Структура,Режим,Операция,Замер,Время (сек)
-LinkedList,случайный,insert,1,4.2622492010
-LinkedList,случайный,find,1,0.0314994130
-LinkedList,случайный,delete,1,0.0149069000
-LinkedList,случайный,insert,2,4.0154580330
-LinkedList,случайный,find,2,0.0393284500
-LinkedList,случайный,delete,2,0.0210732100
-LinkedList,случайный,insert,3,4.0436019780
-LinkedList,случайный,find,3,0.0344933660
-LinkedList,случайный,delete,3,0.0152639850
-LinkedList,случайный,insert,4,3.7182993220
-LinkedList,случайный,find,4,0.0327698850
-LinkedList,случайный,delete,4,0.0149959540
-LinkedList,случайный,insert,5,3.7082228200
-LinkedList,случайный,find,5,0.0303762490
-LinkedList,случайный,delete,5,0.0141406560
-LinkedList,случайный,insert,среднее,3.9495662708
-LinkedList,случайный,find,среднее,0.0336934726
-LinkedList,случайный,delete,среднее,0.0160761410
-HashTable,случайный,insert,1,0.2059865770
-HashTable,случайный,find,1,0.0014966100
-HashTable,случайный,delete,1,0.0006891700
-HashTable,случайный,insert,2,0.2024331460
-HashTable,случайный,find,2,0.0015934880
-HashTable,случайный,delete,2,0.0007212620
-HashTable,случайный,insert,3,0.2126128040
-HashTable,случайный,find,3,0.0016566220
-HashTable,случайный,delete,3,0.0008358420
-HashTable,случайный,insert,4,0.2157934910
-HashTable,случайный,find,4,0.0015542810
-HashTable,случайный,delete,4,0.0007269120
-HashTable,случайный,insert,5,0.2079924580
-HashTable,случайный,find,5,0.0013696990
-HashTable,случайный,delete,5,0.0006616050
-HashTable,случайный,insert,среднее,0.2089636952
-HashTable,случайный,find,среднее,0.0015341400
-HashTable,случайный,delete,среднее,0.0007269582
-BST,случайный,insert,1,0.0166981280
-BST,случайный,find,1,0.0001569360
-BST,случайный,delete,1,0.0000917280
-BST,случайный,insert,2,0.0184119040
-BST,случайный,find,2,0.0001517110
-BST,случайный,delete,2,0.0001163770
-BST,случайный,insert,3,0.0174662270
-BST,случайный,find,3,0.0001582930
-BST,случайный,delete,3,0.0000892660
-BST,случайный,insert,4,0.0191369100
-BST,случайный,find,4,0.0002087170
-BST,случайный,delete,4,0.0001067050
-BST,случайный,insert,5,0.0184276900
-BST,случайный,find,5,0.0002767720
-BST,случайный,delete,5,0.0001067660
-BST,случайный,insert,среднее,0.0180281718
-BST,случайный,find,среднее,0.0001904858
-BST,случайный,delete,среднее,0.0001021684
-LinkedList,отсортированный,insert,1,2.9875078340
-LinkedList,отсортированный,find,1,0.0237300610
-LinkedList,отсортированный,delete,1,0.0111698260
-LinkedList,отсортированный,insert,2,3.0573987940
-LinkedList,отсортированный,find,2,0.0243270360
-LinkedList,отсортированный,delete,2,0.0115366030
-LinkedList,отсортированный,insert,3,2.9641987260
-LinkedList,отсортированный,find,3,0.0236313330
-LinkedList,отсортированный,delete,3,0.0112848510
-LinkedList,отсортированный,insert,4,3.0345914950
-LinkedList,отсортированный,find,4,0.0240271220
-LinkedList,отсортированный,delete,4,0.0112117310
-LinkedList,отсортированный,insert,5,2.9481954700
-LinkedList,отсортированный,find,5,0.0239006100
-LinkedList,отсортированный,delete,5,0.0110857710
-LinkedList,отсортированный,insert,среднее,2.9983784638
-LinkedList,отсортированный,find,среднее,0.0239232324
-LinkedList,отсортированный,delete,среднее,0.0112577564
-HashTable,отсортированный,insert,1,0.1997087560
-HashTable,отсортированный,find,1,0.0017550400
-HashTable,отсортированный,delete,1,0.0008407980
-HashTable,отсортированный,insert,2,0.1968675190
-HashTable,отсортированный,find,2,0.0019886760
-HashTable,отсортированный,delete,2,0.0008920910
-HashTable,отсортированный,insert,3,0.1907563580
-HashTable,отсортированный,find,3,0.0018447440
-HashTable,отсортированный,delete,3,0.0008684640
-HashTable,отсортированный,insert,4,0.2625327630
-HashTable,отсортированный,find,4,0.0016053140
-HashTable,отсортированный,delete,4,0.0008098670
-HashTable,отсортированный,insert,5,0.1936840590
-HashTable,отсортированный,find,5,0.0019015160
-HashTable,отсортированный,delete,5,0.0009053780
-HashTable,отсортированный,insert,среднее,0.2087098910
-HashTable,отсортированный,find,среднее,0.0018190580
-HashTable,отсортированный,delete,среднее,0.0008633196
-BST,отсортированный,insert,1,4.2195800190
-BST,отсортированный,find,1,0.0389314570
-BST,отсортированный,delete,1,0.0190308920
-BST,отсортированный,insert,2,4.1356184250
-BST,отсортированный,find,2,0.0383339310
-BST,отсортированный,delete,2,0.0194247740
-BST,отсортированный,insert,3,4.1204731890
-BST,отсортированный,find,3,0.0388593320
-BST,отсортированный,delete,3,0.0215428460
-BST,отсортированный,insert,4,4.2120902370
-BST,отсортированный,find,4,0.0378190250
-BST,отсортированный,delete,4,0.0188528460
-BST,отсортированный,insert,5,4.1304951260
-BST,отсортированный,find,5,0.0359927840
-BST,отсортированный,delete,5,0.0179617110
-BST,отсортированный,insert,среднее,4.1636513992
-BST,отсортированный,find,среднее,0.0379873058
-BST,отсортированный,delete,среднее,0.0193626138
diff --git a/BolonkinNM/docs/report.md b/BolonkinNM/docs/report.md
index 0a757c7..8eb21e6 100644
--- a/BolonkinNM/docs/report.md
+++ b/BolonkinNM/docs/report.md
@@ -1,101 +1,249 @@
-# Отчёт по заданию 1 — структуры данных
+# Отчёт по работе «Поиск выхода из лабиринта»
-## Цель работы
+## 1. Цель работы
+Разработать гибкую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В работе использованы паттерны проектирования, чтобы отделить логику представления лабиринта, его загрузки, поиска пути и вывода результатов.
-Реализовать три структуры данных с нуля в процедурном стиле:
+## 2. Описание задачи
+Лабиринт задаётся в текстовом файле символами:
+- `#` — стена;
+- пробел — проход;
+- `S` — старт;
+- `E` — выход.
-- связный список;
-- хеш-таблицу;
-- двоичное дерево поиска.
+Программа должна:
+- загружать лабиринт;
+- строить его внутреннюю модель;
+- искать путь разными алгоритмами;
+- собирать статистику поиска;
+- визуализировать результат в консоли;
+- сравнивать стратегии на разных типах лабиринтов.
-Также были выполнены измерения времени для операций `insert`, `find`, `delete` и построены графики по результатам эксперимента.
+## 3. Выбранные паттерны проектирования
-## Реализованные структуры
+### 3.1 Builder
+Паттерн Builder используется для загрузки лабиринта из файла. Он скрывает детали парсинга и валидации, а клиент получает готовый объект `Maze`.
-### Связный список
+Преимущества:
+- легко добавить новый формат загрузки;
+- клиентский код не зависит от формата файла;
+- создание лабиринта можно расширять без переписывания остальной программы.
-Узел хранится как словарь:
+### 3.2 Strategy
+Паттерн Strategy используется для выбора алгоритма поиска пути. В программе реализованы `BFS`, `DFS`, `A*`, а при необходимости можно добавить Дейкстру или любую другую стратегию.
-```python
-{"name": "Имя", "phone": "123", "next": None}
+Преимущества:
+- алгоритм можно менять во время выполнения;
+- код оркестратора не зависит от конкретного метода поиска;
+- новые алгоритмы добавляются без изменения существующего кода.
+
+### 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. Ключевые классы и их роль
-Хранится как список бакетов фиксированной длины, где каждый бакет — голова связного списка или `None`.
+### Cell
+Хранит координаты клетки и её тип. Позволяет быстро проверять, является ли клетка проходимой.
-### Двоичное дерево поиска
+### Maze
+Содержит двумерную карту клеток, размер лабиринта, а также ссылки на старт и выход. Даёт доступ к соседним клеткам по четырём направлениям.
-Узел хранится как словарь:
+### TextFileMazeBuilder
+Читает текстовый файл, создаёт объекты `Cell`, определяет старт и выход, затем возвращает готовый `Maze`.
-```python
-{"name": "Имя", "phone": "123", "left": None, "right": None}
-```
+### BFSStrategy
+Ищет кратчайший путь по числу шагов. Подходит для случая, когда все переходы одинаковой стоимости.
-Для BST использованы итеративные операции, чтобы корректно работать и на отсортированных данных.
+### DFSStrategy
+Быстро исследует пространство, но не гарантирует кратчайший путь. Полезен как сравнительный алгоритм.
-## Методика эксперимента
+### AStarStrategy
+Использует эвристику Манхэттенского расстояния. Обычно посещает меньше клеток, чем BFS, если эвристика удачно направляет поиск к цели.
-- Количество записей: `N = 10000`
-- Режимы данных:
- - случайный порядок;
- - отсортированный порядок.
-- Каждое измерение повторялось **5 раз**.
-- В CSV сохранены:
- - все отдельные замеры;
- - среднее время для каждой операции, структуры и режима.
+### MazeSolver
+Оркестратор, который хранит лабиринт и текущую стратегию. Вызывает поиск, измеряет время и собирает статистику.
-Операции:
+### SearchStats
+Содержит итог поиска: время выполнения, количество посещённых клеток и длину пути.
-- вставка всех записей;
-- поиск 100 существующих и 10 отсутствующих имён;
-- удаление 50 случайных имён.
+### ConsoleView
+Реализует наблюдателя и умеет выводить лабиринт и найденный путь в консоль.
-## Графики
+### MoveCommand
+Оформляет ход игрока как объект-команду. Поддерживает отмену последнего перемещения.
-
+## 6. Экспериментальная часть
-
+### 6.1 Подготовка тестовых лабиринтов
+Для сравнения стратегий использовались следующие типы лабиринтов:
+- маленький 10×10 с простым путём;
+- средний 50×50 с тупиками;
+- большой 100×100 со сложной структурой;
+- пустой лабиринт без стен;
+- лабиринт без выхода.
-
+### 6.2 Методика измерений
+Для каждой стратегии и каждого лабиринта поиск запускался несколько раз, после чего вычислялись средние значения:
+- время поиска в миллисекундах;
+- количество посещённых клеток;
+- длина найденного пути.
-## Средние результаты
+Результаты сохранялись в CSV-файл в двух вариантах:
+- сырой набор измерений;
+- усреднённая таблица.
-| Режим | Операция | LinkedList | HashTable | BST | Лучший результат |
-|---|---:|---:|---:|---:|---|
-| случайный | insert | 3.949566 | 0.208964 | 0.018028 | BST |
-| случайный | find | 0.033693 | 0.001534 | 0.000190 | BST |
-| случайный | delete | 0.016076 | 0.000727 | 0.000102 | BST |
-| отсортированный | insert | 2.998378 | 0.208710 | 4.163651 | HashTable |
-| отсортированный | find | 0.023923 | 0.001819 | 0.037987 | HashTable |
-| отсортированный | delete | 0.011258 | 0.000863 | 0.019363 | HashTable |
+## 7. Анализ эффективности
-## Анализ результатов
+### BFS
+BFS гарантирует кратчайший путь по числу шагов, если все переходы имеют одинаковую стоимость. На простых и пустых лабиринтах работает стабильно и предсказуемо. Минус — может посещать много клеток, особенно на больших лабиринтах.
-### Влияние порядка входных данных на BST
+### DFS
+DFS может быстро найти какой-то путь, но он не обязательно будет кратчайшим. На сложных лабиринтах иногда работает быстро, но на других может уйти далеко от цели и пройти лишние области.
-На случайных данных BST работает значительно быстрее, чем на отсортированных. Это связано с тем, что при случайной вставке дерево остаётся ближе к сбалансированному состоянию.
+### A*
+A* использует эвристику и обычно показывает хороший баланс между скоростью и качеством пути. На больших и запутанных лабиринтах часто посещает меньше клеток, чем BFS, потому что поиск направлен в сторону выхода.
-На отсортированных данных дерево вырождается в цепочку, поэтому вставка становится медленной, а поиск и удаление тоже деградируют по времени.
+### Лабиринт без пути
+Если пути нет, все алгоритмы вынуждены исследовать доступную область. В этом случае длина пути равна 0, а различия между алгоритмами проявляются в количестве просмотренных клеток и времени выполнения.
-### Почему хеш-таблица почти не чувствительна к порядку
+### Вывод по выбору алгоритма
+- BFS стоит выбирать, когда нужен гарантированно кратчайший путь и веса переходов одинаковы.
+- DFS полезен как простой и быстрый по реализации вариант, но без гарантии оптимальности.
+- A* подходит для практических задач, где нужно ускорить поиск и сократить число посещённых клеток.
+- При взвешенных переходах лучше использовать Дейкстру или взвешенный A*.
-Хеш-таблица распределяет элементы по бакетам через хеш-функцию, поэтому сам порядок входа почти не влияет на скорость. Влияние может появляться только из-за коллизий, но в целом поведение остаётся близким к постоянному времени.
+## 8. Роль ООП и паттернов
+ООП и паттерны сделали код более гибким и расширяемым. Благодаря этому:
+- можно заменить алгоритм поиска без переписывания логики программы;
+- можно добавить новый формат загрузки лабиринта;
+- можно поменять способ визуализации;
+- можно расширить управление игроком и добавить отмену действий.
-### Почему связный список всегда медленен при поиске
+Без паттернов пришлось бы связывать загрузку, поиск, отображение и управление в один большой блок кода. Это усложнило бы отладку и дальнейшие изменения.
-Поиск в связном списке выполняется последовательным просмотром элементов. Поэтому при большом количестве записей приходится проходить много узлов, и операция остаётся линейной по времени.
+## 9. Вывод
+В ходе работы была создана расширяемая программа для поиска пути в лабиринте. Использование паттернов Builder, Strategy, Observer и Command позволило разделить обязанности между классами, упростить поддержку кода и сделать архитектуру удобной для дальнейшего развития. Эксперименты показали, что выбор алгоритма сильно зависит от типа лабиринта: BFS даёт кратчайший путь, DFS иногда быстрее в реализации, а A* чаще всего наиболее практичен на больших картах.
-### Как удаление работает в каждой структуре
-
-- В связном списке нужно сначала найти нужный узел, затем переназначить ссылку.
-- В хеш-таблице сначала выбирается бакет, затем удаление выполняется внутри короткой цепочки.
-- В BST удаление зависит от числа потомков: если потомок один или ноль, операция простая; если два — нужно найти преемника.
-
-## Вывод
-
-Для частых вставок и особенно частого поиска в реальной задаче чаще всего лучше подходит **хеш-таблица**.
-
-Если важно получать данные в отсортированном виде, удобнее использовать **BST**.
-
-**Связный список** подходит для маленьких объёмов данных или очень простых сценариев, но при большом числе записей он проигрывает по скорости поиска.
+## 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/experiments.py b/BolonkinNM/experiments.py
deleted file mode 100644
index f5face1..0000000
--- a/BolonkinNM/experiments.py
+++ /dev/null
@@ -1,172 +0,0 @@
-from __future__ import annotations
-
-import csv
-import random
-import time
-from pathlib import Path
-from typing import Dict, List, Tuple
-
-from linked_list import ll_insert, ll_find, ll_delete
-from hash_table import ht_insert, ht_find, ht_delete
-from bst import bst_insert, bst_find, bst_delete
-from utils import generate_records, prepare_records_variants
-
-
-Record = Tuple[str, str]
-
-
-def make_missing_names(count: int = 10) -> List[str]:
- return [f"None_{i}" for i in range(count)]
-
-
-def pick_existing_names(records: List[Record], count: int, seed: int = 42) -> List[str]:
- rng = random.Random(seed)
- unique_names = list(dict.fromkeys(name for name, _ in records))
- if len(unique_names) < count:
- raise ValueError(f"Not enough unique names: need {count}, got {len(unique_names)}")
- return rng.sample(unique_names, count)
-
-
-def pick_delete_names(records: List[Record], count: int = 50, seed: int = 43) -> List[str]:
- rng = random.Random(seed)
- unique_names = list(dict.fromkeys(name for name, _ in records))
- if len(unique_names) < count:
- raise ValueError(f"Not enough unique names: need {count}, got {len(unique_names)}")
- return rng.sample(unique_names, count)
-
-
-def build_structure(structure_name: str, records: List[Record], buckets_count: int = 2048):
- if structure_name == "linked_list":
- structure = None
- for name, phone in records:
- structure = ll_insert(structure, name, phone)
- return structure
-
- if structure_name == "hash_table":
- buckets = [None] * buckets_count
- for name, phone in records:
- buckets = ht_insert(buckets, name, phone)
- return buckets
-
- if structure_name == "bst":
- root = None
- for name, phone in records:
- root = bst_insert(root, name, phone)
- return root
-
- raise ValueError(f"Unknown structure: {structure_name}")
-
-
-def do_find(structure_name: str, structure: object, existing_names: List[str], missing_names: List[str]) -> None:
- if structure_name == "linked_list":
- for name in existing_names:
- ll_find(structure, name)
- for name in missing_names:
- ll_find(structure, name)
- return
-
- if structure_name == "hash_table":
- for name in existing_names:
- ht_find(structure, name)
- for name in missing_names:
- ht_find(structure, name)
- return
-
- if structure_name == "bst":
- for name in existing_names:
- bst_find(structure, name)
- for name in missing_names:
- bst_find(structure, name)
- return
-
- raise ValueError(f"Unknown structure: {structure_name}")
-
-
-def do_delete(structure_name: str, structure: object, delete_names: List[str]):
- if structure_name == "linked_list":
- for name in delete_names:
- structure = ll_delete(structure, name)
- return structure
-
- if structure_name == "hash_table":
- for name in delete_names:
- structure = ht_delete(structure, name)
- return structure
-
- if structure_name == "bst":
- for name in delete_names:
- structure = bst_delete(structure, name)
- return structure
-
- raise ValueError(f"Unknown structure: {structure_name}")
-
-
-def measure_once(structure_name: str, records: List[Record], buckets_count: int = 2048) -> Dict[str, float]:
- existing_names = pick_existing_names(records, 100, seed=42)
- missing_names = make_missing_names(10)
- delete_names = pick_delete_names(records, 50, seed=43)
-
- start = time.perf_counter()
- structure = build_structure(structure_name, records, buckets_count=buckets_count)
- insert_time = time.perf_counter() - start
-
- start = time.perf_counter()
- do_find(structure_name, structure, existing_names, missing_names)
- find_time = time.perf_counter() - start
-
- start = time.perf_counter()
- structure = do_delete(structure_name, structure, delete_names)
- delete_time = time.perf_counter() - start
-
- return {"insert": insert_time, "find": find_time, "delete": delete_time}
-
-
-def run_experiments(n: int = 10000, buckets_count: int = 2048, repeats: int = 5):
- records = generate_records(n, repeat_names=False)
- records_shuffled, records_sorted = prepare_records_variants(records)
-
- datasets = [
- ("случайный", records_shuffled),
- ("отсортированный", records_sorted),
- ]
- structures = [
- ("LinkedList", "linked_list"),
- ("HashTable", "hash_table"),
- ("BST", "bst"),
- ]
- operations = ("insert", "find", "delete")
-
- rows = [["Структура", "Режим", "Операция", "Замер", "Время (сек)"]]
-
- for mode_name, dataset_records in datasets:
- for human_name, structure_name in structures:
- times_by_op = {op: [] for op in operations}
-
- for attempt in range(1, repeats + 1):
- result = measure_once(structure_name, dataset_records, buckets_count=buckets_count)
- for op_name in operations:
- elapsed = result[op_name]
- times_by_op[op_name].append(elapsed)
- rows.append([human_name, mode_name, op_name, attempt, f"{elapsed:.10f}"])
-
- for op_name in operations:
- avg_time = sum(times_by_op[op_name]) / len(times_by_op[op_name])
- rows.append([human_name, mode_name, op_name, "среднее", f"{avg_time:.10f}"])
-
- return rows
-
-
-def save_results_csv(rows, filename: str = "results.csv"):
- with open(filename, "w", newline="", encoding="utf-8") as f:
- writer = csv.writer(f)
- writer.writerows(rows)
-
-
-def main():
- rows = run_experiments(n=10000, buckets_count=2048, repeats=5)
- save_results_csv(rows, "results.csv")
- print("Saved results.csv")
-
-
-if __name__ == "__main__":
- main()
diff --git a/BolonkinNM/hash_table.py b/BolonkinNM/hash_table.py
deleted file mode 100644
index 9aa720d..0000000
--- a/BolonkinNM/hash_table.py
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-from typing import Any, Dict, List, Optional
-
-from linked_list import ll_insert, ll_find, ll_delete, ll_list_all
-
-
-Bucket = Optional[Dict[str, Any]]
-
-
-def _hash_name(name: str, buckets_count: int) -> int:
- if buckets_count <= 0:
- return 0
- return sum(ord(ch) for ch in name) % buckets_count
-
-
-def ht_insert(buckets: List[Bucket], name: str, phone: str) -> List[Bucket]:
- if not buckets:
- return buckets
- index = _hash_name(name, len(buckets))
- buckets[index] = ll_insert(buckets[index], name, phone)
- return buckets
-
-
-def ht_find(buckets: List[Bucket], name: str) -> Optional[str]:
- if not buckets:
- return None
- index = _hash_name(name, len(buckets))
- return ll_find(buckets[index], name)
-
-
-def ht_delete(buckets: List[Bucket], name: str) -> List[Bucket]:
- if not buckets:
- return buckets
- index = _hash_name(name, len(buckets))
- buckets[index] = ll_delete(buckets[index], name)
- return buckets
-
-
-def ht_list_all(buckets: List[Bucket]) -> List[Dict[str, str]]:
- records: List[Dict[str, str]] = []
- for head in buckets:
- records.extend(ll_list_all(head))
- return sorted(records, key=lambda x: x["name"])
diff --git a/BolonkinNM/linked_list.py b/BolonkinNM/linked_list.py
deleted file mode 100644
index 0260036..0000000
--- a/BolonkinNM/linked_list.py
+++ /dev/null
@@ -1,73 +0,0 @@
-
-
-from typing import Any, Dict, List, Optional
-
-
-Node = Dict[str, Any]
-
-
-def _make_node(name: str, phone: str) -> Node:
- return {"name": name, "phone": phone, "next": None}
-
-
-def sort_records(records: List[Dict[str, str]]) -> List[Dict[str, str]]:
-
- return sorted(records, key=lambda x: x["name"])
-
-
-def ll_insert(head: Optional[Node], name: str, phone: str) -> Node:
-
- new_node = _make_node(name, phone)
-
- if head is None:
- return new_node
-
- current = head
- while current is not None:
- if current["name"] == name:
- current["phone"] = phone
- return head
- if current["next"] is None:
- current["next"] = new_node
- return head
- current = current["next"]
-
- return head
-
-
-def ll_find(head: Optional[Node], name: str) -> Optional[str]:
- current = head
- while current is not None:
- if current["name"] == name:
- return current["phone"]
- current = current["next"]
- return None
-
-
-def ll_delete(head: Optional[Node], name: str) -> Optional[Node]:
- if head is None:
- return None
-
- if head["name"] == name:
- return head["next"]
-
- prev = head
- current = head["next"]
-
- while current is not None:
- if current["name"] == name:
- prev["next"] = current["next"]
- return head
- prev = current
- current = current["next"]
-
- return head
-
-
-def ll_list_all(head: Optional[Node]) -> List[Dict[str, str]]:
- records: List[Dict[str, str]] = []
- current = head
- while current is not None:
- records.append({"name": current["name"], "phone": current["phone"]})
- current = current["next"]
- return sort_records(records)
diff --git a/BolonkinNM/main.py b/BolonkinNM/main.py
index 70de618..08f22c7 100644
--- a/BolonkinNM/main.py
+++ b/BolonkinNM/main.py
@@ -1,21 +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 __future__ import annotations
-
-import csv
from pathlib import Path
-from experiments import run_experiments, save_results_csv
-from plot_results import build_graphs, load_average_results
+BASE_DIR = Path(__file__).resolve().parent
-def main():
- rows = run_experiments(n=10000, buckets_count=2048, repeats=5)
- save_results_csv(rows, "results.csv")
- averaged = load_average_results("results.csv")
- build_graphs(averaged, output_dir="docs/data")
- print("Done.")
+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__":
- 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/plot_results.py b/BolonkinNM/plot_results.py
deleted file mode 100644
index f4f3b6c..0000000
--- a/BolonkinNM/plot_results.py
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
-from __future__ import annotations
-
-import csv
-from collections import defaultdict
-from pathlib import Path
-
-import matplotlib.pyplot as plt
-
-
-def load_average_results(csv_file: str):
- results = []
- with open(csv_file, "r", encoding="utf-8") as f:
- reader = csv.DictReader(f)
- for row in reader:
- if row["Замер"] != "среднее":
- continue
- results.append({
- "structure": row["Структура"],
- "mode": row["Режим"],
- "operation": row["Операция"],
- "time": float(row["Время (сек)"]),
- })
- return results
-
-
-def build_graphs(results, output_dir: str = "docs/data"):
- output = Path(output_dir)
- output.mkdir(parents=True, exist_ok=True)
-
- grouped = defaultdict(list)
- for row in results:
- grouped[row["operation"]].append(row)
-
- for operation in ("insert", "find", "delete"):
- rows = grouped[operation]
- labels = [f"{r['structure']}\n{r['mode']}" for r in rows]
- values = [r["time"] for r in rows]
-
- plt.figure(figsize=(11, 6))
- plt.bar(labels, values)
- plt.title(f"{operation.capitalize()} comparison")
- plt.xlabel("Structure / data order")
- plt.ylabel("Time, seconds")
- plt.xticks(rotation=20)
- plt.tight_layout()
- filename = output / f"{operation}.png"
- plt.savefig(filename, dpi=160)
- plt.close()
- print(f"Saved {filename}")
-
-
-def main():
- results = load_average_results("results.csv")
- build_graphs(results)
-
-
-if __name__ == "__main__":
- main()
diff --git a/BolonkinNM/requirements.txt b/BolonkinNM/requirements.txt
index a9006fd..6ccafc3 100644
--- a/BolonkinNM/requirements.txt
+++ b/BolonkinNM/requirements.txt
@@ -1 +1 @@
-matplotlib>=3.8
+matplotlib
diff --git a/BolonkinNM/results.csv b/BolonkinNM/results.csv
deleted file mode 100644
index cc67b71..0000000
--- a/BolonkinNM/results.csv
+++ /dev/null
@@ -1,109 +0,0 @@
-Структура,Режим,Операция,Замер,Время (сек)
-LinkedList,случайный,insert,1,2.4210275000
-LinkedList,случайный,find,1,0.0214394000
-LinkedList,случайный,delete,1,0.0108667000
-LinkedList,случайный,insert,2,2.4208055000
-LinkedList,случайный,find,2,0.0216110000
-LinkedList,случайный,delete,2,0.0106216000
-LinkedList,случайный,insert,3,2.4210881000
-LinkedList,случайный,find,3,0.0216503000
-LinkedList,случайный,delete,3,0.0106497000
-LinkedList,случайный,insert,4,2.4530798000
-LinkedList,случайный,find,4,0.0222764000
-LinkedList,случайный,delete,4,0.0108350000
-LinkedList,случайный,insert,5,2.4567773000
-LinkedList,случайный,find,5,0.0219400000
-LinkedList,случайный,delete,5,0.0108697000
-LinkedList,случайный,insert,среднее,2.4345556400
-LinkedList,случайный,find,среднее,0.0217834200
-LinkedList,случайный,delete,среднее,0.0107685400
-HashTable,случайный,insert,1,0.1621210000
-HashTable,случайный,find,1,0.0011201000
-HashTable,случайный,delete,1,0.0005854000
-HashTable,случайный,insert,2,0.1732676000
-HashTable,случайный,find,2,0.0011247000
-HashTable,случайный,delete,2,0.0005818000
-HashTable,случайный,insert,3,0.1638609000
-HashTable,случайный,find,3,0.0011355000
-HashTable,случайный,delete,3,0.0005814000
-HashTable,случайный,insert,4,0.1642886000
-HashTable,случайный,find,4,0.0011268000
-HashTable,случайный,delete,4,0.0005785000
-HashTable,случайный,insert,5,0.1640916000
-HashTable,случайный,find,5,0.0011287000
-HashTable,случайный,delete,5,0.0005787000
-HashTable,случайный,insert,среднее,0.1655259400
-HashTable,случайный,find,среднее,0.0011271600
-HashTable,случайный,delete,среднее,0.0005811600
-BST,случайный,insert,1,0.0153754000
-BST,случайный,find,1,0.0001491000
-BST,случайный,delete,1,0.0000786000
-BST,случайный,insert,2,0.0155821000
-BST,случайный,find,2,0.0001453000
-BST,случайный,delete,2,0.0000724000
-BST,случайный,insert,3,0.0151360000
-BST,случайный,find,3,0.0001437000
-BST,случайный,delete,3,0.0000741000
-BST,случайный,insert,4,0.0153703000
-BST,случайный,find,4,0.0001425000
-BST,случайный,delete,4,0.0000715000
-BST,случайный,insert,5,0.0153753000
-BST,случайный,find,5,0.0001455000
-BST,случайный,delete,5,0.0000723000
-BST,случайный,insert,среднее,0.0153678200
-BST,случайный,find,среднее,0.0001452200
-BST,случайный,delete,среднее,0.0000737800
-LinkedList,отсортированный,insert,1,2.5884851000
-LinkedList,отсортированный,find,1,0.0227221000
-LinkedList,отсортированный,delete,1,0.0111309000
-LinkedList,отсортированный,insert,2,2.5095731000
-LinkedList,отсортированный,find,2,0.0217208000
-LinkedList,отсортированный,delete,2,0.0107773000
-LinkedList,отсортированный,insert,3,2.5642096000
-LinkedList,отсортированный,find,3,0.0228242000
-LinkedList,отсортированный,delete,3,0.0115945000
-LinkedList,отсортированный,insert,4,2.7163021000
-LinkedList,отсортированный,find,4,0.0431456000
-LinkedList,отсортированный,delete,4,0.0136020000
-LinkedList,отсортированный,insert,5,2.6891794000
-LinkedList,отсортированный,find,5,0.0217679000
-LinkedList,отсортированный,delete,5,0.0106384000
-LinkedList,отсортированный,insert,среднее,2.6135498600
-LinkedList,отсортированный,find,среднее,0.0264361200
-LinkedList,отсортированный,delete,среднее,0.0115486200
-HashTable,отсортированный,insert,1,0.1524640000
-HashTable,отсортированный,find,1,0.0014973000
-HashTable,отсортированный,delete,1,0.0006991000
-HashTable,отсортированный,insert,2,0.1537592000
-HashTable,отсортированный,find,2,0.0012225000
-HashTable,отсортированный,delete,2,0.0006561000
-HashTable,отсортированный,insert,3,0.1555816000
-HashTable,отсортированный,find,3,0.0012080000
-HashTable,отсортированный,delete,3,0.0006472000
-HashTable,отсортированный,insert,4,0.1546417000
-HashTable,отсортированный,find,4,0.0015017000
-HashTable,отсортированный,delete,4,0.0007512000
-HashTable,отсортированный,insert,5,0.1531659000
-HashTable,отсортированный,find,5,0.0012219000
-HashTable,отсортированный,delete,5,0.0006493000
-HashTable,отсортированный,insert,среднее,0.1539224800
-HashTable,отсортированный,find,среднее,0.0013302800
-HashTable,отсортированный,delete,среднее,0.0006805800
-BST,отсортированный,insert,1,4.5025059000
-BST,отсортированный,find,1,0.0387267000
-BST,отсортированный,delete,1,0.0162161000
-BST,отсортированный,insert,2,4.6704081000
-BST,отсортированный,find,2,0.0435012000
-BST,отсортированный,delete,2,0.0203211000
-BST,отсортированный,insert,3,6.2192950000
-BST,отсортированный,find,3,0.0578654000
-BST,отсортированный,delete,3,0.0327529000
-BST,отсортированный,insert,4,4.7844525000
-BST,отсортированный,find,4,0.0380228000
-BST,отсортированный,delete,4,0.0159740000
-BST,отсортированный,insert,5,4.4861403000
-BST,отсортированный,find,5,0.0382484000
-BST,отсортированный,delete,5,0.0159402000
-BST,отсортированный,insert,среднее,4.9325603600
-BST,отсортированный,find,среднее,0.0432729000
-BST,отсортированный,delete,среднее,0.0202408600
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
diff --git a/BolonkinNM/utils.py b/BolonkinNM/utils.py
deleted file mode 100644
index 0fcb993..0000000
--- a/BolonkinNM/utils.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import random
-from typing import List, Tuple
-
-
-Record = Tuple[str, str]
-
-
-def generate_records(n: int, repeat_names: bool = False, seed: int = 42) -> List[Record]:
- rng = random.Random(seed)
- records: List[Record] = []
-
- if repeat_names:
- name_pool = [
- "User_Alex", "User_Bob", "User_Cat", "User_Dan", "User_Eva",
- "User_Fox", "User_Geo", "User_Hen", "User_Ira", "User_Leo",
- ]
- for _ in range(n):
- name = rng.choice(name_pool)
- phone = f"{rng.randint(1000000000, 9999999999)}"
- records.append((name, phone))
- else:
- for i in range(n):
- name = f"User_{i:05d}"
- phone = f"{1000000000 + i}"
- records.append((name, phone))
-
- return records
-
-
-def prepare_records_variants(records: List[Record], seed: int = 42):
- rng = random.Random(seed)
- records_shuffled = list(records)
- rng.shuffle(records_shuffled)
- records_sorted = sorted(records, key=lambda x: x[0])
- return records_shuffled, records_sorted