Merge pull request '[2]Task 2' (#334) from kuzminskiyaa/2026-rff_mp:main into develop

Reviewed-on: #334
This commit is contained in:
git_admin 2026-05-30 11:32:20 +00:00
commit b7274bb9db
7 changed files with 749 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,653 @@
import os
import time
import heapq
from collections import deque
from typing import List, Dict, Optional, Tuple
from abc import ABC, abstractmethod
from dataclasses import dataclass
class Cell:
def __init__(self, x: int, y: int, is_wall: bool = True, is_start: bool = False, is_exit: bool = 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) -> bool:
return not self.is_wall
def __eq__(self, other):
if not isinstance(other, Cell):
return False
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
def __repr__(self):
return f"Cell({self.x}, {self.y})"
class Maze:
def __init__(self, width: int = 0, height: int = 0):
self.width = width
self.height = height
self.grid: List[List[Cell]] = []
self.start: Optional[Cell] = None
self.exit: Optional[Cell] = None
def set_cell(self, x: int, y: int, cell: Cell) -> None:
if 0 <= x < self.width and 0 <= y < self.height:
self.grid[y][x] = cell
def get_cell(self, x: int, y: int) -> Optional[Cell]:
if 0 <= x < self.width and 0 <= y < self.height:
return self.grid[y][x]
return None
def get_neighbors(self, cell: Cell) -> List[Cell]:
neighbors = []
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
for dx, dy in directions:
nx, ny = cell.x + dx, cell.y + dy
neighbor = self.get_cell(nx, ny)
if neighbor and neighbor.is_passable():
neighbors.append(neighbor)
return neighbors
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename: str) -> Maze:
pass
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename: str) -> Maze:
with open(filename, 'r', encoding='utf-8') as file:
lines = [line.rstrip('\n') for line in file.readlines()]
while lines and not lines[0].strip():
lines.pop(0)
while lines and not lines[-1].strip():
lines.pop()
height = len(lines)
width = max(len(line) for line in lines) if height > 0 else 0
maze = Maze(width, height)
maze.grid = [[None for _ in range(width)] for _ in range(height)]
start_count = 0
exit_count = 0
for y, line in enumerate(lines):
for x in range(width):
char = line[x] if x < len(line) else '#'
if char == '#':
cell = Cell(x, y, is_wall=True)
elif char == ' ':
cell = Cell(x, y, is_wall=False)
elif char == 'S':
cell = Cell(x, y, is_wall=False, is_start=True)
maze.start = cell
start_count += 1
elif char == 'E':
cell = Cell(x, y, is_wall=False, is_exit=True)
maze.exit = cell
exit_count += 1
else:
cell = Cell(x, y, is_wall=True)
maze.set_cell(x, y, cell)
if start_count == 0:
raise ValueError("Лабиринт должен содержать старт (S)")
if start_count > 1:
raise ValueError("Лабиринт может содержать только один старт (S)")
if exit_count == 0:
raise ValueError("Лабиринт должен содержать выход (E)")
if exit_count > 1:
raise ValueError("Лабиринт может содержать только один выход (E)")
return maze
class PathFindingStrategy(ABC):
@abstractmethod
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
pass
def _reconstruct_path(self, parents: Dict[Cell, Cell], start: Cell, exit_cell: Cell) -> List[Cell]:
path = []
current = exit_cell
while current != start:
path.append(current)
if current not in parents:
return []
current = parents[current]
path.append(start)
path.reverse()
return path
@property
def name(self) -> str:
return self.__class__.__name__.replace('Strategy', '')
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
queue = deque([start])
visited = {start}
parents: Dict[Cell, Cell] = {}
visited_count = 1
while queue:
current = queue.popleft()
if current == exit_cell:
return self._reconstruct_path(parents, start, exit_cell), visited_count
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
visited_count += 1
parents[neighbor] = current
queue.append(neighbor)
return [], visited_count
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
stack = [(start, [start])]
visited = {start}
visited_count = 1
while stack:
current, path = stack.pop()
if current == exit_cell:
return path, visited_count
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
visited_count += 1
stack.append((neighbor, path + [neighbor]))
return [], visited_count
class AStarStrategy(PathFindingStrategy):
def _heuristic(self, cell: Cell, target: Cell) -> int:
return abs(cell.x - target.x) + abs(cell.y - target.y)
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
counter = 0
open_set = [(0, counter, start)]
came_from: Dict[Cell, Cell] = {}
g_score: Dict[Cell, float] = {start: 0}
f_score: Dict[Cell, float] = {start: self._heuristic(start, exit_cell)}
open_set_hash = {start}
visited_count = 1
while open_set:
current = heapq.heappop(open_set)[2]
open_set_hash.remove(current)
if current == exit_cell:
path = self._reconstruct_path(came_from, start, exit_cell)
return path, visited_count
for neighbor in maze.get_neighbors(current):
tentative_g_score = g_score[current] + 1
if tentative_g_score < g_score.get(neighbor, float('inf')):
came_from[neighbor] = current
g_score[neighbor] = tentative_g_score
f_score[neighbor] = tentative_g_score + self._heuristic(neighbor, exit_cell)
if neighbor not in open_set_hash:
visited_count += 1
counter += 1
heapq.heappush(open_set, (f_score[neighbor], counter, neighbor))
open_set_hash.add(neighbor)
return [], visited_count
@dataclass
class SearchStats:
execution_time_ms: float
path_length: int
visited_cells: int
success: bool
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
self.maze = maze
self.strategy = strategy
def set_strategy(self, strategy: PathFindingStrategy) -> None:
self.strategy = strategy
def solve(self) -> Tuple[List[Cell], SearchStats]:
if not self.maze.start or not self.maze.exit:
raise ValueError("Лабиринт должен содержать старт и выход")
start_time = time.perf_counter()
path, visited_cells = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
end_time = time.perf_counter()
execution_time = (end_time - start_time) * 1000
stats = SearchStats(
execution_time_ms=execution_time,
path_length=len(path),
visited_cells=visited_cells,
success=len(path) > 0
)
return path, stats
class MazeVisualizer:
@staticmethod
def render(maze: Maze, path: List[Cell] = None, player_pos: Cell = None):
print("\n" + "=" * (maze.width + 2))
for y in range(maze.height):
row = "|"
for x in range(maze.width):
cell = maze.get_cell(x, y)
if cell:
if player_pos and cell == player_pos:
row += "P"
elif path and cell in path and not cell.is_start and not cell.is_exit:
row += "."
elif cell.is_start:
row += "S"
elif cell.is_exit:
row += "E"
elif cell.is_wall:
row += "#"
else:
row += " "
else:
row += " "
row += "|"
print(row)
print("=" * (maze.width + 2))
def create_test_mazes():
current_dir = os.path.dirname(os.path.abspath(__file__))
small_maze = """##########
#S #
# ### ## #
# # #
### # ####
# # #
# ### # #
# # E#
##########"""
medium_maze = """####################
#S #
# ### ########### #
# # # # # #
# # # # ####### # #
# # # # # #
# # ######### # # #
# # # # #
# # ########### # #
# # # #
# ############### #
# E#
####################"""
large_maze = """##################################################
#S #
# ############################################# #
# # # #
# # ######################################### # #
# # # # # #
# # # ##################################### # # #
# # # # # # # #
# # # # ################################# # # # #
# # # # # # # # # #
# # # # # ############################# # # # # #
# # # # # # # # # # # #
# # # # # # ######################### # # # # # #
# # # # # # # # # # # # # #
# # # # # # # ##################### # # # # # # #
# # # # # # # # # # # # # # # #
# # # # # # # # ################# # # # # # # # #
# # # # # # # # # # # # # # # # # #
# # # # # # # # # ############# # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # ######### # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # ##### # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # #E#
##################################################"""
empty_maze_lines = ["#" + "#" * 38 + "#"]
empty_maze_lines.append("#S" + " " * 37 + "#")
for i in range(35):
empty_maze_lines.append("#" + " " * 38 + "#")
empty_maze_lines.append("#" + " " * 37 + "E#")
empty_maze_lines.append("#" + "#" * 38 + "#")
empty_maze = "\n".join(empty_maze_lines)
no_exit_maze = """##########
#S #
# ### ## #
# # #
### # ####
# # #
# ### # #
# #
##########"""
mazes = [
("01_small_maze.txt", small_maze),
("02_medium_maze.txt", medium_maze),
("03_large_maze.txt", large_maze),
("04_empty_maze.txt", empty_maze),
("05_no_exit_maze.txt", no_exit_maze)
]
print("\n" + "="*60)
print("CREATING TEST MAZES")
print("="*60)
for filename, content in mazes:
filepath = os.path.join(current_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Created: {filename}")
print("="*60)
def list_available_mazes():
current_dir = os.path.dirname(os.path.abspath(__file__))
maze_files = [f for f in os.listdir(current_dir) if f.endswith('.txt') and ('maze' in f.lower() or f.startswith('0'))]
if not maze_files:
return []
print("\nAVAILABLE MAZES:")
print("-" * 50)
for i, file in enumerate(maze_files, 1):
print(f" {i}. {file}")
return maze_files
class Benchmark:
def __init__(self):
self.strategies = [
BFSStrategy(),
DFSStrategy(),
AStarStrategy()
]
def run_experiment(self, maze: Maze, runs: int = 5) -> Dict:
results = {}
for strategy in self.strategies:
times = []
path_lengths = []
visited_counts = []
for _ in range(runs):
solver = MazeSolver(maze, strategy)
path, stats = solver.solve()
if stats.success:
times.append(stats.execution_time_ms)
path_lengths.append(stats.path_length)
visited_counts.append(stats.visited_cells)
if times:
results[strategy.name] = {
'avg_time_ms': sum(times) / len(times),
'avg_path_length': sum(path_lengths) / len(path_lengths),
'avg_visited_cells': sum(visited_counts) / len(visited_counts),
'success_rate': len(times) / runs * 100
}
else:
results[strategy.name] = {
'avg_time_ms': float('inf'),
'avg_path_length': 0,
'avg_visited_cells': 0,
'success_rate': 0
}
return results
@staticmethod
def print_results(results: Dict, maze_name: str):
print(f"\n{'='*60}")
print(f"RESULTS FOR MAZE: {maze_name}")
print(f"{'='*60}")
print(f"{'Algorithm':<12} {'Time(ms)':<12} {'PathLen':<12} {'Visited':<12} {'Success':<8}")
print(f"{'-'*60}")
for strategy_name, stats in results.items():
print(f"{strategy_name:<12} {stats['avg_time_ms']:>8.3f} "
f"{stats['avg_path_length']:>8.1f} "
f"{stats['avg_visited_cells']:>8.1f} "
f"{stats['success_rate']:>6.1f}%")
def interactive_mode():
builder = TextFileMazeBuilder()
visualizer = MazeVisualizer()
current_dir = os.path.dirname(os.path.abspath(__file__))
while True:
print("\n" + "="*60)
print("MAZE PATHFINDING PROGRAM")
print("="*60)
print("1. Load maze and find path")
print("2. Compare all algorithms on maze")
print("3. Create test mazes (5 pieces)")
print("4. Run benchmark on all mazes")
print("5. Show available mazes")
print("6. Exit")
print("="*60)
choice = input("Choose action (1-6): ").strip()
if choice == '6':
print("\nGoodbye!")
break
elif choice == '3':
create_test_mazes()
input("\nPress Enter to continue...")
elif choice == '5':
maze_files = list_available_mazes()
if not maze_files:
print("\nNo available mazes. Create them first (action 3)")
input("\nPress Enter to continue...")
elif choice == '1':
print("\nAvailable mazes:")
maze_files = list_available_mazes()
if not maze_files:
print("\nNo available mazes. Create them first (action 3)")
continue
filename = input("\nEnter path to maze file: ").strip()
if not os.path.exists(filename):
test_path = os.path.join(current_dir, filename)
if os.path.exists(test_path):
filename = test_path
try:
maze = builder.build_from_file(filename)
print("\nLOADED MAZE:")
visualizer.render(maze)
print("\nChoose algorithm:")
print(" 1. BFS - finds SHORTEST path")
print(" 2. DFS - FAST but path may be longer")
print(" 3. A* - OPTIMAL balance")
algo_choice = input("\nYour choice (1-3): ").strip()
strategies = {
'1': BFSStrategy(),
'2': DFSStrategy(),
'3': AStarStrategy()
}
if algo_choice in strategies:
print("\nSearching for path...")
solver = MazeSolver(maze, strategies[algo_choice])
path, stats = solver.solve()
if stats.success:
print(f"\nPATH FOUND!")
print(f"\nSTATISTICS:")
print(f" Time: {stats.execution_time_ms:.3f} ms")
print(f" Path length: {stats.path_length} steps")
print(f" Visited cells: {stats.visited_cells}")
print(f" Efficiency: {stats.visited_cells/stats.path_length:.1f} cells per step")
print("\nPATH ON MAP (. = path):")
visualizer.render(maze, path)
else:
print("\nPATH NOT FOUND! Exit unreachable from start.")
else:
print("Invalid choice!")
except FileNotFoundError:
print(f"\nFile '{filename}' not found!")
except Exception as e:
print(f"\nError: {e}")
input("\nPress Enter to continue...")
elif choice == '2':
print("\nAvailable mazes:")
maze_files = list_available_mazes()
if not maze_files:
print("\nNo available mazes. Create them first (action 3)")
continue
filename = input("\nEnter path to maze file: ").strip()
if not os.path.exists(filename):
test_path = os.path.join(current_dir, filename)
if os.path.exists(test_path):
filename = test_path
try:
maze = builder.build_from_file(filename)
print("\nLOADED MAZE:")
visualizer.render(maze)
print("\nRunning algorithm comparison (3 runs each)...")
benchmark = Benchmark()
results = benchmark.run_experiment(maze, runs=3)
maze_name = os.path.basename(filename)
benchmark.print_results(results, maze_name)
print("\nANALYSIS:")
print("-" * 40)
fastest = min(results.items(), key=lambda x: x[1]['avg_time_ms'])
print(f"Fastest: {fastest[0]} ({fastest[1]['avg_time_ms']:.3f} ms)")
shortest = min(results.items(), key=lambda x: x[1]['avg_path_length'])
print(f"Shortest path: {shortest[0]} ({shortest[1]['avg_path_length']:.0f} steps)")
efficient = min(results.items(), key=lambda x: x[1]['avg_visited_cells'])
print(f"Most efficient: {efficient[0]} (checked {efficient[1]['avg_visited_cells']:.0f} cells)")
except FileNotFoundError:
print(f"\nFile '{filename}' not found!")
except Exception as e:
print(f"\nError: {e}")
input("\nPress Enter to continue...")
elif choice == '4':
print("\nRUNNING FULL BENCHMARK")
print("="*60)
test_files = [
"01_small_maze.txt",
"02_medium_maze.txt",
"03_large_maze.txt",
"04_empty_maze.txt",
"05_no_exit_maze.txt"
]
benchmark = Benchmark()
all_results = {}
for test_file in test_files:
filepath = os.path.join(current_dir, test_file)
if not os.path.exists(filepath):
print(f"\nFile {test_file} not found. Creating test mazes...")
create_test_mazes()
break
try:
print(f"\nTesting: {test_file}")
maze = builder.build_from_file(filepath)
results = benchmark.run_experiment(maze, runs=5)
benchmark.print_results(results, test_file)
all_results[test_file] = results
except Exception as e:
print(f"Error testing {test_file}: {e}")
if all_results:
csv_filename = os.path.join(current_dir, "benchmark_results.csv")
with open(csv_filename, "w", encoding='utf-8') as f:
f.write("Maze,Algorithm,AvgTimeMs,AvgPathLength,AvgVisitedCells,SuccessPercent\n")
for maze_name, results in all_results.items():
for strategy_name, stats in results.items():
f.write(f"{maze_name},{strategy_name},"
f"{stats['avg_time_ms']:.3f},{stats['avg_path_length']:.1f},"
f"{stats['avg_visited_cells']:.1f},{stats['success_rate']:.1f}\n")
print(f"\nResults saved to: {csv_filename}")
input("\nPress Enter to continue...")
def main():
print("="*60)
print("MAZE PATHFINDING PROGRAM")
print("Patterns: Builder, Strategy")
print("Algorithms: BFS, DFS, A*")
print("="*60)
current_dir = os.path.dirname(os.path.abspath(__file__))
existing_mazes = [f for f in os.listdir(current_dir) if f.endswith('.txt') and ('maze' in f.lower() or f.startswith('0'))]
if not existing_mazes:
print("First run: create test mazes (action 3)\n")
interactive_mode()
if __name__ == "__main__":
main()

Binary file not shown.