Merge pull request '[2]task2' (#322) from romanovpv/2026-rff_mp:task2 into develop

Reviewed-on: #322
This commit is contained in:
AlexanderVah 2026-05-30 11:52:25 +00:00
commit 1f016b85f0
16 changed files with 685 additions and 0 deletions

View File

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

View File

@ -0,0 +1,41 @@
from abc import ABC, abstractmethod
from model import Maze, Cell
class MazeBuilder(ABC):
@abstractmethod
def buildFromFile(self, filename):
pass
class TextFileMazeBuilder(MazeBuilder):
def buildFromFile(self, filename):
with open(filename, "r", encoding="utf-8") as file:
lines = [line.rstrip("\n")
for line in file
]
height = len(lines)
width = len(lines[0])
maze = Maze(width, height)
start_count = 0
exit_count = 0
for x, line in enumerate(lines):
row = []
for y, symbol in enumerate(line):
if symbol == "#":
cell = Cell(x, y, is_wall=True)
elif symbol == "S":
cell = Cell(x, y, is_start=True)
start_count += 1
elif symbol == "E":
cell = Cell(x, y, is_exit=True)
exit_count += 1
elif symbol == " ":
cell = Cell(x, y)
else:
raise ValueError(f"Неизвестный символ: {symbol}")
row.append(cell)
maze.add_row(row)
if start_count != 1:
raise ValueError("Должен быть ровно один старт S")
if exit_count != 1:
raise ValueError("Должен быть ровно один выход E")
return maze

View File

@ -0,0 +1,43 @@
import random
def save_maze(filename, width, height, wall_probability):
maze = []
for i in range(height):
row = ""
for j in range(width):
if i == 0 or i == height-1:
row += "#"
elif j == 0 or j == width-1:
row += "#"
else:
if random.random() < wall_probability:
row += "#"
else:
row += " "
maze.append(list(row))
maze[1][1] = "S"
maze[height-2][width-2] = "E"
for i in range(1, height-1):
maze[i][1] = " "
for j in range(1, width-1):
maze[height-2][j] = " "
maze[1][1] = "S"
maze[height-2][width-2] = "E"
with open(filename, "w", encoding="utf-8") as f:
for row in maze:
f.write("".join(row)+"\n")
save_maze(
"medium_maze.txt",
50,
50,
0.30
)
save_maze(
"big_maze.txt",
100,
100,
0.40
)
print("Лабиринты созданы")

View File

@ -0,0 +1,50 @@
import pandas as pd
import matplotlib.pyplot as plt
from result import results
df = pd.DataFrame(
results[1:],
columns=results[0]
)
time_data = df.pivot(
index="maze",
columns="strategy",
values="time_ms"
)
time_data.plot(kind="bar")
plt.title("Время выполнения")
plt.ylabel("мс")
plt.xticks(rotation=0)
plt.show()
cells_data = df.pivot(
index="maze",
columns="strategy",
values="cells visited"
)
cells_data.plot(kind="bar")
plt.title("Количество посещённых клеток")
plt.ylabel("клетки")
plt.xticks(rotation=0)
plt.show()
path_data = df.pivot(
index="maze",
columns="strategy",
values="path length"
)
path_data.plot(kind="bar")
plt.title("Длина пути")
plt.ylabel("шаги")
plt.xticks(rotation=0)
plt.show()

View File

@ -0,0 +1,68 @@
from builders import (TextFileMazeBuilder)
from strategies import (BFSStrategy, DFSStrategy, AStarStrategy)
from solver import (MazeSolver)
from observer_command import ConsoleView, Player, MoveCommand
import os
builder = TextFileMazeBuilder()
maze = builder.buildFromFile("no_exit_maze.txt")
print("Лабиринт:\n")
maze.printMaze()
print("Выберете алгоритм")
print("1 - BFS")
print("2 - DFS")
print("3 - A*")
choice = input()
if choice == "1":
strategy = BFSStrategy()
elif choice == "2":
strategy = DFSStrategy()
elif choice == "3":
strategy = AStarStrategy()
else:
print("Неверный выбор")
exit()
solver = MazeSolver(maze, strategy)
view = ConsoleView()
solver.addObserver(view)
stats = solver.solve()
print("Результат:")
print(stats)
path, _ = strategy.findPath(
maze,
maze.start,
maze.exit
)
if not path:
print("\nПуть не найден")
exit()
print("\nНайденный путь:")
for cell in path:
print(f"({cell.x}, {cell.y})")
print("\nПошаговое движение игрока")
player = Player(maze.start)
history = []
passed_path = [maze.start]
view.render(maze, player, passed_path)
for cell in path[1:]:
input("\nEnter -> следующий шаг")
command = MoveCommand(player, cell)
command.execute()
history.append(command)
passed_path.append(cell)
os.system('cls' if os.name == 'nt' else 'clear')
view.render(maze, player, passed_path)

View File

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

View File

@ -0,0 +1,86 @@
class Cell:
def __init__(
self,
x: int,
y: int,
is_wall=False,
is_start=False,
is_exit=False
):
self.x = x
self.y = y
self.isWall = is_wall
self.isStart = is_start
self.isExit = is_exit
def isPassable(self):
return not self.isWall
def __repr__(self):
return f"Cell({self.x},{self.y})"
def __eq__(self, other):
return (
isinstance(other, Cell)
and self.x == other.x
and self.y == other.y
)
def __hash__(self):
return hash((self.x, self.y))
class Maze:
def __init__(self, width: int, height: int):
self.width = width
self.height = height
self.cells = []
self.start = None
self.exit = None
def add_row(self, row):
self.cells.append(row)
for cell in row:
if cell.isStart:
self.start = cell
if cell.isExit:
self.exit = cell
def getCell(self, x, y):
if 0 <= x < self.height and 0 <= y < self.width:
return self.cells[x][y]
return None
def getNeighbors(self, cell):
directions = [
(-1, 0), # вверх
(1, 0), # вниз
(0, -1), # влево
(0, 1) # вправо
]
neighbors = []
for dx, dy in directions:
nx = cell.x + dx
ny = cell.y + dy
neighbor = self.getCell(nx, ny)
if (neighbor and neighbor.isPassable()):
neighbors.append(neighbor)
return neighbors
def printMaze(self):
for row in self.cells:
line = ""
for cell in row:
if cell.isStart:
line += "S"
elif cell.isExit:
line += "E"
elif cell.isWall:
line += "#"
else:
line += " "
print(line)

View File

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

View File

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

View File

@ -0,0 +1,68 @@
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, event):
pass
class ConsoleView(Observer):
def update(self, event):
print(f"\n[Событие] {event}")
def render(self, maze, player=None, path=None):
path = path or []
print()
for row in maze.cells:
line = ""
for cell in row:
if player and cell == player.position:
line += "P"
elif cell.isStart:
line += "S"
elif cell.isExit:
line += "E"
elif cell.isWall:
line += "#"
elif cell in path:
line += "*"
else:
line += " "
print(line)
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class Player:
def __init__(self, start_cell):
self.position = start_cell
class MoveCommand(Command):
def __init__(self, player, new_cell):
self.player = player
self.new_cell = new_cell
self.old_cell = None
def execute(self):
self.old_cell = self.player.position
self.player.position = self.new_cell
def undo(self):
self.player.position = self.old_cell

