Merge branch 'task-2' into task-1

This commit is contained in:
oSTEVEo 2026-05-25 03:02:06 +03:00
commit b1a4e4185e
28 changed files with 1743 additions and 1 deletions

View File

@ -341,7 +341,7 @@
"name": "python", "name": "python",
"nbconvert_exporter": "python", "nbconvert_exporter": "python",
"pygments_lexer": "ipython3", "pygments_lexer": "ipython3",
"version": "3.11.2" "version": "3.14.5"
} }
}, },
"nbformat": 4, "nbformat": 4,

903
MusinAA/docs/Report 2.ipynb Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,16 @@
Алгоритм,Лабиринт,Время (мс),Посещённые клетки,Длинна пути
BFS,10x10,0.4038135000882903,53,23
BFS,5x5,0.07533170064562,8,7
BFS,100x100,17.14356810080062,2495,1171
BFS,50x50,3.010086300491821,640,427
BFS,25x25,1.0405578999780118,232,173
DFS,10x10,0.07943829987198114,35,31
DFS,5x5,0.018403499416308478,8,7
DFS,100x100,8.430859900545329,3219,1243
DFS,50x50,2.0664067997131497,995,435
DFS,25x25,0.5787261994555593,316,173
AStar,10x10,0.0671462003083434,23,23
AStar,5x5,0.022370600345311686,8,7
AStar,100x100,4.951790099585196,1286,1171
AStar,50x50,2.081632300541969,496,427
AStar,25x25,0.5791453000711044,186,177
1 Алгоритм Лабиринт Время (мс) Посещённые клетки Длинна пути
2 BFS 10x10 0.4038135000882903 53 23
3 BFS 5x5 0.07533170064562 8 7
4 BFS 100x100 17.14356810080062 2495 1171
5 BFS 50x50 3.010086300491821 640 427
6 BFS 25x25 1.0405578999780118 232 173
7 DFS 10x10 0.07943829987198114 35 31
8 DFS 5x5 0.018403499416308478 8 7
9 DFS 100x100 8.430859900545329 3219 1243
10 DFS 50x50 2.0664067997131497 995 435
11 DFS 25x25 0.5787261994555593 316 173
12 AStar 10x10 0.0671462003083434 23 23
13 AStar 5x5 0.022370600345311686 8 7
14 AStar 100x100 4.951790099585196 1286 1171
15 AStar 50x50 2.081632300541969 496 427
16 AStar 25x25 0.5791453000711044 186 177

View File

@ -0,0 +1,7 @@
Алгоритм,Лабиринт,Время (мс),Посещённые клетки,Длинна пути
BFS,maze_25x25_wo_exit,1.9682294001540868,338,-1
BFS,maze_25x25_empty,4.574537699954817,625,49
DFS,maze_25x25_wo_exit,0.719102000221028,338,-1
DFS,maze_25x25_empty,0.903778699648683,625,337
AStar,maze_25x25_wo_exit,1.0117966015968705,338,-1
AStar,maze_25x25_empty,0.21763520016975235,49,49
1 Алгоритм Лабиринт Время (мс) Посещённые клетки Длинна пути
2 BFS maze_25x25_wo_exit 1.9682294001540868 338 -1
3 BFS maze_25x25_empty 4.574537699954817 625 49
4 DFS maze_25x25_wo_exit 0.719102000221028 338 -1
5 DFS maze_25x25_empty 0.903778699648683 625 337
6 AStar maze_25x25_wo_exit 1.0117966015968705 338 -1
7 AStar maze_25x25_empty 0.21763520016975235 49 49

1
MusinAA/task2/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
maze_generator.py

View File

View File

