Compare commits

..

No commits in common. "develop" and "task-1" have entirely different histories.

1834 changed files with 1 additions and 200506 deletions

View File

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (maze_project_submission) (2)" project-jdk-type="Python SDK" />
</project>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/maze_project_submission.iml" filepath="$PROJECT_DIR$/.idea/maze_project_submission.iml" />
</modules>
</component>
</project>

View File

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="a6ff989d-c5f6-4522-8b0a-933849f2d044" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 2
}]]></component>
<component name="ProjectId" id="3EB20Mq0B865MSq8Kkl2evaRIZW" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"last_opened_file_path": "C:/Users/vaz21/Downloads/Task 2 GLOBAL/maze_project_submission"
}
}]]></component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="a6ff989d-c5f6-4522-8b0a-933849f2d044" name="Changes" comment="" />
<created>1779637417749</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1779637417749</updated>
</task>
<servers />
</component>
</project>

View File

@ -1,24 +0,0 @@
# Maze Solver Project
ООП-проект для поиска выхода из лабиринта с паттернами:
- Builder
- Strategy
- Observer
- Command
## Запуск
```bash
python main.py
```
## Эксперименты
```bash
python experiment.py
```
Результаты сохраняются в папку `experiment_results/`.
## Требования
```bash
pip install -r requirements.txt
```

View File

@ -1,7 +0,0 @@
from abc import ABC, abstractmethod
class MazeBuilder(ABC):
@abstractmethod
def buildFromFile(self, filename):
raise NotImplementedError

View File

@ -1,52 +0,0 @@
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)

View File

@ -1,11 +0,0 @@
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self):
raise NotImplementedError
@abstractmethod
def undo(self):
raise NotImplementedError

View File

@ -1,37 +0,0 @@
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

View File

@ -1,30 +0,0 @@
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

View File

@ -1,26 +0,0 @@
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) + ")"

View File

@ -1,49 +0,0 @@
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))

View File

@ -1,6 +0,0 @@
class Player:
def __init__(self, currentCell):
self.currentCell = currentCell
def setCell(self, cell):
self.currentCell = cell

View File

@ -1,11 +0,0 @@
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 = ""

View File

@ -1 +0,0 @@
Place report files and experiment outputs here.

View File

@ -1,249 +0,0 @@
# Отчёт по работе «Поиск выхода из лабиринта»
## 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 {
<<interface>>
+buildFromFile(filename)
}
class TextFileMazeBuilder {
+buildFromFile(filename)
}
class PathFindingStrategy {
<<interface>>
+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 {
<<interface>>
+update(event)
}
class ConsoleView {
+update(event)
+render(maze, player_position, path)
}
class Command {
<<interface>>
+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-файлы с результатами экспериментов.
- Графики сравнений.
- Файлы с тестовыми лабиринтами.

View File

@ -1,225 +0,0 @@
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()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,121 +0,0 @@
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
1 maze strategy run time_ms visited_cells path_length
2 small_10x10 BFS 1 0.044300 15 15
3 small_10x10 BFS 2 0.022800 15 15
4 small_10x10 BFS 3 0.020400 15 15
5 small_10x10 BFS 4 0.020300 15 15
6 small_10x10 BFS 5 0.018700 15 15
7 small_10x10 DFS 1 0.031200 15 15
8 small_10x10 DFS 2 0.022000 15 15
9 small_10x10 DFS 3 0.021200 15 15
10 small_10x10 DFS 4 0.020800 15 15
11 small_10x10 DFS 5 0.020500 15 15
12 small_10x10 A* 1 0.048900 15 15
13 small_10x10 A* 2 0.034700 15 15
14 small_10x10 A* 3 0.029400 15 15
15 small_10x10 A* 4 0.029100 15 15
16 small_10x10 A* 5 0.029300 15 15
17 small_10x10 Dijkstra 1 0.037900 15 15
18 small_10x10 Dijkstra 2 0.028500 15 15
19 small_10x10 Dijkstra 3 0.026800 15 15
20 small_10x10 Dijkstra 4 0.026400 15 15
21 small_10x10 Dijkstra 5 0.026700 15 15
22 medium_50x50 BFS 1 2.105800 1579 95
23 medium_50x50 BFS 2 1.928700 1579 95
24 medium_50x50 BFS 3 1.969500 1579 95
25 medium_50x50 BFS 4 1.938800 1579 95
26 medium_50x50 BFS 5 1.943600 1579 95
27 medium_50x50 DFS 1 1.927300 1277 647
28 medium_50x50 DFS 2 1.856300 1277 647
29 medium_50x50 DFS 3 1.890100 1277 647
30 medium_50x50 DFS 4 1.868000 1277 647
31 medium_50x50 DFS 5 1.865500 1277 647
32 medium_50x50 A* 1 2.359000 927 95
33 medium_50x50 A* 2 2.193700 927 95
34 medium_50x50 A* 3 2.178400 927 95
35 medium_50x50 A* 4 2.181800 927 95
36 medium_50x50 A* 5 2.174500 927 95
37 medium_50x50 Dijkstra 1 3.534700 1579 95
38 medium_50x50 Dijkstra 2 3.435500 1579 95
39 medium_50x50 Dijkstra 3 3.457600 1579 95
40 medium_50x50 Dijkstra 4 3.417300 1579 95
41 medium_50x50 Dijkstra 5 3.538000 1579 95
42 large_100x100 BFS 1 8.624100 5566 195
43 large_100x100 BFS 2 7.706900 5566 195
44 large_100x100 BFS 3 9.723300 5566 195
45 large_100x100 BFS 4 7.585700 5566 195
46 large_100x100 BFS 5 8.031300 5566 195
47 large_100x100 DFS 1 5.512400 3543 1531
48 large_100x100 DFS 2 5.329300 3543 1531
49 large_100x100 DFS 3 5.223300 3543 1531
50 large_100x100 DFS 4 5.729900 3543 1531
51 large_100x100 DFS 5 5.497400 3543 1531
52 large_100x100 A* 1 2.101500 853 195
53 large_100x100 A* 2 2.264500 853 195
54 large_100x100 A* 3 2.064100 853 195
55 large_100x100 A* 4 2.031700 853 195
56 large_100x100 A* 5 2.046500 853 195
57 large_100x100 Dijkstra 1 25.021300 5571 195
58 large_100x100 Dijkstra 2 13.541100 5571 195
59 large_100x100 Dijkstra 3 12.884100 5571 195
60 large_100x100 Dijkstra 4 13.481800 5571 195
61 large_100x100 Dijkstra 5 12.748000 5571 195
62 empty_30x30 BFS 1 1.234300 896 55
63 empty_30x30 BFS 2 1.163400 896 55
64 empty_30x30 BFS 3 1.145700 896 55
65 empty_30x30 BFS 4 1.177300 896 55
66 empty_30x30 BFS 5 1.175100 896 55
67 empty_30x30 DFS 1 1.338000 842 815
68 empty_30x30 DFS 2 1.296500 842 815
69 empty_30x30 DFS 3 1.296700 842 815
70 empty_30x30 DFS 4 1.280100 842 815
71 empty_30x30 DFS 5 1.290800 842 815
72 empty_30x30 A* 1 2.183400 784 55
73 empty_30x30 A* 2 2.522900 784 55
74 empty_30x30 A* 3 1.985000 784 55
75 empty_30x30 A* 4 1.972100 784 55
76 empty_30x30 A* 5 2.088600 784 55
77 empty_30x30 Dijkstra 1 2.080400 896 55
78 empty_30x30 Dijkstra 2 2.100100 896 55
79 empty_30x30 Dijkstra 3 2.130700 896 55
80 empty_30x30 Dijkstra 4 2.073600 896 55
81 empty_30x30 Dijkstra 5 2.095900 896 55
82 no_path_30x30 BFS 1 0.645900 450 0
83 no_path_30x30 BFS 2 0.566600 450 0
84 no_path_30x30 BFS 3 0.566000 450 0
85 no_path_30x30 BFS 4 0.583500 450 0
86 no_path_30x30 BFS 5 0.568900 450 0
87 no_path_30x30 DFS 1 0.692100 450 0
88 no_path_30x30 DFS 2 0.676900 450 0
89 no_path_30x30 DFS 3 0.703500 450 0
90 no_path_30x30 DFS 4 0.722300 450 0
91 no_path_30x30 DFS 5 0.672000 450 0
92 no_path_30x30 A* 1 1.112700 450 0
93 no_path_30x30 A* 2 1.130000 450 0
94 no_path_30x30 A* 3 1.096100 450 0
95 no_path_30x30 A* 4 1.111400 450 0
96 no_path_30x30 A* 5 1.183500 450 0
97 no_path_30x30 Dijkstra 1 1.023300 450 0
98 no_path_30x30 Dijkstra 2 1.011700 450 0
99 no_path_30x30 Dijkstra 3 1.127200 450 0
100 no_path_30x30 Dijkstra 4 1.110200 450 0
101 no_path_30x30 Dijkstra 5 1.043900 450 0
102 weighted_30x30 BFS 1 1.074700 788 55
103 weighted_30x30 BFS 2 0.997700 788 55
104 weighted_30x30 BFS 3 0.992700 788 55
105 weighted_30x30 BFS 4 1.010800 788 55
106 weighted_30x30 BFS 5 1.035000 788 55
107 weighted_30x30 DFS 1 1.130200 693 479
108 weighted_30x30 DFS 2 1.057400 693 479
109 weighted_30x30 DFS 3 1.049900 693 479
110 weighted_30x30 DFS 4 1.051600 693 479
111 weighted_30x30 DFS 5 1.059100 693 479
112 weighted_30x30 A* 1 0.402200 126 55
113 weighted_30x30 A* 2 0.384100 126 55
114 weighted_30x30 A* 3 0.360000 126 55
115 weighted_30x30 A* 4 0.360700 126 55
116 weighted_30x30 A* 5 0.353500 126 55
117 weighted_30x30 Dijkstra 1 1.834900 781 55
118 weighted_30x30 Dijkstra 2 1.759000 781 55
119 weighted_30x30 Dijkstra 3 1.786300 781 55
120 weighted_30x30 Dijkstra 4 1.740500 781 55
121 weighted_30x30 Dijkstra 5 1.807100 781 55

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,25 +0,0 @@
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
1 maze strategy avg_time_ms avg_visited_cells avg_path_length runs
2 small_10x10 BFS 0.025300 15.00 15.00 5
3 small_10x10 DFS 0.023140 15.00 15.00 5
4 small_10x10 A* 0.034280 15.00 15.00 5
5 small_10x10 Dijkstra 0.029260 15.00 15.00 5
6 medium_50x50 BFS 1.977280 1579.00 95.00 5
7 medium_50x50 DFS 1.881440 1277.00 647.00 5
8 medium_50x50 A* 2.217480 927.00 95.00 5
9 medium_50x50 Dijkstra 3.476620 1579.00 95.00 5
10 large_100x100 BFS 8.334260 5566.00 195.00 5
11 large_100x100 DFS 5.458460 3543.00 1531.00 5
12 large_100x100 A* 2.101660 853.00 195.00 5
13 large_100x100 Dijkstra 15.535260 5571.00 195.00 5
14 empty_30x30 BFS 1.179160 896.00 55.00 5
15 empty_30x30 DFS 1.300420 842.00 815.00 5
16 empty_30x30 A* 2.150400 784.00 55.00 5
17 empty_30x30 Dijkstra 2.096140 896.00 55.00 5
18 no_path_30x30 BFS 0.586180 450.00 0.00 5
19 no_path_30x30 DFS 0.693360 450.00 0.00 5
20 no_path_30x30 A* 1.126740 450.00 0.00 5
21 no_path_30x30 Dijkstra 1.063260 450.00 0.00 5
22 weighted_30x30 BFS 1.022180 788.00 55.00 5
23 weighted_30x30 DFS 1.069640 693.00 479.00 5
24 weighted_30x30 A* 0.372100 126.00 55.00 5
25 weighted_30x30 Dijkstra 1.785560 781.00 55.00 5

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,59 +0,0 @@
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()

View File

@ -1,9 +0,0 @@
S
E

View File

@ -1,11 +0,0 @@
####################################################################################################
#S # # # # # # # # # # # # # # # E#
# # ### ### # ###### # ### # ## # #### # ####### # #### # # ### ## # ## # # ## # ## # ##### ### ##
# # # # # # # # # # # # # # # # # # # # # # # # # # #
# ##### # ######## # ### # ## # #### # ####### ## ### # # #### ####### ## ####### ####### # ### ##
# # # # # # # # # # # # # # # # # # # # #
### # # ###### # ########### ########### ### ####### # ####### ### # # ###### # ### ### # ### ####
# # # # # # # # # # # # # # # # # # # # # #
# ### ###### # ##### # ### # ####### # ### ### ## # ###### # ### # ### ###### # ### # ### ### ## #
# # # # # # # # #
####################################################################################################

View File

@ -1,11 +0,0 @@
##################################################
#S # # # # # # E#
# # ### ### # ###### # ### # ## # #### # ####### ##
# # # # # # # # # # # # # #
# ##### # ######## # ### # ## # #### # ####### ## #
# # # # # # # # # #
### # # ###### # ########### ########### ### ######
# # # # # # # # # # #
# ### ###### # ##### # ### # ####### # ### ### ## #
# # # # #
##################################################

View File

@ -1,9 +0,0 @@
##########
#S #
# ###### #
# # #
##########
# #E#
# ###### #
# #
##########

View File

@ -1,7 +0,0 @@
##########
#S #E#
# ## # # ##
# # #
# #### # #
# # #
##########

View File

@ -1,10 +0,0 @@
1111111111111111111111111111
1S11111111111111111111111111
1111111111111111111111111111
1111111111111111111111111111
1111111111111222222222222111
1111111111111222222222222111
1111111111111333333333333111
1111111111111333333333333111
111111111111111111111111111E
1111111111111111111111111111

View File

@ -1,26 +0,0 @@
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()

View File

@ -1,7 +0,0 @@
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, event):
raise NotImplementedError

View File

@ -1 +0,0 @@
matplotlib

View File

@ -1,50 +0,0 @@
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

View File

@ -1,45 +0,0 @@
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 []

View File

@ -1,31 +0,0 @@
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 []

View File

@ -1,35 +0,0 @@
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 []

View File

@ -1,41 +0,0 @@
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 []

View File

@ -1,30 +0,0 @@
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

View File

@ -1 +0,0 @@

View File

@ -1,71 +0,0 @@
def create_node(name, phone):
return {'name': name, 'phone': phone, 'left': None, 'right': None}
def bst_insert(root, name, phone):
if root is None:
return create_node(name, phone)
if name == root['name']:
root['phone'] = phone
elif name < root['name']:
root['left'] = bst_insert(root['left'], name, phone)
else:
root['right'] = bst_insert(root['right'], name, phone)
return root
def bst_find(root, name):
if root is None:
return None
if name == root['name']:
return root['phone']
elif name < root['name']:
return bst_find(root['left'], name)
else:
return bst_find(root['right'], name)
def _find_min(node):
while node['left'] is not None:
node = node['left']
return node
def bst_delete(root, name):
if root is None:
return None
if name < root['name']:
root['left'] = bst_delete(root['left'], name)
elif name > root['name']:
root['right'] = bst_delete(root['right'], name)
else:
if root['left'] is None:
return root['right']
if root['right'] is None:
return root['left']
min_node = _find_min(root['right'])
root['name'] = min_node['name']
root['phone'] = min_node['phone']
root['right'] = bst_delete(root['right'], min_node['name'])
return root
def bst_list_all(root):
result = []
def inorder(node):
if node is None:
return
inorder(node['left'])
result.append((node['name'], node['phone']))
inorder(node['right'])
inorder(root)
return result
if __name__ == '__main__':
root = None
root = bst_insert(root, 'Иван', '123-456')
root = bst_insert(root, 'Борис', '789-012')
root = bst_insert(root, 'Анна', '345-678')
root = bst_insert(root, 'Иван', '111-222')
print(bst_list_all(root))
print(bst_find(root, 'Иван'))
print(bst_find(root, 'Петр'))
root = bst_delete(root, 'Борис')
print(bst_list_all(root))

View File

@ -1,126 +0,0 @@
import random
import time
import csv
import sys
sys.setrecursionlimit(20000)
from linked_list_phonebook import ll_insert, ll_find, ll_delete, ll_list_all
from hash_table_phonebook import ht_insert, ht_find, ht_delete, ht_list_all
from bst_phonebook import bst_insert, bst_find, bst_delete, bst_list_all
def generate_records(n, seed=42):
random.seed(seed)
records = []
for i in range(1, n+1):
name = f"User_{i:05d}"
phone = f"{random.randint(100,999)}-{random.randint(1000,9999)}"
records.append((name, phone))
return records
def prepare_datasets(base_records):
shuffled = base_records.copy()
random.shuffle(shuffled)
sorted_records = sorted(base_records, key=lambda x: x[0])
return shuffled, sorted_records
def run_experiment(struct_funcs, records, mode_name, repeats=5):
results = []
for rep in range(repeats):
struct = struct_funcs['create']()
start = time.perf_counter()
for name, phone in records:
struct = struct_funcs['insert'](struct, name, phone)
end = time.perf_counter()
insert_time = end - start
existing_names = [name for name, _ in records]
sample_existing = random.sample(existing_names, 100)
nonexistent = [f"NotExist_{i}" for i in range(10)]
search_names = sample_existing + nonexistent
random.shuffle(search_names)
start = time.perf_counter()
for name in search_names:
_ = struct_funcs['find'](struct, name)
end = time.perf_counter()
find_time = end - start
to_delete = random.sample(existing_names, 50)
start = time.perf_counter()
for name in to_delete:
struct = struct_funcs['delete'](struct, name)
end = time.perf_counter()
delete_time = end - start
results.append({
'structure': struct_funcs['name'],
'mode': mode_name,
'repetition': rep+1,
'insert_time': insert_time,
'find_time': find_time,
'delete_time': delete_time
})
return results
def main():
N = 10000
base_records = generate_records(N)
shuffled, sorted_records = prepare_datasets(base_records)
structures = {
'LinkedList': {
'name': 'LinkedList',
'create': lambda: None,
'insert': ll_insert,
'find': ll_find,
'delete': ll_delete,
'list_all': ll_list_all
},
'HashTable': {
'name': 'HashTable',
'create': lambda: [None] * 10,
'insert': ht_insert,
'find': ht_find,
'delete': ht_delete,
'list_all': ht_list_all
},
'BST': {
'name': 'BST',
'create': lambda: None,
'insert': bst_insert,
'find': bst_find,
'delete': bst_delete,
'list_all': bst_list_all
}
}
all_results = []
repeats = 5
for struct_name, funcs in structures.items():
print(f"Testing {struct_name} on random order...")
res_random = run_experiment(funcs, shuffled, 'random', repeats)
all_results.extend(res_random)
print(f"Testing {struct_name} on sorted order...")
res_sorted = run_experiment(funcs, sorted_records, 'sorted', repeats)
all_results.extend(res_sorted)
with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['Structure', 'Mode', 'Repeat', 'Insert (sec)', 'Search (sec)', 'Delete (sec)'])
for r in all_results:
writer.writerow([
r['structure'],
r['mode'],
r['repetition'],
f"{r['insert_time']:.6f}",
f"{r['find_time']:.6f}",
f"{r['delete_time']:.6f}"
])
print("Experiment finished. Results saved to experiment_results.csv")
if __name__ == '__main__':
main()

