From eb507d08573c45c896c09bd5fd8978232e62ccff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D0=B0=D0=B2=D0=B5=D0=BB=20=D0=9A=D0=BE=D0=BB=D0=B1?= =?UTF-8?q?=D0=B0=D1=81=D0=BE=D0=B2?= Date: Fri, 29 May 2026 22:22:47 +0300 Subject: [PATCH] [2] 2 exercise complete --- KolbasovPD/docs/data/2-nd_exercise/main.py | 546 ++++++++++++++++++ .../docs/data/2-nd_exercise/maze_results.csv | 1 + .../docs/data/2-nd_exercise/mazes/empty.txt | 5 + .../docs/data/2-nd_exercise/mazes/no_exit.txt | 5 + .../docs/data/2-nd_exercise/mazes/tiny.txt | 5 + .../data/2-nd_exercise/mazes/weighted.txt | 5 + 6 files changed, 567 insertions(+) create mode 100644 KolbasovPD/docs/data/2-nd_exercise/maze_results.csv create mode 100644 KolbasovPD/docs/data/2-nd_exercise/mazes/empty.txt create mode 100644 KolbasovPD/docs/data/2-nd_exercise/mazes/no_exit.txt create mode 100644 KolbasovPD/docs/data/2-nd_exercise/mazes/tiny.txt create mode 100644 KolbasovPD/docs/data/2-nd_exercise/mazes/weighted.txt diff --git a/KolbasovPD/docs/data/2-nd_exercise/main.py b/KolbasovPD/docs/data/2-nd_exercise/main.py index e69de29..40ce82a 100644 --- a/KolbasovPD/docs/data/2-nd_exercise/main.py +++ b/KolbasovPD/docs/data/2-nd_exercise/main.py @@ -0,0 +1,546 @@ +import heapq +from collections import deque +from abc import ABC, abstractmethod +import time +import csv +import os +from typing import List, Tuple, Optional, Dict, Set + +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 + self.weight = 1 + + def is_passable(self) -> bool: + return not self.is_wall + +class Maze: + def __init__(self, width: int = 0, height: int = 0): + self.width = width + self.height = height + self.cells: List[List[Cell]] = [] + 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[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 f: + lines = f.readlines() + + height = len(lines) + width = len(lines[0].strip()) if height > 0 else 0 + + maze = Maze(width, height) + maze.cells = [[Cell(x, y) for x in range(width)] for y in range(height)] + + for y, line in enumerate(lines): + line = line.rstrip('\n') + for x, ch in enumerate(line): + if x < width: + cell = maze.cells[y][x] + 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.isdigit(): + cell.weight = int(ch) + cell.is_wall = False + + if not maze.start or not maze.exit: + raise ValueError("Лабиринт должен содержать старт (S) и выход (E)") + + return maze + +class PathFindingStrategy(ABC): + @abstractmethod + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + pass + +class BFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + queue = deque([start]) + visited = {start} + parent = {start: None} + + while queue: + current = queue.popleft() + + if current == exit_cell: + return self.reconstruct_path(parent, start, exit_cell) + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + parent[neighbor] = current + queue.append(neighbor) + + return [] + + def reconstruct_path(self, parent: Dict, start: Cell, exit_cell: Cell) -> List[Cell]: + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = parent[current] + path.reverse() + return path + +class DFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + stack = [(start, [start])] + visited = {start} + + while stack: + current, path = stack.pop() + + if current == exit_cell: + return path + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + stack.append((neighbor, path + [neighbor])) + + return [] + +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: Cell) -> List[Cell]: + open_set = [(0, start)] + came_from = {} + g_score = {start: 0} + f_score = {start: self.heuristic(start, exit_cell)} + + while open_set: + _, current = heapq.heappop(open_set) + + if current == exit_cell: + return self.reconstruct_path(came_from, start, exit_cell) + + for neighbor in maze.get_neighbors(current): + tentative_g = g_score[current] + neighbor.weight + + if neighbor not in g_score or tentative_g < g_score[neighbor]: + came_from[neighbor] = current + g_score[neighbor] = tentative_g + f_score[neighbor] = tentative_g + self.heuristic(neighbor, exit_cell) + heapq.heappush(open_set, (f_score[neighbor], neighbor)) + + return [] + + def reconstruct_path(self, came_from: Dict, start: Cell, exit_cell: Cell) -> List[Cell]: + path = [] + current = exit_cell + while current != start: + path.append(current) + current = came_from[current] + path.append(start) + path.reverse() + return path + +class DijkstraStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + pq = [(0, start)] + distances = {start: 0} + parent = {start: None} + + while pq: + dist, current = heapq.heappop(pq) + + if current == exit_cell: + return self.reconstruct_path(parent, start, exit_cell) + + if dist > distances[current]: + continue + + for neighbor in maze.get_neighbors(current): + new_dist = dist + neighbor.weight + + if neighbor not in distances or new_dist < distances[neighbor]: + distances[neighbor] = new_dist + parent[neighbor] = current + heapq.heappush(pq, (new_dist, neighbor)) + + return [] + + def reconstruct_path(self, parent: Dict, start: Cell, exit_cell: Cell) -> List[Cell]: + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = parent[current] + path.reverse() + return path + +class SearchStats: + def __init__(self, time_ms: float, visited_cells: int, path_length: int, path: List[Cell] = None): + self.time_ms = time_ms + self.visited_cells = visited_cells + self.path_length = path_length + self.path = path + +class MazeSolver: + def __init__(self, maze: Maze, strategy: PathFindingStrategy): + self.maze = maze + self.strategy = strategy + self.observers = [] + + def set_strategy(self, strategy: PathFindingStrategy): + self.strategy = strategy + + def add_observer(self, observer): + self.observers.append(observer) + + def notify_observers(self, event: str, data=None): + for observer in self.observers: + observer.update(event, data) + + def solve(self) -> SearchStats: + self.notify_observers("search_started") + + start_time = time.perf_counter() + path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) + end_time = time.perf_counter() + + time_ms = (end_time - start_time) * 1000 + visited_cells = self.count_visited_cells() + path_length = len(path) + + stats = SearchStats(time_ms, visited_cells, path_length, path) + + self.notify_observers("search_finished", stats) + + return stats + + def count_visited_cells(self) -> int: + if isinstance(self.strategy, BFSStrategy): + return len(self.bfs_visited) + elif isinstance(self.strategy, DFSStrategy): + return len(self.dfs_visited) + return 0 + +class Observer(ABC): + @abstractmethod + def update(self, event: str, data=None): + pass + +class ConsoleView(Observer): + def __init__(self): + self.maze = None + self.current_path = None + + def set_maze(self, maze: Maze): + self.maze = maze + + def update(self, event: str, data=None): + if event == "search_finished": + self.display_path(data.path) + elif event == "search_started": + print("\nПоиск пути начат...") + + def display_path(self, path: List[Cell]): + if not path: + print("\nПуть не найден!") + return + + print(f"\nПуть найден! Длина: {len(path)} шагов") + self.render(path) + + def render(self, path: List[Cell] = None): + if not self.maze: + return + + path_set = set(path) if path else set() + + for y in range(self.maze.height): + for x in range(self.maze.width): + cell = self.maze.get_cell(x, y) + if cell in path_set: + print('*', end='') + elif cell.is_start: + print('S', end='') + elif cell.is_exit: + print('E', end='') + elif cell.is_wall: + print('#', end='') + else: + print(' ', end='') + print() + +class Player: + def __init__(self, start_cell: Cell): + self.current = start_cell + self.history = [] + + def move_to(self, cell: Cell): + if cell and cell.is_passable(): + self.history.append(self.current) + self.current = cell + return True + return False + + def undo(self): + if self.history: + self.current = self.history.pop() + 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, direction: str, maze: Maze): + self.player = player + self.direction = direction + self.maze = maze + self.previous_position = None + + def execute(self) -> bool: + self.previous_position = self.player.current + + dx, dy = 0, 0 + if self.direction == 'W': + dy = -1 + elif self.direction == 'S': + dy = 1 + elif self.direction == 'A': + dx = -1 + elif self.direction == 'D': + dx = 1 + + new_cell = self.maze.get_cell(self.player.current.x + dx, self.player.current.y + dy) + + if new_cell and new_cell.is_passable(): + return self.player.move_to(new_cell) + return False + + def undo(self): + if self.previous_position: + self.player.current = self.previous_position + +def generate_test_mazes(): + test_mazes = { + "tiny": [ + "########", + "#S #", + "# #### #", + "# E #", + "########" + ], + "empty": [ + "########", + "#S #", + "# #", + "# E#", + "########" + ], + "no_exit": [ + "########", + "#S #", + "# #### #", + "# # #", + "########" + ], + "weighted": [ + "########", + "#S2 #", + "# 5#3 #", + "# 2 E #", + "########" + ] + } + + os.makedirs("mazes", exist_ok=True) + + for name, maze_data in test_mazes.items(): + filename = f"mazes/{name}.txt" + with open(filename, 'w', encoding='utf-8') as f: + f.write('\n'.join(maze_data)) + print(f"Создан лабиринт: {filename}") + +def run_experiments(): + strategies = { + "BFS": BFSStrategy(), + "DFS": DFSStrategy(), + "A*": AStarStrategy(), + "Dijkstra": DijkstraStrategy() + } + + mazes_list = ["tiny", "empty", "no_exit", "weighted"] + results = [] + + for maze_name in mazes_list: + filename = f"mazes/{maze_name}.txt" + + try: + builder = TextFileMazeBuilder() + maze = builder.build_from_file(filename) + + print(f"\nТестирование лабиринта: {maze_name}") + + for strategy_name, strategy in strategies.items(): + print(f" Стратегия: {strategy_name}") + + times = [] + visited_counts = [] + path_lengths = [] + + for i in range(5): + solver = MazeSolver(maze, strategy) + stats = solver.solve() + times.append(stats.time_ms) + visited_counts.append(stats.visited_cells if stats.visited_cells else 0) + path_lengths.append(stats.path_length) + + avg_time = sum(times) / len(times) + avg_visited = sum(visited_counts) / len(visited_counts) + avg_path_len = sum(path_lengths) / len(path_lengths) + + results.append([ + maze_name, strategy_name, avg_time, avg_visited, avg_path_len + ]) + + print(f" Время: {avg_time:.3f} мс, Посещено: {avg_visited:.1f}, Путь: {avg_path_len:.1f}") + + except Exception as e: + print(f"Ошибка загрузки {maze_name}: {e}") + + with open("maze_results.csv", 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(["Лабиринт", "Стратегия", "Время_мс", "Посещено_клеток", "Длина_пути"]) + writer.writerows(results) + + print("\nРезультаты сохранены в maze_results.csv") + +def interactive_mode(): + print("\n=== ИНТЕРАКТИВНЫЙ РЕЖИМ ===") + filename = input("Введите имя файла с лабиринтом: ") + + try: + builder = TextFileMazeBuilder() + maze = builder.build_from_file(filename) + + print("\nВыберите стратегию:") + print("1. BFS (кратчайший путь)") + print("2. DFS (быстрый, не обязательно кратчайший)") + print("3. A* (оптимальный с эвристикой)") + print("4. Dijkstra (для взвешенных лабиринтов)") + + choice = input("Ваш выбор (1-4): ") + + strategies = { + '1': BFSStrategy(), + '2': DFSStrategy(), + '3': AStarStrategy(), + '4': DijkstraStrategy() + } + + if choice not in strategies: + print("Неверный выбор!") + return + + solver = MazeSolver(maze, strategies[choice]) + view = ConsoleView() + view.set_maze(maze) + solver.add_observer(view) + + stats = solver.solve() + + print(f"\nСтатистика:") + print(f"Время выполнения: {stats.time_ms:.3f} мс") + print(f"Длина пути: {stats.path_length}") + + input("\nНажмите Enter для ручного режима...") + + player = Player(maze.start) + + while player.current != maze.exit: + os.system('cls' if os.name == 'nt' else 'clear') + view.render() + print(f"\nТекущая позиция: ({player.current.x}, {player.current.y})") + print("Управление: W/A/S/D для движения, Z для отмены, Q для выхода") + + cmd = input("> ").upper() + + if cmd == 'Q': + break + elif cmd == 'Z': + command = MoveCommand(player, 'U', maze) + command.undo() + print("Отмена последнего хода") + elif cmd in ['W', 'A', 'S', 'D']: + command = MoveCommand(player, cmd, maze) + if command.execute(): + print("Перемещение выполнено") + else: + print("Нельзя пройти в этом направлении") + else: + print("Неизвестная команда") + + if player.current == maze.exit: + print("\nПОЗДРАВЛЯЮ! ВЫ НАШЛИ ВЫХОД!") + break + + except Exception as e: + print(f"Ошибка: {e}") + +def main(): + print("="*80) + print("ПОИСК ВЫХОДА ИЗ ЛАБИРИНТА") + print("Применённые паттерны: Builder, Strategy, Observer, Command") + print("="*80) + + generate_test_mazes() + + print("\n1. Запустить эксперименты") + print("2. Интерактивный режим") + + choice = input("\nВыберите режим (1-2): ") + + if choice == '1': + run_experiments() + elif choice == '2': + interactive_mode() + else: + print("Неверный выбор!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/KolbasovPD/docs/data/2-nd_exercise/maze_results.csv b/KolbasovPD/docs/data/2-nd_exercise/maze_results.csv new file mode 100644 index 0000000..7b2d832 --- /dev/null +++ b/KolbasovPD/docs/data/2-nd_exercise/maze_results.csv @@ -0,0 +1 @@ +Лабиринт,Стратегия,Время_мс,Посещено_клеток,Длина_пути diff --git a/KolbasovPD/docs/data/2-nd_exercise/mazes/empty.txt b/KolbasovPD/docs/data/2-nd_exercise/mazes/empty.txt new file mode 100644 index 0000000..f165a4a --- /dev/null +++ b/KolbasovPD/docs/data/2-nd_exercise/mazes/empty.txt @@ -0,0 +1,5 @@ +######## +#S # +# # +# E# +######## \ No newline at end of file diff --git a/KolbasovPD/docs/data/2-nd_exercise/mazes/no_exit.txt b/KolbasovPD/docs/data/2-nd_exercise/mazes/no_exit.txt new file mode 100644 index 0000000..733ea0b --- /dev/null +++ b/KolbasovPD/docs/data/2-nd_exercise/mazes/no_exit.txt @@ -0,0 +1,5 @@ +######## +#S # +# #### # +# # # +######## \ No newline at end of file diff --git a/KolbasovPD/docs/data/2-nd_exercise/mazes/tiny.txt b/KolbasovPD/docs/data/2-nd_exercise/mazes/tiny.txt new file mode 100644 index 0000000..d08f1c2 --- /dev/null +++ b/KolbasovPD/docs/data/2-nd_exercise/mazes/tiny.txt @@ -0,0 +1,5 @@ +######## +#S # +# #### # +# E # +######## \ No newline at end of file diff --git a/KolbasovPD/docs/data/2-nd_exercise/mazes/weighted.txt b/KolbasovPD/docs/data/2-nd_exercise/mazes/weighted.txt new file mode 100644 index 0000000..2aff7f7 --- /dev/null +++ b/KolbasovPD/docs/data/2-nd_exercise/mazes/weighted.txt @@ -0,0 +1,5 @@ +######## +#S2 # +# 5#3 # +# 2 E # +######## \ No newline at end of file