View File

@ -0,0 +1,21 @@
import csv
results = [
["maze", "strategy", "time_ms", "cells visited", "path length"],
["small_maze", "BFS", 0.173, 15, 15],
["small_maze", "DFS", 0.198, 15, 15],
["small_maze", "A*", 0.195, 15, 15],
["medium_maze", "BFS", 7.228, 95, 95],
["medium_maze", "DFS", 1.361, 189, 189],
["medium_maze", "A*", 3.050, 95, 95],
["big_maze", "BFS", 18.487, 195, 195],
["big_maze", "DFS", 10.021, 497, 497],
["big_maze", "A*", 4.471, 195, 195],
["no_wall_maze", "BFS", 0.325, 15, 15],
["no_wall_maze", "DFS", 0.251, 29, 29],
["no_wall_maze", "A*", 0.396, 15, 15],
]
with open("results.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerows(results)

View File

@ -0,0 +1,13 @@
maze,strategy,time_ms,cells visited,path length
small_maze,BFS,0.173,15,15
small_maze,DFS,0.198,15,15
small_maze,A*,0.195,15,15
medium_maze,BFS,7.228,95,95
medium_maze,DFS,1.361,189,189
medium_maze,A*,3.05,95,95
big_maze,BFS,18.487,195,195
big_maze,DFS,10.021,497,497
big_maze,A*,4.471,195,195
no_wall_maze,BFS,0.325,15,15
no_wall_maze,DFS,0.251,29,29
no_wall_maze,A*,0.396,15,15
1 maze strategy time_ms cells visited path length
2 small_maze BFS 0.173 15 15
3 small_maze DFS 0.198 15 15
4 small_maze A* 0.195 15 15
5 medium_maze BFS 7.228 95 95
6 medium_maze DFS 1.361 189 189
7 medium_maze A* 3.05 95 95
8 big_maze BFS 18.487 195 195
9 big_maze DFS 10.021 497 497
10 big_maze A* 4.471 195 195
11 no_wall_maze BFS 0.325 15 15
12 no_wall_maze DFS 0.251 29 29
13 no_wall_maze A* 0.396 15 15

View File

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

View File

@ -0,0 +1,42 @@
import time
class SearchStats:
def __init__(self, time_ms, visited_cells, path_length):
self.time_ms = time_ms
self.visited_cells = visited_cells
self.path_length = path_length
def __str__(self):
return (
f"Время: "
f"{self.time_ms:.3f} мс\n"
f"Посещено клеток: "
f"{self.visited_cells}\n"
f"Длина пути: "
f"{self.path_length}"
)
class MazeSolver:
def __init__(self,maze, strategy):
self.maze = maze
self.strategy = strategy
self.observers = []
def setStrategy(self, strategy
):
self.strategy = strategy
def solve(self):
self.notify("Начат поиск")
start_time = (time.perf_counter())
path, visited = (self.strategy.findPath(self.maze,self.maze.start,self.maze.exit))
end_time = (time.perf_counter())
self.notify("Путь найден")
time_ms = ((end_time-start_time)*1000)
visited = len(path)
stats = SearchStats(time_ms,visited,len(path))
return stats
def addObserver(self, observer):
self.observers.append(observer)
def notify(self, event):
for observer in self.observers:
observer.update(event)

View File

@ -0,0 +1,73 @@
from abc import ABC, abstractmethod
from collections import deque
import heapq
class PathFindingStrategy(ABC):
@abstractmethod
def findPath(self, maze, start, exit_cell):
pass
def restorePath(self, parent, start, exit_cell):
path = []
current = exit_cell
while current != start:
path.append(current)
current = parent[current]
path.append(start)
path.reverse()
return path
class BFSStrategy(PathFindingStrategy):
def findPath( self, maze, start, exit_cell):
queue = deque([start])
visited = {start}
parent = {}
while queue:
current = queue.popleft()
if current == exit_cell:
return (self.restorePath(parent, start, exit_cell), len(visited))
for neighbor in maze.getNeighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
queue.append(neighbor)
return [], len(visited)
class DFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit_cell):
stack = [start]
visited = {start}
parent = {}
while stack:
current = stack.pop()
if current == exit_cell:
return (self.restorePath(parent,start,exit_cell),len(visited))
for neighbor in maze.getNeighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
stack.append(neighbor)
return [], len(visited)
class AStarStrategy(PathFindingStrategy):
def heuristic(self,cell,exit_cell):
return (abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y))
def findPath(self, maze, start, exit_cell):
pq = []
heapq.heappush(pq,(0, id(start), start))
parent = {}
g_score = {start: 0}
visited = set()
while pq:
_, _, current = (heapq.heappop(pq))
if current in visited:
continue
visited.add(current)
if current == exit_cell:
return (self.restorePath(parent, start, exit_cell), len(visited))
for neighbor in maze.getNeighbors(current):
new_cost = (g_score[current]+ 1)
if (neighbor not in g_score or new_cost < g_score[neighbor] ):
g_score[neighbor] = new_cost
parent[neighbor] = current
priority = (new_cost + self.heuristic(neighbor, exit_cell))
heapq.heappush(pq,(priority,id(neighbor),neighbor))
return [], len(visited)

Binary file not shown.