@ -0,0 +1,94 @@
"""
Реализовать класс ConsoleView, который отображает лабиринт,
текущее положение игрока (если реализован пошаговый режим) и найденный путь.
Метод render(maze, player_position, path) рисует карту в консоли."""
import os
from task2.mazeObjects.cell import Cell
from task2.mazeObjects.maze import Maze
from task2.mazeObjects.path import Path
from task2.observerSubject import MazeEvent, MazeEventType, Observer
SBROS = "\033[0m"
WALL = "#"
EXIT = "E"
START = "S"
PATH_SYMBOL = "+"
SPACE_SYMBOL = " "
# Я убрал аргументы из render(), чтобы не передавать их при каждом запуске.
# И работать внутри класса приятнее, чем тянуть эти аргументы туда-сюда
class ConsoleView(Observer):
maze:Maze|None
path:Path|None
def __init__(self, maze:Maze|None=None, path:Path|None=None):
super().__init__()
self.maze = maze
self.path = path
def _getCellColored(self, cell:Cell) -> str:
if cell.isWall:
# Белый
return self._fmt_str(7, 7, WALL)
elif cell.isExit:
# Кислотно-зелёный
return self._fmt_str(12, 7, EXIT)
elif cell.isStart:
# Кислотно-красный
return self._fmt_str(9, 7, START)
elif self.path and self.path.array:
if cell in self.path.array:
# Градиент
percent = self.path.array.index(cell) / len(self.path.array)
n = self._ANSICalculator(*self._getGradient(percent))
return self._fmt_str(n, 10, PATH_SYMBOL)
return SPACE_SYMBOL
def _fmt_str(self, bg:int, fg:int, symbol:str) -> str:
return f"\033[48;5;{bg}m\033[38;5;{fg}m{symbol}{SBROS}"
def _ANSICalculator(self, r:int, g:int, b:int):
r = max(0, min(5, r))
g = max(0, min(5, g))
b = max(0, min(5, b))
return 16 + 36*r + 6*g + b
def _getGradient(self, percent:float):
r = 5 * (1-percent)
g = 0
b = 5 * percent
return int(round(r)), int(round(g)), int(round(b))
def render(self, player_position=None):
"""
Печатем ячейку.
Цвет зависит от индекса ячейчки в массиве path.
Если в массиве нет - просто белый.
"""
os.system('cls' if os.name == 'nt' else 'clear')
if not self.maze:
print("Лабиринт ещё не загружен")
return None
output = ""
for y in range(self.maze.height):
for x in range(self.maze.width):
cell = self.maze.getCell(x, y)
output += self._getCellColored(cell)
output += "\n"
print(output)
def update(self, event: MazeEvent):
if event.evtype in (MazeEventType.MAZE_LOADED, MazeEventType.PATH_FOUND, MazeEventType.MOVE):
if event.evtype == MazeEventType.PATH_FOUND:
if not event.data: raise ValueError
self.path = event.data
if event.evtype == MazeEventType.MAZE_LOADED:
if not event.data: raise ValueError
self.maze = self.maze
self.render()

View File

@ -0,0 +1,72 @@
from abc import ABC, abstractmethod
from itertools import product
import sys
import os.path as path
from task2.mazeObjects.maze import Maze
from task2.mazeObjects.cell import Cell
from task2.observerSubject import MazeEvent, MazeEventType, Subject
class MazeBuilder(ABC):
"""Интерфейс MazeBuilder с методом buildFromFile(filename)"""
@abstractmethod
def buildFromFile(self, filename: str):
"""Создание лабиринта из файла."""
class TextFileMazeBuilder(MazeBuilder):
"""Читает файл, парсит символы,
создаёт объекты Cell,
задаёт координаты и флаги,
после чего возвращает готовый Maze."""
start:dict
end:dict
def _cellStrategy(self, letter: str) -> Cell:
if letter == '#':
return Cell(isWall=True)
elif letter == ' ':
return Cell()
elif letter == 'S':
return Cell(isStart=True)
elif letter == 'E':
return Cell(isExit=True)
else:
sys.stderr.write(f"Неизвестный символ '{letter}' при загрузке из файла\n")
return Cell()
def _updateStartEnd(self, letter: str, x:int, y:int) -> None:
if letter == 'S':
self.start = {'x': x, 'y': y}
elif letter == 'E':
self.end = {'x': x, 'y': y}
def _generate_row_from_txt(self, filename: str) -> list[str]:
with open(filename) as file:
text = file.read()
text = text.strip()
if not text:
raise ValueError(f"Файл \"{filename}\" пуст")
text = text.split('\n')
return text
def buildFromFile(self, filename: str):
rows = self._generate_row_from_txt(filename)
height = len(rows)
width = len(rows[0])
array = [[Cell() for j in range(width)] for i in range(height)]
try:
for x, y in product(range(width), range(height)):
cell = self._cellStrategy(rows[y][x])
self._updateStartEnd(rows[y][x], x, y)
cell.x = x
cell.y = y
array[y][x] = cell
except IndexError:
raise ValueError(f"В файле {filename}: Строка {y+1} имеет длину {len(rows[y])}, ожидалось {width}")
maze_name, _ = path.splitext(path.basename(filename))
return Maze(array, self.start, self.end, name=maze_name)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,25 @@
S # # #
# ##### # # # ####### #
# # # # # #
#### # ##### # # #######
# # # # # # #
## # # # ##### ### # # #
# # # # # # # # #
### # # # # ### # # # #
# # # # # # # #
# ### # # ### ### # ####
# # # # # # #
### # # # ####### #####
# # # #
# ################# ###
# # # #
# # # ####### ####### ##
# # # # #
### ##### # ### ### ###
# # # # # #
# # # ##### # # #######
# # # # # #
##### # ####### # ### ##
# # # # # # #
#### # # # ### ##### # #
# # # #