View File

@ -1,31 +0,0 @@
Structure,Mode,Repeat,Insert (sec),Search (sec),Delete (sec)
LinkedList,random,1,4.432559,0.034196,0.014270
LinkedList,random,2,4.999931,0.038043,0.020281
LinkedList,random,3,4.771456,0.030191,0.014131
LinkedList,random,4,4.707315,0.033500,0.016198
LinkedList,random,5,4.721361,0.036586,0.011988
LinkedList,sorted,1,4.139028,0.024011,0.010482
LinkedList,sorted,2,4.212383,0.024592,0.011765
LinkedList,sorted,3,4.674211,0.027756,0.012189
LinkedList,sorted,4,4.610210,0.031519,0.012244
LinkedList,sorted,5,4.565687,0.029739,0.012747
HashTable,random,1,0.659990,0.003889,0.001728
HashTable,random,2,0.666055,0.005980,0.002002
HashTable,random,3,0.669948,0.004087,0.002176
HashTable,random,4,0.661882,0.007439,0.001897
HashTable,random,5,0.680420,0.004016,0.001649
HashTable,sorted,1,0.648261,0.004277,0.002922
HashTable,sorted,2,0.654924,0.004136,0.001793
HashTable,sorted,3,0.645509,0.003900,0.002249
HashTable,sorted,4,0.637906,0.004056,0.001657
HashTable,sorted,5,0.643536,0.003846,0.001741
BST,random,1,0.029415,0.000515,0.000183
BST,random,2,0.027684,0.000216,0.000142
BST,random,3,0.026213,0.000252,0.000159
BST,random,4,0.026987,0.000207,0.000135
BST,random,5,0.028321,0.000271,0.000183
BST,sorted,1,10.293772,0.093178,0.053520
BST,sorted,2,10.142204,0.088924,0.049079
BST,sorted,3,10.142037,0.078281,0.059416
BST,sorted,4,10.139818,0.100162,0.056881
BST,sorted,5,10.102982,0.082247,0.051973
1 Structure Mode Repeat Insert (sec) Search (sec) Delete (sec)
2 LinkedList random 1 4.432559 0.034196 0.014270
3 LinkedList random 2 4.999931 0.038043 0.020281
4 LinkedList random 3 4.771456 0.030191 0.014131
5 LinkedList random 4 4.707315 0.033500 0.016198
6 LinkedList random 5 4.721361 0.036586 0.011988
7 LinkedList sorted 1 4.139028 0.024011 0.010482
8 LinkedList sorted 2 4.212383 0.024592 0.011765
9 LinkedList sorted 3 4.674211 0.027756 0.012189
10 LinkedList sorted 4 4.610210 0.031519 0.012244
11 LinkedList sorted 5 4.565687 0.029739 0.012747
12 HashTable random 1 0.659990 0.003889 0.001728
13 HashTable random 2 0.666055 0.005980 0.002002
14 HashTable random 3 0.669948 0.004087 0.002176
15 HashTable random 4 0.661882 0.007439 0.001897
16 HashTable random 5 0.680420 0.004016 0.001649
17 HashTable sorted 1 0.648261 0.004277 0.002922
18 HashTable sorted 2 0.654924 0.004136 0.001793
19 HashTable sorted 3 0.645509 0.003900 0.002249
20 HashTable sorted 4 0.637906 0.004056 0.001657
21 HashTable sorted 5 0.643536 0.003846 0.001741
22 BST random 1 0.029415 0.000515 0.000183
23 BST random 2 0.027684 0.000216 0.000142
24 BST random 3 0.026213 0.000252 0.000159
25 BST random 4 0.026987 0.000207 0.000135
26 BST random 5 0.028321 0.000271 0.000183
27 BST sorted 1 10.293772 0.093178 0.053520
28 BST sorted 2 10.142204 0.088924 0.049079
29 BST sorted 3 10.142037 0.078281 0.059416
30 BST sorted 4 10.139818 0.100162 0.056881
31 BST sorted 5 10.102982 0.082247 0.051973

View File

@ -1,47 +0,0 @@
from linked_list_phonebook import ll_insert, ll_find, ll_delete, ll_list_all
def hash_function(name, table_size):
return hash(name) % table_size
def ht_insert(buckets, name, phone):
idx = hash_function(name, len(buckets))
head = buckets[idx]
new_head = ll_insert(head, name, phone)
buckets[idx] = new_head
return buckets
def ht_find(buckets, name):
idx = hash_function(name, len(buckets))
head = buckets[idx]
return ll_find(head, name)
def ht_delete(buckets, name):
idx = hash_function(name, len(buckets))
head = buckets[idx]
new_head = ll_delete(head, name)
buckets[idx] = new_head
return buckets
def ht_list_all(buckets):
all_records = []
for head in buckets:
current = head
while current is not None:
all_records.append((current['name'], current['phone']))
current = current['next']
all_records.sort(key=lambda x: x[0])
return all_records
if __name__ == '__main__':
SIZE = 5
buckets = [None] * SIZE
ht_insert(buckets, 'Иван', '123-456')
ht_insert(buckets, 'Борис', '789-012')
ht_insert(buckets, 'Анна', '345-678')
ht_insert(buckets, 'Иван', '111-222')
print(ht_list_all(buckets))
print(ht_find(buckets, 'Анна'))
print(ht_find(buckets, 'Петр'))
ht_delete(buckets, 'Борис')
print(ht_list_all(buckets))

View File

