From 7cbd75ee342c44df445e7313ba5b3d4881aeaef7 Mon Sep 17 00:00:00 2001 From: not why Date: Mon, 25 May 2026 03:13:08 +0300 Subject: [PATCH] added main.py --- soninrv/docs/data/lab2/main.py | 611 +++++++++++++++++++++++++++++++++ 1 file changed, 611 insertions(+) create mode 100644 soninrv/docs/data/lab2/main.py diff --git a/soninrv/docs/data/lab2/main.py b/soninrv/docs/data/lab2/main.py new file mode 100644 index 0000000..cd6e0c9 --- /dev/null +++ b/soninrv/docs/data/lab2/main.py @@ -0,0 +1,611 @@ +""" +maze_solver.py — Поиск выхода из лабиринта. + +Паттерны GoF: Builder, Strategy, Observer, Command. +Алгоритмы: BFS, DFS, A*, Dijkstra. + +Запуск: + python maze_solver.py --solve mazes/small_10x10.txt --algo bfs + python maze_solver.py --walk mazes/small_10x10.txt + python maze_solver.py --experiment +""" + +from __future__ import annotations + +import argparse +import csv +import heapq +import os +import statistics +import sys +import time +from abc import ABC, abstractmethod +from collections import deque +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +sys.setrecursionlimit(100_000) + + +# ЭТАП 1. МОДЕЛЬ ЛАБИРИНТА — Cell, Maze + +@dataclass +class Cell: + """Одна клетка лабиринта.""" + x: int + y: int + is_wall: bool = False + is_start: bool = False + is_exit: bool = False + weight: int = 1 # 1=асфальт 2=песок 3=болото + + def is_passable(self) -> bool: + return not self.is_wall + + def __repr__(self) -> str: + if self.is_wall: return "#" + if self.is_start: return "S" + if self.is_exit: return "E" + return " " + + # heapq требует сравнения объектов + def __lt__(self, other: Cell) -> bool: + return (self.x, self.y) < (other.x, other.y) + + def __hash__(self): + return hash((self.x, self.y)) + + def __eq__(self, other): + return isinstance(other, Cell) and self.x == other.x and self.y == other.y + + +class Maze: + """Двумерная сетка клеток.""" + + def __init__(self, width: int, height: int): + self.width = width + self.height = height + self._cells: List[List[Cell]] = [ + [Cell(x, y) for x in range(width)] for y in range(height) + ] + 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]: + """Четыре соседа — только проходимые, в пределах границ.""" + result = [] + for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)): + nb = self.get_cell(cell.x + dx, cell.y + dy) + if nb and nb.is_passable(): + result.append(nb) + return result + + + +# ЭТАП 2. ПАТТЕРН BUILDER — загрузка лабиринта из файла + +_WEIGHT_MAP = {' ': 1, '.': 2, '~': 3} + + +class MazeBuilder(ABC): + """Интерфейс строителя лабиринта.""" + + @abstractmethod + def build_from_file(self, filename: str) -> Maze: ... + + @abstractmethod + def build_from_string(self, text: str) -> Maze: ... + + +class TextFileMazeBuilder(MazeBuilder): + """ + Строит Maze из текстового файла: + # — стена S — старт E — выход + ' '— путь (w=1) . — песок (w=2) ~ — болото (w=3) + """ + + def build_from_file(self, filename: str) -> Maze: + return self.build_from_string(Path(filename).read_text(encoding="utf-8")) + + def build_from_string(self, text: str) -> Maze: + lines = text.splitlines() + while lines and not lines[-1].strip(): + lines.pop() + if not lines: + raise ValueError("Пустой лабиринт") + + height = len(lines) + width = max(len(l) for l in lines) + 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 + else: + cell.weight = _WEIGHT_MAP.get(ch, 1) + for x in range(len(line), width): # дополнить стенами + c = maze.get_cell(x, y) + if c: + c.is_wall = True + + if maze.start is None: + raise ValueError("Нет стартовой клетки 'S'") + if maze.exit is None: + raise ValueError("Нет выходной клетки 'E'") + return maze + + + +# ЭТАП 3. ПАТТЕРН STRATEGY — алгоритмы поиска пути + + +def _reconstruct(parent: Dict[Cell, Optional[Cell]], end: Cell) -> List[Cell]: + """Восстановить путь от старта до end по словарю предшественников.""" + path, node = [], end + while node is not None: + path.append(node) + node = parent.get(node) + path.reverse() + return path + + +class PathFindingStrategy(ABC): + """Интерфейс стратегии поиска пути.""" + + @property + @abstractmethod + def name(self) -> str: ... + + @abstractmethod + def find_path( + self, maze: Maze, start: Cell, exit_cell: Cell + ) -> Tuple[List[Cell], int]: + """Возвращает (path, visited_count). path пуст если пути нет.""" + ... + + +class BFSStrategy(PathFindingStrategy): + """Поиск в ширину — гарантирует кратчайший путь (по числу шагов).""" + + @property + def name(self) -> str: + return "BFS" + + def find_path(self, maze, start, exit_cell): + queue = deque([start]) + parent: Dict[Cell, Optional[Cell]] = {start: None} + visited = 0 + while queue: + cur = queue.popleft() + visited += 1 + if cur == exit_cell: + return _reconstruct(parent, exit_cell), visited + for nb in maze.get_neighbors(cur): + if nb not in parent: + parent[nb] = cur + queue.append(nb) + return [], visited + + +class DFSStrategy(PathFindingStrategy): + """Поиск в глубину — не гарантирует кратчайший путь.""" + + @property + def name(self) -> str: + return "DFS" + + def find_path(self, maze, start, exit_cell): + stack = [start] + parent: Dict[Cell, Optional[Cell]] = {start: None} + visited = 0 + while stack: + cur = stack.pop() + visited += 1 + if cur == exit_cell: + return _reconstruct(parent, exit_cell), visited + for nb in maze.get_neighbors(cur): + if nb not in parent: + parent[nb] = cur + stack.append(nb) + return [], visited + + +class AStarStrategy(PathFindingStrategy): + """A* с манхэттенской эвристикой — направленный поиск, учитывает веса.""" + + @property + def name(self) -> str: + return "A*" + + @staticmethod + def _h(a: Cell, b: Cell) -> int: + return abs(a.x - b.x) + abs(a.y - b.y) + + def find_path(self, maze, start, exit_cell): + heap: List[Tuple[int, int, Cell]] = [(0, 0, start)] + g: Dict[Cell, int] = {start: 0} + parent: Dict[Cell, Optional[Cell]] = {start: None} + visited = 0 + while heap: + _, g_cur, cur = heapq.heappop(heap) + visited += 1 + if cur == exit_cell: + return _reconstruct(parent, exit_cell), visited + if g_cur > g.get(cur, float('inf')): + continue + for nb in maze.get_neighbors(cur): + new_g = g_cur + nb.weight + if new_g < g.get(nb, float('inf')): + g[nb] = new_g + parent[nb] = cur + heapq.heappush(heap, (new_g + self._h(nb, exit_cell), new_g, nb)) + return [], visited + + +class DijkstraStrategy(PathFindingStrategy): + """Дейкстра — оптимален для взвешенных графов, без эвристики.""" + + @property + def name(self) -> str: + return "Dijkstra" + + def find_path(self, maze, start, exit_cell): + dist: Dict[Cell, int] = {start: 0} + parent: Dict[Cell, Optional[Cell]] = {start: None} + heap: List[Tuple[int, Cell]] = [(0, start)] + visited = 0 + while heap: + d, cur = heapq.heappop(heap) + visited += 1 + if cur == exit_cell: + return _reconstruct(parent, exit_cell), visited + if d > dist.get(cur, float('inf')): + continue + for nb in maze.get_neighbors(cur): + new_d = d + nb.weight + if new_d < dist.get(nb, float('inf')): + dist[nb] = new_d + parent[nb] = cur + heapq.heappush(heap, (new_d, nb)) + return [], visited + + + +# ПАТТЕРН OBSERVER + + +@dataclass +class SearchStats: + time_ms: float + visited_cells: int + path_length: int + strategy_name: str + + def __str__(self) -> str: + return (f"[{self.strategy_name}] " + f"время={self.time_ms:.2f} мс " + f"посещено={self.visited_cells} " + f"длина пути={self.path_length}") + + +class Observer(ABC): + @abstractmethod + def update(self, event: dict) -> None: ... + + +class ConsoleView(Observer): + """Выводит события и рисует лабиринт в консоли.""" + + def update(self, event: dict) -> None: + kind = event.get("type") + if kind == "maze_loaded": + print(f"[Лабиринт загружен] {event['width']}×{event['height']}") + elif kind == "search_start": + print(f"[Поиск] алгоритм={event['strategy']}") + elif kind == "path_found": + print(f"[Готово] {event['stats']}") + elif kind == "no_path": + print("[Результат] Путь не найден!") + + def render( + self, + maze: Maze, + player_pos: Optional[Cell] = None, + path: Optional[List[Cell]] = None, + ) -> None: + path_set = set(path) if path else set() + for y in range(maze.height): + row = [] + for x in range(maze.width): + cell = maze.get_cell(x, y) + if cell is None: + row.append("?") + elif player_pos and cell == player_pos: + row.append("@") + elif cell.is_wall: + row.append("█") + elif cell.is_start: + row.append("S") + elif cell.is_exit: + row.append("E") + elif cell in path_set: + row.append("*") + elif cell.weight == 3: + row.append("~") + elif cell.weight == 2: + row.append(".") + else: + row.append(" ") + print("".join(row)) + print() + + + +# ЭТАП 4. ОРКЕСТРАТОР MazeSolver + +class MazeSolver: + """ + Связывает Maze + PathFindingStrategy + список Observer. + Паттерны: Strategy (алгоритм подключается снаружи), + Observer (уведомления при завершении). + """ + + def __init__(self, maze: Maze, strategy: PathFindingStrategy): + self.maze = maze + self._strategy = strategy + self._observers: List[Observer] = [] + self._last_path: List[Cell] = [] + + def add_observer(self, obs: Observer) -> None: + self._observers.append(obs) + + def set_strategy(self, strategy: PathFindingStrategy) -> None: + self._strategy = strategy + + def _notify(self, event: dict) -> None: + for obs in self._observers: + obs.update(event) + + def solve(self) -> SearchStats: + if not self.maze.start or not self.maze.exit: + raise ValueError("Лабиринт не содержит старт или выход") + + self._notify({"type": "search_start", "strategy": self._strategy.name}) + + t0 = time.perf_counter() + path, visited = self._strategy.find_path( + self.maze, self.maze.start, self.maze.exit + ) + elapsed_ms = (time.perf_counter() - t0) * 1000 + + self._last_path = path + stats = SearchStats(elapsed_ms, visited, len(path), self._strategy.name) + + self._notify({"type": "path_found" if path else "no_path", "stats": stats}) + return stats + + @property + def last_path(self) -> List[Cell]: + return self._last_path + + + +# ЭТАП 5. ПАТТЕРН COMMAND — пошаговое управление игроком + + +_DIRECTIONS = {'W': (0, -1), 'S': (0, 1), 'A': (-1, 0), 'D': (1, 0)} + + +class Command(ABC): + @abstractmethod + def execute(self) -> bool: ... + @abstractmethod + def undo(self) -> None: ... + + +class Player: + def __init__(self, cell: Cell): + self.current_cell = cell + + def move_to(self, cell: Cell) -> None: + self.current_cell = cell + + +class MoveCommand(Command): + """Перемещение игрока в направлении direction (W/A/S/D).""" + + def __init__(self, player: Player, direction: str, maze: Maze, + observers: Optional[List[Observer]] = None): + self._player = player + self._direction = direction.upper() + self._maze = maze + self._observers = observers or [] + self._prev: Optional[Cell] = None + + def execute(self) -> bool: + dx, dy = _DIRECTIONS.get(self._direction, (0, 0)) + target = self._maze.get_cell( + self._player.current_cell.x + dx, + self._player.current_cell.y + dy, + ) + if target and target.is_passable(): + self._prev = self._player.current_cell + self._player.move_to(target) + for obs in self._observers: + obs.update({"type": "move", "cell": target}) + return True + return False + + def undo(self) -> None: + if self._prev: + self._player.move_to(self._prev) + self._prev = None + + +class CommandHistory: + """Стек выполненных команд (undo/redo).""" + + def __init__(self): + self._stack: List[Command] = [] + + def execute(self, cmd: Command) -> bool: + ok = cmd.execute() + if ok: + self._stack.append(cmd) + return ok + + def undo(self) -> bool: + if self._stack: + self._stack.pop().undo() + return True + return False + + + +# ЭТАП 6. ЭКСПЕРИМЕНТАЛЬНАЯ ЧАСТЬ + + +def run_experiment( + maze_files: List[Tuple[str, str]], + strategies: List[PathFindingStrategy], + repeats: int = 7, + out_csv: str = "results.csv", +) -> None: + builder = TextFileMazeBuilder() + rows = [["maze", "strategy", "run", "time_ms", "visited_cells", "path_length"]] + + for maze_name, maze_file in maze_files: + print(f"\n=== {maze_name} ===") + maze = builder.build_from_file(maze_file) + print(f" Размер: {maze.width}×{maze.height}") + + for strategy in strategies: + solver = MazeSolver(maze, strategy) + times, visits, lengths = [], [], [] + + for run in range(1, repeats + 1): + stats = solver.solve() + times.append(stats.time_ms) + visits.append(stats.visited_cells) + lengths.append(stats.path_length) + rows.append([maze_name, strategy.name, run, + round(stats.time_ms, 4), + stats.visited_cells, + stats.path_length]) + + print(f" {strategy.name:10} | " + f"t={statistics.mean(times):.3f} мс | " + f"посещено={statistics.mean(visits):.0f} | " + f"путь={statistics.mean(lengths):.0f}") + + os.makedirs(os.path.dirname(os.path.abspath(out_csv)), exist_ok=True) + with open(out_csv, "w", newline="", encoding="utf-8") as f: + csv.writer(f).writerows(rows) + print(f"\nСохранено: {out_csv}") + + + +# CLI + + +_ALGO_MAP = { + "bfs": BFSStrategy(), + "dfs": DFSStrategy(), + "astar": AStarStrategy(), + "dijkstra": DijkstraStrategy(), +} + +_MAZES = [ + ("small_10x10", "small_10x10.txt"), + ("medium_50x50", "medium_50x50.txt"), + ("large_100x100", "large_100x100.txt"), + ("empty_30x30", "empty_30x30.txt"), + ("no_exit_20x20", "no_exit_20x20.txt"), + ("weighted_40x40", "weighted_40x40.txt"), +] + + +def cmd_solve(maze_file: str, algo: str) -> None: + view = ConsoleView() + maze = TextFileMazeBuilder().build_from_file(maze_file) + view.update({"type": "maze_loaded", "width": maze.width, "height": maze.height}) + + strategy = _ALGO_MAP.get(algo.lower()) + if not strategy: + print(f"Неизвестный алгоритм '{algo}'. Доступны: {', '.join(_ALGO_MAP)}") + return + + solver = MazeSolver(maze, strategy) + solver.add_observer(view) + solver.solve() + view.render(maze, path=solver.last_path) + + +def cmd_walk(maze_file: str) -> None: + view = ConsoleView() + maze = TextFileMazeBuilder().build_from_file(maze_file) + player = Player(maze.start) + history = CommandHistory() + + solver = MazeSolver(maze, BFSStrategy()) + solver.add_observer(view) + solver.solve() + path = solver.last_path + + print("W=вверх S=вниз A=влево D=вправо Z=отмена Q=выход") + while True: + view.render(maze, player_pos=player.current_cell, path=path) + if player.current_cell == maze.exit: + print("Вы достигли выхода!") + break + key = input("Ход: ").strip().upper() + if key == "Q": + break + elif key == "Z": + if not history.undo(): + print("Нечего отменять.") + elif key in _DIRECTIONS: + if not history.execute(MoveCommand(player, key, maze, [view])): + print("Туда нельзя — стена.") + else: + print("Неизвестная клавиша.") + + +def cmd_experiment() -> None: + run_experiment(_MAZES, list(_ALGO_MAP.values())) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Maze solver") + parser.add_argument("--solve", metavar="FILE", help="Решить лабиринт") + parser.add_argument("--algo", metavar="ALGO", default="bfs", + help="bfs | dfs | astar | dijkstra") + parser.add_argument("--walk", metavar="FILE", help="Ручное управление (WASD)") + parser.add_argument("--experiment", action="store_true", + help="Запустить замеры и сохранить CSV") + args = parser.parse_args() + + if args.solve: + cmd_solve(args.solve, args.algo) + elif args.walk: + cmd_walk(args.walk) + elif args.experiment: + cmd_experiment() + else: + parser.print_help() \ No newline at end of file