View File

View File

@ -0,0 +1,13 @@
class Cell:
"""Хранит координаты (x, y)
флаги isWall, isStart, isExit
метод isPassable() (возвращает True для прохода, если не стена)."""
def __init__(self, x: int = 0, y: int = 0, isWall:bool = False, isStart:bool = False, isExit:bool = False):
self.x = x
self.y = y
self.isWall = isWall
self.isStart = isStart
self.isExit = isExit
def isPassable(self):
return not self.isWall

View File

@ -0,0 +1,41 @@
from task2.mazeObjects.cell import Cell
class Maze:
"""Хранит двумерный массив клеток,
ширину, высоту, ссылки на стартовую и выходную клетку.
Методы:
getCell(x, y), getNeighbors(cell) возвращает список соседних проходимых клеток
(вверх, вниз, влево, вправо, если в пределах границ и не стена)."""
def __init__(self, mazeArray: list[list[Cell]], start: dict, end: dict, name:str="") -> None:
self.mazeArray = mazeArray
self.height = len(mazeArray) # X
self.width = len(mazeArray[0]) # Y
self.startCell = self.getCell(start['x'], start['y'])
self.endCell = self.getCell(end['x'], end['y'])
self.name = name
def getCell(self, x: int, y: int):
return self.mazeArray[y][x]
def checkCell(self, x: int, y: int):
if not(0 <= x and x < self.width):
return False
if not(0 <= y and y < self.height):
return False
return self.getCell(x, y).isPassable()
def getNeighbors(self, cell: Cell):
point = (cell.x, cell.y)
offsets = ((0, 1),
(0, -1),
(-1, 0),
(1, 0))
passableCells = []
for ofst in offsets:
x = point[0]+ofst[0]
y = point[1]+ofst[1]
if self.checkCell(x, y):
passableCells.append(self.getCell(x, y))
return passableCells

View File

@ -0,0 +1,6 @@
from task2.mazeObjects.cell import Cell
class Path:
def __init__(self, array:list[Cell]|None, visited_cells:int):
self.array = array
self.visited_cells = visited_cells

View File

@ -0,0 +1,66 @@
from task2.mazeObjects.maze import Maze
from task2.mazeObjects.cell import Cell
from task2.observerSubject import MazeEvent, MazeEventType, Subject
from task2.strategyObjects.pathFindingStrategy import PathFindingStrategy
from task2.strategyObjects.BFS import BFS
import time
class SearchStats:
maze_name:str = "None"
"""Время выполнения в миллисекундах, количество посещённых клеток, длина найденного пути"""
def __init__(self, path: list[Cell]|None, duration:float, visited_cells:int, path_len:int, strategy_name:str):
self.duration = duration
self.visited_cells = visited_cells
self.path_len = path_len
self.path = path
self.strategy_name = strategy_name
def toDict(self,):
return {
"strategy_name" : self.strategy_name,
"maze_name" : self.maze_name,
"duration" : self.duration,
"visited_cells" : self.visited_cells,
"path_len" : self.path_len
}
class MazeSolver(Subject):
"""
MazeSolver содержит поля maze и strategy.
Метод setStrategy(strategy) для динамической смены алгоритма.
Метод solve() вызывает strategy.findPath(...) и возвращает объект SearchStats (время выполнения в миллисекундах,
количество посещённых клеток, длина найденного пути).
Для замера времени используйте time.perf_counter() до и после вызова стратегии.
"""
def __init__(self, strategy:PathFindingStrategy, maze:Maze|None=None):
super().__init__()
self._maze = maze
self.strategy = strategy
def setMaze(self, maze: Maze|None):
self._maze = maze
self.notify(MazeEvent(MazeEventType.MAZE_LOADED, data=maze))
def setStrategy(self, strategy:PathFindingStrategy):
self.strategy = strategy
def getStrategyName(self):
return self.strategy.__class__.__name__
def solve(self):
if not self._maze:
raise ValueError
t_start = time.perf_counter()
path = self.strategy.findPath(self._maze, self._maze.startCell, self._maze.endCell)
duration = (time.perf_counter() - t_start) * 1000
path_len = len(path.array) if path.array else -1
strategy_name = self.getStrategyName()
stats = SearchStats(path.array, duration, path.visited_cells, path_len, strategy_name)
self.notify(MazeEvent(MazeEventType.PATH_FOUND, data=path))
return stats

