diff --git a/starikovta/docs/task2/report.md b/starikovta/docs/task2/report.md new file mode 100644 index 0000000..e69de29 diff --git a/starikovta/maze.txt b/starikovta/maze.txt new file mode 100644 index 0000000..991c57b --- /dev/null +++ b/starikovta/maze.txt @@ -0,0 +1,12 @@ +########### +S # +# ####### # +# # # # +# # ### # # +# # # # # +# ### # # # +# # # +####### # # +# E +########### + diff --git a/starikovta/maze_results.csv b/starikovta/maze_results.csv new file mode 100644 index 0000000..40b3ab7 --- /dev/null +++ b/starikovta/maze_results.csv @@ -0,0 +1,4 @@ +Стратегия,Время(мс),Посещено клеток,Длина пути,Путь найден +BFS,0.16149994917213917,31,31,True +DFS,0.17100002150982618,31,31,True +A*,0.3127999370917678,31,31,True diff --git a/starikovta/maze_solver.py b/starikovta/maze_solver.py new file mode 100644 index 0000000..7f121ff --- /dev/null +++ b/starikovta/maze_solver.py @@ -0,0 +1,444 @@ +import time +import csv +import os +from collections import deque +import heapq +from abc import ABC, abstractmethod + +# ========== ЭТАП 1: МОДЕЛЬ ЛАБИРИНТА ========== + +class Cell: + """Клетка лабиринта""" + def __init__(self, x, y, is_wall=False, is_start=False, is_exit=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): + return not self.is_wall + + +class Maze: + """Лабиринт: сетка клеток + старт + выход""" + def __init__(self, width, height): + self.width = width + self.height = height + self.grid = [[Cell(x, y) for x in range(width)] for y in range(height)] + self.start = None + self.exit = None + + def get_cell(self, x, y): + if 0 <= x < self.width and 0 <= y < self.height: + return self.grid[y][x] + return None + + def get_neighbors(self, 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 + + def set_cell(self, x, y, cell): + if 0 <= x < self.width and 0 <= y < self.height: + self.grid[y][x] = cell + + +# ========== ЭТАП 2: ПАТТЕРН BUILDER ========== + +class MazeBuilder(ABC): + """Интерфейс строителя лабиринта""" + @abstractmethod + def build_from_file(self, filename): + pass + + +class TextFileMazeBuilder(MazeBuilder): + """Загружает лабиринт из текстового файла""" + + def build_from_file(self, filename): + with open(filename, 'r', encoding='utf-8') as f: + lines = f.readlines() + + # Убираем лишние пробелы и переносы строк + lines = [line.rstrip('\n\r') for line in lines] + + height = len(lines) + width = len(lines[0]) if height > 0 else 0 + + maze = Maze(width, height) + start = None + exit_cell = None + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + if ch == '#': + cell = Cell(x, y, is_wall=True) + elif ch == 'S': + cell = Cell(x, y, is_start=True) + start = cell + elif ch == 'E': + cell = Cell(x, y, is_exit=True) + exit_cell = cell + else: # пробел или '.' — проход + cell = Cell(x, y, is_wall=False) + + maze.set_cell(x, y, cell) + + maze.start = start + maze.exit = exit_cell + + # Валидация: проверяем, что есть и старт, и выход + if maze.start is None or maze.exit is None: + raise ValueError("В лабиринте должны быть S (старт) и E (выход)") + + return maze + + +# ========== ЭТАП 3: ПАТТЕРН STRATEGY ========== + +class PathFindingStrategy(ABC): + """Интерфейс стратегии поиска пути""" + @abstractmethod + def find_path(self, maze, start, exit_cell): + pass + + +def reconstruct_path(parents, start, exit_cell): + """Восстанавливает путь от выхода до старта""" + path = [] + current = exit_cell + while current != start: + path.append(current) + current = parents.get((current.x, current.y)) + if current is None: + return [] + path.append(start) + path.reverse() + return path + + +class BFSStrategy(PathFindingStrategy): + """Поиск в ширину (гарантирует кратчайший путь)""" + + def find_path(self, maze, start, exit_cell): + queue = deque() + queue.append(start) + visited = {(start.x, start.y)} + parents = {} + + while queue: + current = queue.popleft() + + if current == exit_cell: + return reconstruct_path(parents, start, exit_cell) + + for neighbor in maze.get_neighbors(current): + if (neighbor.x, neighbor.y) not in visited: + visited.add((neighbor.x, neighbor.y)) + parents[(neighbor.x, neighbor.y)] = current + queue.append(neighbor) + + return [] # Путь не найден + + +class DFSStrategy(PathFindingStrategy): + """Поиск в глубину (быстрый, но не гарантирует кратчайший путь)""" + + def find_path(self, maze, start, exit_cell): + stack = [start] + visited = {(start.x, start.y)} + parents = {} + + while stack: + current = stack.pop() + + if current == exit_cell: + return reconstruct_path(parents, start, exit_cell) + + for neighbor in maze.get_neighbors(current): + if (neighbor.x, neighbor.y) not in visited: + visited.add((neighbor.x, neighbor.y)) + parents[(neighbor.x, neighbor.y)] = current + stack.append(neighbor) + + return [] + + +class AStarStrategy(PathFindingStrategy): + """A* с манхэттенской эвристикой""" + + def _heuristic(self, cell, exit_cell): + """Манхэттенское расстояние""" + return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y) + + def find_path(self, maze, start, exit_cell): + # Приоритетная очередь: (f_score, counter, клетка) + counter = 0 + open_set = [(0, counter, start)] + heapq.heapify(open_set) + + came_from = {} + + g_score = {(start.x, start.y): 0} + f_score = {(start.x, start.y): self._heuristic(start, exit_cell)} + + while open_set: + current = heapq.heappop(open_set)[2] + + if current == exit_cell: + return reconstruct_path(came_from, start, exit_cell) + + for neighbor in maze.get_neighbors(current): + tentative_g = g_score.get((current.x, current.y), float('inf')) + 1 + + if tentative_g < g_score.get((neighbor.x, neighbor.y), float('inf')): + came_from[(neighbor.x, neighbor.y)] = current + g_score[(neighbor.x, neighbor.y)] = tentative_g + f = tentative_g + self._heuristic(neighbor, exit_cell) + f_score[(neighbor.x, neighbor.y)] = f + counter += 1 + heapq.heappush(open_set, (f, counter, neighbor)) + + return [] + + +# ========== ЭТАП 4: КЛАСС-ОРКЕСТРАТОР ========== + +class SearchStats: + """Статистика поиска""" + def __init__(self, time_ms=0, visited_cells=0, path_length=0, path_found=False): + self.time_ms = time_ms + self.visited_cells = visited_cells + self.path_length = path_length + self.path_found = path_found + + def __repr__(self): + return f"Stats(time={self.time_ms:.3f}ms, visited={self.visited_cells}, length={self.path_length}, found={self.path_found})" + + +class MazeSolver: + """Оркестратор: принимает лабиринт и стратегию, выполняет поиск""" + + def __init__(self, maze, strategy=None): + self.maze = maze + self.strategy = strategy + self.observers = [] + + def set_strategy(self, strategy): + self.strategy = strategy + + def add_observer(self, observer): + self.observers.append(observer) + + def _notify(self, event): + for observer in self.observers: + observer.update(event) + + def solve(self): + if self.strategy is None: + raise ValueError("Стратегия не установлена") + + if self.maze.start is None or self.maze.exit is None: + raise ValueError("Лабиринт не содержит старт или выход") + + self._notify("Начало поиска пути...") + + 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 + + stats = SearchStats( + time_ms=time_ms, + visited_cells=len(path) if path else 0, + path_length=len(path), + path_found=bool(path) + ) + + self._notify(f"Поиск завершён. Путь найден: {stats.path_found}") + + return path, stats + + +# ========== ЭТАП 5: ПАТТЕРНЫ OBSERVER И COMMAND ========== + +class Observer(ABC): + @abstractmethod + def update(self, event): + pass + + +class ConsoleView(Observer): + """Визуализация лабиринта в консоли""" + + def __init__(self): + self.last_path = [] + + def render(self, maze, path=None, player_pos=None): + """Отрисовывает лабиринт в консоли""" + # Очистка консоли + os.system('cls' if os.name == 'nt' else 'clear') + + symbols = { + 'wall': '#', + 'path': '.', + 'start': 'S', + 'exit': 'E', + 'player': 'P', + 'way': 'O' + } + + 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, y): + line += symbols['player'] + elif cell == maze.start: + line += symbols['start'] + elif cell == maze.exit: + line += symbols['exit'] + elif cell.is_wall: + line += symbols['wall'] + elif path and cell in path: + line += symbols['way'] + else: + line += symbols['path'] + print(line) + + print(f"\nРазмер: {maze.width}x{maze.height}") + + def update(self, event): + print(f"[ConsoleView] {event}") + + +class Command(ABC): + @abstractmethod + def execute(self): + pass + + @abstractmethod + def undo(self): + pass + + +class Player: + """Игрок, который может перемещаться по лабиринту""" + def __init__(self, start_cell): + self.current = start_cell + self.previous = start_cell + + def move_to(self, cell): + self.previous = self.current + self.current = cell + + def undo_move(self): + self.current, self.previous = self.previous, self.current + + +class MoveCommand(Command): + """Команда перемещения игрока""" + def __init__(self, player, maze, direction): + self.player = player + self.maze = maze + self.direction = direction + self.executed = False + + def execute(self): + dx, dy = self.direction + new_cell = self.maze.get_cell(self.player.current.x + dx, self.player.current.y + dy) + + if new_cell and new_cell.is_passable(): + self.player.move_to(new_cell) + self.executed = True + return True + return False + + def undo(self): + if self.executed: + self.player.undo_move() + self.executed = False + + +# ========== ДЕМОНСТРАЦИЯ РАБОТЫ ========== + +def create_test_maze(): + """Создаёт тестовый лабиринт для проверки (если нет файла)""" + maze = Maze(10, 10) + + # Заполняем стенами + for y in range(10): + for x in range(10): + maze.set_cell(x, y, Cell(x, y, is_wall=True)) + + # Создаём коридор + for x in range(10): + maze.set_cell(x, 5, Cell(x, 5, is_wall=False)) + + maze.set_cell(0, 5, Cell(0, 5, is_start=True)) + maze.set_cell(9, 5, Cell(9, 5, is_exit=True)) + maze.start = maze.get_cell(0, 5) + maze.exit = maze.get_cell(9, 5) + + return maze + + +if __name__ == "__main__": + print("=== Лабиринт: поиск выхода ===\n") + + # Создаём лабиринт через Builder (или тестовый) + builder = TextFileMazeBuilder() + + try: + # Попробуем загрузить из файла + maze = builder.build_from_file("maze.txt") + print("Лабиринт загружен из файла maze.txt") + except FileNotFoundError: + print("Файл maze.txt не найден. Использую тестовый лабиринт.") + maze = create_test_maze() + + # Создаём визуализацию + view = ConsoleView() + + # Тестируем стратегии + strategies = { + "BFS": BFSStrategy(), + "DFS": DFSStrategy(), + "A*": AStarStrategy() + } + + solver = MazeSolver(maze) + solver.add_observer(view) + + results = [] + + for name, strategy in strategies.items(): + print(f"\n--- {name} ---") + solver.set_strategy(strategy) + path, stats = solver.solve() + + results.append([name, stats.time_ms, stats.visited_cells, stats.path_length, stats.path_found]) + + view.render(maze, path) + print(f"Время: {stats.time_ms:.3f} мс") + print(f"Длина пути: {stats.path_length}") + + input("\nНажми Enter для продолжения...") + + # Сохраняем результаты + 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") \ No newline at end of file