[2] task2 #343

Merged
kit8nino merged 1 commits from kuznetsovTD/2026-rff_mp:task2 into develop 2026-05-30 11:56:48 +00:00
36 changed files with 565 additions and 0 deletions
Showing only changes of commit 6d17293486 - Show all commits

View File

View File

View File

@ -0,0 +1,3 @@
class MazeBuilder:
def build_from_file(self, filename):
raise NotImplementedError

View File

@ -0,0 +1,52 @@
from builders.maze_builder import MazeBuilder
from models.cell import Cell
from models.maze import Maze
class TextFileMazeBuilder(MazeBuilder):
SYMBOL_MAP = {
"#": {"is_wall": True},
"S": {"is_start": True},
"E": {"is_exit": True},
" ": {},
}
def create_cell(self, symbol, x, y):
props = self.SYMBOL_MAP.get(symbol)
if props is None:
raise ValueError(f"Unknown symbol: {symbol}")
return Cell(x, y, **props)
def build_from_file(self, filename):
with open(filename, "r", encoding="utf-8") as file:
rows = [line.rstrip("\n") for line in file]
if not rows:
raise ValueError("File is empty")
width = len(rows[0])
for row in rows:
if len(row) != width:
raise ValueError("Maze rows must have same length")
cells = []
start_cell = None
exit_cell = None
for y, row in enumerate(rows):
current_row = []
for x, symbol in enumerate(row):
cell = self.create_cell(symbol, x, y)
if cell.is_start:
start_cell = cell
if cell.is_exit:
exit_cell = cell
current_row.append(cell)
cells.append(current_row)
if start_cell is None:
raise ValueError("Start not found")
if exit_cell is None:
raise ValueError("Exit not found")
return Maze(cells, start_cell, exit_cell)

View File

@ -0,0 +1,42 @@
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from builders.text_file_maze_builder import TextFileMazeBuilder
from strategies.bfs_strategy import BFSStrategy
from strategies.dfs_strategy import DFSStrategy
from strategies.astar_strategy import AStarStrategy
from solver.maze_solver import MazeSolver
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MAZES_DIR = os.path.join(PROJECT_ROOT, "mazes")
mazes = ["small.txt", "medium.txt", "large.txt", "no_exit.txt", "empty.txt"]
strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy()]
results = []
builder = TextFileMazeBuilder()
for maze_file in mazes:
try:
maze_path = os.path.join(MAZES_DIR, maze_file)
maze = builder.build_from_file(maze_path)
for strategy in strategies:
solver = MazeSolver(maze, strategy)
stats = solver.solve(maze_file)
results.append(stats)
except ValueError as e:
print(f"Error with {maze_file}: {e}")
except FileNotFoundError as e:
print(f"File not found: {e}")
os.makedirs(os.path.join(PROJECT_ROOT, "experiments"), exist_ok=True)
results_path = os.path.join(PROJECT_ROOT, "experiments", "results.csv")
with open(results_path, "w") as file:
file.write("maze,strategy,time_ms,visited_cells,path_length\n")
for stat in results:
file.write(stat.to_csv_row())
print(f"Saved {len(results)} results to {results_path}")

View File