@ -1,67 +0,0 @@
def create_node(name, phone):
return {'name': name, 'phone': phone, 'next': None}
def ll_insert(head, name, phone):
current = head
while current is not None:
if current['name'] == name:
current['phone'] = phone
return head
current = current['next']
new_node = create_node(name, phone)
if head is None:
return new_node
current = head
while current['next'] is not None:
current = current['next']
current['next'] = new_node
return head
def ll_find(head, name):
current = head
while current is not None:
if current['name'] == name:
return current['phone']
current = current['next']
return None
def ll_delete(head, name):
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):
records = []
current = head
while current is not None:
records.append((current['name'], current['phone']))
current = current['next']
records.sort(key=lambda pair: pair[0])
return records
if __name__ == '__main__':
head = None
head = ll_insert(head, 'Иван', '123-456')
head = ll_insert(head, 'Борис', '789-012')
head = ll_insert(head, 'Анна', '345-678')
head = ll_insert(head, 'Иван', '111-222')
print(ll_list_all(head))
print(ll_find(head, 'Иван'))
print(ll_find(head, 'Петр'))
head = ll_delete(head, 'Борис')
print(ll_list_all(head))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View File

@ -1,39 +0,0 @@
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
df = pd.read_csv('experiment_results.csv')
mean_times = df.groupby(['Structure', 'Mode'])[['Insert (sec)', 'Search (sec)', 'Delete (sec)']].mean().reset_index()
structures = mean_times['Structure'].unique()
modes = mean_times['Mode'].unique()
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
operations = ['Insert (sec)', 'Search (sec)', 'Delete (sec)']
titles = ['Insertion', 'Search', 'Deletion']
for ax, op, title in zip(axes, operations, titles):
x = np.arange(len(structures))
width = 0.35
random_vals = []
sorted_vals = []
for s in structures:
random_row = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'random')]
sorted_row = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'sorted')]
random_vals.append(random_row[op].values[0] if not random_row.empty else 0)
sorted_vals.append(sorted_row[op].values[0] if not sorted_row.empty else 0)
ax.bar(x - width/2, random_vals, width, label='Random')
ax.bar(x + width/2, sorted_vals, width, label='Sorted')
ax.set_xticks(x)
ax.set_xticklabels(structures)
ax.set_ylabel('Time (seconds)')
ax.set_title(title)
ax.legend()
plt.tight_layout()
plt.savefig('performance_comparison.png', dpi=150)
plt.show()

View File

@ -1,16 +0,0 @@
maze,strategy,time_ms,visited_cells,path_length
Small 10x6,BFS,0.05722500009142095,25.0,16.0
Small 10x6,DFS,0.05680966667872175,24.0,16.0
Small 10x6,AStar,0.04801966664066034,23.0,16.0
Medium 10x10,BFS,0.04772166676048073,47.0,16.0
Medium 10x10,DFS,0.034641333362136116,44.0,30.0
Medium 10x10,AStar,0.0983669999641279,47.0,16.0
Large 20x20,BFS,0.09949400002066493,100.0,36.0
Large 20x20,DFS,0.07004933331700158,75.0,68.0
Large 20x20,AStar,0.16450733316257052,85.0,36.0
Empty 15x15,BFS,0.13264433331035738,133.0,17.0
Empty 15x15,DFS,0.11371733338213137,161.0,89.0
Empty 15x15,AStar,0.1543506666621397,65.0,17.0
No exit 10x10,BFS,0.04392100011803753,25.0,0.0
No exit 10x10,DFS,0.05871466661725814,25.0,0.0
No exit 10x10,AStar,0.046440666665148456,25.0,0.0
1 maze strategy time_ms visited_cells path_length
2 Small 10x6 BFS 0.05722500009142095 25.0 16.0
3 Small 10x6 DFS 0.05680966667872175 24.0 16.0
4 Small 10x6 AStar 0.04801966664066034 23.0 16.0
5 Medium 10x10 BFS 0.04772166676048073 47.0 16.0
6 Medium 10x10 DFS 0.034641333362136116 44.0 30.0
7 Medium 10x10 AStar 0.0983669999641279 47.0 16.0
8 Large 20x20 BFS 0.09949400002066493 100.0 36.0
9 Large 20x20 DFS 0.07004933331700158 75.0 68.0
10 Large 20x20 AStar 0.16450733316257052 85.0 36.0
11 Empty 15x15 BFS 0.13264433331035738 133.0 17.0
12 Empty 15x15 DFS 0.11371733338213137 161.0 89.0
13 Empty 15x15 AStar 0.1543506666621397 65.0 17.0
14 No exit 10x10 BFS 0.04392100011803753 25.0 0.0
15 No exit 10x10 DFS 0.05871466661725814 25.0 0.0
16 No exit 10x10 AStar 0.046440666665148456 25.0 0.0

View File

@ -1,438 +0,0 @@
import sys
import os
from collections import deque
import heapq
import time
import csv
import matplotlib.pyplot as plt
import numpy as np
class GridPoint:
def __init__(self, x, y):
self.x = x
self.y = y
self.blocked = False
self.is_start = False
self.is_exit = False
def can_step(self):
return not self.blocked
class Labyrinth:
def __init__(self, w, h):
self.w = w
self.h = h
self.grid = [[GridPoint(x, y) for x in range(w)] for y in range(h)]
self.start_point = None
self.exit_point = None
def get_point(self, x, y):
if 0 <= x < self.w and 0 <= y < self.h:
return self.grid[y][x]
return None
def set_point(self, x, y, typ):
p = self.get_point(x, y)
if not p:
return
if typ == 'wall':
p.blocked = True
elif typ == 'start':
if self.start_point:
self.start_point.is_start = False
p.is_start = True
p.blocked = False
self.start_point = p
elif typ == 'exit':
if self.exit_point:
self.exit_point.is_exit = False
p.is_exit = True
p.blocked = False
self.exit_point = p
elif typ == 'path':
p.blocked = False
def neighbors(self, p):
dirs = [(0, -1), (0, 1), (-1, 0), (1, 0)]
res = []
for dx, dy in dirs:
nx, ny = p.x + dx, p.y + dy
nb = self.get_point(nx, ny)
if nb and nb.can_step():
res.append(nb)
return res
class MazeLoader:
def load(self, filename):
raise NotImplementedError
class TextMazeLoader(MazeLoader):
def load(self, filename):
with open(filename, 'r') as f:
lines = [line.rstrip('\n') for line in f]
h = len(lines)
w = max(len(line) for line in lines) if h > 0 else 0
start_cnt = 0
exit_cnt = 0
lab = Labyrinth(w, h)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if ch == '#':
lab.set_point(x, y, 'wall')
elif ch == 'S':
lab.set_point(x, y, 'start')
start_cnt += 1
elif ch == 'E':
lab.set_point(x, y, 'exit')
exit_cnt += 1
else:
lab.set_point(x, y, 'path')
if start_cnt != 1 or exit_cnt != 1:
raise ValueError(f"Need exactly one S and one E. Found S={start_cnt}, E={exit_cnt}")
return lab
class SearchAlgorithm:
def find_way(self, lab, start, goal):
raise NotImplementedError
def _build_path(self, prev, start, goal):
path = []
cur = goal
while cur:
path.append(cur)
cur = prev.get(cur)
path.reverse()
return path
def get_visited(self):
return getattr(self, '_visited', 0)
class BreadthFirst(SearchAlgorithm):
def find_way(self, lab, start, goal):
q = deque([start])
prev = {start: None}
seen = {start}
while q:
cur = q.popleft()
if cur == goal:
self._visited = len(seen)
return self._build_path(prev, start, goal)
for nb in lab.neighbors(cur):
if nb not in seen:
seen.add(nb)
prev[nb] = cur
q.append(nb)
self._visited = len(seen)
return []
class DepthFirst(SearchAlgorithm):
def find_way(self, lab, start, goal):
stack = [start]
prev = {start: None}
seen = {start}
while stack:
cur = stack.pop()
if cur == goal:
self._visited = len(seen)
return self._build_path(prev, start, goal)
for nb in lab.neighbors(cur):
if nb not in seen:
seen.add(nb)
prev[nb] = cur
stack.append(nb)
self._visited = len(seen)
return []
class AStar(SearchAlgorithm):
def _dist(self, a, b):
return abs(a.x - b.x) + abs(a.y - b.y)
def find_way(self, lab, start, goal):
heap = []
cnt = 0
start_f = self._dist(start, goal)
heapq.heappush(heap, (start_f, cnt, start))
cnt += 1
prev = {}
g = {start: 0}
f = {start: start_f}
seen = set()
while heap:
cur_f, _, cur = heapq.heappop(heap)
seen.add(cur)
if cur == goal:
self._visited = len(seen)
return self._build_path(prev, start, goal)
if cur_f > f.get(cur, float('inf')):
continue
for nb in lab.neighbors(cur):
new_g = g[cur] + 1
if new_g < g.get(nb, float('inf')):
prev[nb] = cur
g[nb] = new_g
new_f = new_g + self._dist(nb, goal)
f[nb] = new_f
heapq.heappush(heap, (new_f, cnt, nb))
cnt += 1
self._visited = len(seen)
return []
class LabyrinthSolver:
def __init__(self, lab):
self.lab = lab
self.algorithm = None
def set_algorithm(self, algo):
self.algorithm = algo
def solve(self):
if not self.algorithm:
return None
t0 = time.perf_counter()
path = self.algorithm.find_way(self.lab, self.lab.start_point, self.lab.exit_point)
t1 = time.perf_counter()
ms = (t1 - t0) * 1000
return ms, self.algorithm.get_visited(), len(path)
class Player:
def __init__(self, start, lab):
self.current = start
self.last = None
self.lab = lab
def move(self, cell):
if cell and cell.can_step():
self.last = self.current
self.current = cell
return True
return False
def undo(self):
if self.last:
self.current, self.last = self.last, None
return True
return False
class Command:
def do(self):
raise NotImplementedError
def revert(self):
raise NotImplementedError
class MoveCommand(Command):
def __init__(self, player, dx, dy, lab):
self.player = player
self.dx = dx
self.dy = dy
self.lab = lab
self.done = False
def do(self):
nx = self.player.current.x + self.dx
ny = self.player.current.y + self.dy
target = self.lab.get_point(nx, ny)
if target and target.can_step():
self.player.move(target)
self.done = True
return True
return False
def revert(self):
if self.done:
self.player.undo()
self.done = False
return True
return False
class InteractiveView:
def __init__(self, lab, player):
self.lab = lab
self.player = player
def render(self):
os.system('cls' if os.name == 'nt' else 'clear')
print("=" * (self.lab.w * 2 + 4))
print(" LABYRINTH (P = player)")
print("=" * (self.lab.w * 2 + 4))
for y in range(self.lab.h):
print(" ", end='')
for x in range(self.lab.w):
p = self.lab.get_point(x, y)
if self.player.current == p:
print('P', end=' ')
elif p == self.lab.start_point:
print('S', end=' ')
elif p == self.lab.exit_point:
print('E', end=' ')
elif p.blocked:
print('#', end=' ')
else:
print('.', end=' ')
print()
print("=" * (self.lab.w * 2 + 4))
print(f" Position: ({self.player.current.x},{self.player.current.y})")
print(" Controls: h(left) j(down) k(up) l(right) u=undo q=quit")
print(" Auto-search: b=BFS d=DFS a=A*")
def run_experiment(maze_file, algo, runs=5):
loader = TextMazeLoader()
lab = loader.load(maze_file)
total_ms = 0
total_visited = 0
total_len = 0
for _ in range(runs):
solver = LabyrinthSolver(lab)
solver.set_algorithm(algo)
stats = solver.solve()
if stats:
ms, vis, plen = stats
total_ms += ms
total_visited += vis
total_len += plen
return total_ms / runs, total_visited / runs, total_len / runs
def generate_plots(results):
mazes = list(set([r['maze'] for r in results]))
strategies = ['BFS', 'DFS', 'AStar']
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
x = np.arange(len(mazes))
width = 0.25
for i, strat in enumerate(strategies):
times = []
for maze in mazes:
val = next((r['time_ms'] for r in results if r['maze'] == maze and r['strategy'] == strat), 0)
times.append(val)
axes[0].bar(x + i*width, times, width, label=strat)
axes[0].set_xlabel('Maze')
axes[0].set_ylabel('Time (ms)')
axes[0].set_title('Execution Time')
axes[0].set_xticks(x + width)
axes[0].set_xticklabels(mazes, rotation=45, ha='right')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
for i, strat in enumerate(strategies):
visited = []
for maze in mazes:
val = next((r['visited_cells'] for r in results if r['maze'] == maze and r['strategy'] == strat), 0)
visited.append(val)
axes[1].bar(x + i*width, visited, width, label=strat)
axes[1].set_xlabel('Maze')
axes[1].set_ylabel('Visited Cells')
axes[1].set_title('Visited Cells')
axes[1].set_xticks(x + width)
axes[1].set_xticklabels(mazes, rotation=45, ha='right')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
for i, strat in enumerate(strategies):
lengths = []
for maze in mazes:
val = next((r['path_length'] for r in results if r['maze'] == maze and r['strategy'] == strat), 0)
lengths.append(val)
axes[2].bar(x + i*width, lengths, width, label=strat)
axes[2].set_xlabel('Maze')
axes[2].set_ylabel('Path Length')
axes[2].set_title('Path Length')
axes[2].set_xticks(x + width)
axes[2].set_xticklabels(mazes, rotation=45, ha='right')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('performance_comparison.png', dpi=150, bbox_inches='tight')
plt.show()
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
print("Running experiments on all mazes...")
maze_files = [
("maze/maze1.txt", "Small 10x6"),
("maze/maze10x10.txt", "Medium 10x10"),
("maze/maze20x20.txt", "Large 20x20"),
("maze/maze_empty.txt", "Empty 15x15"),
("maze/maze_no_exit.txt", "No exit 10x10")
]
algorithms = [
("BFS", BreadthFirst()),
("DFS", DepthFirst()),
("AStar", AStar())
]
results = []
for fname, label in maze_files:
print(f"Testing {label}...")
for aname, algo in algorithms:
try:
avg_t, avg_v, avg_l = run_experiment(fname, algo, runs=3)
results.append({
'maze': label,
'strategy': aname,
'time_ms': avg_t,
'visited_cells': avg_v,
'path_length': avg_l
})
print(f" {aname}: time={avg_t:.3f}ms visited={avg_v:.0f} length={avg_l:.0f}")
except Exception as e:
print(f" {aname}: ERROR {e}")
# save csv
with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['maze', 'strategy', 'time_ms', 'visited_cells', 'path_length'])
writer.writeheader()
writer.writerows(results)
generate_plots(results)
print("Done. Results saved to experiment_results.csv and performance_comparison.png")
sys.exit(0)
# else interactive mode
loader = TextMazeLoader()
lab = loader.load("maze/maze1.txt")
player = Player(lab.start_point, lab)
view = InteractiveView(lab, player)
view.render()
solver = LabyrinthSolver(lab)
history = []
while True:
key = input("\n > ").lower()
if key == 'q':
print("Goodbye!")
break
elif key == 'b':
solver.set_algorithm(BreadthFirst())
ms, vis, plen = solver.solve()
print(f"BFS: {ms:.3f}ms, visited={vis}, length={plen}")
elif key == 'd':
solver.set_algorithm(DepthFirst())
ms, vis, plen = solver.solve()
print(f"DFS: {ms:.3f}ms, visited={vis}, length={plen}")
elif key == 'a':
solver.set_algorithm(AStar())
ms, vis, plen = solver.solve()
print(f"A*: {ms:.3f}ms, visited={vis}, length={plen}")
elif key in ('h','j','k','l'):
moves = {'h': (-1,0), 'l': (1,0), 'k': (0,-1), 'j': (0,1)}
dx, dy = moves[key]
cmd = MoveCommand(player, dx, dy, lab)
if cmd.do():
history.append(cmd)
view.render()
if player.current == lab.exit_point:
print("\n*** YOU REACHED THE EXIT! ***")
print(f"Total moves: {len(history)}")
break
else:
print("Can't go there - wall!")
elif key == 'u':
if history:
cmd = history.pop()
cmd.revert()
view.render()
print("Undo last move")
else:
print("Nothing to undo")
else:
print("Unknown command")