View File

@ -0,0 +1,43 @@
"""
Создать интерфейс Observer с методом update(event),
где event может быть строкой или объектом с типом события ("path_found", "move", "maze_loaded").
"""
from enum import Enum
from abc import ABC, abstractmethod
class MazeEventType(Enum):
PATH_FOUND = "path_found"
MOVE = "move"
MAZE_LOADED = "maze_loaded"
class MazeEvent:
data=None
def __init__(self, evtype: MazeEventType, data=None):
if not isinstance(evtype, MazeEventType):
raise TypeError(f"evtype must be an EventType, got {type(evtype)}")
self.evtype = evtype
self.data = data
class Observer(ABC):
@abstractmethod
def update(self, event: MazeEvent):
raise NotImplementedError
class Subject(ABC):
"""Издатель: управляет подписчиками и отправляет им уведомления."""
def __init__(self):
self._observers:set[Observer] = set()
def attach(self, obs:Observer):
"Подписать наблюдателя"
self._observers.add(obs)
def detach(self, obs:Observer):
"Отписать наблюдателя"
self._observers.discard(obs)
def notify(self, event:MazeEvent):
for obs in self._observers:
obs.update(event)

View File

@ -0,0 +1,46 @@
import heapq
from itertools import count
from task2.strategyObjects.pathFindingStrategy import PathFindingStrategy
from task2.strategyObjects.util import restorePath
from task2.mazeObjects.maze import Maze
from task2.mazeObjects.cell import Cell
from task2.mazeObjects.path import Path
class AStar(PathFindingStrategy):
"""Алгоритм с эвристикой (etc. манхэттенское расстояние) компромисс между скоростью и оптимальностью."""
def heuristic(self, first: Cell, second: Cell) -> int:
return abs(first.x - second.x) + abs(first.y - second.y)
def findPath(self, maze: Maze, start: Cell, exit: Cell) -> Path:
tie_breaker = count()
start_heuristic = self.heuristic(start, exit)
heap: list[tuple[int, int, int, Cell]] = [
(start_heuristic, start_heuristic, next(tie_breaker), start)
]
g_score: dict[Cell, int] = {start: 0}
parents: dict[Cell, Cell | None] = {start: None}
visited: set[Cell] = set()
while heap:
_, _, _, current = heapq.heappop(heap)
if current in visited:
continue
visited.add(current)
if current.isExit:
return Path(restorePath(parents, exit), len(visited))
for neighbor in maze.getNeighbors(current):
tentative_score = g_score[current]
if tentative_score < g_score.get(neighbor, 10**12):
g_score[neighbor] = tentative_score
parents[neighbor] = current
heuristic = self.heuristic(neighbor, exit)
priority = tentative_score + heuristic
heapq.heappush(
heap,
(priority, heuristic, next(tie_breaker), neighbor),
)
return Path(None, len(visited))

View File

@ -0,0 +1,43 @@
from task2.strategyObjects.pathFindingStrategy import PathFindingStrategy
from task2.strategyObjects.util import restorePath
from task2.mazeObjects.maze import Maze
from task2.mazeObjects.cell import Cell
from task2.mazeObjects.path import Path
import queue
class BFS(PathFindingStrategy):
"""Поиск в ширину гарантирует кратчайший путь по количеству шагов.
Возвращает None, если пути нет"""
def findPath(self, maze: Maze, start: Cell, exit: Cell) -> Path:
visited = dict()
parents = dict()
q = queue.Queue()
q.put(start)
visited[start] = 0
parents[start] = None
found_exit = False
while not q.empty():
current = q.get()
# Условие нахождение выхода
if current.isExit:
found_exit = True
break
# Перебор соседей
for hood in maze.getNeighbors(current):
if hood in visited:
continue
visited[hood] = visited[current] + 1
parents[hood] = current
q.put(hood)
if not found_exit:
path_list = None
else:
path_list = restorePath(parents, exit)
return Path(path_list, len(visited))

View File