@ -0,0 +1,36 @@
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import pandas as pd
import matplotlib.pyplot as plt
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
RESULTS_PATH = os.path.join(PROJECT_ROOT, "experiments", "results.csv")
PLOTS_DIR = os.path.join(PROJECT_ROOT, "experiments", "plots")
try:
data = pd.read_csv(RESULTS_PATH)
except FileNotFoundError:
print(f"Run benchmark.py first to generate {RESULTS_PATH}")
exit(1)
os.makedirs(PLOTS_DIR, exist_ok=True)
for maze_name in data["maze"].unique():
maze_data = data[data["maze"] == maze_name]
plt.figure(figsize=(8, 5))
plt.bar(maze_data["strategy"], maze_data["time_ms"], color=['blue', 'green', 'red'])
plt.title(f"{maze_name} maze - Performance Comparison", fontsize=14)
plt.ylabel("Time (ms)", fontsize=12)
plt.xlabel("Strategy", fontsize=12)
plt.grid(axis='y', alpha=0.3)
plt.tight_layout()
plot_filename = os.path.join(PLOTS_DIR, f"{maze_name.replace('.txt', '')}.png")
plt.savefig(plot_filename, dpi=150)
plt.close()
print(f"Saved plot for {maze_name}")
print(f"All plots saved to {PLOTS_DIR}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,16 @@
maze,strategy,time_ms,visited_cells,path_length
small.txt,BFSStrategy,0.022,8,7
small.txt,DFSStrategy,0.011,8,7
small.txt,AStarStrategy,0.017,8,7
medium.txt,BFSStrategy,0.057,53,23
medium.txt,DFSStrategy,0.033,31,31
medium.txt,AStarStrategy,0.070,46,23
large.txt,BFSStrategy,0.762,718,431
large.txt,DFSStrategy,0.482,451,431
large.txt,AStarStrategy,1.031,591,431
no_exit.txt,BFSStrategy,0.004,1,0
no_exit.txt,DFSStrategy,0.002,1,0
no_exit.txt,AStarStrategy,0.002,1,0
empty.txt,BFSStrategy,0.113,100,19
empty.txt,DFSStrategy,0.064,55,55
empty.txt,AStarStrategy,0.154,100,19
1 maze strategy time_ms visited_cells path_length
2 small.txt BFSStrategy 0.022 8 7
3 small.txt DFSStrategy 0.011 8 7
4 small.txt AStarStrategy 0.017 8 7
5 medium.txt BFSStrategy 0.057 53 23
6 medium.txt DFSStrategy 0.033 31 31
7 medium.txt AStarStrategy 0.070 46 23
8 large.txt BFSStrategy 0.762 718 431
9 large.txt DFSStrategy 0.482 451 431
10 large.txt AStarStrategy 1.031 591 431
11 no_exit.txt BFSStrategy 0.004 1 0
12 no_exit.txt DFSStrategy 0.002 1 0
13 no_exit.txt AStarStrategy 0.002 1 0
14 empty.txt BFSStrategy 0.113 100 19
15 empty.txt DFSStrategy 0.064 55 55
16 empty.txt AStarStrategy 0.154 100 19

View File

@ -0,0 +1,60 @@
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from builders.text_file_maze_builder import TextFileMazeBuilder
from strategies.bfs_strategy import BFSStrategy
from strategies.dfs_strategy import DFSStrategy
from strategies.astar_strategy import AStarStrategy
from solver.maze_solver import MazeSolver
from observer.console_view import ConsoleView
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
MAZES_DIR = os.path.join(PROJECT_ROOT, "mazes")
mazes = {
"1": "small.txt",
"2": "medium.txt",
"3": "large.txt",
"4": "no_exit.txt"
}
strategies = {
"1": BFSStrategy(),
"2": DFSStrategy(),
"3": AStarStrategy()
}
print("Choose maze:")
print("1 - small")
print("2 - medium")
print("3 - large")
print("4 - no_exit")
maze_choice = input("> ").strip()
while maze_choice not in mazes:
print("Invalid choice. Try again.")
maze_choice = input("> ").strip()
print("\nChoose strategy:")
print("1 - BFS")
print("2 - DFS")
print("3 - A*")
strategy_choice = input("> ").strip()
while strategy_choice not in strategies:
print("Invalid choice. Try again.")
strategy_choice = input("> ").strip()
builder = TextFileMazeBuilder()
maze_path = os.path.join(MAZES_DIR, mazes[maze_choice])
maze = builder.build_from_file(maze_path)
strategy = strategies[strategy_choice]
solver = MazeSolver(maze, strategy)
view = ConsoleView(maze)
solver.attach(view)
stats = solver.solve(mazes[maze_choice])
print(stats)

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

@ -0,0 +1,13 @@
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 is_passable(self):
return not self.is_wall
def __repr__(self):
return f"Cell({self.x}, {self.y})"

View File

@ -0,0 +1,32 @@
from models.cell import Cell
class Maze:
def __init__(self, cells, start_cell, exit_cell):
self.cells = cells
self.height = len(cells)
self.width = len(cells[0])
self.start_cell = start_cell
self.exit_cell = exit_cell
def get_cell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[y][x]
return None
def check_cell(self, x, y):
cell = self.get_cell(x, y)
return cell and cell.is_passable()
def get_neighbors(self, cell: Cell):
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
neighbors = []
for dx, dy in directions:
x = cell.x + dx
y = cell.y + dy
if self.check_cell(x, y):
neighbors.append(self.get_cell(x, y))
return neighbors
def __repr__(self):
return f"Maze({self.width}x{self.height})"

View File

@ -0,0 +1,21 @@
class SearchStats:
def __init__(self, strategy, maze_name, duration, visited_cells, path_length):
self.strategy = strategy
self.maze_name = maze_name
self.duration = duration
self.visited_cells = visited_cells
self.path_length = path_length
def to_csv_row(self):
return f"{self.maze_name},{self.strategy},{self.duration:.3f},{self.visited_cells},{self.path_length}\n"
def __str__(self):
return (
f"\n=== SEARCH RESULT ===\n"
f"Strategy : {self.strategy}\n"
f"Maze : {self.maze_name}\n"
f"Time (ms) : {self.duration:.3f}\n"
f"Visited cells : {self.visited_cells}\n"
f"Path length : {self.path_length}\n"
f"=====================\n"
)

View File

View File

@ -0,0 +1,37 @@
import os
from observer.observer import Observer
from observer.maze_event import MazeEventType
class ConsoleView(Observer):
def __init__(self, maze=None):
self.maze = maze
self.path = []
def update(self, event):
if event.event_type == MazeEventType.PATH_FOUND:
self.path = event.data if event.data else []
self.render()
def render(self):
if self.maze is None:
return
os.system("cls" if os.name == "nt" else "clear")
path_positions = {(cell.x, cell.y) for cell in self.path}
for row in self.maze.cells:
line = ""
for cell in row:
pos = (cell.x, cell.y)
if cell.is_wall:
line += "#"
elif cell.is_start:
line += "S"
elif cell.is_exit:
line += "E"
elif pos in path_positions:
line += "*"
else:
line += " "
print(line)

View File

@ -0,0 +1,12 @@
from enum import Enum
class MazeEventType(Enum):
MAZE_LOADED = 1
PATH_FOUND = 2
class MazeEvent:
def __init__(self, event_type, data=None):
self.event_type = event_type
self.data = data

View File

@ -0,0 +1,3 @@
class Observer:
def update(self, event):
raise NotImplementedError

View File

@ -0,0 +1,13 @@
class Subject:
def __init__(self):
self.observers = []
def attach(self, observer):
self.observers.append(observer)
def detach(self, observer):
self.observers.remove(observer)
def notify(self, event):
for observer in self.observers:
observer.update(event)

View File

View File

@ -0,0 +1,31 @@
import time
from observer.subject import Subject
from observer.maze_event import MazeEvent, MazeEventType
from models.search_stats import SearchStats
class MazeSolver(Subject):
def __init__(self, maze, strategy):
super().__init__()
self.maze = maze
self.strategy = strategy
def set_strategy(self, strategy):
self.strategy = strategy
def solve(self, maze_name="maze"):
start_time = time.perf_counter()
path, visited = self.strategy.find_path(
self.maze, self.maze.start_cell, self.maze.exit_cell
)
end_time = time.perf_counter()
self.notify(MazeEvent(MazeEventType.PATH_FOUND, path))
return SearchStats(
strategy=self.strategy.__class__.__name__,
maze_name=maze_name,
duration=(end_time - start_time) * 1000,
visited_cells=visited,
path_length=len(path) if path else 0 # ЭТУ СТРОКУ ИЗМЕНИТЬ
)

View File

@ -0,0 +1,45 @@
import heapq
import itertools
from strategies.pathfinding_strategy import PathFindingStrategy
class AStarStrategy(PathFindingStrategy):
def heuristic(self, cell, exit_cell):
return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y)
def find_path(self, maze, start_cell, exit_cell):
open_set = []
counter = itertools.count()
heapq.heappush(open_set, (0, next(counter), start_cell))
parents = {start_cell: None}
g_score = {start_cell: 0}
visited = set()
visited_count = 0
while open_set:
_, _, current = heapq.heappop(open_set)
if current in visited:
continue
visited.add(current)
visited_count += 1
if current == exit_cell:
path = []
while current is not None:
path.append(current)
current = parents[current]
path.reverse()
return path, visited_count
for neighbor in maze.get_neighbors(current):
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
parents[neighbor] = current
g_score[neighbor] = tentative_g
f_score = tentative_g + self.heuristic(neighbor, exit_cell)
heapq.heappush(open_set, (f_score, next(counter), neighbor))
return [], visited_count