View File

@ -1,7 +0,0 @@
##########
#S #
# ####### #
# # # #
# # ### # #
# # E #
##########

View File

@ -1,10 +0,0 @@
##########
#S #
# # #### #
# # #
# #### # #
# # #
# #### # #
# # #
# #
########E#

View File

@ -1,21 +0,0 @@
####################
#S #
# ############### #
# # # #
# # ######### # # #
# # # # # # #
# # # ##### # # # #
# # # # # # # # #
# # # # # # # # # #
# # # # # # # # #
# # # ##### # # # #
# # # # # # #
# # ######### # # #
# # # #
# ############### #
# #
# ############### #
# # # #
# # ########### # #
# E#
####################

View File

@ -1,15 +0,0 @@
###############
#S #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# E #
###############

View File

@ -1,7 +0,0 @@
##########
#S #
# # #
# # #### #
# # #
##########
E#########

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View File

@ -1,94 +0,0 @@
# Отчёт по лабораторной работе «Структуры данных для телефонного справочника»
## 1. Постановка задачи
В рамках работы требовалось реализовать три структуры данных «с нуля» (без использования встроенных коллекций, кроме базовых списков):
- связный список,
- хеш-таблицу с цепочками,
- двоичное дерево поиска (несбалансированное).
Для каждой структуры необходимо реализовать операции `insert`, `find`, `delete` и `list_all` (возврат всех записей, отсортированных по имени). Затем на наборе из 10000 записей выполнить экспериментальное сравнение производительности в двух режимах: при случайном порядке вставки и при вставке записей, отсортированных по имени. Каждый эксперимент повторялся 5 раз.
## 2. Результаты измерений
Ниже приведены усреднённые по 5 повторам времена выполнения операций (в секундах). Исходные сырые данные сохранены в файле `experiment_results.csv`.
| Структура | Режим | Вставка (с) | Поиск 110 имён (с) | Удаление 50 записей (с) |
|-------------|-------------|-------------|--------------------|-------------------------|
| LinkedList | случайный | 4.7265 | 0.0345 | 0.0154 |
| LinkedList | сортир. | 4.4403 | 0.0275 | 0.0119 |
| HashTable | случайный | 0.6677 | 0.0051 | 0.0019 |
| HashTable | сортир. | 0.6460 | 0.0040 | 0.0021 |
| BST | случайный | 0.0277 | 0.00029 | 0.00016 |
| BST | сортир. | 10.1642 | 0.0886 | 0.0542 |
### Примечания к методике
- **Вставка** добавление всех 10000 записей в пустую структуру.
- **Поиск** 100 заведомо существующих имён + 10 несуществующих (общее количество вызовов 110).
- **Удаление** 50 случайных существующих записей.
- Все замеры выполнены с помощью `time.perf_counter()`.
- Для хеш-таблицы использовалось 10 корзин.
- Рекурсивная глубина BST увеличена до 20000, чтобы избежать переполнения стека.
## 3. Анализ полученных данных
### 3.1. Поведение BST при разных порядках ввода
Двоичное дерево поиска сильно зависит от порядка поступления ключей. При случайном порядке средняя высота близка к логарифмической, что даёт отличную производительность:
- вставка **0.0277 с**,
- поиск **0.00029 с** (самый быстрый среди всех структур в этом режиме).
Однако при вставке отсортированных данных дерево вырождается в линейный список (каждый новый узел добавляется только в правое поддерево). Последствия:
- время вставки возрастает **в 367 раз** (с 0.0277 до 10.16 с),
- поиск замедляется **в 305 раз**,
- удаление **в 339 раз**.
Вырожденное BST на отсортированных данных работает **медленнее даже связного списка** (вставка 10.16 с против 4.44 с, поиск 0.088 с против 0.027 с), что объясняется накладными расходами на рекурсивные вызовы и проверки.
### 3.2. Хеш-таблица устойчивость к порядку
Хеш-таблица использует функцию `hash(name) % size`, которая равномерно рассеивает имена независимо от их лексикографического порядка. Поэтому результаты в двух режимах практически идентичны:
- вставка: 0.668 с (случайный) против 0.646 с (отсортированный) разница менее 4%,
- поиск: 0.0051 с против 0.0040 с,
- удаление: 0.0019 с против 0.0021 с.
Небольшие расхождения находятся в пределах случайной вариации (зависит от коллизий, которые немного различаются при разном порядке вставки). Средняя сложность операций остаётся **O(1)**.
### 3.3. Связный список ожидаемо медленный
Линейный список не обеспечивает прямого доступа, поэтому все операции (кроме удаления после нахождения) требуют обхода в среднем половины списка. Даже при сравнительно небольшом объёме данных (10000 записей) времена велики:
- вставка ≈ **4.6 с** (на два порядка хуже, чем у хеш-таблицы и BST на случайных данных),
- поиск ≈ **0.03 с** (в 610 раз медленнее, чем у других структур).
Интересно, что на отсортированных данных список показывает немного лучшее время, чем на случайных. Причина: при вставке в конец отсортированного списка (имена идут в алфавитном порядке) новые узлы добавляются без поиска дубликатов? Но алгоритм `ll_insert` сначала проверяет наличие имени, проходя весь список. Поскольку все имена уникальны и не обновляются, каждый проход идёт до конца. Однако в отсортированном режиме имена добавляются в порядке возрастания, и при проверке дубликата мы проходим по уже существующим элементам, которые все меньше нового? Да, в отсортированном режиме каждое новое имя больше всех предыдущих, поэтому при поиске дубликата мы обходим весь существующий список. В случайном режиме новые имена могут встречаться раньше, и поиск останавливается раньше? Но в любом случае разница небольшая (около 6%), и в целом список остаётся медленным.
### 3.4. Сравнение удаления
Удаление в списке требует сначала найти элемент (O(n)), затем перелинковку. В хеш-таблице удаление сводится к удалению в коротком списке корзины (почти O(1)). В BST на случайных данных удаление очень быстрое (0.00016 с), на отсортированных катастрофически замедляется (0.054 с). Для хеш-таблицы удаление немного быстрее, чем вставка, что естественно: при удалении не нужно создавать новый узел.
## 4. Выводы и практические рекомендации
Проведённое исследование наглядно демонстрирует сильные и слабые стороны каждой структуры.
1. **Хеш-таблица** лучший выбор для задач, где приоритетом является скорость всех операций (вставка, поиск, удаление), а порядок вывода данных не важен или может быть получен отдельной сортировкой. Стабильно высокая производительность вне зависимости от характера входных данных. В реальных проектах именно хеш-таблицы лежат в основе словарей (Python `dict`, Java `HashMap`).
2. **Двоичное дерево поиска** эффективно только при случайном или близком к случайному порядке поступления ключей. Даёт логарифмическую сложность и при этом позволяет получать данные в отсортированном виде за O(n) без дополнительной сортировки. Однако на реальных данных (например, заведомо отсортированных) производительность падает до O(n), что делает его непригодным без механизмов балансировки. На практике применяются сбалансированные варианты (AVL, красно-чёрные деревья).
3. **Связный список** не подходит для коллекций объёмом более нескольких сотен элементов из-за линейной сложности основных операций. Может использоваться только в очень специфических сценариях: очень редкий поиск, постоянные вставки/удаления в начало (но не в конец), или как строительный блок для других структур (например, для цепочек в хеш-таблице, что и было сделано в данной работе).
### Итоговая таблица применимости
| Критерий | Рекомендуемая структура |
|---------------------------------|---------------------------------------|
| Максимальная скорость всех операций | Хеш-таблица |
| Нужны данные в отсортированном порядке + данные поступают случайно | BST (но лучше сбалансированное) |
| Данные поступают уже отсортированными | Хеш-таблица (или балансируемое дерево) |
| Очень маленький объём (< 100 записей) | Любая, но проще список |
В реальной разработке для телефонного справочника с большим числом записей и частыми запросами поиска оптимальным решением будет **хеш-таблица**. Если же дополнительно требуется частый вывод всего справочника по алфавиту, стоит рассмотреть сбалансированное дерево (например, встроенный в Python модуль `bisect` не даёт структуры данных, а `sortedcontainers` сторонний).

View File

