diff --git a/KuzminskiyAA/Task 2/Docs/Data/01_small_maze.txt b/KuzminskiyAA/Task 2/Docs/Data/01_small_maze.txt new file mode 100644 index 0000000..eaea1a7 --- /dev/null +++ b/KuzminskiyAA/Task 2/Docs/Data/01_small_maze.txt @@ -0,0 +1,9 @@ +########## +#S # +# ### ## # +# # # +### # #### +# # # +# ### # # +# # E# +########## \ No newline at end of file diff --git a/KuzminskiyAA/Task 2/Docs/Data/02_medium_maze.txt b/KuzminskiyAA/Task 2/Docs/Data/02_medium_maze.txt new file mode 100644 index 0000000..6fa4e38 --- /dev/null +++ b/KuzminskiyAA/Task 2/Docs/Data/02_medium_maze.txt @@ -0,0 +1,13 @@ +#################### +#S # +# ### ########### # +# # # # # # +# # # # ####### # # +# # # # # # +# # ######### # # # +# # # # # +# # ########### # # +# # # # +# ############### # +# E# +#################### \ No newline at end of file diff --git a/KuzminskiyAA/Task 2/Docs/Data/03_large_maze.txt b/KuzminskiyAA/Task 2/Docs/Data/03_large_maze.txt new file mode 100644 index 0000000..81ed583 --- /dev/null +++ b/KuzminskiyAA/Task 2/Docs/Data/03_large_maze.txt @@ -0,0 +1,26 @@ +################################################## +#S # +# ############################################# # +# # # # +# # ######################################### # # +# # # # # # +# # # ##################################### # # # +# # # # # # # # +# # # # ################################# # # # # +# # # # # # # # # # +# # # # # ############################# # # # # # +# # # # # # # # # # # # +# # # # # # ######################### # # # # # # +# # # # # # # # # # # # # # +# # # # # # # ##################### # # # # # # # +# # # # # # # # # # # # # # # # +# # # # # # # # ################# # # # # # # # # +# # # # # # # # # # # # # # # # # # +# # # # # # # # # ############# # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # ######### # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # ##### # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # #E# +################################################## \ No newline at end of file diff --git a/KuzminskiyAA/Task 2/Docs/Data/04_empty_maze.txt b/KuzminskiyAA/Task 2/Docs/Data/04_empty_maze.txt new file mode 100644 index 0000000..298e3f4 --- /dev/null +++ b/KuzminskiyAA/Task 2/Docs/Data/04_empty_maze.txt @@ -0,0 +1,39 @@ +######################################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +######################################## \ No newline at end of file diff --git a/KuzminskiyAA/Task 2/Docs/Data/05_no_exit_maze.txt b/KuzminskiyAA/Task 2/Docs/Data/05_no_exit_maze.txt new file mode 100644 index 0000000..1c53d9f --- /dev/null +++ b/KuzminskiyAA/Task 2/Docs/Data/05_no_exit_maze.txt @@ -0,0 +1,9 @@ +########## +#S # +# ### ## # +# # # +### # #### +# # # +# ### # # +# # +########## \ No newline at end of file diff --git a/KuzminskiyAA/Task 2/Docs/Data/2.py b/KuzminskiyAA/Task 2/Docs/Data/2.py new file mode 100644 index 0000000..4e849a3 --- /dev/null +++ b/KuzminskiyAA/Task 2/Docs/Data/2.py @@ -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() \ No newline at end of file diff --git a/KuzminskiyAA/Task 2/Docs/Отчет.docx b/KuzminskiyAA/Task 2/Docs/Отчет.docx new file mode 100644 index 0000000..37f93ee Binary files /dev/null and b/KuzminskiyAA/Task 2/Docs/Отчет.docx differ