View File

@ -0,0 +1,31 @@
from collections import deque
from strategies.pathfinding_strategy import PathFindingStrategy
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze, start_cell, exit_cell):
queue = deque([start_cell])
parents = {start_cell: None}
visited = {start_cell}
visited_count = 0
while queue:
current = queue.popleft()
visited_count += 1
if current == exit_cell:
path = []
while current is not None:
path.append(current)
current = parents[current]
path.reverse()
return path, visited_count
for neighbor in maze.get_neighbors(current):
if neighbor in visited:
continue
visited.add(neighbor)
parents[neighbor] = current
queue.append(neighbor)
return [], visited_count

View File

@ -0,0 +1,30 @@
from strategies.pathfinding_strategy import PathFindingStrategy
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze, start_cell, exit_cell):
stack = [start_cell]
parents = {start_cell: None}
visited = {start_cell}
visited_count = 0
while stack:
current = stack.pop()
visited_count += 1
if current == exit_cell:
path = []
while current is not None:
path.append(current)
current = parents[current]
path.reverse()
return path, visited_count
for neighbor in maze.get_neighbors(current):
if neighbor in visited:
continue
visited.add(neighbor)
parents[neighbor] = current
stack.append(neighbor)
return [], visited_count

View File

@ -0,0 +1,3 @@
class PathFindingStrategy:
def find_path(self, maze, start_cell, exit_cell):
raise NotImplementedError