@ -1,92 +0,0 @@
# Отчёт по лабораторной работе: Алгоритмы поиска пути в лабиринте
## 1. Цель работы
Разработка программы для загрузки лабиринта из текстового файла, реализации трёх алгоритмов поиска пути (BFS, DFS, A\*) и проведения экспериментального сравнения их эффективности на лабиринтах различной сложности.
## 2. Структура программы
Программа написана на Python 3 и состоит из следующих основных классов:
- `GridPoint` представление клетки лабиринта (координаты, проходимость, флаги старта/выхода);
- `Labyrinth` модель лабиринта (сетка клеток, методы получения соседей);
- `TextMazeLoader` загрузка лабиринта из файла с символами `#` (стена), `S` (старт), `E` (выход);
- `SearchAlgorithm` (и наследники `BreadthFirst`, `DepthFirst`, `AStar`) реализация алгоритмов поиска;
- `LabyrinthSolver` класс-оркестратор, позволяющий сменить стратегию и измеряющий время выполнения;
- `Player`, `Command`, `MoveCommand`, `InteractiveView` для интерактивного режима с отменой ходов;
- функции `run_experiment` и `generate_plots` для многократных запусков и построения графиков.
## 3. Описание алгоритмов
### 3.1 BFS (поиск в ширину)
Использует очередь. Гарантирует нахождение кратчайшего пути (по числу шагов). Обходит клетки в порядке увеличения расстояния от старта.
### 3.2 DFS (поиск в глубину)
Использует стек. Идёт «вглубь» по одному пути, не гарантирует кратчайший путь. Обычно быстрее по времени и памяти на больших лабиринтах.
### 3.3 A* (звездочка)
Использует приоритетную очередь и эвристику (манхэттенское расстояние). Оценивает клетку по формуле `f = g + h`, где `g` пройденное расстояние, `h` эвристика. Находит оптимальный путь, если эвристика допустима.
## 4. Методика эксперимента
Для каждого лабиринта каждый алгоритм запускался 3 раза, результаты усреднялись. Измерялись:
- время выполнения (в миллисекундах);
- количество посещённых клеток;
- длина найденного пути.
Тестовые лабиринты:
| Название | Размер | Описание |
|----------|--------|-----------|
| Small 10x6 | 10×6 | Простой лабиринт с извилистым коридором |
| Medium 10x10 | 10×10 | Лабиринт среднего размера с несколькими тупиками |
| Large 20x20 | 20×20 | Большой запутанный лабиринт |
| Empty 15x15 | 15×15 | Пустой лабиринт без стен (прямая линия от S до E) |
| No exit 10x10 | 10×10 | Лабиринт без буквы E (путь отсутствует) |
## 5. Результаты экспериментов
| Лабиринт | Алгоритм | Время, мс | Посещено клеток | Длина пути |
|----------------|----------|-----------|-----------------|------------|
| Small 10x6 | BFS | 0.057 | 25 | 16 |
| Small 10x6 | DFS | 0.057 | 24 | 16 |
| Small 10x6 | A* | 0.048 | 23 | 16 |
| Medium 10x10 | BFS | 0.048 | 47 | 16 |
| Medium 10x10 | DFS | 0.035 | 44 | 30 |
| Medium 10x10 | A* | 0.098 | 47 | 16 |
| Large 20x20 | BFS | 0.099 | 100 | 36 |
| Large 20x20 | DFS | 0.070 | 75 | 68 |
| Large 20x20 | A* | 0.165 | 85 | 36 |
| Empty 15x15 | BFS | 0.133 | 133 | 17 |
| Empty 15x15 | DFS | 0.114 | 161 | 89 |
| Empty 15x15 | A* | 0.154 | 65 | 17 |
| No exit 10x10 | BFS | 0.044 | 25 | 0 |
| No exit 10x10 | DFS | 0.059 | 25 | 0 |
| No exit 10x10 | A* | 0.046 | 25 | 0 |
## 6. Анализ результатов
### 6.1. Нахождение кратчайшего пути
- **BFS** и **A*** нашли оптимальные пути во всех лабиринтах, где выход существовал (длина пути совпадает для них в каждом случае).
- **DFS** в лабиринтах Medium, Large и Empty дал существенно более длинные пути (30 против 16, 68 против 36, 89 против 17), что характерно для глубинного обхода без эвристики.
### 6.2. Время выполнения
- На малых лабиринтах все алгоритмы работают сопоставимо (0.0350.099 мс).
- На лабиринте Large 20×20 BFS выполнился за 0.099 мс, A* 0.165 мс (медленнее из-за сложности поддержки очереди с приоритетом), DFS быстрее всех (0.070 мс).
- В пустом лабиринте BFS и A* обошли почти все клетки (133 и 65 посещённых соответственно), но A* за счёт эвристики посетил вдвое меньше клеток, хотя время оказалось чуть выше, чем у BFS (0.154 против 0.133 мс). Это объясняется накладными расходами на вычисление эвристики и управление кучей.
### 6.3. Количество посещённых клеток
- **A*** показал лучшую эффективность в пустом лабиринте (65 посещённых против 133 у BFS и 161 у DFS). В лабиринтах со стенами разница не столь заметна, но A* почти всегда посещал меньше клеток, чем BFS.
- **DFS** в среднем посещает меньше клеток, чем BFS, но при этом путь часто неоптимален.
- **BFS** вынужден обходить всю область равных расстояний, поэтому посещённых клеток обычно больше.
### 6.4. Поведение при отсутствии выхода
Все алгоритмы корректно завершились, вернув пустой путь (длина 0). В лабиринте без выхода BFS, DFS и A* посетили 25 клеток это все доступные клетки.
## 7. Выводы
1. **BFS** надёжен для поиска кратчайшего пути, но может быть медленнее на больших открытых пространствах из-за широкого обхода.
2. **DFS** самый быстрый по времени и экономный по памяти, но не гарантирует оптимальность пути. Его применение оправдано, когда любой путь подходит.
3. **A*** демонстрирует лучший баланс: находит кратчайший путь и при этом посещает меньше клеток, чем BFS. Небольшое замедление на сложных лабиринтах компенсируется меньшим числом обработанных клеток.
4. Программа успешно справляется с лабиринтами разного размера и конфигурации, включая отсутствие выхода.
5. Интерактивный режим с отменой ходов (паттерн Command) и выбором алгоритма (паттерн Strategy) реализован и работает корректно.

View File

