diff --git a/SimonovaMS/lab2/experiments.py b/SimonovaMS/lab2/experiments.py new file mode 100644 index 0000000..7a48d31 --- /dev/null +++ b/SimonovaMS/lab2/experiments.py @@ -0,0 +1,200 @@ +# experiments.py +import time +import csv +from typing import List, Dict +from maze_model import Maze +from maze_builder import TextFileMazeBuilder +from pathfinding_strategies import BFSStrategy, DFSStrategy, AStarStrategy +from maze_solver import MazeSolver, SearchStats + + +class ExperimentRunner: + + def __init__(self): + self.builder = TextFileMazeBuilder() + self.strategies = [ + BFSStrategy(), + DFSStrategy(), + AStarStrategy(), + ] + self.results: List[Dict] = [] + + def create_test_maze_file(self, filename: str, maze_data: List[str]) -> None: + with open(filename, 'w', encoding='utf-8') as f: + f.write('\n'.join(maze_data)) + + def generate_simple_maze(self) -> List[str]: + maze = [ + "S E", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ] + return maze + + def generate_complex_maze(self, size: int = 50) -> List[str]: + import random + random.seed(42) + + maze = [] + for y in range(size): + row = [] + for x in range(size): + if (x == 0 and y == 0): + row.append('S') + elif (x == size - 1 and y == size - 1): + row.append('E') + elif random.random() < 0.3: # 30% стен + row.append('#') + else: + row.append(' ') + maze.append(''.join(row)) + + for i in range(size): + if maze[i][0] == '#': + row = list(maze[i]) + row[0] = ' ' + maze[i] = ''.join(row) + if maze[0][i] == '#': + row = list(maze[0]) + row[i] = ' ' + maze[0] = ''.join(row) + + return maze + + def generate_empty_maze(self, size: int = 50) -> List[str]: + maze = [] + for y in range(size): + row = [] + for x in range(size): + if x == 0 and y == 0: + row.append('S') + elif x == size - 1 and y == size - 1: + row.append('E') + else: + row.append(' ') + maze.append(''.join(row)) + return maze + + def generate_no_exit_maze(self, size: int = 20) -> List[str]: + maze = [] + for y in range(size): + row = [] + for x in range(size): + if x == 0 and y == 0: + row.append('S') + elif x == size - 1 and y == size - 1: + row.append('#') # Выход заблокирован + else: + row.append('#') # Всё стены + maze.append(''.join(row)) + + # выход в тупике + row = list(maze[size - 1]) + row[size - 1] = 'E' + maze[size - 1] = ''.join(row) + + return maze + + def run_experiment(self, maze_name: str, maze_data: List[str], + num_runs: int = 5) -> List[Dict]: + filename = f"test_{maze_name}.txt" + self.create_test_maze_file(filename, maze_data) + + maze = self.builder.build_from_file(filename) + results = [] + + for strategy in self.strategies: + solver = MazeSolver(maze, strategy) + + times = [] + path_lengths = [] + + for run in range(num_runs): + stats = solver.solve() + times.append(stats.time_ms) + path_lengths.append(stats.path_length) + + avg_time = sum(times) / len(times) + avg_path_length = sum(path_lengths) / len(path_lengths) + + result = { + 'maze': maze_name, + 'strategy': strategy.name, + 'avg_time_ms': round(avg_time, 3), + 'min_time_ms': round(min(times), 3), + 'max_time_ms': round(max(times), 3), + 'path_length': int(avg_path_length) if avg_path_length else 0, + 'path_found': avg_path_length > 0 + } + results.append(result) + + print(f"{maze_name} - {strategy.name}: " + f"{avg_time:.3f} мс, путь: {int(avg_path_length)}") + + return results + + def run_all_experiments(self): + + experiments = [ + ("simple_10x10", self.generate_simple_maze()), + ("complex_50x50", self.generate_complex_maze(50)), + ("large_100x100", self.generate_complex_maze(100)), + ("empty_50x50", self.generate_empty_maze(50)), + ("no_exit_20x20", self.generate_no_exit_maze(20)) + ] + + all_results = [] + + for name, data in experiments: + print(f"\n Лабиринт: {name} ---") + results = self.run_experiment(name, data) + all_results.extend(results) + + self.save_to_csv(all_results, "experiment_results.csv") + + + + return all_results + + def save_to_csv(self, results: List[Dict], filename: str): + if not results: + return + + with open(filename, 'w', newline='', encoding='utf-8') as csvfile: + fieldnames = results[0].keys() + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(results) + + +def print_analysis(results: List[Dict]): + + # Группировка + mazes = set(r['maze'] for r in results) + + for maze in sorted(mazes): + print(f"\nЛабиринт: {maze}") + print("-" * 40) + + maze_results = [r for r in results if r['maze'] == maze] + + #по времени + sorted_results = sorted(maze_results, key=lambda x: x['avg_time_ms']) + + for r in sorted_results: + status = "✓" if r['path_found'] else "✗" + print(f" {status} {r['strategy']:8} | " + f"Время: {r['avg_time_ms']:8.3f} мс | " + f"Путь: {r['path_length']:4} шагов") + + # Определяем лучший + fastest = sorted_results[0] + print(f"\n → Самый быстрый: {fastest['strategy']} " + f"({fastest['avg_time_ms']:.3f} мс)") \ No newline at end of file diff --git a/SimonovaMS/lab2/main.py b/SimonovaMS/lab2/main.py new file mode 100644 index 0000000..c3ec3d0 --- /dev/null +++ b/SimonovaMS/lab2/main.py @@ -0,0 +1,146 @@ +import sys +from maze_builder import TextFileMazeBuilder +from pathfinding_strategies import BFSStrategy, DFSStrategy, AStarStrategy +from maze_solver import MazeSolver +from visualization import ConsoleView, GameController, EventType +from experiments import ExperimentRunner, print_analysis +from analysis import plot_results + +def create_sample_maze(): + sample_maze = [ + "S ##### ", + "# # ### ", + "# # # # ", + "# # ### # ", + "# # # ", + "### # ### ", + "# # # ", + "# ####### ", + "# E ", + "##########" + ] + + filename = "sample_maze.txt" + with open(filename, 'w', encoding='utf-8') as f: + f.write('\n'.join(sample_maze)) + + return filename + + +def interactive_mode(): + + + builder = TextFileMazeBuilder() + filename = create_sample_maze() + + try: + maze = builder.build_from_file(filename) + print(f"Лабиринт загружен: {maze.width}x{maze.height}") + except Exception as e: + print(f"Ошибка загрузки: {e}") + return + + view = ConsoleView() + controller = GameController(maze, view) + + strategies = { + '1': BFSStrategy(), + '2': DFSStrategy(), + '3': AStarStrategy(), + } + + print("\nДоступные алгоритмы поиска пути:") + print(" 1. BFS (поиск в ширину) - кратчайший путь") + print(" 2. DFS (поиск в глубину) - быстрый, не оптимальный") + print(" 3. A* - оптимальный с эвристикой") + + # Выбор стратегии + while True: + choice = input("\nВыберите алгоритм (1-3): ").strip() + if choice in strategies: + strategy = strategies[choice] + break + print("Неверный выбор. Попробуйте снова.") + + # Поиск пути + print(f"\nИспользуем: {strategy.name}") + print("Поиск пути...") + + solver = MazeSolver(maze, strategy) + stats = solver.solve() + + if stats.path_found: + print(f" Путь найден! Победа! Длина: {stats.path_length} шагов") + print(f" Время: {stats.time_ms:.3f} мс") + + path = strategy.find_path(maze, maze.start, maze.exit) + controller.set_path(path) + + # Интерактивное управление + print("\nДемонстрация паттерна Command:") + print(" Используйте W/A/S/D для перемещения") + print(" Нажмите U для отмены последнего хода") + print(" Нажмите Q для выхода") + print("\nТочка '.' показывает найденный путь") + print("Буква 'P' показывает текущую позицию игрока") + + controller._render() + + while True: + key = input("\n> ").lower() + if key == 'q': + break + elif key == 'w': + from visualization import Direction + controller.move(Direction.UP) + elif key == 's': + from visualization import Direction + controller.move(Direction.DOWN) + elif key == 'a': + from visualization import Direction + controller.move(Direction.LEFT) + elif key == 'd': + from visualization import Direction + controller.move(Direction.RIGHT) + elif key == 'u': + controller.undo() + print("Ход отменён!") + else: + print("Команды: W(вверх), S(вниз), A(влево), D(вправо), U(отмена), Q(выход)") + else: + print("Путь не найден, грустно") + + +def experimental_mode(): + print("эксперименты") + print("Запуск экспериментов на лабиринтах разной сложности...") + + runner = ExperimentRunner() + results = runner.run_all_experiments() + print_analysis(results) + + #графики + plot_results(results) + + +def main(): + + + print("\nВыберите режим работы:") + print(" 1. Интерактивный режим (с визуализацией)") + print(" 2. Экспериментальный режим (замеры производительности)") + print(" 3. Выход") + + choice = input("\nВаш выбор (1-3): ").strip() + + if choice == '1': + interactive_mode() + elif choice == '2': + experimental_mode() + else: + print("Adios!") + sys.exit(0) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/SimonovaMS/lab2/maze_builder.py b/SimonovaMS/lab2/maze_builder.py new file mode 100644 index 0000000..7522a27 --- /dev/null +++ b/SimonovaMS/lab2/maze_builder.py @@ -0,0 +1,65 @@ +from abc import ABC, abstractmethod +from typing import Tuple +import os +from maze_model import Maze, Cell + + +class MazeBuilder(ABC): + + @abstractmethod + def build_from_file(self, filename: str) -> Maze: + pass + + +class TextFileMazeBuilder(MazeBuilder): + + def build_from_file(self, filename: str) -> Maze: + if not os.path.exists(filename): + raise FileNotFoundError(f"Файл {filename} не найден..") + + with open(filename, 'r', encoding='utf-8') as file: + lines = [line.rstrip('\n') for line in file.readlines()] + + if not lines: + raise ValueError("Пусто(") + + height = len(lines) + width = len(lines[0]) if lines else 0 + + for i, line in enumerate(lines): + if len(line) != width: + raise ValueError(f"Лабиринт не прямоугольный, что-то не так с размерами!") + + maze = Maze(width, height) + start_found = False + exit_found = False + + for y, line in enumerate(lines): + for x, char in enumerate(line): + cell = Cell(x, y) + + if char == '#': + cell.is_wall = True + elif char == 'S': + cell.is_start = True + cell.is_wall = False + maze.start = cell + start_found = True + elif char == 'E': + cell.is_exit = True + cell.is_wall = False + maze.exit = cell + exit_found = True + elif char == ' ': + cell.is_wall = False + else: + raise ValueError(f"Недопустимый символ-'{char}' в позиции ({x}, {y}), уберите его") + + maze.set_cell(x, y, cell) + + if not start_found: + raise ValueError("В лабиринте нет начала") + if not exit_found: + raise ValueError("В лабиринте нет конца") + + return maze \ No newline at end of file diff --git a/SimonovaMS/lab2/maze_model.py b/SimonovaMS/lab2/maze_model.py new file mode 100644 index 0000000..fcb5b98 --- /dev/null +++ b/SimonovaMS/lab2/maze_model.py @@ -0,0 +1,67 @@ +# maze_model.py +from __future__ import annotations +from typing import List, Optional +from dataclasses import dataclass + + +@dataclass +class Cell: + x: int + y: int + is_wall: bool = False + is_start: bool = False + is_exit: bool = False + + def is_passable(self) -> bool: + return not self.is_wall + + def __hash__(self) -> int: + return hash((self.x, self.y)) + + def __eq__(self, other) -> bool: + if not isinstance(other, Cell): + return False + return 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]] = [] + self.start: Optional[Cell] = None + self.exit: Optional[Cell] = None + + for y in range(height): + row = [] + for x in range(width): + row.append(Cell(x, y)) + self._cells.append(row) + + def set_cell(self, x: int, y: int, cell: Cell) -> None: + if 0 <= x < self.width and 0 <= y < self.height: + self._cells[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._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: + neighbor = self.get_cell(cell.x + dx, cell.y + dy) + if neighbor and neighbor.is_passable(): + neighbors.append(neighbor) + + return neighbors + + def get_all_cells(self) -> List[Cell]: + cells = [] + for row in self._cells: + cells.extend(row) + return cells \ No newline at end of file diff --git a/SimonovaMS/lab2/maze_solver.py b/SimonovaMS/lab2/maze_solver.py new file mode 100644 index 0000000..dcb5d8c --- /dev/null +++ b/SimonovaMS/lab2/maze_solver.py @@ -0,0 +1,52 @@ +import time +from dataclasses import dataclass +from typing import List, Optional +from maze_model import Maze, Cell +from pathfinding_strategies import PathFindingStrategy + + +@dataclass +class SearchStats: + time_ms: float + visited_cells: int + path_length: int + path_found: bool + strategy_name: str + + +class MazeSolver: + def __init__(self, maze: Maze, strategy: Optional[PathFindingStrategy] = None): + self.maze = maze + self._strategy = strategy + + def set_strategy(self, strategy: PathFindingStrategy) -> None: + self._strategy = strategy + + def solve(self) -> SearchStats: + if self._strategy is None: + raise ValueError("Стратегии нет!") + + if self.maze.start is None or self.maze.exit is None: + raise ValueError("Лабиринт не содержит начала или конца") + + start_time = time.perf_counter() + + if hasattr(self._strategy, '_find_path_with_stats'): + path, visited = self._strategy._find_path_with_stats( + self.maze, self.maze.start, self.maze.exit + ) + else: + path = self._strategy.find_path( + self.maze, self.maze.start, self.maze.exit + ) + visited = 0 + + end_time = time.perf_counter() + + return SearchStats( + time_ms=(end_time - start_time) * 1000, + visited_cells=visited, + path_length=len(path) if path else 0, + path_found=len(path) > 0, + strategy_name=self._strategy.name + ) \ No newline at end of file diff --git a/SimonovaMS/lab2/otchet_l2.docx b/SimonovaMS/lab2/otchet_l2.docx new file mode 100644 index 0000000..58a0237 Binary files /dev/null and b/SimonovaMS/lab2/otchet_l2.docx differ diff --git a/SimonovaMS/lab2/pathfinding_strategies.py b/SimonovaMS/lab2/pathfinding_strategies.py new file mode 100644 index 0000000..9b29d5b --- /dev/null +++ b/SimonovaMS/lab2/pathfinding_strategies.py @@ -0,0 +1,142 @@ +from abc import ABC, abstractmethod +from typing import List, Dict, Optional, Tuple +from collections import deque +import heapq +from maze_model import Maze, Cell + + +class PathFindingStrategy(ABC):#интерфейс стратегии поиска + + @abstractmethod + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + pass + + @property + @abstractmethod + def name(self) -> str: + pass + + + +class BFSStrategy(PathFindingStrategy):#в ширину + @property + def name(self) -> str: + return "BFS" + + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + path, _ = self._find_path_with_stats(maze, start, exit_cell) + return path + + def _find_path_with_stats(self, maze: Maze, start: Cell, exit_cell: Cell) -> tuple: + if start == exit_cell: + return [start], 1 + + from collections import deque + queue = deque([start]) + visited = {start} + parent = {start: None} + + while queue: + current = queue.popleft() + + if current == exit_cell: + return self._reconstruct_path(parent, exit_cell), len(visited) + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + parent[neighbor] = current + queue.append(neighbor) + + return [], len(visited) + + def _reconstruct_path(self, parent: dict, exit_cell: Cell) -> List[Cell]: + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = parent[current] + return list(reversed(path)) + + +class DFSStrategy(PathFindingStrategy):#в глубину + @property + def name(self) -> str: + return "DFS" + + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + path, _ = self._find_path_with_stats(maze, start, exit_cell) + return path + + def _find_path_with_stats(self, maze: Maze, start: Cell, exit_cell: Cell) -> tuple: + if start == exit_cell: + return [start], 1 + + stack = [(start, [start])] + visited = {start} + + while stack: + current, path = stack.pop() + + if current == exit_cell: + return path, len(visited) + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + stack.append((neighbor, path + [neighbor])) + + return [], len(visited) + + +class AStarStrategy(PathFindingStrategy): #A* + @property + def name(self) -> str: + return "A*" + + 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) -> List[Cell]: + path, _ = self._find_path_with_stats(maze, start, exit_cell) + return path + + def _find_path_with_stats(self, maze: Maze, start: Cell, exit_cell: Cell) -> tuple: + import heapq + + if start == exit_cell: + return [start], 1 + + counter = 0 + open_set = [(0, counter, start)] + came_from = {} + visited = {start} + + g_score = {start: 0} + f_score = {start: self._heuristic(start, exit_cell)} + + while open_set: + current = heapq.heappop(open_set)[2] + + if current == exit_cell: + return self._reconstruct_path(came_from, exit_cell), len(visited) + + for neighbor in maze.get_neighbors(current): + visited.add(neighbor) + tentative_g = g_score[current] + 1 + + 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) + counter += 1 + heapq.heappush(open_set, (f_score[neighbor], counter, neighbor)) + + return [], len(visited) + + def _reconstruct_path(self, came_from: dict, current: Cell) -> List[Cell]: + path = [current] + while current in came_from: + current = came_from[current] + path.append(current) + return list(reversed(path)) \ No newline at end of file diff --git a/SimonovaMS/lab2/sample_maze.txt b/SimonovaMS/lab2/sample_maze.txt new file mode 100644 index 0000000..c51ff60 --- /dev/null +++ b/SimonovaMS/lab2/sample_maze.txt @@ -0,0 +1,10 @@ +S ##### +# # ### +# # # # +# # ### # +# # # +### # ### +# # # +# ####### +# E +########## \ No newline at end of file diff --git a/SimonovaMS/lab2/test_complex_50x50.txt b/SimonovaMS/lab2/test_complex_50x50.txt new file mode 100644 index 0000000..990951b --- /dev/null +++ b/SimonovaMS/lab2/test_complex_50x50.txt @@ -0,0 +1,50 @@ +S + ## # # ## ## ## # # ### # + # # # # # # # # # # # # # # # # + ## ### # ## ## ## # ## ## # + # # # # ## # # ## ## # # # + # ### # # # ### # # # # ## # ## + ## ### # # # # # ### # + # # ## # # # ## ## + # ## #### # # # # # # ## + ## #### ## # # # ## # # + # # # # # ### #### # # # ## + # # # # # # # # ## ## + # ## ##### ## ###### # # + ## # ## # # ## #### ## + ## ## ## ## ## # # # + # # # ## # # # # # + ## # # # # # # + ## # # # # ## # # ### # # # # + # # # # ## ## # # # + # ### ## # # # # # # + # ## # ## # ## # # #### ## # ## # + # ## ## # # # # # # ## + # # ## # ## # # # # # + # # # # # # # ### # # # ## ## + # # # ### # ## ## # # + ### ## # # ## # # + ## ### # # # # # # # + ## # # # # # ## # # ## # ### # + # # # ## # # # ## # # # + ### # # # # # # # # + # ## ## ## # # # # + ### # # # # #### # # + # ## # ### # # #### # # + # # # # # # # # ## + # # # # # # # ## # ## + # ## # ### ## ## # # # # + # # # # # # # # # # ## + ## # # ## ### # ## # # ### + # # # # # ## # # # # # # + # #### # # # #### # ## # # + # # # # ### # ## # + # # # # # # ### # # # # + ## # # # # # # #### # # + ### # ## ## # ### # # + ## # ## ## ### # # # # # # # + ### ## # # # # # # + # # # # ## ## # # + # # # ### # # # # # ## # # + # ### # # # # # ## ## ## # ## + # # # # ##### # ## # # #E \ No newline at end of file diff --git a/SimonovaMS/lab2/test_empty_50x50.txt b/SimonovaMS/lab2/test_empty_50x50.txt new file mode 100644 index 0000000..3d2a539 --- /dev/null +++ b/SimonovaMS/lab2/test_empty_50x50.txt @@ -0,0 +1,50 @@ +S + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + E \ No newline at end of file diff --git a/SimonovaMS/lab2/test_large_100x100.txt b/SimonovaMS/lab2/test_large_100x100.txt new file mode 100644 index 0000000..7d93a21 --- /dev/null +++ b/SimonovaMS/lab2/test_large_100x100.txt @@ -0,0 +1,100 @@ +S + # # # # # # # # # # # # # # # # ## ### # ## ## ## # ## ## # + # # # # ## # # ## ## # # # # # ### # # # ### # # # # ## # ## + ## ### # # # # # ### # # # ## # # # ## ## + # ## #### # # # # # # ## ## #### ## # # # ## # # + # # # # # ### #### # # # ## # # # # # # # # # ## ## + # ## ##### ## ###### # # ## # ## # # ## #### ## + ## ## ## ## ## # # # # # # ## # # # # # + ## # # # # # # ## # # # # ## # # ### # # # # + # # # # ## ## # # # # ### ## # # # # # # + # ## # ## # ## # # #### ## # ## # # # ## ## # # # # # # ## + # # ## # ## # # # # ### # # # # # # ### # # # ## ## + # # # ### # ## ## # # ### ## # # ## # # + ## ### # # # # # # # # ## # # # # # ## # # ## # ### # + # # # ## # # # ## # # # #### # # # # # # # # + # ## ## ## # # # # ### # # # # #### # # + # ## # ### # # #### # # # # # # # # # # ## + # # # # # # # ## # ### # ## # ### ## ## # # # # + # # # # # # # # # # ### ## # # ## ### # ## # # ### + # # # # # ## # # # # # # # #### # # # #### # ## # # + # # # # ### # ## # # # # # # # ### # # # # + ## # # # # # # #### # # # ### # ## ## # ### # # + ## # ## ## ### # # # # # # # ### ## # # # # # # + # # # # ## ## # # # # # ### # # # # # ## # # + # ### # # # # # ## ## ## # ## # # # # ##### # ## # # # + # # # # # # ## ## # # # # # # # # # # ## # ## # # # # # # ## + ### ## # # ##### # # # # ## # # ## # # ## # + # # # # # ## # # # ## # ## # # # # # # ### # ## ### ## + ### # ## # # # ## # # ## # # # # #### # ## #### # # # # # # # + # # # ### # ## # # ## # # # # # # # # # # ###### # + ## # ## # # # #### #### # # ##### # # # ### # # # # # + # # # # ## ### # # # # # # # ## # # ## # # ## # # # # + # # # ## # # ### # ## # # # # #### # # ## # ## + ## # ## # # ## # # # # # ## # # # # # # # # # # # # ###### # + ## # # ## ### # #### # # # # # # # # # # # # # ## # # # # # + ## # # # ## # ## # # # ### # # # # # # # # # # + # # # # ## # ### # # # # # ## ## ## # # ## # ### + ### # # # # # # ## # # ## ## # # # # # # + ## ## ## ### # # ### # # # # ### # # # # # + # # ## # # # ### ## ## ## ## # # ### # ## # # # # ## ## # + # # # # ## # # # ## # # # # ## # ### #### # ## ## + ### ### # # # ### ### # # ## # # # ### ## # ## # + # ## # # # ### #### # # # # ### # # # ## ### ## # # + #### # ### ## # # # # # # # # ### # # # # ## # ### ### + # # # # # # # # # # ## ### ## ### # ## # # # ## # #### # + ## # # # # # # # # # # # ### # # # # # ## # # # + # # ## # # # # ## # # # # ## ## ## # # ## # ## # # ## # + ## # # ## # # # # ### # # # # # # # ## # # # ## # ### ## # # + # # ## # ## ### ## # # # # ## # # # # # # + # ## # # ### # # # # # ## # # # # # ## # ## # # # + # # ## # ### # ## # # ## # # # # # # # # + # # # ## #### # # ### # ## # # ## # # ## + # # # ## # ### # ## ## # # # # ### # # # # + # # # # # # ## # ## ## ### ### # # ## # # # ## # # + # ## # # ### ##### # # # # ## # # # # # ## # # # + # # # ## # # ## # ## ## # ## # ### # # # # ## ## + # ### # ## ### # # ## # # # # # # # # # # # ## ## # # + # # # # # # # # ## # # # # # # # # # # # ## # # # ## # # + # # # ## # # ## # # ## # # # ### ### # # # # # # # + ## # ## # # # # # # # ## # # ## # ### ### # # ## ## + # ## # # #### # # # # ##### # ## #### # # # # # # #### + ## # ### ### # ## # ## # # ## # # # # # # ## #### # ## # # + # # # ## # # # # # # # ## # # # # # # + ## # ## ## # ### #### # # # # ## # ## # ## # + # # # ## ## # ## # ## # # # # # # ## + # # # # # # ### ## ### # ## # # #### # # # ##### # + ## ## # # # ## # ## ## # # # # # # # # # # ## + ##### # ### # ## # # # ## # ### #### # # ### # ## # + ### ## ## # ## # ### # ## ### # ## ## ## ## # # # # + # # ### # ## # # ## # # # # ## ## # ## # ## # + # ## # ## # ## # ## # # # # # # # # # + # ## # # # ####### # ## ## ## # # # # # # # # # ## # + # # # # ## # # ### # # # ## #### # # # # # # + ### # ### # ### # ### ## # # # # # ## # # # # # # # # + # # ##### # ## ##### #### ## # # # ## # ## # # ## # + # ### ## ## # ##### # ## # # # # # # + # # # ## ## # ## ## ## # ## # ## #### # # ## # # # # # ## + # # ## # # # #### # # ## # ## ## # # ## # ## ## # # ## # # + # # # #### # ## # # # ## ### ## #### # # # # # + ## ### # # # ## # # # # # # # ## # ## ### + # ## # ## # # # # # # # # # # # ### # # # ## # + # ## ## # #### # ## # # # # # # # + # # ## ### # # # ## ## # ## # # # ## # # # # # #### + # # ## ### # # ## ## # # # # ### # # ## # # # ## + ## # # # # ## ## # ## # # #### # # # # + # ## # # # # # # ### ## # #### # # ## # # # # ### ## # ## + ### # ## ## # # # # # # ## # # # ## # #### # ##### # + # # # # # # # ## ## ### # ### ### # # #### # # # # ## # ## + # # # #### # # # # ## # # ## # # ## # # ## # ## + # # # ## ## # ## # # # ## ## # ### ## # ## # # # # # # # + ## # # # ## # ## ## ## # # ## # # # # # ## # # # # + ### # # # ## # # # # # # # # # # # # ## # # # ## + # # # ## # # # # ## # # ## # # # # # # ## # # # # + # # # ## # ## # ### # # ### # ## # # # ## # ### # + ## # # # ## # # ## # # # ## # # #### ## # # # ### # + # #### ## ### ### # # ### # # ## # # # ### # ####### # ## + # # ## ## ### ## ### # # # # # # # # # # # + # ### # ## # ### # ## ## ## # # # # # # # ## ## # ### + # ## ### ## # # # # # # # # # # # # ### + # # # # # ## ### # # ## ## ## ### # # # # # # ## # # E \ No newline at end of file diff --git a/SimonovaMS/lab2/test_no_exit_20x20.txt b/SimonovaMS/lab2/test_no_exit_20x20.txt new file mode 100644 index 0000000..f39bf98 --- /dev/null +++ b/SimonovaMS/lab2/test_no_exit_20x20.txt @@ -0,0 +1,20 @@ +S################### +#################### +#################### +#################### +#################### +#################### +#################### +#################### +#################### +#################### +#################### +#################### +#################### +#################### +#################### +#################### +#################### +#################### +#################### +###################E \ No newline at end of file diff --git a/SimonovaMS/lab2/test_simple_10x10.txt b/SimonovaMS/lab2/test_simple_10x10.txt new file mode 100644 index 0000000..9bd9e1b --- /dev/null +++ b/SimonovaMS/lab2/test_simple_10x10.txt @@ -0,0 +1,10 @@ +S E + + + + + + + + + \ No newline at end of file diff --git a/SimonovaMS/lab2/visualization.py b/SimonovaMS/lab2/visualization.py new file mode 100644 index 0000000..39a542a --- /dev/null +++ b/SimonovaMS/lab2/visualization.py @@ -0,0 +1,160 @@ +from abc import ABC, abstractmethod +from typing import List, Optional, Set +from enum import Enum +from maze_model import Maze, Cell + + +class EventType(Enum): + PATH_FOUND = "path_found" + MOVE = "move" + MAZE_LOADED = "maze_loaded" + SOLVE_START = "solve_start" + SOLVE_END = "solve_end" + + +class Observer(ABC): + + @abstractmethod + def update(self, event_type: EventType, data: any) -> None: + pass + + +class ConsoleView(Observer): + + def __init__(self): + self.last_path: Optional[List[Cell]] = None + + def update(self, event_type: EventType, data: any) -> None: + if event_type == EventType.MAZE_LOADED: + print("Лабиринт загружен") + elif event_type == EventType.SOLVE_START: + print("Начинается поиск пути...") + elif event_type == EventType.SOLVE_END: + print(f"Поиск завершён. Статистика: {data}") + elif event_type == EventType.PATH_FOUND: + self.last_path = data + + def render(self, maze: Maze, player_pos: Optional[Cell] = None, + path: Optional[List[Cell]] = None) -> None: #рисует лаб + import os + os.system('cls' if os.name == 'nt' else 'clear') + + path_set = set(path) if path else set() + + # Верх + print("┌" + "─" * maze.width + "┐") + + for y in range(maze.height): + line = "│" + for x in range(maze.width): + cell = maze.get_cell(x, y) + if player_pos and player_pos.x == x and player_pos.y == y: + line += "P" + elif cell == maze.start: + line += "S" + elif cell == maze.exit: + line += "E" + elif cell is not None and cell.is_wall: + line += "#" + elif path and cell in path_set: + line += "." + else: + line += " " + line += "│" + print(line) + + # Низ + print("└" + "─" * maze.width + "┘") + + if path: + print(f"\nПуть найден! Длина: {len(path)} шагов") + elif path == []: + print("\nПуть не найден:(") + + +class Player: + + def __init__(self, start_cell: Cell): + self.current_cell = start_cell + + def move_to(self, cell: Cell) -> None: + self.current_cell = cell + + def get_position(self) -> Cell: + return self.current_cell + + +class Direction(Enum): + UP = (0, -1) + DOWN = (0, 1) + LEFT = (-1, 0) + RIGHT = (1, 0) + + +class Command(ABC): + + @abstractmethod + def execute(self) -> None: + pass + + @abstractmethod + def undo(self) -> None: + pass + + +class MoveCommand(Command): + + def __init__(self, player: Player, maze: Maze, direction: Direction): + self.player = player + self.maze = maze + self.direction = direction + self.previous_cell = player.current_cell + + def execute(self) -> None: + dx, dy = self.direction.value + 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.previous_cell = self.player.current_cell + self.player.move_to(new_cell) + return True + return False + + def undo(self) -> None: + self.player.move_to(self.previous_cell) + + +class GameController: + + def __init__(self, maze: Maze, view: ConsoleView): + if maze.start is None: + raise ValueError("Лабиринт не имеет стартовой клетки") + + self.maze = maze + self.view = view + self.player = Player(maze.start) + self.command_history: List[Command] = [] + self.found_path: Optional[List[Cell]] = None + + def move(self, direction: Direction) -> bool: + command = MoveCommand(self.player, self.maze, direction) + if command.execute(): + self.command_history.append(command) + self._render() + return True + return False + + def undo(self) -> None: + if self.command_history: + command = self.command_history.pop() + command.undo() + self._render() + + def set_path(self, path: List[Cell]) -> None: + self.found_path = path + self._render() + + def _render(self) -> None: + self.view.render(self.maze, self.player.get_position(), self.found_path) \ No newline at end of file