Compare commits
No commits in common. "develop" and "2-nd-exercise" have entirely different histories.
develop
...
2-nd-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,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
|
|
||||||
)
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
class SearchStats:
|
|
||||||
def __init__(self, time_ms, visited, path_len):
|
|
||||||
self.time_ms = time_ms
|
|
||||||
self.visited = visited
|
|
||||||
self.path_len = path_len
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"SearchStats(time={self.time_ms:.2f}ms, visited={self.visited}, path={self.path_len})"
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import heapq
|
|
||||||
|
|
||||||
def manhattan(a, b):
|
|
||||||
return abs(a.x - b.x) + abs(a.y - b.y)
|
|
||||||
|
|
||||||
class AStarStrategy:
|
|
||||||
def find_path(self, maze, start, exit_):
|
|
||||||
g = {start: 0}
|
|
||||||
parent = {start: None}
|
|
||||||
|
|
||||||
counter = 0
|
|
||||||
open_heap = [(0, counter, start)]
|
|
||||||
in_open = {start}
|
|
||||||
visited = set()
|
|
||||||
|
|
||||||
while open_heap:
|
|
||||||
_, _, cur = heapq.heappop(open_heap)
|
|
||||||
in_open.discard(cur)
|
|
||||||
visited.add(cur)
|
|
||||||
|
|
||||||
if cur == exit_:
|
|
||||||
return self._reconstruct(parent, start, exit_), visited
|
|
||||||
|
|
||||||
for n in maze.get_neighbors(cur):
|
|
||||||
tentative = g[cur] + 1
|
|
||||||
if tentative < g.get(n, float('inf')):
|
|
||||||
g[n] = tentative
|
|
||||||
parent[n] = cur
|
|
||||||
f = tentative + manhattan(n, exit_)
|
|
||||||
if n not in in_open:
|
|
||||||
counter += 1
|
|
||||||
heapq.heappush(open_heap, (f, counter, n))
|
|
||||||
in_open.add(n)
|
|
||||||
|
|
||||||
return None, visited
|
|
||||||
|
|
||||||
def _reconstruct(self, parent, start, exit_):
|
|
||||||
path = []
|
|
||||||
cur = exit_
|
|
||||||
while cur:
|
|
||||||
path.append(cur)
|
|
||||||
cur = parent[cur]
|
|
||||||
return list(reversed(path))
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
class BFSStrategy:
|
|
||||||
def find_path(self, maze, start, exit_):
|
|
||||||
queue = deque([start])
|
|
||||||
parent = {start: None}
|
|
||||||
visited = {start}
|
|
||||||
|
|
||||||
while queue:
|
|
||||||
cur = queue.popleft()
|
|
||||||
|
|
||||||
if cur == exit_:
|
|
||||||
return self._reconstruct(parent, start, exit_), visited
|
|
||||||
|
|
||||||
for n in maze.get_neighbors(cur):
|
|
||||||
if n not in visited:
|
|
||||||
visited.add(n)
|
|
||||||
parent[n] = cur
|
|
||||||
queue.append(n)
|
|
||||||
|
|
||||||
return None, visited
|
|
||||||
|
|
||||||
def _reconstruct(self, parent, start, exit_):
|
|
||||||
path = []
|
|
||||||
cur = exit_
|
|
||||||
while cur:
|
|
||||||
path.append(cur)
|
|
||||||
cur = parent[cur]
|
|
||||||
return list(reversed(path))
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
class DFSStrategy:
|
|
||||||
def find_path(self, maze, start, exit_):
|
|
||||||
stack = [start]
|
|
||||||
parent = {start: None}
|
|
||||||
visited = {start}
|
|
||||||
|
|
||||||
while stack:
|
|
||||||
cur = stack.pop()
|
|
||||||
|
|
||||||
if cur == exit_:
|
|
||||||
return self._reconstruct(parent, start, exit_), visited
|
|
||||||
|
|
||||||
for n in maze.get_neighbors(cur):
|
|
||||||
if n not in visited:
|
|
||||||
visited.add(n)
|
|
||||||
parent[n] = cur
|
|
||||||
stack.append(n)
|
|
||||||
|
|
||||||
return None, visited
|
|
||||||
|
|
||||||
def _reconstruct(self, parent, start, exit_):
|
|
||||||
path = []
|
|
||||||
cur = exit_
|
|
||||||
while cur:
|
|
||||||
path.append(cur)
|
|
||||||
cur = parent[cur]
|
|
||||||
return list(reversed(path))
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import List
|
|
||||||
from src.model.cell import Cell
|
|
||||||
from src.model.maze import Maze
|
|
||||||
|
|
||||||
class PathFindingStrategy(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> List[Cell]:
|
|
||||||
pass
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
class Command(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def execute(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def undo(self):
|
|
||||||
pass
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import os
|
|
||||||
from typing import List
|
|
||||||
from src.model.cell import Cell
|
|
||||||
from src.model.maze import Maze
|
|
||||||
from .observer import Observer
|
|
||||||
|
|
||||||
class ConsoleView(Observer):
|
|
||||||
def update(self, event: str):
|
|
||||||
print(f"[EVENT] {event}")
|
|
||||||
|
|
||||||
def render(self, maze: Maze, player_pos: Cell = None, path: List[Cell] = None):
|
|
||||||
os.system('cls' if os.name == 'nt' else 'clear')
|
|
||||||
|
|
||||||
path_set = set(path) if path else set()
|
|
||||||
|
|
||||||
for y in range(maze.height):
|
|
||||||
row = ""
|
|
||||||
for x in range(maze.width):
|
|
||||||
cell = maze.get_cell(x, y)
|
|
||||||
|
|
||||||
if cell.is_wall:
|
|
||||||
row += "#"
|
|
||||||
elif cell.is_start:
|
|
||||||
row += "S"
|
|
||||||
elif cell.is_exit:
|
|
||||||
row += "E"
|
|
||||||
elif player_pos and cell.x == player_pos.x and cell.y == player_pos.y:
|
|
||||||
row += "@"
|
|
||||||
elif cell in path_set:
|
|
||||||
row += "*"
|
|
||||||
else:
|
|
||||||
row += " "
|
|
||||||
print(row)
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
from src.model.cell import Cell
|
|
||||||
from .command import Command
|
|
||||||
from .player import Player
|
|
||||||
|
|
||||||
class MoveCommand(Command):
|
|
||||||
def __init__(self, player: Player, new_cell: Cell):
|
|
||||||
self.player = player
|
|
||||||
self.new_cell = new_cell
|
|
||||||
self.prev_cell = None
|
|
||||||
|
|
||||||
def execute(self):
|
|
||||||
self.prev_cell = self.player.current_cell
|
|
||||||
self.player.move_to(self.new_cell)
|
|
||||||
|
|
||||||
def undo(self):
|
|
||||||
if self.prev_cell:
|
|
||||||
self.player.move_to(self.prev_cell)
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
class Observer(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def update(self, event: str):
|
|
||||||
pass
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
from src.model.cell import Cell
|
|
||||||
|
|
||||||
class Player:
|
|
||||||
def __init__(self, start_cell: Cell):
|
|
||||||
self.current_cell = start_cell
|
|
||||||
|
|
||||||
def move_to(self, cell: Cell):
|
|
||||||
self.current_cell = cell
|
|
||||||