@ -1,725 +0,0 @@
from abc import ABC, abstractclassmethod
from collections import deque
import heapq
import time
import os
import time
import csv
import random
class Cell:
def __init__(self, x, y):
self.x = x
self.y = y
self.isWall = False
self.isStart = False
self.isExit = False
def __eq__(self, other):
if other is None:
return False
return self.x == other.x and self.y == other.y
def __lt__(self, other):
if other is None:
return False
return (self.x, self.y) < (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
def __repr__(self):
return f"Cell({self.x}, {self.y})"
def isPassable(self):
return not self.isWall
class Maze:
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = [[Cell(x, y) for y in range(height)] for x in range(width)]
self.start = None
self.exit = None
def getCell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self.grid[x][y]
return None
def getNeighbors(self, cell):
neighbors = []
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
for dx, dy in directions:
neighbor = self.getCell(cell.x + dx, cell.y + dy)
if neighbor and neighbor.isPassable():
neighbors.append(neighbor)
return neighbors
def setStart(self, x, y):
cell = self.getCell(x, y)
if cell:
cell.isStart = True
self.start = cell
def setExit(self, x, y):
cell = self.getCell(x, y)
if cell:
cell.isExit = True
self.exit = cell
class MazeBuilder(ABC):
def buildFromFile(self, filename):
pass
class TextileMazeBuilder(MazeBuilder):
def buildFromFile(self, filename):
with open(filename, 'r', encoding='utf-8') as f:
lines = f.readlines()
lines = [line.rstrip('\n\r') for line in lines]
height = len(lines)
width = len(lines[0]) if height > 0 else 0
for line in lines:
if len(line) != width:
raise ValueError("все строки одинаковой длины")
maze = Maze(width, height)
for y in range(height):
for x in range(width):
char = lines[y][x]
cell = maze.getCell(x, y)
if char == '#':
cell.isWall = True
elif char == ' ':
cell.isWall = False
elif char == 's':
cell.isWall = False
cell.isStart = True
maze.start = cell
elif char == 'e':
cell.isWall = False
cell.isExit = True
maze.exit = cell
else:
raise ValueError(f"неизв сим")
if maze.start is None:
raise ValueError("в лабиринте не найден старт")
if maze.exit is None:
raise ValueError("в лабиринте не найден выход")
return maze
class PathFindingStrategy:
def findPath(self, maze, start, exit):
pass
class BFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
if exit is None:
return []
queue = deque([start])
visited = {start}
parent = {start: None}
while queue:
current = queue.popleft()
if current == exit:
return self._reconstruct_path(parent, start, exit)
for neighbor in maze.getNeighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
queue.append(neighbor)
return []
def _reconstruct_path(self, parent, start, exit):
path = []
current = exit
while current is not None:
path.append(current)
current = parent[current]
path.reverse()
return path
class DFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
if exit is None:
return []
stack = [start]
visited = {start}
parent = {start: None}
while stack:
current = stack.pop()
if current == exit:
return self._reconstruct_path(parent, start, exit)
for neighbor in maze.getNeighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
stack.append(neighbor)
return []
def _reconstruct_path(self, parent, start, exit):
path = []
current = exit
while current is not None:
path.append(current)
current = parent[current]
path.reverse()
return path
class AStrategy(PathFindingStrategy):
def _heuristic(self, cell, exit):
if exit is None:
return 0
return abs(cell.x - exit.x) + abs(cell.y - exit.y)
def findPath(self, maze, start, exit):
if exit is None:
return []
open_set = []
heapq.heappush(open_set, (0, start))
came_from = {start: None}
g_score = {start: 0}
while open_set:
current = heapq.heappop(open_set)[1]
if current == exit:
return self._reconstruct_path(came_from, start, exit)
for neighbor in maze.getNeighbors(current):
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score = tentative_g + self._heuristic(neighbor, exit)
heapq.heappush(open_set, (f_score, neighbor))
return []
def _reconstruct_path(self, came_from, start, exit):
path = []
current = exit
while current is not None:
path.append(current)
current = came_from[current]
path.reverse()
return path
class SearchStats:
def __init__(self, time_ms=0, visited_cells=0, path_length=0):
self.time_ms = time_ms
self.visited_cells = visited_cells
self.path_length = path_length
def __str__(self):
return f"Время: {self.time_ms:.3f} мс | Посещено: {self.visited_cells} | Длина пути: {self.path_length}"
class MazeSolver:
def __init__(self, maze):
self.maze = maze
self.strategy = None
def setStrategy(self, strategy):
self.strategy = strategy
def solve(self):
if self.strategy is None:
raise ValueError("Стратегия не установлена")
start_time = time.perf_counter()
path = self.strategy.findPath(self.maze, self.maze.start, self.maze.exit)
end_time = time.perf_counter()
elapsed_ms = (end_time - start_time) * 1000
stats = SearchStats(
time_ms=elapsed_ms,
visited_cells=len(path),
path_length=len(path)
)
return path, stats
class Observer:
def update(self, event):
pass
class ConsoleView(Observer):
def render(self, maze, player_position=None, path=None):
"""отрисовка"""
os.system('cls' if os.name == 'nt' else 'clear')
path_set = set(path) if path else set()
for y in range(maze.height):
for x in range(maze.width):
cell = maze.getCell(x, y)
if player_position and cell == player_position:
print('P', end='')
elif cell == maze.start:
print('S', end='')
elif cell == maze.exit:
print('E', end='')
elif cell in path_set:
print('.', end='')
elif cell.isWall:
print('#', end='')
else:
print(' ', end='')
print()
def update(self, event):
if event['type'] == 'path_found':
print(f"длина пути {len(event['path'])}")
self.render(event['maze'], path=event['path'])
elif event['type'] == 'move':
print(f"шаг {event['step']}")
self.render(event['maze'], event['player'], event['path'])
elif event['type'] == 'maze_loaded':
print("перегрузка")
self.render(event['maze'])
class ObservableMazeSolver:
def __init__(self, maze):
self.maze = maze
self.strategy = None
self.observers = []
def attach(self, observer):
self.observers.append(observer)
def notify(self, event):
for observer in self.observers:
observer.update(event)
def setStrategy(self, strategy):
self.strategy = strategy
def solve(self):
if self.strategy is None:
raise ValueError("")
path = self.strategy.findPath(self.maze, self.maze.start, self.maze.exit)
self.notify({
'type': 'path_found',
'maze': self.maze,
'path': path
})
return path
class Player:
def __init__(self, start_cell):
self.currentCell = start_cell
self.previousCell = None
def moveTo(self, cell):
self.previousCell = self.currentCell
self.currentCell = cell
def undoMove(self):
if self.previousCell:
self.currentCell, self.previousCell = self.previousCell, None
return True
return False
class Command:
def execute(self):
pass
def undo(self):
pass
class MoveCommand(Command):
def __init__(self, player, direction, maze):
self.player = player
self.dx, self.dy = direction
self.maze = maze
self.executed = False
def execute(self):
new_x = self.player.currentCell.x + self.dx
new_y = self.player.currentCell.y + self.dy
new_cell = self.maze.getCell(new_x, new_y)
if new_cell and new_cell.isPassable():
self.player.moveTo(new_cell)
self.executed = True
return True
return False
def undo(self):
if self.executed:
self.player.undoMove()
self.executed = False
return True
return False
def clear_console():
os.system('cls' if os.name == 'nt' else 'clear')
def render_maze_with_player(maze, player, path=None):
path_set = set(path) if path else set()
for y in range(maze.height):
for x in range(maze.width):
cell = maze.getCell(x, y)
if cell == player.currentCell:
print('P', end='')
elif cell == maze.start:
print('S', end='')
elif cell == maze.exit:
print('E', end='')
elif cell in path_set:
print('.', end='')
elif cell.isWall:
print('#', end='')
else:
print(' ', end='')
print()
def run_game(maze, path=None):
player = Player(maze.start)
history = []
directions = {
'w': (0, -1),
's': (0, 1),
'a': (-1, 0),
'd': (1, 0)
}
print(" W/A/S/D - движение, U - отмена, Q - выход")
if path:
print(f"мин путь {len(path)} шагов")
while True:
print()
render_maze_with_player(maze, player, path)
if player.currentCell == maze.exit:
print("\n*** выход ***")
break
key = input("\n> ").lower()
if key == 'q':
print("выход из игры")
break
elif key == 'u':
if history:
cmd = history.pop()
cmd.undo()
print("отмена хода")
else:
print("нет ходов")
elif key in directions:
cmd = MoveCommand(player, directions[key], maze)
if cmd.execute():
history.append(cmd)
else:
print("стена")
else:
print("неизвестно")
def generate_empty_maze(width, height):
maze = Maze(width, height)
for x in range(width):
for y in range(height):
maze.getCell(x, y).isWall = False
maze.setStart(0, 0)
maze.setExit(width-1, height-1)
return maze
def generate_maze_with_walls(width, height, wall_probability=0.3):
maze = Maze(width, height)
for x in range(width):
for y in range(height):
if random.random() < wall_probability:
maze.getCell(x, y).isWall = True
else:
maze.getCell(x, y).isWall = False
maze.getCell(0, 0).isWall = False
maze.getCell(width-1, height-1).isWall = False
maze.setStart(0, 0)
maze.setExit(width-1, height-1)
return maze
def generate_maze_no_exit(width, height):
maze = generate_maze_with_walls(width, height, 0.3)
exit_cell = maze.getCell(width-1, height-1)
exit_cell.isWall = True
maze.exit = None
return maze
def save_maze_to_file(maze, filename):
with open(filename, 'w') as f:
for y in range(maze.height):
for x in range(maze.width):
cell = maze.getCell(x, y)
if cell == maze.start:
f.write('s')
elif cell == maze.exit:
f.write('e')
elif cell.isWall:
f.write('#')
else:
f.write(' ')
f.write('\n')
def create_test_mazes():
mazes = []
small = generate_maze_with_walls(10, 10, 0.2)
save_maze_to_file(small, "maze_small.txt")
mazes.append(('маленький (10x10)', small))
medium = generate_maze_with_walls(50, 50, 0.3)
save_maze_to_file(medium, "maze_medium.txt")
mazes.append(('средний (50x50)', medium))
large = generate_maze_with_walls(100, 100, 0.3)
save_maze_to_file(large, "maze_large.txt")
mazes.append(('большой (100x100)', large))
empty = generate_empty_maze(50, 50)
save_maze_to_file(empty, "maze_empty.txt")
mazes.append(('пустой (50x50)', empty))
no_exit = generate_maze_no_exit(20, 20)
save_maze_to_file(no_exit, "maze_no_exit.txt")
mazes.append(('без выхода (20x20)', no_exit))
return mazes
def run_experiment(maze, strategy, name, repeats=5):
times = []
visited_counts = []
path_lengths = []
for _ in range(repeats):
solver = MazeSolver(maze)
solver.setStrategy(strategy())
start_time = time.perf_counter()
path, stats = solver.solve()
end_time = time.perf_counter()
times.append((end_time - start_time) * 1000)
visited_counts.append(len(path) if path else 0)
path_lengths.append(len(path) if path else 0)
return {
'лабиринт': name,
'стратегия': strategy.__name__.replace('Strategy', ''),
'время_ср': sum(times) / repeats,
'время_мин': min(times),
'время_макс': max(times),
'посещено_ср': sum(visited_counts) / repeats,
'длина_пути_ср': sum(path_lengths) / repeats,
'путь_найден': path is not None and len(path) > 0
}
def run_all_experiments():
strategies = [BFSStrategy, DFSStrategy, AStrategy]
results = []
mazes = create_test_mazes()
for maze_name, maze in mazes:
for strategy in strategies:
print(f" тест {strategy.__name__}...", end=" ", flush=True)
result = run_experiment(maze, strategy, maze_name)
results.append(result)
print(f"время={result['время_ср']:.2f}мс, путь={result['длина_пути_ср']:.0f}")
save_results_to_csv(results)
return results
def save_results_to_csv(results):
filename = "resultslab.csv"
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
writer = csv.DictWriter(f, fieldnames=[
'лабиринт', 'стратегия', 'время_ср', 'время_мин', 'время_макс',
'посещено_ср', 'длина_пути_ср', 'путь_найден'
])
writer.writeheader()
writer.writerows(results)
def plot_results(results):
try:
import matplotlib.pyplot as plt
import numpy as np
labyrinths = list(set(r['лабиринт'] for r in results))
strategies = ['BFS', 'DFS', 'A']
n_rows = 3
n_cols = 2
fig, axes = plt.subplots(n_rows, n_cols, figsize=(14, 12))
axes = axes.flatten()
for idx, lab in enumerate(labyrinths):
ax = axes[idx]
times = []
for strat in strategies:
for r in results:
if r['лабиринт'] == lab and r['стратегия'] == strat:
times.append(r['время_ср'])
break
x = np.arange(len(strategies))
bars = ax.bar(x, times, color=['#1a5632', '#0e5fb4', '#051f45'])
ax.set_title(f'{lab}')
ax.set_xticks(x)
ax.set_xticklabels(strategies)
ax.set_ylabel('Время (мс)')
for bar, t in zip(bars, times):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
f'{t:.1f}', ha='center', va='bottom', fontsize=8)
if len(labyrinths) < len(axes):
axes[-1].set_visible(False)
plt.tight_layout()
plt.savefig('maze_time_comparison.png', dpi=150)
plt.show()
plt.figure(figsize=(10, 6))
colors = ['#d8d262', '#0e5fb4', '#ed254e']
for idx, strat in enumerate(strategies):
lengths = []
for lab in labyrinths:
for r in results:
if r['лабиринт'] == lab and r['стратегия'] == strat:
lengths.append(r['длина_пути_ср'])
break
plt.plot(labyrinths, lengths, marker='o', label=strat, color=colors[idx]) # добавьте color
plt.xlabel('Лабиринт')
plt.ylabel('Длина пути ')
plt.title('Сравнение длины найденного пути')
plt.legend()
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig('maze_path_length.png', dpi=150)
plt.show()
except ImportError:
print("")
def print_analysis(results):
strat_data = {}
for r in results:
strat = r['стратегия']
if strat not in strat_data:
strat_data[strat] = {'time': [], 'visited': [], 'labyrinth': []}
strat_data[strat]['time'].append(r['время_ср'])
strat_data[strat]['visited'].append(r['посещено_ср'])
strat_data[strat]['labyrinth'].append(r['лабиринт'])
for strat, data in strat_data.items():
avg_time = sum(data['time']) / len(data['time'])
print(f" {strat}: среднее время {avg_time:.2f} мс")
print(" BFS медленный на большом лабсамый короткий путить находит")
print(" DFS быстрый, но не всегда самый короткий")
print(" A быстрый и находит самый короткий путь")
print(" без выхода лаб. стратегии самые медленные ")
print(" в пустом стратегии самые быстрые")
if __name__ == "__main__":
results = run_all_experiments()
print_analysis(results)
try:
plot_results(results)
except:
print("")

View File

@ -1,50 +0,0 @@
s
e

View File

@ -1,100 +0,0 @@
s## # # # # # # # ### # ## # # # # # # ### # # # # # ###
# ## # ## ### ## # # ## ## # # # ### ## ## #
# ## # # # # # ## ## # # # ## # ## # # # # ## # # # #
## # # # ##### # # # # # ## # ## # # # # # # # # # # # # #
# ### ## # # # # ## ## ## # # # # # ## #
# # # # # # ## # ### ## ## # ##### # # # # # ### ## # ### # # #
# # # ## ### # # # # # ### # # # ## ##### # #
# # ### # # ## ### # # # # # ## # ## ## # ## # ## ## # #
# # # ## # # # # ## # # # # # # # # # # # ### ## # # #
# # # # # ## # # # # # ## ## ### ###### ## ## ### #
## ## # # # # # # ### # # # ### # ## #
# # ###### # # # ## # # # ## # # # ## #### # # #
## # # # ### # # # # # #### # # # ## # # # #
# # # # ### ## ## # # # ## # # # # # ### # # # ### # # #### # ##
# ## # # # # # # # # # # # # # # ## ### ## # # ## #
### # # # # ## ### # # ## # # # ## # ## # ## #
### # # # # ### # # # # # ## # # # # # ## # # ## #
# # # # ## # # ### # ## ## # ### # # ### ## #
### ## # ## # ## # # # # # # # # # # # ####### ##
## ## # # # # ## # # # ## ### ### # # # ### # # # # ## # ###
### #### ### # # # # ## ## # #### # # # # # # ## # #
### # ## ## # ## ## ## # # # ## # # ## # ## # #
# # # # # # # #### ## # # #### ## # ## ## # # #
# ## # ## # # # # ### # ## # ## # # ## # # # ## # # #### # #
# # ## # # # # # # # # # ## ## # # # ### # #
# # # # # # ## # # # ### # ## # # # # ## # # # # # #
# # # ### # # # # # ## ## # # ## # # ## # # #
## ## ### # # ## # # # # # # # # # # # # # ## ## # # # #
# # # # # # ## # # # # # # # ##
# # # # # # ## # ## # ## # # # ## ## ## ## ### # # # # # #
# # # # # # # #### # ## # # # # ## ## # # # ## # #
## # # # # # # # ###### # # ### # # ## # # # # ### ##
# # ## # # # # #### # #### # # # ## ## ## #
# # # # # # # ## # # # # # ### ### # # # # # # #
# # # # ## # # # # # ## # ## # # ## # ## ### # #
#### # # # # ## # # # # # ## ### # # # # ### # ## #
# # # # ## ## # # # # # # # # # # # # # # # ## ## # # ##
# # # # # # ## # # # ## ## # # # # # # ## #
# # ## ## ### ## # # ## # # # ## # # # # # # # # #
## ## # # # # # # ## ### # # # ## # # ## # ### # ### ##
## ## # # # # # # # # # ## # # ## # ### # # # #
## # # ## ## ## # # ## # # ## # # # # ## # #
## # ## # ## ## # # # # # # # # # # ### # # # # # ## # #
# ## ## # # # # # #### ## # # # # # # # # #
# # # # # # # # ## # # # # # ### # # # # #### ## ### ####
# ## # # #### # # # # #### # # # # # ### # # ### # ## ##
## # ## # ## # # # # # # ### # # # # # ## # # #
# # # # ## # # # ### #### ## # # # # ## ## ## #
## # ## # ## # # # # ## # # # # # # # # # # # ## #
# # ## # # # ### # ## # ## # # ### # # # # ### #
# # ## # ## #### # # # # # # # ## ## # ## ###
# ## # # ## ## # # ## # # # ### # ## # # # # # # # #
# # ## # # ## # # # # # # # # # # # ## # ### ##
# ## # # # # # # # ## # # ## ## ## # # ## ## # # ## ### ### ####
### # # # # # # # ## # # # ## # ## # # # ## # # ## # # # #
# # # # # # # # ## ## ### # # # # # # ## # # # #
# # # # ## # ### # # # # ## # # # ### # ## ## # # # ##
# # # # # ## # ## # # # ### # ## ## # # # # # # # #
## ### ## # # # # ## # # # #### # #### # # ## # ## #
## ## ## # # # # ## # # ## ## ### # # # # # ### # ### ##
# # ### # # # # # # # # # ## # ### # # # ### ## ##
# # ## # ## # ## ## # # # ## ## # ## # # ##
# # ### # ## ## # # ### # # # # # # ## ## # ##
# # #### # # # # # # # ### # # # # # # ## # ### # # ### ###
# # ## # # ##### # ## # # ## ## # # # ## # # # ## ##
# ### # ## # # ###### ### # ## # ## # # ## # # # # ## ## # ## #
# # # # # # # # ## ## # # # ## # # ## ## # # # # #
## # ## ## # ### # # # # # # # # ## # # # # # ###### # ##
## # # # # ### # # ### ## # # ## # # # # ##### #
# # ### # # # # # # ## #### # # ### # # # ## # ##
# # ## ## # ## # #### # ## # # # # # ## ## # # # # ## ## #
## # # # # ## # # ## # # # ## # # ## # # # # # #
# # # ## # # # # ## # ## # # # # # ## # # ##
# ## ## # # # # # # ### # ## # # # # # # # # #
# # ## # # # # # # # ##### ## ## ### # # ###
# # # # # # ## ## ## # # # # # # ## # ##### # ##
# # ## # # # ## # # #### # ## # # # # # ## # # #
# # # # # ## ## # ## # # # # # #### # ##
## # # # # ## # ## ## ## # # ## # # # ## # ## # # #
## # # # # # # # ## ### # # # ## # # ## #
### # ## # # # ## ## # ### # # # # # ### # # # ##### #
## # # ## # ## # # # # # ## # # # ## ####### ### # #
#### # # # # # # # # # # ## # ## # # ### # ## # # #
# # # # # # # # # # ## # # ## # # # # ## # ### # #
# # # # #### ## ## # # # # ## # # # # # # ### ### # ##
#### # ## # # # ### ## # ## ## # ## # # ## # #
# # ## # # # # # # # # ## # # ## # # ### # ##
# # # # ## ## # # ## # # # # ## # ## ##
### ## # # # ## ## ## ## # # # ## ## # # # # # # # # # ## # # #
## # # # # # # # # # # ## #### # # ## ### ### ## # # #
# # ##### # # # ## ## # # ## ## # # ## # #### ##### # # ## ##
# # # # # # ## # # # # # # # # # # ## #
## ### # # ## ## # ## ## ## # # ## # # ### # # ## ### #
# # # ## # ## # # # ## # # # # ## # # # #
# # # # # #### # # # ## # # # ## # # # # # # # # # #
# # # ## # # ## # # ### # # ## # # ## # # ##
# # # ## # # ### # # # # # ## ## ##
# # # # ### # # # # # # # # # # # ## ## # ### # ## # # # #
# ###### # # ## ## ## # ### # # # ## # # # #####
# ## # # # # ## # # # # # # # # #### # # e