@ -0,0 +1,41 @@
from task2.strategyObjects.pathFindingStrategy import PathFindingStrategy
from task2.strategyObjects.util import restorePath
from task2.mazeObjects.maze import Maze
from task2.mazeObjects.cell import Cell
from task2.mazeObjects.path import Path
class DFS(PathFindingStrategy):
"""Поиск в глубину быстрый, но не обязательно кратчайший.
Возвращает None, если пути нет"""
def findPath(self, maze: Maze, start: Cell, exit: Cell) -> Path:
visited = dict()
parents = dict()
stack = []
stack.append(start)
visited[start] = 0
parents[start] = None
found_exit = False
while stack:
current = stack.pop()
# Условие нахождение выхода
if current.isExit:
found_exit = True
break
# Перебор соседей
for hood in maze.getNeighbors(current):
if hood in visited:
continue
visited[hood] = visited[current] + 1
parents[hood] = current
stack.append(hood)
if not found_exit:
path_list = None
else:
path_list = restorePath(parents, exit)
return Path(path_list, len(visited))

View File

@ -0,0 +1,14 @@
from abc import ABC, abstractmethod
from task2.mazeObjects.maze import Maze
from task2.mazeObjects.cell import Cell
from task2.mazeObjects.path import Path
class PathFindingStrategy(ABC):
"""Интерфейс PathFindingStrategy с методом findPath(maze, start, exit),
возвращающим список клеток пути (от старта до выхода включительно) или пустой список, если пути нет."""
@abstractmethod
def findPath(self, maze: Maze, start: Cell, exit: Cell) -> Path:
"""Возвращает список клеток пути от старта до выхода включительно. Пути нет - пустой список."""
raise NotImplementedError

View File

@ -0,0 +1,12 @@
from task2.mazeObjects.maze import Maze
from task2.mazeObjects.cell import Cell
def restorePath(parents: dict, exit: Cell) -> list[Cell]|None:
path = []
current = exit
while current:
path.append(current)
if current not in parents:
return None
current = parents[current]
return path[::-1]

84
MusinAA/task2/tester.py Normal file
View File

@ -0,0 +1,84 @@
from task2.mazeBuilder import MazeBuilder
from task2.mazeObjects.maze import Maze
from task2.mazeSolver import MazeSolver, SearchStats
from task2.strategyObjects.BFS import BFS
from task2.strategyObjects.DFS import DFS
from task2.strategyObjects.AStar import AStar
import csv
import os
TEST_ITERATIONS = 10
class Tester():
"""Для каждого лабиринта и каждой стратегии запустить solve() 510 раз,
усреднить время, количество посещённых клеток, длину пути.
Записать результаты в CSV:
лабиринт,стратегия,время_мс,посещено_клеток,длина_пути."""
result:list[SearchStats]
def __init__(self, builder:MazeBuilder, writefile:str):
self._builder = builder
self.writefile = "../" + writefile
def setTestingDirectory(self, directory:str):
if directory[-1] != "/":
directory += "/"
self._directory = "../" + directory
def _getMazes(self) -> list[Maze]:
arr = []
files = os.listdir(self._directory)
only_txt_files = [f for f in files if os.path.isfile(os.path.join(self._directory, f)) and os.path.splitext(f)[1] == ".txt"]
for f in only_txt_files:
arr.append(self._builder.buildFromFile(os.path.join(self._directory, f)))
return arr
def _solveAvg(self, solver: MazeSolver):
avgtime = 0
for i in range(TEST_ITERATIONS):
result = solver.solve()
# Всё кроме времени будет одинаковым
avgtime += result.duration/TEST_ITERATIONS
result.duration = avgtime
return result
def saveCSV(self):
rows = []
for r in self.result:
r = r.toDict()
row = (r["strategy_name"],
r["maze_name"],
r["duration"],
r["visited_cells"],
r["path_len"]
)
rows.append(row)
with open(self.writefile, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["Алгоритм", "Лабиринт", "Время (мс)", "Посещённые клетки", "Длинна пути"])
writer.writerows(rows)
def test(self):
self.result = []
arr = self._getMazes()
for algoritm in (BFS, DFS, AStar):
solver = MazeSolver(algoritm()) # это прикол
for maze in arr:
solver.setMaze(maze)
self.result.append(self._solveAvg(solver))
self.result[-1].maze_name = maze.name
return self.result
if __name__ == "__main__":
exit()
from task2.mazeBuilder import TextFileMazeBuilder
builder = TextFileMazeBuilder()
tester = Tester(builder, "docs/data/task2/results.csv")
tester.setTestingDirectory("task2/mazeExamples")
tester.test()
tester.saveCSV()