2026-rff_mp/KuzminskiyAA/Task 2/Docs/Data/2.py
2026-05-25 08:33:17 +03:00

653 lines
23 KiB
Python

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()