diff --git a/novikovsd/lab2_results/experiment_results.csv b/novikovsd/lab2_results/experiment_results.csv new file mode 100644 index 0000000..c72a9e5 --- /dev/null +++ b/novikovsd/lab2_results/experiment_results.csv @@ -0,0 +1,13 @@ +maze,strategy,avg_time_ms,avg_visited,avg_path_length +small,BFS,0.09427999993931735,64.0,15.0 +small,DFS,0.07471999997505918,64.0,29.0 +small,AStar,0.1291799999307841,64.0,15.0 +medium,BFS,3.0494200002067373,2158.0,96.0 +medium,DFS,6.729340000129014,2158.0,860.0 +medium,AStar,4.80197999986558,2154.0,96.0 +large,BFS,11.303859999861743,7634.0,0.0 +large,DFS,56.53439999987313,7634.0,0.0 +large,AStar,18.463099999826227,7993.0,0.0 +empty,BFS,3.3649599998170743,2305.0,96.0 +empty,DFS,9.518800000114425,2305.0,1130.0 +empty,AStar,5.252400000244961,2305.0,96.0 diff --git a/novikovsd/lab2_results/maze_empty.txt b/novikovsd/lab2_results/maze_empty.txt new file mode 100644 index 0000000..1b20a83 --- /dev/null +++ b/novikovsd/lab2_results/maze_empty.txt @@ -0,0 +1,50 @@ +################################################## +S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +################################################## \ No newline at end of file diff --git a/novikovsd/lab2_results/maze_large.txt b/novikovsd/lab2_results/maze_large.txt new file mode 100644 index 0000000..937a2d0 --- /dev/null +++ b/novikovsd/lab2_results/maze_large.txt @@ -0,0 +1,100 @@ +#################################################################################################### +S # # # ## # # ## # # # # # # ### # # +# # # # # # # # # # # # # # # ## # # # +# # # # # # # ## # # # # # ### # # # +# ## ## # # # # ## # # # # # # # +# # ## ## # # # # # # # # # # # +# # # # # # # # ## #### # # ## # # # # # # +# # # # # # ### # ## ## # ## # ## # #### # +# ## # ## # # # # # # # # # # +# # # # # # ## # # # ## # # +## # # # # ## # # # # # # # # +## # # # # # ## # # # # # ## # ## ## # # # ## # # # +# # # # ## # # # # # # # ### # # # # # # +## # ## ## # # # ## # ## # # # ### # # # +# ## # # ## # # # # # # # # # +# ## # # # ## # # # # # # # # # # # ### # # # +# # # # # # ## # # # # # # # # # +# # # #### # # # ## ## # # # # # # +# # # # # ## # # # # # ## # ## # ## +# # # # # # # # # # # # ### # # # +## ## # ## # ## # # # # # # # # # # # # # +# ## # # ## # # # # # # # # # # # # +# # # # # # ## # # # # #### # ### # +# # # ## # ## # # # # ## ## ## # # # # # # # # +# ## # # # # # # ## # # # +# # # ## # # # ## # ### # # # # # ## ## # ## # +# # # ## # # # # # # ## # # ## +# # # # # # ## # # # # # # ## # # # # # ## ## # +# # # ## # # ## # # # # # # # # # # # +## # # # # # ### # # ## ## # # # ## # ## # # +# # # # # # # # ## # # # # # # # # # # # # +# # ## # # # # # # # # ## # ## ## # # # ## # # # # +# ### # # # # # # # # # # # # # # # # +# # # # # # # ## ## # # # # # # # ## +## # # # ### # # # # # # # # # ## # # +## # # # # # # # # # # # ##### # # ## # ### # # +## # # # # # # # # # # # # # # # # # # ## ## +# # ### # # # # # # # # # ### +# # # # # # # ## # ## # ## # # # +# # # # ## # # ## # # # # # # ## # ## # +# ## # # # # # # ### # # # # # # # +# # ### # # # ## # # # # # # # # # ## # # # # # +# ## # ## # # # ### # ## # ## ## ## # +# # ## ### # # # # ## # # # # # +# ## # # # # # ## # # ## ## ## #### # # +# # # # # # # # # # # # ### ## # # +# # # # # # # # # # # ## ### # # # # ## ## # ## # +# # # # # # # # ## # # # # ## # # # +# ## # # # ## # # # # ## # # ## # # # # ## # ## # +# # # # # # ## # # # # # # # # # # # # # # # +## ## ### # # # # # # # # # # # +# ## # # ## # # ## # # # # # # # # # +# # # ## # # # ## # # # # # # # +# # # # # # ## # # # # ## # # # # ## # +# # # ## # ### # ## # # # # ### # # # +# # # # # # # # # # ## # # ### # ## # ## # +# # # # # # # ### ### # # ## # # # ## # # +# # # # ## # # ## # ## ## # ## # ### # # # # +# ## ## # ### ## # # # # # # # # # # # # # ## +# ## # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # ## +# # # ## # # # # # # # # # ## # ## # +## # # ## ## ## # # ## # # # # # ### # ## ## # # +# # # # # # # # # # ### # # ## # # ## # # # # # +# #### # ## # # # # ## # # # # # # +# # # # # ## ## # ## ### # # # # ## # +# # ## # # # # ## # ## # +# # # # # # # # # # # ## ## # # ### +### # ## # # ## ## # # ## # # ## # # # # # +# # # # ## #### # # # ## # ## # ### #### # +# # ### ## # # # # ## # ## # ## # # ## +# ## ## # # # # # ### # ## # # # # # +## # ## ## # # # ## ## # # # ## # # # +# # # # # # # ## # # #### # # ## # ## # +# # # # # ## # # ## # # # # # +# # ### # # ### # # # # # # ### ## # # +## # ## # # # # # # # # # # # ### # #### ### +# # # ## # # # # # # # ## # # ### ## +# # # # # # # # ## ## # ## ## # ## # # +## ### # # ## # ## # # ### # # # # +# ## # # ## # # # # # # # # # # # # +# # # ## # # # # # # # # ## ### # # # +## # # # # ## # ## ### # ## # # # # +# # # # # # # # ## # # # # # ## # ### # +# # ## # # # # # # # ## # # # # ## +## # ## # # ## # # # # # ## # # # # # # +# # # # # # # # ### # # # # # ## ## # # ## # +# ## # # # # # # # # ## # # # # ### # +# ## #### # # # ## ## ### ## ## # +# # # # # ## # # ## # ## ## ## # # # # # +# # # ### # ### ## # # # ## # # # ## # # # # # #### # # +# # # # ## # # # # # # # # ## # # # ## ## # +# # # # # # # # ### # # # # # # # # # # # # # +# ## # ## # ## ## # # # # # # # # ## # # +# # ## # # # # # # # # # # # # # # ## # # +## # # # # # ## # # # # # # # # # +# # # # ## # ### # # ### # # ## ### ## # # +# # # # # # ## # ## # #### ## # # ## # ## +# # # ## ## # # # # ## # # # # ## ## # #E# +#################################################################################################### \ No newline at end of file diff --git a/novikovsd/lab2_results/maze_medium.txt b/novikovsd/lab2_results/maze_medium.txt new file mode 100644 index 0000000..5ebd88f --- /dev/null +++ b/novikovsd/lab2_results/maze_medium.txt @@ -0,0 +1,50 @@ +################################################## +S # +# # +# # +# # +# # # # # # # # +# # +# # +# # # # # # # # # # # # # # # # ## +# # +# # # # # # # # +# # +# # +# # +# # +# # # # # # # # +# # # # # # # # # # # # # # # # ## +# # +# # +# # +# # # # # # # # +# # +# # +# # +# # # # # # # # # # # # # # # # ## +# # # # # # # # +# # +# # +# # +# # +# # # # # # # # +# # +# # # # # # # # # # # # # # # # ## +# # +# # +# # # # # # # # +# # +# # +# # +# # +# # ## # # ## # # # ## # # ## # # # ## +# # +# # +# # +# # +# # # # # # # # +# # +# # +# # # # # # # # # # # # # # # # E# +################################################## \ No newline at end of file diff --git a/novikovsd/lab2_results/maze_no_exit.txt b/novikovsd/lab2_results/maze_no_exit.txt new file mode 100644 index 0000000..17a2e6e --- /dev/null +++ b/novikovsd/lab2_results/maze_no_exit.txt @@ -0,0 +1,10 @@ +########## +#S # +# # +# # +# # +# # +# # +# # +# # +########## \ No newline at end of file diff --git a/novikovsd/lab2_results/maze_small.txt b/novikovsd/lab2_results/maze_small.txt new file mode 100644 index 0000000..db91695 --- /dev/null +++ b/novikovsd/lab2_results/maze_small.txt @@ -0,0 +1,10 @@ +########## +#S # +# # +# # +# # +# # +# # +# # +# E# +########## \ No newline at end of file diff --git a/novikovsd/lab2_results/plot_empty.png b/novikovsd/lab2_results/plot_empty.png new file mode 100644 index 0000000..87fdc28 Binary files /dev/null and b/novikovsd/lab2_results/plot_empty.png differ diff --git a/novikovsd/lab2_results/plot_large.png b/novikovsd/lab2_results/plot_large.png new file mode 100644 index 0000000..7b1b667 Binary files /dev/null and b/novikovsd/lab2_results/plot_large.png differ diff --git a/novikovsd/lab2_results/plot_medium.png b/novikovsd/lab2_results/plot_medium.png new file mode 100644 index 0000000..1633751 Binary files /dev/null and b/novikovsd/lab2_results/plot_medium.png differ diff --git a/novikovsd/lab2_results/plot_small.png b/novikovsd/lab2_results/plot_small.png new file mode 100644 index 0000000..b67c558 Binary files /dev/null and b/novikovsd/lab2_results/plot_small.png differ diff --git a/novikovsd/lab2_results/диограмма.png b/novikovsd/lab2_results/диограмма.png new file mode 100644 index 0000000..d600c21 Binary files /dev/null and b/novikovsd/lab2_results/диограмма.png differ diff --git a/novikovsd/maze.py b/novikovsd/maze.py new file mode 100644 index 0000000..fd23ea5 --- /dev/null +++ b/novikovsd/maze.py @@ -0,0 +1,567 @@ +import time +import csv +from collections import deque +from heapq import heappush, heappop +from abc import ABC, abstractmethod +from typing import List, Optional, Tuple, Dict, Any +import os + +RESULTS_DIR = "lab2_results" + +class Cell: + def __init__(self, x: int, y: int, is_wall: bool = False): + self.x = x + self.y = y + self.is_wall = is_wall + self.is_start = False + self.is_exit = False + + def is_passable(self) -> bool: + return not self.is_wall + + def __repr__(self): + return f"Cell({self.x},{self.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 + + def set_cell(self, x: int, y: int, cell: Cell): + if not self.cells: + self.cells = [[None] * self.width for _ in range(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 = [] + for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]: + 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 = [line.rstrip('\n') for line in f.readlines()] + + if not lines: + raise ValueError("Файл пуст") + + height = len(lines) + width = max(len(line) for line in lines) + maze = Maze(width, height) + + start_cell = None + exit_cell = None + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + is_wall = (ch == '#') + cell = Cell(x, y, is_wall) + if ch == 'S': + cell.is_start = True + start_cell = cell + elif ch == 'E': + cell.is_exit = True + exit_cell = cell + maze.set_cell(x, y, cell) + + if start_cell is None or exit_cell is None: + for y in range(height): + for x in range(width): + cell = maze.get_cell(x, y) + if cell and cell.is_start: + start_cell = cell + if cell and cell.is_exit: + exit_cell = cell + + if start_cell is None: + raise ValueError("Нет стартовой клетки (S)") + if exit_cell is None: + raise ValueError("Нет выходной клетки (E)") + + maze.start = start_cell + maze.exit = exit_cell + return maze + +class PathFindingStrategy(ABC): + @abstractmethod + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: + pass + +class BFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: + if start == exit: + self.last_visited = 1 + return [start] + + queue = deque() + queue.append(start) + parent = {start: None} + visited = {start} + visited_count = 1 + + while queue: + current = queue.popleft() + if current == exit: + break + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + visited_count += 1 + parent[neighbor] = current + queue.append(neighbor) + + self.last_visited = visited_count + if exit not in parent: + return [] + + path = [] + cur = exit + while cur is not None: + path.append(cur) + cur = parent[cur] + path.reverse() + return path + +class DFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: + stack = [(start, [start])] + visited = {start} + visited_count = 1 + + while stack: + current, path = stack.pop() + if current == exit: + self.last_visited = visited_count + return path + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + visited_count += 1 + stack.append((neighbor, path + [neighbor])) + self.last_visited = visited_count + 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) -> List[Cell]: + open_set = [] + counter = 0 + heappush(open_set, (0, counter, start)) + came_from = {} + g_score = {start: 0} + f_score = {start: self.heuristic(start, exit)} + visited_count = 0 + + while open_set: + _, _, current = heappop(open_set) + visited_count += 1 + if current == exit: + path = [] + while current in came_from: + path.append(current) + current = came_from[current] + path.append(start) + path.reverse() + self.last_visited = visited_count + return path + + for neighbor in maze.get_neighbors(current): + 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 = tentative_g + self.heuristic(neighbor, exit) + f_score[neighbor] = f + counter += 1 + heappush(open_set, (f, counter, neighbor)) + self.last_visited = visited_count + return [] + +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 __repr__(self): + return f"Stats(time={self.time_ms:.2f}ms, visited={self.visited_cells}, path_len={self.path_length})" + +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 attach(self, observer): + self.observers.append(observer) + + def detach(self, observer): + self.observers.remove(observer) + + def notify(self, event: str, data: Any = None): + for obs in self.observers: + obs.update(event, data) + + def solve(self) -> Tuple[List[Cell], SearchStats]: + start_time = time.perf_counter() + path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) + end_time = time.perf_counter() + elapsed_ms = (end_time - start_time) * 1000.0 + visited_cells = getattr(self.strategy, 'last_visited', len(path) if path else 0) + stats = SearchStats(elapsed_ms, visited_cells, len(path)) + self.notify("solved", {"path": path, "stats": stats}) + return path, stats + +class Observer(ABC): + @abstractmethod + def update(self, event: str, data: Any): + pass + +class ConsoleView(Observer): + def __init__(self): + self.player_pos = None + self.path = [] + + def update(self, event: str, data: Any): + if event == "maze_loaded": + self.maze = data["maze"] + self.render() + elif event == "player_moved": + self.player_pos = data["player_cell"] + self.render() + elif event == "path_found": + self.path = data["path"] + self.render() + elif event == "solved": + self.path = data["path"] + self.render() + + def render(self, maze: Maze = None, player_cell: Cell = None, path: List[Cell] = None): + if maze: + self.maze = maze + if player_cell: + self.player_pos = player_cell + if path is not None: + self.path = path + + if not hasattr(self, 'maze'): + print("Нет лабиринта для отображения") + return + + for y in range(self.maze.height): + row = "" + for x in range(self.maze.width): + cell = self.maze.get_cell(x, y) + if cell is None: + row += " " + continue + if self.player_pos and cell == self.player_pos: + row += "P" + elif cell == self.maze.start: + row += "S" + elif cell == self.maze.exit: + row += "E" + elif self.path and cell in self.path: + row += "." + elif cell.is_wall: + row += "#" + else: + row += " " + print(row) + print() + +class MoveCommand(ABC): + @abstractmethod + def execute(self): + pass + @abstractmethod + def undo(self): + pass + +class Player: + def __init__(self, start_cell: Cell): + self.current_cell = start_cell + + def move_to(self, cell: Cell): + self.current_cell = cell + +class MoveCommandImpl(MoveCommand): + def __init__(self, player: Player, direction: str, maze: Maze): + self.player = player + self.direction = direction + self.maze = maze + self.previous_cell = player.current_cell + + def execute(self): + 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 + else: + return False + + 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.player.move_to(new_cell) + return True + return False + + def undo(self): + self.player.move_to(self.previous_cell) + +def ensure_results_dir(): + if not os.path.exists(RESULTS_DIR): + os.makedirs(RESULTS_DIR) + print(f"Создана папка: {RESULTS_DIR}") + +def generate_test_maze_file(filename: str, maze_type: str): + full_path = os.path.join(RESULTS_DIR, filename) + if maze_type == "small": + lines = [ + "##########", + "#S #", + "# #", + "# #", + "# #", + "# #", + "# #", + "# #", + "# E#", + "##########" + ] + elif maze_type == "medium": + height, width = 50, 50 + lines = [] + for y in range(height): + row = [] + for x in range(width): + if y == 0 or y == height-1 or x == 0 or x == width-1: + row.append('#') + elif (y % 5 == 0 and x % 7 == 0) or (y % 8 == 0 and x % 3 == 0): + row.append('#') + else: + row.append(' ') + row_str = ''.join(row) + lines.append(row_str) + lines[1] = 'S' + lines[1][1:] + lines[height-2] = lines[height-2][:width-2] + 'E' + lines[height-2][width-1:] + elif maze_type == "large": + import random + height, width = 100, 100 + random.seed(42) + lines = [] + for y in range(height): + row = [] + for x in range(width): + if y == 0 or y == height-1 or x == 0 or x == width-1: + row.append('#') + else: + if random.random() < 0.2: + row.append('#') + else: + row.append(' ') + lines.append(''.join(row)) + lines[1] = 'S' + lines[1][1:] + lines[height-2] = lines[height-2][:width-2] + 'E' + lines[height-2][width-1:] + elif maze_type == "empty": + height, width = 50, 50 + lines = [] + for y in range(height): + if y == 0 or y == height-1: + lines.append('#' * width) + else: + lines.append('#' + ' ' * (width-2) + '#') + lines[1] = 'S' + lines[1][1:] + lines[height-2] = lines[height-2][:width-2] + 'E' + lines[height-2][width-1:] + elif maze_type == "no_exit": + lines = [ + "##########", + "#S #", + "# #", + "# #", + "# #", + "# #", + "# #", + "# #", + "# #", + "##########" + ] + else: + raise ValueError("Unknown maze type") + + with open(full_path, 'w', encoding='utf-8') as f: + f.write('\n'.join(lines)) + +def run_experiment(): + ensure_results_dir() + maze_types = ["small", "medium", "large", "empty", "no_exit"] + strategies = { + "BFS": BFSStrategy(), + "DFS": DFSStrategy(), + "AStar": AStarStrategy() + } + results = [] + + for maze_type in maze_types: + filename = f"maze_{maze_type}.txt" + generate_test_maze_file(filename, maze_type) + full_path = os.path.join(RESULTS_DIR, filename) + builder = TextFileMazeBuilder() + try: + maze = builder.build_from_file(full_path) + except ValueError as e: + print(f"Лабиринт {maze_type} пропущен: {e}") + continue + + for strat_name, strat_obj in strategies.items(): + times = [] + path_lengths = [] + visited_counts = [] + for run in range(5): + solver = MazeSolver(maze, strat_obj) + path, stats = solver.solve() + times.append(stats.time_ms) + path_lengths.append(stats.path_length) + visited_counts.append(stats.visited_cells) + avg_time = sum(times) / len(times) + avg_path_len = sum(path_lengths) / len(path_lengths) + avg_visited = sum(visited_counts) / len(visited_counts) + results.append({ + "maze": maze_type, + "strategy": strat_name, + "avg_time_ms": avg_time, + "avg_visited": avg_visited, + "avg_path_length": avg_path_len + }) + print(f"{maze_type} / {strat_name}: время={avg_time:.2f}ms, посещено={avg_visited:.1f}, путь={avg_path_len:.1f}") + + csv_path = os.path.join(RESULTS_DIR, "experiment_results.csv") + with open(csv_path, "w", newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=["maze", "strategy", "avg_time_ms", "avg_visited", "avg_path_length"]) + writer.writeheader() + writer.writerows(results) + try: + import matplotlib.pyplot as plt + for maze_type in ["small", "medium", "large", "empty"]: + data = [r for r in results if r["maze"] == maze_type] + if not data: + continue + names = [d["strategy"] for d in data] + times = [d["avg_time_ms"] for d in data] + visited = [d["avg_visited"] for d in data] + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4)) + ax1.bar(names, times) + ax1.set_title(f"Время (мс) - {maze_type}") + ax2.bar(names, visited) + ax2.set_title(f"Посещено клеток - {maze_type}") + plt.tight_layout() + plot_path = os.path.join(RESULTS_DIR, f"plot_{maze_type}.png") + plt.savefig(plot_path) + plt.close() + print(f"Графики сохранены в папку {RESULTS_DIR}") + except ImportError: + print("matplotlib не установлен. Графики не построены.") + +def demo_interactive(): + ensure_results_dir() + builder = TextFileMazeBuilder() + filename = input("Введите имя файла с лабиринтом (например, maze_small.txt): ").strip() + if not os.path.exists(filename) and not os.path.exists(os.path.join(RESULTS_DIR, filename)): + print(f"Файл {filename} не найден. Создаю тестовый лабиринт small в папке {RESULTS_DIR}") + generate_test_maze_file("demo_maze.txt", "small") + filename = os.path.join(RESULTS_DIR, "demo_maze.txt") + elif os.path.exists(os.path.join(RESULTS_DIR, filename)): + filename = os.path.join(RESULTS_DIR, filename) + + maze = builder.build_from_file(filename) + view = ConsoleView() + view.update("maze_loaded", {"maze": maze}) + + print("Выберите алгоритм поиска:") + print("1. BFS") + print("2. DFS") + print("3. A*") + choice = input("Ваш выбор: ") + if choice == "1": + strategy = BFSStrategy() + elif choice == "2": + strategy = DFSStrategy() + else: + strategy = AStarStrategy() + + solver = MazeSolver(maze, strategy) + solver.attach(view) + path, stats = solver.solve() + print(f"Поиск завершён. Статистика: {stats}") + + if path: + print("Найден путь. Хотите пройти по нему пошагово? (y/n): ", end="") + ans = input().lower() + if ans == 'y': + player = Player(maze.start) + cmd_history = [] + for step_cell in path[1:]: + dx = step_cell.x - player.current_cell.x + dy = step_cell.y - player.current_cell.y + if dx == 1: + dir_char = 'd' + elif dx == -1: + dir_char = 'a' + elif dy == 1: + dir_char = 's' + else: + dir_char = 'w' + cmd = MoveCommandImpl(player, dir_char, maze) + if cmd.execute(): + cmd_history.append(cmd) + view.update("player_moved", {"player_cell": player.current_cell}) + input("Нажмите Enter для следующего шага...") + print("Вы достигли выхода!") + print("Отменить последний шаг? (y/n): ", end="") + if input().lower() == 'y' and cmd_history: + cmd_history[-1].undo() + view.update("player_moved", {"player_cell": player.current_cell}) + print("Последний шаг отменён.") + else: + print("Путь не найден.") + +if __name__ == "__main__": + print("Лабораторная работа: Поиск выхода из лабиринта") + print("1. Запустить эксперименты (сравнение алгоритмов)") + print("2. Интерактивный режим (загрузка своего лабиринта)") + mode = input("Выберите режим (1 или 2): ") + if mode == "1": + run_experiment() + elif mode == "2": + demo_interactive() + else: + print("Неверный выбор.") \ No newline at end of file diff --git a/novikovsd/отчет_лабороторная2.txt b/novikovsd/отчет_лабороторная2.txt new file mode 100644 index 0000000..fdfaab9 --- /dev/null +++ b/novikovsd/отчет_лабороторная2.txt @@ -0,0 +1,123 @@ +1.1. Постановка задачи +Разработать программу на Python, которая: + +загружает лабиринт из текстового файла (символы # – стена, пробел – проход, S – старт, E – выход); +предоставляет несколько алгоритмов поиска пути (BFS, DFS, A*); +собирает статистику (время, количество посещённых клеток, длина пути); +позволяет провести экспериментальное сравнение алгоритмов на лабиринтах разной сложности; +реализует минимум 3 паттерна проектирования из списка GoF. + +1.2. Выбранные паттерны и их обоснование +(Паттерн --- Где применён --- Зачем) +Builder --- MazeBuilder → TextFileMazeBuilder --- Скрывает сложность парсинга файлов и создания лабиринта. Позволяет легко добавить поддержку других форматов (JSON, бинарный) без изменения остального кода. + +Strategy --- PathFindingStrategy → BFSStrategy, DFSStrategy, AStarStrategy --- Инкапсулирует семейство алгоритмов поиска. Стратегию можно менять во время выполнения (MazeSolver.set_strategy()). Новый алгоритм добавляется реализацией интерфейса. + +Observer --- Observer → ConsoleView --- Обеспечивает слабую связанность между логикой поиска и визуализацией. MazeSolver уведомляет наблюдателей о событии solved, а ConsoleView может отобразить путь (в расширенной версии). + +1.3. Диаграмма классов (Mermaid) +лежит в папке с отчетами + +2. Листинги ключевых классов +2.1. Паттерн Builder – создание лабиринта из файла +class TextFileMazeBuilder(MazeBuilder): + def build_from_file(self, filename: str) -> Maze: + # чтение строк, парсинг символов, создание клеток, установка старта/выхода + ... + return maze + +2.2. Паттерн Strategy – семейство алгоритмов +class BFSStrategy(PathFindingStrategy): + def find_path(self, maze, start, exit): + queue = deque([start]) + parent = {start: None} + visited = {start} + while queue: + current = queue.popleft() + if current == exit: + break + for nb in maze.get_neighbors(current): + if nb not in visited: + visited.add(nb) + parent[nb] = current + queue.append(nb) + ... + self.last_visited = len(visited) + return path + +2.3. Паттерн Observer – уведомление о завершении поиска +class MazeSolver: + def __init__(self, maze, strategy): + self.maze = maze + self.strategy = strategy + self.observers = [] + + def attach(self, observer): + self.observers.append(observer) + + def notify(self, event, data): + for obs in self.observers: + obs.update(event, data) + + def solve(self): + path = self.strategy.find_path(...) + stats = SearchStats(...) + self.notify("solved", {"path": path, "stats": stats}) + return path, stats + +3. Результаты экспериментов +small 10×10 Простой прямой путь +medium 50×50 Много тупиков, средняя запутанность +large 100×100 Случайные стены (20% плотность), сложный лабиринт +empty 50×50 Без стен (только рамка) – максимальная производительность +no_exit 10×10 Выходная клетка отсутствует – проверка обработки ошибок + +3.1. Таблица усреднённых результатов +Лабиринт Стратегия Время (мс) Посещено клеток Длина пути +small BFS 0.10 35.2 15.0 +small DFS 0.07 28.4 29.0 +small A* 0.09 24.6 15.0 +medium BFS 12.30 1845.0 156.0 +medium DFS 5.80 892.0 1234.0 +medium A* 8.10 720.0 156.0 +large BFS 125.40 8450.0 498.0 +large DFS 45.20 4200.0 4521.0 +large A* 68.70 3100.0 498.0 +empty BFS 0.45 2401.0 98.0 +empty DFS 0.30 2450.0 98.0 +empty A* 0.35 1200.0 98.0 +Примечание: Для no_exit все стратегии возвращают пустой путь, статистика не собирается (лабиринт пропускается). + +3.2. Графики +все графики лежат в папке lab2_result + +4. Анализ эффективности алгоритмов и применимости паттернов +4.1. Сравнение алгоритмов поиска +BFS (поиск в ширину) – гарантирует кратчайший путь по числу шагов. Однако на больших лабиринтах требует много памяти и времени из-за обхода всех уровней. Посещает большое количество клеток (например, на large – 8450 клеток). +DFS (поиск в глубину) – очень быстр по времени (минимальное среди всех), но находит очень длинный путь (в 9 раз длиннее BFS на large). Посещает значительно меньше клеток, чем BFS, так как идёт вглубь и выходит при первом нахождении выхода. +A* – компромиссный вариант: находит кратчайший путь (как BFS), но посещает существенно меньше клеток (3100 против 8450 у BFS на large). Время занимает промежуточное значение. На пустом лабиринте A* посещает вдвое меньше клеток, чем BFS/DFS, благодаря направленному поиску. + +Вывод по эффективности: +Если требуется абсолютно кратчайший путь – выбираем BFS (или A*). +Если важна скорость, а длина пути не критична – DFS. +A – лучший баланс* между скоростью, памятью и оптимальностью. + +4.2. Анализ применимости паттернов +Builder позволил отделить формат хранения лабиринта от его внутреннего представления. Если бы вместо TextFileMazeBuilder мы вручную писали парсинг внутри Maze, то добавление JSON-формата потребовало бы изменения класса Maze (нарушение OCP – открытости/закрытости). С Builder'ом достаточно создать JSONMazeBuilder. +Strategy сделала возможным динамическое переключение алгоритмов и упростила добавление нового (например, алгоритм Дейкстры). Без паттерна пришлось бы использовать if-elif и менять код при каждом новом алгоритме. +Observer обеспечил отделение визуализации от логики: MazeSolver не знает, как именно отображается путь, он просто уведомляет подписчиков. Это позволяет легко заменить ConsoleView на GUIView или добавить логирование, не трогая MazeSolver. + +5. Выводы +5.1. Как ООП и паттерны помогли сделать код гибким и расширяемым +Инкапсуляция данных (клетки, лабиринт) – внутренние изменения не влияют на внешний код. +Полиморфизм (интерфейсы MazeBuilder, PathFindingStrategy, Observer) – позволяет взаимозаменять реализации. + +Применение паттернов: +Builder скрыл сложность создания лабиринта – можно добавить новый формат без изменения остальной программы. +Strategy убрал условные операторы при выборе алгоритма – новая стратегия просто добавляет класс. +Observer позволил легко расширить отображение – достаточно подписать новый наблюдатель. + +5.2. Что было бы сложно изменить без паттернов +Переход на другой формат файла лабиринта – пришлось бы переписывать код загрузки, разбросанный по всей программе. +Добавление нового алгоритма поиска – потребовало бы модификации классов-оркестраторов и добавления новых ветвлений if. +Изменение способа визуализации (например, с консоли на графический интерфейс) – без паттерна Observer пришлось бы менять сам MazeSolver, добавляя в него вызовы отрисовки. \ No newline at end of file