View File

@ -1,50 +0,0 @@
s # ## # # ### # ## # # #
## # # ## ## # # # #
# # ## # # # # ##
### # # # # # # ## ## # ## # #
# # # ## # # # # ## # #
# # # # ## # ## # # #
## # # # # # # # ## # #
# ## # # # ## # ## # # # # #
## # # # # ## # # ## # ##
# # # # # ## # # ## # # #
# # # # ## # # # # ## # ## # #
# ## # # # # # # # ## ##
## # ## ### # # # ## # ##
##### ### # # # # ## # # # #
# # ### ## # ## ## #### ###
## # # # # ### # # ## # #
# # ## # # # # # # ##
## # # # ### # ## # # ## # # ## ##
# #### # # # # # ### # ##
# ## # ## # # ## ### ## ### #
# # ### ## # # # ##
# # ## # # # # # # #
# ## # ### #### # ## # ### ## # #
# # ## # # # # # # #
# # ##### # # # # # # # ## # ##
## # # # # ## ## # ## ## #
# # # # # # # ## # # #
## # # # ## # # ## # #
# ### # # # # # # # # # ###
### # # # # # ### # # # # # ##
# # # # # ## # # # # # ##
# ## ## ## # # # # # # ## #
# #### # # # ## # ## #
## # # # # ## # # # # #
## # ## ## # # # ## # # ## #
# # # # # # # # # # ### # # #
# # ## # # # # # ###
# # #### ##
# # ## # # ## ### # # ##
##### # # # # # # # # # #
## # # # # # #
# # ## ## # # # # ## ### # #
# # ### ## ### ### # ## # #
## # ### # ## # # # #
# # # # # ## # # # # #
# # ## # # ## ### # # # #
# # # # # ## # ### #
## # # ## # # #
# # ## # ### # ### # ## # ## # ##
# # # # # # # ## # # e

View File

@ -1,20 +0,0 @@
s ## ###
# # # # # ##
# # # # #
# # ##
# # # # #
# # ### # #
# # # # #
# # ## ## ###
# ## #
# # ###
# # # # #
### # #
# # # #
## # # # #
## # # # # ##
# # #
# #
# # # #
# # #
# # # # ## #

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

View File

@ -1,10 +0,0 @@
s #
#
# #
# #
# #
#
# #
#
# # e

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

View File

@ -1,252 +0,0 @@
# Отчёт: Задание 2 — Поиск выхода из лабиринта
## Цель работы
Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов
## Выбранные паттерны и их обоснование
### Builder
Для загрузки лабиринта из файла был использован паттерн Builder.
Создан интерфейс:
class MazeBuilder():
и его реализация:
class TextFileMazeBuilder(MazeBuilder):
Преимущества использования Builder:
пользоватеь не знает деталей создания лабиринта;
можно добавить новые форматы (JSON, XML, бинарный);
код загрузки изолирован от остальной программы.
### Strategy
Для алгоритмов поиска пути использован паттерн Strategy
Создан общий интерфейс:
class PathFindingStrategy():
Реализованы стратегии:
BFSStrategy;
DFSStrategy;
AStrategy;
Каждая стратегия реализует собственный алгоритм поиска пути по правилам.
Преимущества паттерна:
алгоритмы можно менять во время выполнения;
код MazeSolver не зависит от конкретного алгоритма;
новые алгоритмы можно добавлять без изменения существующего кода.
### Observer
Для уведомления интерфейса о событиях использован паттерн Observer
Создан интерфейс:
class Observer():
и реализация:
class ConsoleView(Observer):
MazeSolver хранит список наблюдателей и уведомляет их о событиях:
начало поиска;
окончание поиска;
перемещение игрока.
Преимущества:
логика интерфейса отделена от логики поиска;
можно легко добавить графический интерфейс;
### Command
Для пошагового перемещения игрока использован паттерн Command.
Создан интерфейс:
class Command():
и реализация:
class MoveCommand(Command):
Каждая команда умеет:
execute() — выполнить действие;
undo() — отменить действие
Преимущества:
поддержка undo;
возможность расширения системы команд
## Листинги ключевых классов
### Паттерн Strategy
class PathFindingStrategy:
def findPath(self, maze, start, exit):
pass
class BFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
if exit is None:
return []
queue = deque([start])
visited = {start}
parent = {start: None}
while queue:
current = queue.popleft()
if current == exit:
return self._reconstruct_path(parent, start, exit)
for neighbor in maze.getNeighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
queue.append(neighbor)
return []
class AStrategy(PathFindingStrategy):
def _heuristic(self, cell, exit):
if exit is None:
return 0
return abs(cell.x - exit.x) + abs(cell.y - exit.y)
def findPath(self, maze, start, exit):
if exit is None:
return []
open_set = []
heapq.heappush(open_set, (0, start))
came_from = {start: None}
g_score = {start: 0}
while open_set:
current = heapq.heappop(open_set)[1]
if current == exit:
return self._reconstruct_path(came_from, start, exit)
for neighbor in maze.getNeighbors(current):
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score = tentative_g + self._heuristic(neighbor, exit)
heapq.heappush(open_set, (f_score, neighbor))
return []
### Паттерн Command
class Command:
def execute(self): pass
def undo(self): pass
class MoveCommand(Command):
def __init__(self, player, direction, maze):
self.player = player
self.dx, self.dy = direction
self.maze = maze
self.executed = False
def execute(self):
new_x = self.player.currentCell.x + self.dx
new_y = self.player.currentCell.y + self.dy
new_cell = self.maze.getCell(new_x, new_y)
if new_cell and new_cell.isPassable():
self.player.moveTo(new_cell)
self.executed = True
return True
return False
def undo(self):
if self.executed:
self.player.undoMove()
self.executed = False
return True
return False
## Результаты
| Лабиринт | Стратегия | Время (с) | Посещено | Длина пути | Путь найден |
|---|---|---|---|---|---|
| маленький (10x10) | BFS | 0.9148200158961117 | 19.0 | 19.0 | True |
| маленький (10x10) | DFS | 0.717819994315505 | 39.0 | 39.0 | True |
| маленький (10x10) | A | 1.577159995213151 | 19.0 | 19.0 | True |
| средний (50x50) | BFS | 14.496059995144606 | 99.0 | 99.0 | True |
| средний (50x50) | DFS | 8.470179990399629 | 393.0 | 393.0 |True |
| средний (50x50) | A | 9.11291999509558 | 99.0 | 99.0 | True |
| большой (100x100) | BFS | 0.013179995585232973 | 0.0 | 0.0 | False |
| большой (100x100) | A | 0.013079994823783636 | 0.0 | 0.0 | False |
| пустой (50x50) | BFS | 29.2012800113298 | 99.0 | 99.0 | True |
| пустой (50x50) | DFS | 13.176999986171722 | 1275.0 | 1275.0 | True |
| пустой (50x50) | A | 50.366899999789894 | 99.0 | 99.0 | True |
| без выхода (20x20) | BFS | 0.004239997360855341 | 0.0 | 0.0 | False |
| без выхода (20x20) | DFS | 0.006399990525096655 | 0.0 | 0.0 | False |
| без выхода (20x20) | A | 0.008680007886141539 | 0.0 | 0.0 | False |
### Графики
![Сравнение длины](maze_path_length.png)
![Сравнение времён](maze_time_comparison.png)
## Анализ эффективности алгоритмов
В ходе экспериментов были получены следующие результаты.
### BFS
Преимущества:
всегда находит кратчайший путь;
простая реализация.
Недостатки:
посещает большое количество клеток;
требует много памяти.
Выходит, что наиболее эффективен в небольших невзвешенных лабиринтах.
### DFS
Преимущества:
простая реализация;
самым быстрым находит произвольный путь.
Недостатки:
не гарантирует кратчайший путь;
может уходить в тупики.
Подходит для быстрого поиска любого решения.
### A
Преимущества:
высокая скорость;
посещает меньше клеток;
Недостатки:
требует выбора хорошей эвристики.
Показал хорошие результаты на больших лабиринтах.
## Анализ применимости паттернов
### Builder
Без Builder код загрузки лабиринта был бы жёстко связан с классом Maze, а добавление нового формата потребовало бы изменения существующего кода.
Strategy
Без Strategy пришлось бы:
хранить все алгоритмы внутри одного класса;
использовать большое количество условных операторов;
изменять код MazeSolver при добавлении новых алгоритмов
Strategy помог полностью отделить алгоритмы друг от друга.
### Observer
Без Observer логика интерфейса смешивалась бы с логикой поиска.
Это усложнило бы:
добавление GUI;
логирование;
визуализацию.
### Command
Без Command было бы сложно реализовать:
undo;
историю действий;
расширяемую систему управления.
## Выводы
### В проекте были успешно реализованы:
загрузка лабиринта из файла;
несколько алгоритмов поиска пути;
визуализация;
система наблюдателей;
система команд;
экспериментальное сравнение алгоритмов.
### Использование паттернов GoF позволило:
сделать архитектуру гибкой;
уменьшить связанность компонентов;
упростить расширение программы;
облегчить сопровождение кода.

View File

