forked from UNN/2026-rff_mp
Merge pull request '[2] lab2' (#350) from BrychkinKA/2026-rff_mp:task2 into develop
Reviewed-on: UNN/2026-rff_mp#350
This commit is contained in:
commit
5e3f0b10e7
47
BrychkinKA/docs/class_diagram.mmd
Normal file
47
BrychkinKA/docs/class_diagram.mmd
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
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
|
||||
135
BrychkinKA/docs/conclusion.md
Normal file
135
BrychkinKA/docs/conclusion.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
# Отчёт по заданию №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 с результатами
|
||||
- Диаграммы
|
||||
21
BrychkinKA/docs/diagrams.md
Normal file
21
BrychkinKA/docs/diagrams.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Диаграммы проекта
|
||||
|
||||
## 1. Диаграмма классов
|
||||
|
||||
См. файл `class_diagram.mmd`.
|
||||
|
||||
## 2. Структура каталогов
|
||||
|
||||
```
|
||||
vinichukan/
|
||||
├── src/
|
||||
├── mazes/
|
||||
├── experiments/
|
||||
└── docs/
|
||||
```
|
||||
|
||||
## 3. Логика работы алгоритмов
|
||||
|
||||
- BFS — поиск в ширину
|
||||
- DFS — поиск в глубину
|
||||
- A\* — эвристический поиск с манхэттенской метрикой
|
||||
65
BrychkinKA/experiments/benchmark.py
Normal file
65
BrychkinKA/experiments/benchmark.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
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()
|
||||
76
BrychkinKA/experiments/plot_graphs.py
Normal file
76
BrychkinKA/experiments/plot_graphs.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
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()
|
||||
BIN
BrychkinKA/experiments/plot_path.png
Normal file
BIN
BrychkinKA/experiments/plot_path.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
BIN
BrychkinKA/experiments/plot_time.png
Normal file
BIN
BrychkinKA/experiments/plot_time.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
BrychkinKA/experiments/plot_visited.png
Normal file
BIN
BrychkinKA/experiments/plot_visited.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
16
BrychkinKA/experiments/results.csv
Normal file
16
BrychkinKA/experiments/results.csv
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
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
|
||||
|
13
BrychkinKA/mazes/big.txt
Normal file
13
BrychkinKA/mazes/big.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
####################################################################################################
|
||||
#S # ########### # # ######### # #
|
||||
# ####### ######### ########### ###### ######## ######## ######## ######## ######## ########## #####
|
||||
# # # # # # # # # # # #
|
||||
######## ######### ######### ######## ######## ######## ######## ######## ######## ######## #######
|
||||
# # # # # # # # # # # # #
|
||||
# ######## ##### # # ##### ######## ######## ######## ######## ######## ######## ######## #########
|
||||
# # # # # # # # # # # # # #
|
||||
######## ####### # ####### ######## ######## ######## ######## ######## ######## ######## #########
|
||||
# # # # # # # # # # # #
|
||||
# #### ######## ######## ######## ######## ######## ######## ######## ######## ######## ###########
|
||||
# # # # # # # # # # # E#
|
||||
####################################################################################################
|
||||
1
BrychkinKA/mazes/empty.txt
Normal file
1
BrychkinKA/mazes/empty.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
S E
|
||||
5
BrychkinKA/mazes/medium.txt
Normal file
5
BrychkinKA/mazes/medium.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
###############
|
||||
#S # E#
|
||||
# ### ####### #
|
||||
# #
|
||||
###############
|
||||
3
BrychkinKA/mazes/no_exit.txt
Normal file
3
BrychkinKA/mazes/no_exit.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#######
|
||||
#S #
|
||||
#######
|
||||
3
BrychkinKA/mazes/small.txt
Normal file
3
BrychkinKA/mazes/small.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
##########
|
||||
#S E#
|
||||
##########
|
||||
7
BrychkinKA/src/builder/maze_builder.py
Normal file
7
BrychkinKA/src/builder/maze_builder.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from src.model.maze import Maze
|
||||
|
||||
class MazeBuilder(ABC):
|
||||
@abstractmethod
|
||||
def build_from_file(self, filename: str) -> Maze:
|
||||
pass
|
||||
36
BrychkinKA/src/builder/text_file_maze_builder.py
Normal file
36
BrychkinKA/src/builder/text_file_maze_builder.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
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_)
|
||||
101
BrychkinKA/src/main.py
Normal file
101
BrychkinKA/src/main.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
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()
|
||||
19
BrychkinKA/src/model/cell.py
Normal file
19
BrychkinKA/src/model/cell.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
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
|
||||
23
BrychkinKA/src/model/maze.py
Normal file
23
BrychkinKA/src/model/maze.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
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
|
||||
32
BrychkinKA/src/solver/maze_solver.py
Normal file
32
BrychkinKA/src/solver/maze_solver.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
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
|
||||
)
|
||||
8
BrychkinKA/src/solver/search_stats.py
Normal file
8
BrychkinKA/src/solver/search_stats.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
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})"
|
||||
43
BrychkinKA/src/strategy/astar_strategy.py
Normal file
43
BrychkinKA/src/strategy/astar_strategy.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
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))
|
||||
29
BrychkinKA/src/strategy/bfs_strategy.py
Normal file
29
BrychkinKA/src/strategy/bfs_strategy.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
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))
|
||||
27
BrychkinKA/src/strategy/dfs_strategy.py
Normal file
27
BrychkinKA/src/strategy/dfs_strategy.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
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))
|
||||
9
BrychkinKA/src/strategy/path_finding_strategy.py
Normal file
9
BrychkinKA/src/strategy/path_finding_strategy.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
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
|
||||
10
BrychkinKA/src/ui/command.py
Normal file
10
BrychkinKA/src/ui/command.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from abc import ABC, abstractmethod
|
||||
|
||||
class Command(ABC):
|
||||
@abstractmethod
|
||||
def execute(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def undo(self):
|
||||
pass
|
||||
33
BrychkinKA/src/ui/console_view.py
Normal file
33
BrychkinKA/src/ui/console_view.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
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)
|
||||
17
BrychkinKA/src/ui/move_command.py
Normal file
17
BrychkinKA/src/ui/move_command.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
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)
|
||||
6
BrychkinKA/src/ui/observer.py
Normal file
6
BrychkinKA/src/ui/observer.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from abc import ABC, abstractmethod
|
||||
|
||||
class Observer(ABC):
|
||||
@abstractmethod
|
||||
def update(self, event: str):
|
||||
pass
|
||||
8
BrychkinKA/src/ui/player.py
Normal file
8
BrychkinKA/src/ui/player.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
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
|
||||
Loading…
Reference in New Issue
Block a user