Compare commits
No commits in common. "develop" and "1-st-exercise" have entirely different histories.
develop
...
1-st-exerc
|
|
@ -1,6 +0,0 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class MazeBuilder(ABC):
|
||||
@abstractmethod
|
||||
def buildFromFile(self, filename):
|
||||
raise NotImplementedError
|
||||
|
|
@ -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)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class Command(ABC):
|
||||
@abstractmethod
|
||||
def execute(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def undo(self):
|
||||
raise NotImplementedError
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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) + ")"
|
||||
|
|
@ -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))
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
class Player:
|
||||
def __init__(self, currentCell):
|
||||
self.currentCell = currentCell
|
||||
|
||||
def setCell(self, cell):
|
||||
self.currentCell = cell
|
||||
|
|
@ -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 = ""
|
||||
|
|
@ -1 +0,0 @@
|
|||
Place report files and experiment outputs here.
|
||||
|
|
@ -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-файлы с результатами экспериментов.
|
||||
- Графики сравнений.
|
||||
- Файлы с тестовыми лабиринтами.
|
||||
|
|
@ -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()
|
||||
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
|
@ -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
|
||||
|
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
|
@ -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
|
||||
|
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
|
@ -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()
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
S
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
E
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
####################################################################################################
|
||||
#S # # # # # # # # # # # # # # # E#
|
||||
# # ### ### # ###### # ### # ## # #### # ####### # #### # # ### ## # ## # # ## # ## # ##### ### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ##### # ######## # ### # ## # #### # ####### ## ### # # #### ####### ## ####### ####### # ### ##
|
||||
# # # # # # # # # # # # # # # # # # # # #
|
||||
### # # ###### # ########### ########### ### ####### # ####### ### # # ###### # ### ### # ### ####
|
||||
# # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### ###### # ##### # ### # ####### # ### ### ## # ###### # ### # ### ###### # ### # ### ### ## #
|
||||
# # # # # # # # #
|
||||
####################################################################################################
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
##################################################
|
||||
#S # # # # # # E#
|
||||
# # ### ### # ###### # ### # ## # #### # ####### ##
|
||||
# # # # # # # # # # # # # #
|
||||
# ##### # ######## # ### # ## # #### # ####### ## #
|
||||
# # # # # # # # # #
|
||||
### # # ###### # ########### ########### ### ######
|
||||
# # # # # # # # # # #
|
||||
# ### ###### # ##### # ### # ####### # ### ### ## #
|
||||
# # # # #
|
||||
##################################################
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
##########
|
||||
#S #
|
||||
# ###### #
|
||||
# # #
|
||||
##########
|
||||
# #E#
|
||||
# ###### #
|
||||
# #
|
||||
##########
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
##########
|
||||
#S #E#
|
||||
# ## # # ##
|
||||
# # #
|
||||
# #### # #
|
||||
# # #
|
||||
##########
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
1111111111111111111111111111
|
||||
1S11111111111111111111111111
|
||||
1111111111111111111111111111
|
||||
1111111111111111111111111111
|
||||
1111111111111222222222222111
|
||||
1111111111111222222222222111
|
||||
1111111111111333333333333111
|
||||
1111111111111333333333333111
|
||||
111111111111111111111111111E
|
||||
1111111111111111111111111111
|
||||
|
|
@ -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()
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class Observer(ABC):
|
||||
@abstractmethod
|
||||
def update(self, event):
|
||||
raise NotImplementedError
|
||||
|
|
@ -1 +0,0 @@
|
|||
matplotlib
|
||||
|
|
@ -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
|
||||
|
|
@ -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 []
|
||||
|
|
@ -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 []
|
||||
|
|
@ -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 []
|
||||
|
|
@ -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 []
|
||||
|
|
@ -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
|
||||
|
|
@ -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,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")
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
##########
|
||||
#S #
|
||||
# ####### #
|
||||
# # # #
|
||||
# # ### # #
|
||||
# # E #
|
||||
##########
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
##########
|
||||
#S #
|
||||
# # #### #
|
||||
# # #
|
||||
# #### # #
|
||||
# # #
|
||||
# #### # #
|
||||
# # #
|
||||
# #
|
||||
########E#
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
####################
|
||||
#S #
|
||||
# ############### #
|
||||
# # # #
|
||||
# # ######### # # #
|
||||
# # # # # # #
|
||||
# # # ##### # # # #
|
||||
# # # # # # # # #
|
||||
# # # # # # # # # #
|
||||
# # # # # # # # #
|
||||
# # # ##### # # # #
|
||||
# # # # # # #
|
||||
# # ######### # # #
|
||||
# # # #
|
||||
# ############### #
|
||||
# #
|
||||
# ############### #
|
||||
# # # #
|
||||
# # ########### # #
|
||||
# E#
|
||||
####################
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
###############
|
||||
#S #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# E #
|
||||
###############
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
##########
|
||||
#S #
|
||||
# # #
|
||||
# # #### #
|
||||
# # #
|
||||
##########
|
||||
E#########
|
||||
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
|
@ -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.035–0.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) реализован и работает корректно.
|
||||
|
|
@ -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("")
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
s
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
e
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
s## # # # # # # # ### # ## # # # # # # ### # # # # # ###
|
||||
# ## # ## ### ## # # ## ## # # # ### ## ## #
|
||||
# ## # # # # # ## ## # # # ## # ## # # # # ## # # # #
|
||||
## # # # ##### # # # # # ## # ## # # # # # # # # # # # # #
|
||||
# ### ## # # # # ## ## ## # # # # # ## #
|
||||
# # # # # # ## # ### ## ## # ##### # # # # # ### ## # ### # # #
|
||||
# # # ## ### # # # # # ### # # # ## ##### # #
|
||||
# # ### # # ## ### # # # # # ## # ## ## # ## # ## ## # #
|
||||
# # # ## # # # # ## # # # # # # # # # # # ### ## # # #
|
||||
# # # # # ## # # # # # ## ## ### ###### ## ## ### #
|
||||
## ## # # # # # # ### # # # ### # ## #
|
||||
# # ###### # # # ## # # # ## # # # ## #### # # #
|
||||
## # # # ### # # # # # #### # # # ## # # # #
|
||||
# # # # ### ## ## # # # ## # # # # # ### # # # ### # # #### # ##
|
||||
# ## # # # # # # # # # # # # # # ## ### ## # # ## #
|
||||
### # # # # ## ### # # ## # # # ## # ## # ## #
|
||||
### # # # # ### # # # # # ## # # # # # ## # # ## #
|
||||
# # # # ## # # ### # ## ## # ### # # ### ## #
|
||||
### ## # ## # ## # # # # # # # # # # # ####### ##
|
||||
## ## # # # # ## # # # ## ### ### # # # ### # # # # ## # ###
|
||||
### #### ### # # # # ## ## # #### # # # # # # ## # #
|
||||
### # ## ## # ## ## ## # # # ## # # ## # ## # #
|
||||
# # # # # # # #### ## # # #### ## # ## ## # # #
|
||||
# ## # ## # # # # ### # ## # ## # # ## # # # ## # # #### # #
|
||||
# # ## # # # # # # # # # ## ## # # # ### # #
|
||||
# # # # # # ## # # # ### # ## # # # # ## # # # # # #
|
||||
# # # ### # # # # # ## ## # # ## # # ## # # #
|
||||
## ## ### # # ## # # # # # # # # # # # # # ## ## # # # #
|
||||
# # # # # # ## # # # # # # # ##
|
||||
# # # # # # ## # ## # ## # # # ## ## ## ## ### # # # # # #
|
||||
# # # # # # # #### # ## # # # # ## ## # # # ## # #
|
||||
## # # # # # # # ###### # # ### # # ## # # # # ### ##
|
||||
# # ## # # # # #### # #### # # # ## ## ## #
|
||||
# # # # # # # ## # # # # # ### ### # # # # # # #
|
||||
# # # # ## # # # # # ## # ## # # ## # ## ### # #
|
||||
#### # # # # ## # # # # # ## ### # # # # ### # ## #
|
||||
# # # # ## ## # # # # # # # # # # # # # # # ## ## # # ##
|
||||
# # # # # # ## # # # ## ## # # # # # # ## #
|
||||
# # ## ## ### ## # # ## # # # ## # # # # # # # # #
|
||||
## ## # # # # # # ## ### # # # ## # # ## # ### # ### ##
|
||||
## ## # # # # # # # # # ## # # ## # ### # # # #
|
||||
## # # ## ## ## # # ## # # ## # # # # ## # #
|
||||
## # ## # ## ## # # # # # # # # # # ### # # # # # ## # #
|
||||
# ## ## # # # # # #### ## # # # # # # # # #
|
||||
# # # # # # # # ## # # # # # ### # # # # #### ## ### ####
|
||||
# ## # # #### # # # # #### # # # # # ### # # ### # ## ##
|
||||
## # ## # ## # # # # # # ### # # # # # ## # # #
|
||||
# # # # ## # # # ### #### ## # # # # ## ## ## #
|
||||
## # ## # ## # # # # ## # # # # # # # # # # # ## #
|
||||
# # ## # # # ### # ## # ## # # ### # # # # ### #
|
||||
# # ## # ## #### # # # # # # # ## ## # ## ###
|
||||
# ## # # ## ## # # ## # # # ### # ## # # # # # # # #
|
||||
# # ## # # ## # # # # # # # # # # # ## # ### ##
|
||||
# ## # # # # # # # ## # # ## ## ## # # ## ## # # ## ### ### ####
|
||||
### # # # # # # # ## # # # ## # ## # # # ## # # ## # # # #
|
||||
# # # # # # # # ## ## ### # # # # # # ## # # # #
|
||||
# # # # ## # ### # # # # ## # # # ### # ## ## # # # ##
|
||||
# # # # # ## # ## # # # ### # ## ## # # # # # # # #
|
||||
## ### ## # # # # ## # # # #### # #### # # ## # ## #
|
||||
## ## ## # # # # ## # # ## ## ### # # # # # ### # ### ##
|
||||
# # ### # # # # # # # # # ## # ### # # # ### ## ##
|
||||
# # ## # ## # ## ## # # # ## ## # ## # # ##
|
||||
# # ### # ## ## # # ### # # # # # # ## ## # ##
|
||||
# # #### # # # # # # # ### # # # # # # ## # ### # # ### ###
|
||||
# # ## # # ##### # ## # # ## ## # # # ## # # # ## ##
|
||||
# ### # ## # # ###### ### # ## # ## # # ## # # # # ## ## # ## #
|
||||
# # # # # # # # ## ## # # # ## # # ## ## # # # # #
|
||||
## # ## ## # ### # # # # # # # # ## # # # # # ###### # ##
|
||||
## # # # # ### # # ### ## # # ## # # # # ##### #
|
||||
# # ### # # # # # # ## #### # # ### # # # ## # ##
|
||||
# # ## ## # ## # #### # ## # # # # # ## ## # # # # ## ## #
|
||||
## # # # # ## # # ## # # # ## # # ## # # # # # #
|
||||
# # # ## # # # # ## # ## # # # # # ## # # ##
|
||||
# ## ## # # # # # # ### # ## # # # # # # # # #
|
||||
# # ## # # # # # # # ##### ## ## ### # # ###
|
||||
# # # # # # ## ## ## # # # # # # ## # ##### # ##
|
||||
# # ## # # # ## # # #### # ## # # # # # ## # # #
|
||||
# # # # # ## ## # ## # # # # # #### # ##
|
||||
## # # # # ## # ## ## ## # # ## # # # ## # ## # # #
|
||||
## # # # # # # # ## ### # # # ## # # ## #
|
||||
### # ## # # # ## ## # ### # # # # # ### # # # ##### #
|
||||
## # # ## # ## # # # # # ## # # # ## ####### ### # #
|
||||
#### # # # # # # # # # # ## # ## # # ### # ## # # #
|
||||
# # # # # # # # # # ## # # ## # # # # ## # ### # #
|
||||
# # # # #### ## ## # # # # ## # # # # # # ### ### # ##
|
||||
#### # ## # # # ### ## # ## ## # ## # # ## # #
|
||||
# # ## # # # # # # # # ## # # ## # # ### # ##
|
||||
# # # # ## ## # # ## # # # # ## # ## ##
|
||||
### ## # # # ## ## ## ## # # # ## ## # # # # # # # # # ## # # #
|
||||
## # # # # # # # # # # ## #### # # ## ### ### ## # # #
|
||||
# # ##### # # # ## ## # # ## ## # # ## # #### ##### # # ## ##
|
||||
# # # # # # ## # # # # # # # # # # ## #
|
||||
## ### # # ## ## # ## ## ## # # ## # # ### # # ## ### #
|
||||
# # # ## # ## # # # ## # # # # ## # # # #
|
||||
# # # # # #### # # # ## # # # ## # # # # # # # # # #
|
||||
# # # ## # # ## # # ### # # ## # # ## # # ##
|
||||
# # # ## # # ### # # # # # ## ## ##
|
||||
# # # # ### # # # # # # # # # # # ## ## # ### # ## # # # #
|
||||
# ###### # # ## ## ## # ### # # # ## # # # #####
|
||||
# ## # # # # ## # # # # # # # # #### # # e
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
s # ## # # ### # ## # # #
|
||||
## # # ## ## # # # #
|
||||
# # ## # # # # ##
|
||||
### # # # # # # ## ## # ## # #
|
||||
# # # ## # # # # ## # #
|
||||
# # # # ## # ## # # #
|
||||
## # # # # # # # ## # #
|
||||
# ## # # # ## # ## # # # # #
|
||||
## # # # # ## # # ## # ##
|
||||
# # # # # ## # # ## # # #
|
||||
# # # # ## # # # # ## # ## # #
|
||||
# ## # # # # # # # ## ##
|
||||
## # ## ### # # # ## # ##
|
||||
##### ### # # # # ## # # # #
|
||||
# # ### ## # ## ## #### ###
|
||||
## # # # # ### # # ## # #
|
||||
# # ## # # # # # # ##
|
||||
## # # # ### # ## # # ## # # ## ##
|
||||
# #### # # # # # ### # ##
|
||||
# ## # ## # # ## ### ## ### #
|
||||
# # ### ## # # # ##
|
||||
# # ## # # # # # # #
|
||||
# ## # ### #### # ## # ### ## # #
|
||||
# # ## # # # # # # #
|
||||
# # ##### # # # # # # # ## # ##
|
||||
## # # # # ## ## # ## ## #
|
||||
# # # # # # # ## # # #
|
||||
## # # # ## # # ## # #
|
||||
# ### # # # # # # # # # ###
|
||||
### # # # # # ### # # # # # ##
|
||||
# # # # # ## # # # # # ##
|
||||
# ## ## ## # # # # # # ## #
|
||||
# #### # # # ## # ## #
|
||||
## # # # # ## # # # # #
|
||||
## # ## ## # # # ## # # ## #
|
||||
# # # # # # # # # # ### # # #
|
||||
# # ## # # # # # ###
|
||||
# # #### ##
|
||||
# # ## # # ## ### # # ##
|
||||
##### # # # # # # # # # #
|
||||
## # # # # # #
|
||||
# # ## ## # # # # ## ### # #
|
||||
# # ### ## ### ### # ## # #
|
||||
## # ### # ## # # # #
|
||||
# # # # # ## # # # # #
|
||||
# # ## # # ## ### # # # #
|
||||
# # # # # ## # ### #
|
||||
## # # ## # # #
|
||||
# # ## # ### # ### # ## # ## # ##
|
||||
# # # # # # # ## # # e
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
s ## ###
|
||||
# # # # # ##
|
||||
# # # # #
|
||||
# # ##
|
||||
# # # # #
|
||||
# # ### # #
|
||||
# # # # #
|
||||
# # ## ## ###
|
||||
# ## #
|
||||
# # ###
|
||||
# # # # #
|
||||
### # #
|
||||
# # # #
|
||||
## # # # #
|
||||
## # # # # ##
|
||||
# # #
|
||||
# #
|
||||
# # # #
|
||||
# # #
|
||||
# # # # ## #
|
||||
|
Before Width: | Height: | Size: 101 KiB |
|
|
@ -1,10 +0,0 @@
|
|||
s #
|
||||
|
||||
#
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
#
|
||||
# #
|
||||
#
|
||||
# # e
|
||||
|
Before Width: | Height: | Size: 103 KiB |
|
|
@ -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 |
|
||||
|
||||
### Графики
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Анализ эффективности алгоритмов
|
||||
|
||||
В ходе экспериментов были получены следующие результаты.
|
||||
|
||||
### BFS
|
||||
Преимущества:
|
||||
всегда находит кратчайший путь;
|
||||
простая реализация.
|
||||
Недостатки:
|
||||
посещает большое количество клеток;
|
||||
требует много памяти.
|
||||
Выходит, что наиболее эффективен в небольших невзвешенных лабиринтах.
|
||||
|
||||
### DFS
|
||||
Преимущества:
|
||||
простая реализация;
|
||||
самым быстрым находит произвольный путь.
|
||||
Недостатки:
|
||||
не гарантирует кратчайший путь;
|
||||
может уходить в тупики.
|
||||
Подходит для быстрого поиска любого решения.
|
||||
|
||||
### A
|
||||
Преимущества:
|
||||
высокая скорость;
|
||||
посещает меньше клеток;
|
||||
Недостатки:
|
||||
требует выбора хорошей эвристики.
|
||||
Показал хорошие результаты на больших лабиринтах.
|
||||
|
||||
## Анализ применимости паттернов
|
||||
|
||||
### Builder
|
||||
Без Builder код загрузки лабиринта был бы жёстко связан с классом Maze, а добавление нового формата потребовало бы изменения существующего кода.
|
||||
Strategy
|
||||
Без Strategy пришлось бы:
|
||||
хранить все алгоритмы внутри одного класса;
|
||||
использовать большое количество условных операторов;
|
||||
изменять код MazeSolver при добавлении новых алгоритмов
|
||||
Strategy помог полностью отделить алгоритмы друг от друга.
|
||||
|
||||
### Observer
|
||||
Без Observer логика интерфейса смешивалась бы с логикой поиска.
|
||||
Это усложнило бы:
|
||||
добавление GUI;
|
||||
логирование;
|
||||
визуализацию.
|
||||
|
||||
### Command
|
||||
Без Command было бы сложно реализовать:
|
||||
undo;
|
||||
историю действий;
|
||||
расширяемую систему управления.
|
||||
|
||||
## Выводы
|
||||
|
||||
### В проекте были успешно реализованы:
|
||||
загрузка лабиринта из файла;
|
||||
несколько алгоритмов поиска пути;
|
||||
визуализация;
|
||||
система наблюдателей;
|
||||
система команд;
|
||||
экспериментальное сравнение алгоритмов.
|
||||
|
||||
### Использование паттернов GoF позволило:
|
||||
сделать архитектуру гибкой;
|
||||
уменьшить связанность компонентов;
|
||||
упростить расширение программы;
|
||||
облегчить сопровождение кода.
|
||||
|
|
@ -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,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
|
||||
|
|
@ -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 с результатами
|
||||
- Диаграммы
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
# Диаграммы проекта
|
||||
|
||||
## 1. Диаграмма классов
|
||||
|
||||
См. файл `class_diagram.mmd`.
|
||||
|
||||
## 2. Структура каталогов
|
||||
|
||||
```
|
||||
vinichukan/
|
||||
├── src/
|
||||
├── mazes/
|
||||
├── experiments/
|
||||
└── docs/
|
||||
```
|
||||
|
||||
## 3. Логика работы алгоритмов
|
||||
|
||||
- BFS — поиск в ширину
|
||||
- DFS — поиск в глубину
|
||||
- A\* — эвристический поиск с манхэттенской метрикой
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
|
@ -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,13 +0,0 @@
|
|||
####################################################################################################
|
||||
#S # ########### # # ######### # #
|
||||
# ####### ######### ########### ###### ######## ######## ######## ######## ######## ########## #####
|
||||
# # # # # # # # # # # #
|
||||
######## ######### ######### ######## ######## ######## ######## ######## ######## ######## #######
|
||||
# # # # # # # # # # # # #
|
||||
# ######## ##### # # ##### ######## ######## ######## ######## ######## ######## ######## #########
|
||||
# # # # # # # # # # # # # #
|
||||
######## ####### # ####### ######## ######## ######## ######## ######## ######## ######## #########
|
||||
# # # # # # # # # # # #
|
||||
# #### ######## ######## ######## ######## ######## ######## ######## ######## ######## ###########
|
||||
# # # # # # # # # # # E#
|
||||
####################################################################################################
|
||||
|
|
@ -1 +0,0 @@
|
|||
S E
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
###############
|
||||
#S # E#
|
||||
# ### ####### #
|
||||
# #
|
||||
###############
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
#######
|
||||
#S #
|
||||
#######
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
##########
|
||||
#S E#
|
||||
##########
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from src.model.maze import Maze
|
||||
|
||||
class MazeBuilder(ABC):
|
||||
@abstractmethod
|
||||
def build_from_file(self, filename: str) -> Maze:
|
||||
pass
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
from src.model.cell import Cell
|
||||
from src.model.maze import Maze
|
||||
|
||||
class TextFileMazeBuilder:
|
||||
def build_from_file(self, filename):
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
lines = [line.rstrip("\n") for line in f]
|
||||
|
||||
height = len(lines)
|
||||
width = max(len(line) for line in lines)
|
||||
|
||||
cells = []
|
||||
start = None
|
||||
exit_ = None
|
||||
|
||||
for y, line in enumerate(lines):
|
||||
row = []
|
||||
for x, ch in enumerate(line.ljust(width)):
|
||||
is_wall = (ch == "#")
|
||||
is_start = (ch == "S")
|
||||
is_exit = (ch == "E")
|
||||
|
||||
cell = Cell(x, y, is_wall, is_start, is_exit)
|
||||
row.append(cell)
|
||||
|
||||
if is_start:
|
||||
start = cell
|
||||
if is_exit:
|
||||
exit_ = cell
|
||||
|
||||
cells.append(row)
|
||||
|
||||
if start is None:
|
||||
raise ValueError("Файл должен содержать S (старт)")
|
||||
|
||||
return Maze(width, height, cells, start, exit_)
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
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
|
||||
from src.ui.console_view import ConsoleView
|
||||
from src.ui.player import Player
|
||||
from src.ui.move_command import MoveCommand
|
||||
|
||||
|
||||
def choose_maze():
|
||||
mazes = {
|
||||
"1": ("small.txt", "Small — маленький лабиринт"),
|
||||
"2": ("medium.txt", "Medium — средний лабиринт"),
|
||||
"3": ("big.txt", "Big — большой лабиринт(тупиковый)"),
|
||||
"4": ("empty.txt", "Empty — пустой лабиринт"),
|
||||
"5": ("no_exit.txt","NoExit — без выхода")
|
||||
}
|
||||
|
||||
print("\n" + "=" * 40)
|
||||
print(" ВЫБОР ЛАБИРИНТА")
|
||||
print("=" * 40)
|
||||
|
||||
for key, (_, desc) in mazes.items():
|
||||
print(f" {key}. {desc}")
|
||||
|
||||
print("=" * 40)
|
||||
|
||||
choice = input("Введите номер: ").strip()
|
||||
|
||||
if choice not in mazes:
|
||||
print("Неверный выбор, загружаю small.txt")
|
||||
return "small.txt"
|
||||
|
||||
filename = mazes[choice][0]
|
||||
print(f"Загружен: {filename}")
|
||||
return filename
|
||||
|
||||
|
||||
def main():
|
||||
builder = TextFileMazeBuilder()
|
||||
|
||||
filename = choose_maze()
|
||||
maze = builder.build_from_file(f"mazes/{filename}")
|
||||
|
||||
view = ConsoleView()
|
||||
view.update(f"Maze '{filename}' loaded")
|
||||
|
||||
strategies = {
|
||||
"bfs": BFSStrategy(),
|
||||
"dfs": DFSStrategy(),
|
||||
"astar": AStarStrategy()
|
||||
}
|
||||
|
||||
print("\nВыберите алгоритм:")
|
||||
print(" bfs — поиск в ширину")
|
||||
print(" dfs — поиск в глубину")
|
||||
print(" astar — A*")
|
||||
algo = input("Введите название: ").strip().lower()
|
||||
|
||||
strategy = strategies.get(algo, BFSStrategy())
|
||||
|
||||
solver = MazeSolver(maze, strategy)
|
||||
stats = solver.solve()
|
||||
print(stats)
|
||||
|
||||
path, visited = strategy.find_path(maze, maze.start, maze.exit)
|
||||
view.render(maze, None, path)
|
||||
|
||||
player = Player(maze.start)
|
||||
|
||||
while True:
|
||||
cmd = input("Ход (w/a/s/d) или q для выхода: ").strip().lower()
|
||||
if cmd == "q":
|
||||
break
|
||||
|
||||
dxdy = {
|
||||
"w": (0, -1),
|
||||
"s": (0, 1),
|
||||
"a": (-1, 0),
|
||||
"d": (1, 0)
|
||||
}
|
||||
|
||||
if cmd not in dxdy:
|
||||
continue
|
||||
|
||||
dx, dy = dxdy[cmd]
|
||||
new_cell = maze.get_cell(player.current_cell.x + dx,
|
||||
player.current_cell.y + dy)
|
||||
|
||||
if not new_cell or not new_cell.is_passable():
|
||||
print("Там стена, туда нельзя.")
|
||||
continue
|
||||
|
||||
move = MoveCommand(player, new_cell)
|
||||
move.execute()
|
||||
view.render(maze, player.current_cell, path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
class Cell:
|
||||
def __init__(self, x, y, is_wall=False, is_start=False, is_exit=False):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.is_wall = is_wall
|
||||
self.is_start = is_start
|
||||
self.is_exit = is_exit
|
||||
|
||||
def __repr__(self):
|
||||
return f"Cell({self.x},{self.y})"
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.x, self.y))
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Cell) and self.x == other.x and self.y == other.y
|
||||
|
||||
def is_passable(self):
|
||||
return not self.is_wall
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
class Maze:
|
||||
def __init__(self, width, height, cells, start, exit_):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.cells = cells
|
||||
self.start = start
|
||||
self.exit = exit_
|
||||
|
||||
def get_cell(self, x, y):
|
||||
return self.cells[y][x]
|
||||
|
||||
def get_neighbors(self, cell):
|
||||
dirs = [(1,0), (-1,0), (0,1), (0,-1)]
|
||||
result = []
|
||||
|
||||
for dx, dy in dirs:
|
||||
nx, ny = cell.x + dx, cell.y + dy
|
||||
if 0 <= nx < self.width and 0 <= ny < self.height:
|
||||
n = self.get_cell(nx, ny)
|
||||
if not n.is_wall:
|
||||
result.append(n)
|
||||
|
||||
return result
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
from src.solver.search_stats import SearchStats
|
||||
|
||||
class MazeSolver:
|
||||
def __init__(self, maze, strategy):
|
||||
self.maze = maze
|
||||
self.strategy = strategy
|
||||
|
||||
def solve(self):
|
||||
import time
|
||||
t0 = time.perf_counter()
|
||||
|
||||
if self.maze.exit is None:
|
||||
t1 = time.perf_counter()
|
||||
return SearchStats(
|
||||
time_ms=(t1 - t0) * 1000,
|
||||
visited=0,
|
||||
path_len=0
|
||||
)
|
||||
|
||||
path, visited = self.strategy.find_path(
|
||||
self.maze,
|
||||
self.maze.start,
|
||||
self.maze.exit
|
||||
)
|
||||
|
||||
t1 = time.perf_counter()
|
||||
|
||||
return SearchStats(
|
||||
time_ms=(t1 - t0) * 1000,
|
||||
visited=len(visited),
|
||||
path_len=len(path) if path else 0
|
||||
)
|
||||