@ -1,16 +0,0 @@
лабиринт,стратегия,время_срремя_мин,время_макс,посещено_ср,длина_пути_ср,путь_найден
маленький (10x10),BFS,0.9148200158961117,0.8840999798849225,0.9673000313341618,19.0,19.0,True
маленький (10x10),DFS,0.717819994315505,0.5779999773949385,0.8650000090710819,39.0,39.0,True
маленький (10x10),A,1.577159995213151,1.531599962618202,1.7019000370055437,19.0,19.0,True
средний (50x50),BFS,14.496059995144606,12.946999981068075,18.392199999652803,99.0,99.0,True
средний (50x50),DFS,8.470179990399629,7.544599997345358,9.55930002965033,393.0,393.0,True
средний (50x50),A,9.11291999509558,8.53859999915585,9.788900031708181,99.0,99.0,True
большой (100x100),BFS,0.013179995585232973,0.009100011084228754,0.026200024876743555,0.0,0.0,False
большой (100x100),DFS,0.012619991321116686,0.008300004992634058,0.026499968953430653,0.0,0.0,False
большой (100x100),A,0.013079994823783636,0.008699949830770493,0.027500034775584936,0.0,0.0,False
пустой (50x50),BFS,29.2012800113298,19.71900003263727,47.252200020011514,99.0,99.0,True
пустой (50x50),DFS,13.176999986171722,12.441499973647296,13.887099979911,1275.0,1275.0,True
пустой (50x50),A,50.366899999789894,47.1535999677144,60.296199982985854,99.0,99.0,True
без выхода (20x20),BFS,0.004239997360855341,0.002700020559132099,0.00909995287656784,0.0,0.0,False
без выхода (20x20),DFS,0.006399990525096655,0.003200024366378784,0.012699980288743973,0.0,0.0,False
без выхода (20x20),A,0.008680007886141539,0.005399982910603285,0.01810002140700817,0.0,0.0,False
1 лабиринт стратегия время_ср время_мин время_макс посещено_ср длина_пути_ср путь_найден
2 маленький (10x10) BFS 0.9148200158961117 0.8840999798849225 0.9673000313341618 19.0 19.0 True
3 маленький (10x10) DFS 0.717819994315505 0.5779999773949385 0.8650000090710819 39.0 39.0 True
4 маленький (10x10) A 1.577159995213151 1.531599962618202 1.7019000370055437 19.0 19.0 True
5 средний (50x50) BFS 14.496059995144606 12.946999981068075 18.392199999652803 99.0 99.0 True
6 средний (50x50) DFS 8.470179990399629 7.544599997345358 9.55930002965033 393.0 393.0 True
7 средний (50x50) A 9.11291999509558 8.53859999915585 9.788900031708181 99.0 99.0 True
8 большой (100x100) BFS 0.013179995585232973 0.009100011084228754 0.026200024876743555 0.0 0.0 False
9 большой (100x100) DFS 0.012619991321116686 0.008300004992634058 0.026499968953430653 0.0 0.0 False
10 большой (100x100) A 0.013079994823783636 0.008699949830770493 0.027500034775584936 0.0 0.0 False
11 пустой (50x50) BFS 29.2012800113298 19.71900003263727 47.252200020011514 99.0 99.0 True
12 пустой (50x50) DFS 13.176999986171722 12.441499973647296 13.887099979911 1275.0 1275.0 True
13 пустой (50x50) A 50.366899999789894 47.1535999677144 60.296199982985854 99.0 99.0 True
14 без выхода (20x20) BFS 0.004239997360855341 0.002700020559132099 0.00909995287656784 0.0 0.0 False
15 без выхода (20x20) DFS 0.006399990525096655 0.003200024366378784 0.012699980288743973 0.0 0.0 False
16 без выхода (20x20) A 0.008680007886141539 0.005399982910603285 0.01810002140700817 0.0 0.0 False

Binary file not shown.

View File

@ -1,47 +0,0 @@
classDiagram
class Maze {
+width
+height
+cells
+start
+exit
+get_neighbors()
}
class Cell {
+x
+y
+is_wall
+is_start
+is_exit
}
class MazeBuilder {
<<interface>>
+build_from_file()
}
class TextFileMazeBuilder {
+build_from_file()
}
class PathFindingStrategy {
<<interface>>
+find_path()
}
class BFSStrategy
class DFSStrategy
class AStarStrategy
class MazeSolver {
+solve()
}
Maze --> Cell
TextFileMazeBuilder ..|> MazeBuilder
BFSStrategy ..|> PathFindingStrategy
DFSStrategy ..|> PathFindingStrategy
AStarStrategy ..|> PathFindingStrategy
MazeSolver --> PathFindingStrategy
MazeSolver --> Maze

View File

@ -1,135 +0,0 @@
# Отчёт по заданию №2
### Реализация поиска пути в лабиринте с использованием паттернов проектирования
---
## 1. Цель работы
Разработать архитектуру и реализацию системы поиска пути в лабиринте, применив паттерны:
- Builder — построение лабиринта из файла
- Strategy — выбор алгоритма поиска
- Observer — отображение состояния
- Command — управление игроком
Также провести экспериментальное сравнение алгоритмов BFS, DFS и A\*.
---
## 2. Архитектура проекта
Структура каталогов:
```
BrychkinKA/
├── src/
│ ├── builder/
│ ├── model/
│ ├── solver/
│ ├── strategy/
│ └── ui/
├── mazes/
├── experiments/
└── docs/
```
---
## 3. Используемые паттерны
### 3.1 Builder
Абстрагирует процесс построения лабиринта из текстового файла.
### 3.2 Strategy
Позволяет переключать алгоритмы поиска пути без изменения остального кода.
### 3.3 Observer
Используется для отображения состояния лабиринта в консоли.
### 3.4 Command
Реализует управление игроком и пошаговое перемещение.
---
## 4. Диаграмма классов
Диаграмма находится в файле: `class_diagram.mmd`
---
## 5. Эксперименты
Эксперименты проводились на пяти лабиринтах:
- small.txt — простой, проходимый
- medium.txt — средний по сложности
- empty.txt — полностью свободное поле
- no_exit.txt — отсутствует выход
- big.txt — большой лабиринт, путь отсутствует
Алгоритмы:
- BFS
- DFS
- A\*
---
## 6. Результаты
### 6.1 Таблица результатов
| Файл | Алгоритм | Посещено | Длина пути |
| ----------- | -------- | -------- | ---------- |
| big.txt | BFS | 27 | 0 |
| big.txt | DFS | 27 | 0 |
| big.txt | A\* | 27 | 0 |
| empty.txt | BFS | 10 | 10 |
| empty.txt | DFS | 10 | 10 |
| empty.txt | A\* | 10 | 10 |
| medium.txt | BFS | 21 | 17 |
| medium.txt | DFS | 19 | 17 |
| medium.txt | A\* | 21 | 17 |
| no_exit.txt | BFS | 0 | 0 |
| no_exit.txt | DFS | 0 | 0 |
| no_exit.txt | A\* | 0 | 0 |
| small.txt | BFS | 7 | 7 |
| small.txt | DFS | 7 | 7 |
| small.txt | A\* | 7 | 7 |
---
## 7. Графики
Графики находятся в файле:
`experiments/plot_graphs.py`
- время работы алгоритмов
- количество посещённых клеток
---
## 8. Выводы
1. A\* показывает лучшие результаты на средних и больших лабиринтах, но имеет небольшой накладной расход.
2. DFS посещает меньше клеток, но не гарантирует кратчайший путь.
3. BFS всегда находит кратчайший путь, но исследует больше пространства.
4. На лабиринтах без выхода все алгоритмы корректно возвращают `path_len = 0`.
5. Архитектура с паттернами позволяет легко расширять проект и добавлять новые алгоритмы.
---
## 9. Приложения
- Исходный код
- Лабиринты
- CSV с результатами
- Диаграммы

View File

@ -1,21 +0,0 @@
# Диаграммы проекта
## 1. Диаграмма классов
См. файл `class_diagram.mmd`.
## 2. Структура каталогов
```
vinichukan/
├── src/
├── mazes/
├── experiments/
└── docs/
```
## 3. Логика работы алгоритмов
- BFS — поиск в ширину
- DFS — поиск в глубину
- A\* — эвристический поиск с манхэттенской метрикой

View File

@ -1,65 +0,0 @@
import os
import sys
import csv
from time import perf_counter
# Добавляем корневую папку BrychkinKA в sys.path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.builder.text_file_maze_builder import TextFileMazeBuilder
from src.strategy.bfs_strategy import BFSStrategy
from src.strategy.dfs_strategy import DFSStrategy
from src.strategy.astar_strategy import AStarStrategy
from src.solver.maze_solver import MazeSolver
def run_experiments():
builder = TextFileMazeBuilder()
strategies = {
"BFS": BFSStrategy(),
"DFS": DFSStrategy(),
"A*": AStarStrategy()
}
# Папка с лабиринтами относительно корня
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
maze_dir = os.path.join(root_dir, "mazes")
files = [f for f in os.listdir(maze_dir) if f.endswith(".txt")]
results = []
for maze_file in files:
maze_path = os.path.join(maze_dir, maze_file)
maze = builder.build_from_file(maze_path)
for name, strategy in strategies.items():
solver = MazeSolver(maze, strategy)
t0 = perf_counter()
stats = solver.solve()
t1 = perf_counter()
results.append([
maze_file,
name,
stats.time_ms,
stats.visited,
stats.path_len
])
print(f"{maze_file} | {name} | {stats}")
# Сохраняем results.csv в папку experiments
output_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "results.csv")
with open(output_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["maze", "algorithm", "time_ms", "visited", "path_len"])
writer.writerows(results)
print(f"\nРезультаты сохранены в {output_path}")
if __name__ == "__main__":
run_experiments()

View File

@ -1,76 +0,0 @@
import csv
import matplotlib.pyplot as plt
import os
def plot_results():
# Определяем правильный путь к results.csv
script_dir = os.path.dirname(os.path.abspath(__file__))
csv_path = os.path.join(script_dir, "results.csv")
results = []
with open(csv_path, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
row['time_ms'] = float(row['time_ms'])
row['visited'] = int(row['visited'])
row['path_len'] = int(row['path_len'])
results.append(row)
mazes = sorted(set(r['maze'] for r in results))
algorithms = sorted(set(r['algorithm'] for r in results))
x_labels = []
for m in mazes:
for a in algorithms:
x_labels.append(f"{m.replace('.txt','')}\n{a}")
# График 1: Время выполнения
plt.figure(figsize=(12, 6))
times = []
for m in mazes:
for a in algorithms:
val = [r['time_ms'] for r in results if r['maze'] == m and r['algorithm'] == a]
times.append(val[0] if val else 0)
plt.bar(x_labels, times)
plt.ylabel("Время (мс)")
plt.title("Сравнение времени выполнения алгоритмов")
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.savefig(os.path.join(script_dir, "plot_time.png"), dpi=150)
plt.close()
print("Сохранён: experiments/plot_time.png")
# График 2: Посещённые клетки
plt.figure(figsize=(12, 6))
visited_list = []
for m in mazes:
for a in algorithms:
val = [r['visited'] for r in results if r['maze'] == m and r['algorithm'] == a]
visited_list.append(val[0] if val else 0)
plt.bar(x_labels, visited_list)
plt.ylabel("Посещено клеток")
plt.title("Сравнение количества посещённых клеток")
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.savefig(os.path.join(script_dir, "plot_visited.png"), dpi=150)
plt.close()
print("Сохранён: experiments/plot_visited.png")
# График 3: Длина пути
plt.figure(figsize=(12, 6))
path_list = []
for m in mazes:
for a in algorithms:
val = [r['path_len'] for r in results if r['maze'] == m and r['algorithm'] == a]
path_list.append(val[0] if val else 0)
plt.bar(x_labels, path_list)
plt.ylabel("Длина пути")
plt.title("Сравнение длины найденного пути")
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.savefig(os.path.join(script_dir, "plot_path.png"), dpi=150)
plt.close()
print("Сохранён: experiments/plot_path.png")
if __name__ == "__main__":
plot_results()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View File

@ -1,16 +0,0 @@
maze,algorithm,time_ms,visited,path_len
big.txt,BFS,0.14230050146579742,27,0
big.txt,DFS,0.1100003719329834,27,0
big.txt,A*,0.23249909281730652,27,0
empty.txt,BFS,0.07219985127449036,10,10
empty.txt,DFS,0.046100467443466187,10,10
empty.txt,A*,0.08819997310638428,10,10
medium.txt,BFS,0.09160116314888,21,17
medium.txt,DFS,0.07379986345767975,19,17
medium.txt,A*,0.15410035848617554,21,17
no_exit.txt,BFS,0.0007003545761108398,0,0
no_exit.txt,DFS,0.0027008354663848877,0,0
no_exit.txt,A*,0.0001993030309677124,0,0
small.txt,BFS,0.06789900362491608,7,7
small.txt,DFS,0.03989972174167633,7,7
small.txt,A*,0.09530037641525269,7,7
1 maze algorithm time_ms visited path_len
2 big.txt BFS 0.14230050146579742 27 0
3 big.txt DFS 0.1100003719329834 27 0
4 big.txt A* 0.23249909281730652 27 0
5 empty.txt BFS 0.07219985127449036 10 10
6 empty.txt DFS 0.046100467443466187 10 10
7 empty.txt A* 0.08819997310638428 10 10
8 medium.txt BFS 0.09160116314888 21 17
9 medium.txt DFS 0.07379986345767975 19 17
10 medium.txt A* 0.15410035848617554 21 17
11 no_exit.txt BFS 0.0007003545761108398 0 0
12 no_exit.txt DFS 0.0027008354663848877 0 0
13 no_exit.txt A* 0.0001993030309677124 0 0
14 small.txt BFS 0.06789900362491608 7 7
15 small.txt DFS 0.03989972174167633 7 7
16 small.txt A* 0.09530037641525269 7 7

Some files were not shown because too many files have changed in this diff Show More