From 9a8a82a9780d9111bf7348015c4620fa7a7d8dd0 Mon Sep 17 00:00:00 2001 From: agafonovdm Date: Mon, 25 May 2026 13:10:22 +0300 Subject: [PATCH] [4] 2-nd_ex --- agafonovdm/docs/data/2zad/2-nd_ex.py | 589 ++++++++++++++++++ .../docs/data/2zad/experiment_results.csv | 21 + 2 files changed, 610 insertions(+) create mode 100644 agafonovdm/docs/data/2zad/2-nd_ex.py create mode 100644 agafonovdm/docs/data/2zad/experiment_results.csv diff --git a/agafonovdm/docs/data/2zad/2-nd_ex.py b/agafonovdm/docs/data/2zad/2-nd_ex.py new file mode 100644 index 0000000..8215963 --- /dev/null +++ b/agafonovdm/docs/data/2zad/2-nd_ex.py @@ -0,0 +1,589 @@ +import time +import heapq +from collections import deque +from typing import List, Optional, Dict, Tuple +from abc import ABC, abstractmethod +import csv +import random + + +class Cell: + def __init__(self, x: int, y: int): + self.x = x + self.y = y + self.is_wall = False + self.is_start = False + self.is_exit = False + + def is_passable(self) -> bool: + return not self.is_wall + + +class Maze: + def __init__(self, width: int, height: int): + self.width = width + self.height = height + self.cells = [[Cell(x, y) for y in range(height)] for x in range(width)] + self.start: Optional[Cell] = None + self.exit: Optional[Cell] = None + + def get_cell(self, x: int, y: int) -> Optional[Cell]: + if 0 <= x < self.width and 0 <= y < self.height: + return self.cells[x][y] + return None + + def get_neighbors(self, cell: Cell) -> List[Cell]: + neighbors = [] + for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: + nx, ny = cell.x + dx, cell.y + dy + nb = self.get_cell(nx, ny) + if nb and nb.is_passable(): + neighbors.append(nb) + 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 f: + lines = [line.rstrip('\n') for line in f.readlines()] + + height = len(lines) + width = max(len(line) for line in lines) if height > 0 else 0 + maze = Maze(width, height) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + cell = maze.get_cell(x, y) + if cell is None: + continue + if ch == '#': + cell.is_wall = True + elif ch == 'S': + cell.is_start = True + maze.start = cell + elif ch == 'E': + cell.is_exit = True + maze.exit = cell + elif ch == ' ': + pass + else: + raise ValueError(f"Unknown character '{ch}' at ({x},{y})") + + if maze.start is None or maze.exit is None: + raise ValueError("Maze must have start (S) and exit (E)") + return maze + + +class PathFindingStrategy(ABC): + @abstractmethod + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: + pass + + @abstractmethod + def get_name(self) -> str: + pass + + +class BFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: + queue = deque([start]) + came_from = {start: None} + + while queue: + current = queue.popleft() + if current == exit: + break + for nb in maze.get_neighbors(current): + if nb not in came_from: + came_from[nb] = current + queue.append(nb) + + if exit not in came_from: + return [] + + path = [] + cur = exit + while cur: + path.append(cur) + cur = came_from[cur] + path.reverse() + return path + + def get_name(self) -> str: + return "BFS" + + +class DFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: + stack = [start] + came_from = {start: None} + + while stack: + current = stack.pop() + if current == exit: + break + for nb in maze.get_neighbors(current): + if nb not in came_from: + came_from[nb] = current + stack.append(nb) + + if exit not in came_from: + return [] + + path = [] + cur = exit + while cur: + path.append(cur) + cur = came_from[cur] + path.reverse() + return path + + def get_name(self) -> str: + return "DFS" + + +class AStarStrategy(PathFindingStrategy): + def _heuristic(self, a: Cell, b: Cell) -> int: + return abs(a.x - b.x) + abs(a.y - b.y) + + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: + open_set = [] + heapq.heappush(open_set, (0, id(start), start)) + came_from = {} + g_score = {start: 0} + f_score = {start: self._heuristic(start, exit)} + + while open_set: + _, _, current = heapq.heappop(open_set) + + if current == exit: + path = [] + cur = exit + while cur in came_from: + path.append(cur) + cur = came_from[cur] + path.append(start) + path.reverse() + return path + + for neighbor in maze.get_neighbors(current): + tentative_g = g_score[current] + 1 + if tentative_g < g_score.get(neighbor, float('inf')): + came_from[neighbor] = current + g_score[neighbor] = tentative_g + f_score[neighbor] = tentative_g + self._heuristic(neighbor, exit) + heapq.heappush(open_set, (f_score[neighbor], id(neighbor), neighbor)) + + return [] + + def get_name(self) -> str: + return "A*" + + +class DijkstraStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: + pq = [(0, id(start), start)] + distances = {start: 0} + came_from = {start: None} + + while pq: + dist, _, current = heapq.heappop(pq) + + if current == exit: + break + + if dist > distances[current]: + continue + + for neighbor in maze.get_neighbors(current): + new_dist = dist + 1 + if new_dist < distances.get(neighbor, float('inf')): + distances[neighbor] = new_dist + came_from[neighbor] = current + heapq.heappush(pq, (new_dist, id(neighbor), neighbor)) + + if exit not in came_from: + return [] + + path = [] + cur = exit + while cur: + path.append(cur) + cur = came_from[cur] + path.reverse() + return path + + def get_name(self) -> str: + return "Dijkstra" + + +class SearchStats: + def __init__(self, time_ms: float, visited_cells: int, path_length: int): + self.time_ms = time_ms + self.visited_cells = visited_cells + self.path_length = path_length + + def __str__(self): + return f"Time: {self.time_ms:.2f}ms, Visited: {self.visited_cells}, Path: {self.path_length}" + + +class MazeSolver: + def __init__(self, maze: Maze, strategy: PathFindingStrategy): + self.maze = maze + self.strategy = strategy + + def set_strategy(self, strategy: PathFindingStrategy): + self.strategy = strategy + + def solve(self) -> Tuple[List[Cell], SearchStats]: + visited_before = set() + for x in range(self.maze.width): + for y in range(self.maze.height): + cell = self.maze.get_cell(x, y) + if cell and cell.is_passable(): + visited_before.add(cell) + + start_time = time.perf_counter() + path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) + end_time = time.perf_counter() + + visited_after = set() + for x in range(self.maze.width): + for y in range(self.maze.height): + cell = self.maze.get_cell(x, y) + if cell and cell.is_passable(): + visited_after.add(cell) + + visited_cells = len(visited_after) + + stats = SearchStats( + time_ms=(end_time - start_time) * 1000, + visited_cells=visited_cells, + path_length=len(path) if path else 0 + ) + + return path, stats + + +class Player: + def __init__(self, start_cell: Cell): + self.current_cell = start_cell + self.previous_cell = None + + def move_to(self, cell: Cell) -> bool: + if cell.is_passable(): + self.previous_cell = self.current_cell + self.current_cell = cell + return True + return False + + def undo(self): + if self.previous_cell: + self.current_cell, self.previous_cell = self.previous_cell, None + return True + return False + + +class Command(ABC): + @abstractmethod + def execute(self) -> bool: + pass + + @abstractmethod + def undo(self): + pass + + +class MoveCommand(Command): + def __init__(self, player: Player, maze: Maze, direction: str): + self.player = player + self.maze = maze + self.direction = direction + self.executed = False + + def execute(self) -> bool: + dx, dy = 0, 0 + if self.direction == 'W' or self.direction == 'w': + dy = -1 + elif self.direction == 'S' or self.direction == 's': + dy = 1 + elif self.direction == 'A' or self.direction == 'a': + dx = -1 + elif self.direction == 'D' or self.direction == 'd': + dx = 1 + + new_x = self.player.current_cell.x + dx + new_y = self.player.current_cell.y + dy + new_cell = self.maze.get_cell(new_x, new_y) + + if new_cell and new_cell.is_passable(): + self.executed = self.player.move_to(new_cell) + return self.executed + return False + + def undo(self): + if self.executed: + self.player.undo() + self.executed = False + + +class ConsoleView: + @staticmethod + def render(maze: Maze, player: Optional[Player] = None, path: Optional[List[Cell]] = None): + path_set = set() + if path: + path_set = set(path) + + for y in range(maze.height): + line = "" + for x in range(maze.width): + cell = maze.get_cell(x, y) + if not cell: + line += " " + elif player and player.current_cell == cell: + line += "P" + elif cell.is_start: + line += "S" + elif cell.is_exit: + line += "E" + elif cell.is_wall: + line += "#" + elif path and cell in path_set: + line += "." + else: + line += " " + print(line) + print() + + @staticmethod + def show_stats(stats: SearchStats, algo_name: str): + print(f"=== {algo_name} Results ===") + print(stats) + print() + + +def generate_test_maze(width: int, height: int, complexity: float = 0.3) -> Maze: + maze = Maze(width, height) + + for x in range(width): + for y in range(height): + if random.random() < complexity: + maze.cells[x][y].is_wall = True + + maze.start = maze.get_cell(0, 0) + if maze.start: + maze.start.is_start = True + maze.start.is_wall = False + + maze.exit = maze.get_cell(width - 1, height - 1) + if maze.exit: + maze.exit.is_exit = True + maze.exit.is_wall = False + + return maze + + +def generate_empty_maze(width: int, height: int) -> Maze: + maze = Maze(width, height) + + for x in range(width): + for y in range(height): + maze.cells[x][y].is_wall = False + + maze.start = maze.get_cell(0, 0) + if maze.start: + maze.start.is_start = True + + maze.exit = maze.get_cell(width - 1, height - 1) + if maze.exit: + maze.exit.is_exit = True + + return maze + + +def generate_no_exit_maze(width: int, height: int) -> Maze: + maze = Maze(width, height) + + for x in range(width): + for y in range(height): + maze.cells[x][y].is_wall = False + + for x in range(width): + maze.cells[x][height // 2].is_wall = True + + maze.start = maze.get_cell(0, 0) + if maze.start: + maze.start.is_start = True + + maze.exit = maze.get_cell(width - 1, height - 1) + if maze.exit: + maze.exit.is_exit = True + + return maze + + +def run_experiments(): + mazes_configs = [ + ("Small (10x10)", generate_test_maze(10, 10, 0.2)), + ("Medium (50x50)", generate_test_maze(50, 50, 0.25)), + ("Large (100x100)", generate_test_maze(100, 100, 0.3)), + ("Empty (30x30)", generate_empty_maze(30, 30)), + ("No Exit (20x20)", generate_no_exit_maze(20, 20)) + ] + + strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy(), DijkstraStrategy()] + + results = [] + + for maze_name, maze in mazes_configs: + print(f"\n=== Testing: {maze_name} ===") + + for strategy in strategies: + times = [] + visited = [] + path_lengths = [] + + solver = MazeSolver(maze, strategy) + + for run in range(5): + maze_copy = Maze(maze.width, maze.height) + for x in range(maze.width): + for y in range(maze.height): + orig = maze.get_cell(x, y) + copy = maze_copy.get_cell(x, y) + if orig: + copy.is_wall = orig.is_wall + copy.is_start = orig.is_start + copy.is_exit = orig.is_exit + maze_copy.start = maze_copy.get_cell(maze.start.x, maze.start.y) if maze.start else None + maze_copy.exit = maze_copy.get_cell(maze.exit.x, maze.exit.y) if maze.exit else None + + solver.maze = maze_copy + solver.set_strategy(strategy) + path, stats = solver.solve() + + times.append(stats.time_ms) + visited.append(stats.visited_cells) + path_lengths.append(stats.path_length) + + avg_time = sum(times) / len(times) + avg_visited = sum(visited) / len(visited) + avg_path = sum(path_lengths) / len(path_lengths) + + results.append({ + 'maze': maze_name, + 'algorithm': strategy.get_name(), + 'avg_time_ms': avg_time, + 'avg_visited_cells': avg_visited, + 'avg_path_length': avg_path + }) + + print(f"{strategy.get_name()}: {avg_time:.2f}ms, {avg_visited:.0f} cells, path={avg_path:.0f}") + + with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=['maze', 'algorithm', 'avg_time_ms', 'avg_visited_cells', 'avg_path_length']) + writer.writeheader() + writer.writerows(results) + + print("\nResults saved to experiment_results.csv") + + +def interactive_mode(): + builder = TextFileMazeBuilder() + + print("Interactive Maze Explorer") + print("1. Load maze from file") + print("2. Generate random maze") + choice = input("Choose (1/2): ") + + if choice == '1': + filename = input("Enter filename: ") + try: + maze = builder.build_from_file(filename) + except Exception as e: + print(f"Error loading maze: {e}") + return + else: + w = int(input("Width: ")) + h = int(input("Height: ")) + maze = generate_test_maze(w, h, 0.3) + + player = Player(maze.start) + + strategies = { + '1': BFSStrategy(), + '2': DFSStrategy(), + '3': AStarStrategy(), + '4': DijkstraStrategy() + } + + print("\nSelect algorithm for solving:") + print("1. BFS (shortest path)") + print("2. DFS (fast, not optimal)") + print("3. A* (heuristic)") + print("4. Dijkstra") + algo_choice = input("Choose: ") + + solver = MazeSolver(maze, strategies.get(algo_choice, BFSStrategy())) + path, stats = solver.solve() + + view = ConsoleView() + + if path: + print(f"\nPath found! Length: {len(path)}") + view.show_stats(stats, solver.strategy.get_name()) + else: + print("\nNo path found!") + + while True: + view.render(maze, player, path if path else None) + + if player.current_cell == maze.exit: + print("Congratulations! You reached the exit!") + break + + cmd = input("Move (W/A/S/D) | U=undo | Q=quit | S=solve: ").upper() + + if cmd == 'Q': + break + elif cmd == 'U': + player.undo() + print("Undo last move") + elif cmd == 'S' and path: + for cell in path: + if cell == player.current_cell: + continue + player.move_to(cell) + view.render(maze, player, path) + input("Press Enter to continue...") + if player.current_cell == maze.exit: + print("You reached the exit!") + break + elif cmd in ['W', 'A', 'S', 'D']: + move_cmd = MoveCommand(player, maze, cmd) + if move_cmd.execute(): + print("Moved") + else: + print("Can't move there!") + + +def main(): + print("Maze Solver with Design Patterns") + print("1. Run experiments") + print("2. Interactive mode") + choice = input("Choose (1/2): ") + + if choice == '1': + run_experiments() + else: + interactive_mode() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/agafonovdm/docs/data/2zad/experiment_results.csv b/agafonovdm/docs/data/2zad/experiment_results.csv new file mode 100644 index 0000000..5417132 --- /dev/null +++ b/agafonovdm/docs/data/2zad/experiment_results.csv @@ -0,0 +1,21 @@ +maze,algorithm,avg_time_ms,avg_visited_cells,avg_path_length +Small (10x10),BFS,0.08572000006097369,79.0,19.0 +Small (10x10),DFS,0.039739999920129776,79.0,31.0 +Small (10x10),A*,0.13467999997374136,79.0,19.0 +Small (10x10),Dijkstra,0.11474000057205558,79.0,19.0 +Medium (50x50),BFS,1.8074600004183594,1874.0,99.0 +Medium (50x50),DFS,0.5937599995377241,1874.0,429.0 +Medium (50x50),A*,1.6300600003887666,1874.0,99.0 +Medium (50x50),Dijkstra,3.1870400001935195,1874.0,99.0 +Large (100x100),BFS,0.014439999722526409,7033.0,0.0 +Large (100x100),DFS,0.014839999857940711,7033.0,0.0 +Large (100x100),A*,0.02542000001994893,7033.0,0.0 +Large (100x100),Dijkstra,0.02548000011302065,7033.0,0.0 +Empty (30x30),BFS,0.784620000194991,900.0,59.0 +Empty (30x30),DFS,0.5252399994787993,900.0,465.0 +Empty (30x30),A*,1.150900000357069,900.0,59.0 +Empty (30x30),Dijkstra,1.564640000287909,900.0,59.0 +No Exit (20x20),BFS,0.2002399993216386,380.0,0.0 +No Exit (20x20),DFS,0.2512400002160575,380.0,0.0 +No Exit (20x20),A*,0.5590400000073714,380.0,0.0 +No Exit (20x20),Dijkstra,0.35640000060084276,380.0,0.0