diff --git a/shekurovaa/2/docs/Report.docx b/shekurovaa/2/docs/Report.docx new file mode 100644 index 0000000..1d5ac2a Binary files /dev/null and b/shekurovaa/2/docs/Report.docx differ diff --git a/shekurovaa/2/docs/data/builder.py b/shekurovaa/2/docs/data/builder.py new file mode 100644 index 0000000..321b6be --- /dev/null +++ b/shekurovaa/2/docs/data/builder.py @@ -0,0 +1,46 @@ +from model import Cell, Maze + +class MazeBuilder: + def buildFromFile(self, filename: str) -> Maze: + raise NotImplementedError + + +class TextFileMazeBuilder(MazeBuilder): + def buildFromFile(self, filename: str) -> Maze: + with open(filename, "r", encoding="utf-8") as f: + raw_lines = [line.rstrip("\n") for line in f if line.strip("\n") != ""] + + width = max(len(line) for line in raw_lines) + grid = [] + + start_count = 0 + exit_count = 0 + + for y, line in enumerate(raw_lines): + row = [] + padded = line.ljust(width) + for x, ch in enumerate(padded): + if ch == "#": + row.append(Cell(x, y, isWall=True)) + elif ch == "S": + row.append(Cell(x, y, isStart=True)) + start_count += 1 + elif ch == "E": + row.append(Cell(x, y, isExit=True)) + exit_count += 1 + elif ch == "1": + row.append(Cell(x, y, weight=1)) + elif ch == "2": + row.append(Cell(x, y, weight=2)) + elif ch == "3": + row.append(Cell(x, y, weight=3)) + else: + row.append(Cell(x, y)) + grid.append(row) + + maze = Maze(grid) + + if start_count != 1 or exit_count != 1: + raise ValueError("В лабиринте должен быть ровно один S и один E") + + return maze \ No newline at end of file diff --git a/shekurovaa/2/docs/data/command.py b/shekurovaa/2/docs/data/command.py new file mode 100644 index 0000000..d5ed005 --- /dev/null +++ b/shekurovaa/2/docs/data/command.py @@ -0,0 +1,44 @@ +class Command: + def execute(self): + raise NotImplementedError + + def undo(self): + raise NotImplementedError + + +class Player: + def __init__(self, position): + self.position = position + + +class MoveCommand(Command): + DIRS = { + "W": (0, -1), + "S": (0, 1), + "A": (-1, 0), + "D": (1, 0), + } + + def __init__(self, maze, player, direction): + self.maze = maze + self.player = player + self.direction = direction.upper() + self.prev_position = None + + def execute(self): + if self.direction not in self.DIRS: + return False + dx, dy = self.DIRS[self.direction] + current = self.player.position + nxt = self.maze.getCell(current.x + dx, current.y + dy) + if nxt is None or not nxt.isPassable(): + return False + self.prev_position = current + self.player.position = nxt + return True + + def undo(self): + if self.prev_position is not None: + self.player.position = self.prev_position + return True + return False \ No newline at end of file diff --git a/shekurovaa/2/docs/data/experiments.py b/shekurovaa/2/docs/data/experiments.py new file mode 100644 index 0000000..41df502 --- /dev/null +++ b/shekurovaa/2/docs/data/experiments.py @@ -0,0 +1,30 @@ +import csv +from statistics import mean + +def run_experiments(maze_files, strategies, runs=5, out_csv="output/results.csv"): + rows = [] + for maze_name, maze in maze_files.items(): + for strat_name, strat_cls in strategies.items(): + times = [] + visiteds = [] + lengths = [] + for _ in range(runs): + solver = maze["solver_factory"](strat_cls()) + stats = solver.solve() + times.append(stats.timeMs) + visiteds.append(stats.visitedCells) + lengths.append(stats.pathLength) + rows.append({ + "maze": maze_name, + "strategy": strat_name, + "time_ms": round(mean(times), 3), + "visited_cells": round(mean(visiteds), 1), + "path_length": round(mean(lengths), 1) + }) + + with open(out_csv, "w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=["maze", "strategy", "time_ms", "visited_cells", "path_length"]) + writer.writeheader() + writer.writerows(rows) + + return rows \ No newline at end of file diff --git a/shekurovaa/2/docs/data/main.py b/shekurovaa/2/docs/data/main.py new file mode 100644 index 0000000..f8e59fe --- /dev/null +++ b/shekurovaa/2/docs/data/main.py @@ -0,0 +1,33 @@ +from builder import TextFileMazeBuilder +from strategies import BFSStrategy, DFSStrategy, AStarStrategy +from solver import MazeSolver +from observer import ConsoleView +from command import Player, MoveCommand + +def main(): + builder = TextFileMazeBuilder() + maze = builder.buildFromFile("mazes/small.txt") + + console = ConsoleView() + console.update({"type": "message", "text": "Лабиринт загружен:"}) + console.update({"type": "render", "maze": maze}) + + strategies = { + "BFS": BFSStrategy(), + "DFS": DFSStrategy(), + "A*": AStarStrategy() + } + + for name, strat in strategies.items(): + solver = MazeSolver(maze, strat) + stats = solver.solve() + print(f"{name}: time={stats.timeMs:.3f} ms, visited={stats.visitedCells}, path={stats.pathLength}") + console.update({"type": "render", "maze": maze, "path": stats.path}) + + player = Player(maze.start) + cmd = MoveCommand(maze, player, "D") + cmd.execute() + console.update({"type": "render", "maze": maze, "player": player.position}) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/shekurovaa/2/docs/data/mazes/small.txt b/shekurovaa/2/docs/data/mazes/small.txt new file mode 100644 index 0000000..f3d092c --- /dev/null +++ b/shekurovaa/2/docs/data/mazes/small.txt @@ -0,0 +1,10 @@ +########## +#S # # +# ## # # # +# ## # # +# ### # +### ## # +# # # +# # ###E # +# # +########## \ No newline at end of file diff --git a/shekurovaa/2/docs/data/model.py b/shekurovaa/2/docs/data/model.py new file mode 100644 index 0000000..587691b --- /dev/null +++ b/shekurovaa/2/docs/data/model.py @@ -0,0 +1,67 @@ +from dataclasses import dataclass +from typing import List, Optional + +@dataclass(frozen=True) +class Cell: + x: int + y: int + isWall: bool = False + isStart: bool = False + isExit: bool = False + weight: int = 1 + + def isPassable(self) -> bool: + return not self.isWall + + +class Maze: + def __init__(self, grid: List[List[Cell]]): + self.grid = grid + self.height = len(grid) + self.width = len(grid[0]) if self.height else 0 + self.start: Optional[Cell] = None + self.exit: Optional[Cell] = None + + for row in grid: + for cell in row: + if cell.isStart: + self.start = cell + if cell.isExit: + self.exit = cell + + def getCell(self, x: int, y: int) -> Optional[Cell]: + if 0 <= y < self.height and 0 <= x < self.width: + return self.grid[y][x] + return None + + def getNeighbors(self, cell: Cell) -> List[Cell]: + result = [] + for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]: + nxt = self.getCell(cell.x + dx, cell.y + dy) + if nxt is not None and nxt.isPassable(): + result.append(nxt) + return result + + def render(self, path=None, player_position=None) -> str: + path_set = {(c.x, c.y) for c in path} if path else set() + player_xy = (player_position.x, player_position.y) if player_position else None + + lines = [] + for y in range(self.height): + row = [] + for x in range(self.width): + c = self.grid[y][x] + if player_xy == (x, y): + row.append("P") + elif c.isStart: + row.append("S") + elif c.isExit: + row.append("E") + elif (x, y) in path_set: + row.append(".") + elif c.isWall: + row.append("#") + else: + row.append(" ") + lines.append("".join(row)) + return "\n".join(lines) \ No newline at end of file diff --git a/shekurovaa/2/docs/data/observer.py b/shekurovaa/2/docs/data/observer.py new file mode 100644 index 0000000..43628e8 --- /dev/null +++ b/shekurovaa/2/docs/data/observer.py @@ -0,0 +1,15 @@ +class Observer: + def update(self, event): + raise NotImplementedError + + +class ConsoleView(Observer): + def update(self, event): + if isinstance(event, dict) and event.get("type") == "message": + print(event["text"]) + elif isinstance(event, dict) and event.get("type") == "render": + maze = event["maze"] + path = event.get("path") + player = event.get("player") + print(maze.render(path=path, player_position=player)) + print() \ No newline at end of file diff --git a/shekurovaa/2/docs/data/solver.py b/shekurovaa/2/docs/data/solver.py new file mode 100644 index 0000000..57d22c5 --- /dev/null +++ b/shekurovaa/2/docs/data/solver.py @@ -0,0 +1,38 @@ +import time +from dataclasses import dataclass + +@dataclass +class SearchStats: + timeMs: float + visitedCells: int + pathLength: int + path: list + + +class MazeSolver: + def __init__(self, maze, strategy): + self.maze = maze + self.strategy = strategy + + def setStrategy(self, strategy): + self.strategy = strategy + + def solve(self) -> SearchStats: + if self.maze.start is None or self.maze.exit is None: + raise ValueError("Лабиринт должен содержать start и exit") + + t0 = time.perf_counter() + result = self.strategy.findPath(self.maze, self.maze.start, self.maze.exit) + t1 = time.perf_counter() + + if isinstance(result, tuple): + path, visited = result + else: + path, visited = result, 0 + + return SearchStats( + timeMs=(t1 - t0) * 1000, + visitedCells=visited, + pathLength=len(path), + path=path + ) \ No newline at end of file diff --git a/shekurovaa/2/docs/data/strategies.py b/shekurovaa/2/docs/data/strategies.py new file mode 100644 index 0000000..0274d9a --- /dev/null +++ b/shekurovaa/2/docs/data/strategies.py @@ -0,0 +1,103 @@ +from collections import deque +import heapq +from math import inf + +class PathFindingStrategy: + def findPath(self, maze, start, exit): + raise NotImplementedError + + +def reconstruct_path(parent, start, goal): + if goal not in parent and goal != start: + return [] + path = [] + cur = goal + while cur != start: + path.append(cur) + cur = parent[cur] + path.append(start) + path.reverse() + return path + + +class BFSStrategy(PathFindingStrategy): + def findPath(self, maze, start, exit): + queue = deque([start]) + visited = {start} + parent = {} + visited_count = 0 + + while queue: + current = queue.popleft() + visited_count += 1 + if current == exit: + path = reconstruct_path(parent, start, exit) + return path, visited_count + + for nxt in maze.getNeighbors(current): + if nxt not in visited: + visited.add(nxt) + parent[nxt] = current + queue.append(nxt) + + return [], visited_count + + +class DFSStrategy(PathFindingStrategy): + def findPath(self, maze, start, exit): + stack = [start] + visited = {start} + parent = {} + visited_count = 0 + + while stack: + current = stack.pop() + visited_count += 1 + if current == exit: + path = reconstruct_path(parent, start, exit) + return path, visited_count + + for nxt in maze.getNeighbors(current): + if nxt not in visited: + visited.add(nxt) + parent[nxt] = current + stack.append(nxt) + + return [], visited_count + + +class AStarStrategy(PathFindingStrategy): + def h(self, a, b): + return abs(a.x - b.x) + abs(a.y - b.y) + + def findPath(self, maze, start, exit): + open_heap = [] + heapq.heappush(open_heap, (0, 0, start)) + parent = {} + g = {start: 0} + visited = set() + visited_count = 0 + counter = 1 + + while open_heap: + _, _, current = heapq.heappop(open_heap) + if current in visited: + continue + + visited.add(current) + visited_count += 1 + + if current == exit: + path = reconstruct_path(parent, start, exit) + return path, visited_count + + for nxt in maze.getNeighbors(current): + tentative_g = g[current] + nxt.weight + if tentative_g < g.get(nxt, inf): + g[nxt] = tentative_g + parent[nxt] = current + f = tentative_g + self.h(nxt, exit) + heapq.heappush(open_heap, (f, counter, nxt)) + counter += 1 + + return [], visited_count \ No newline at end of file diff --git a/shekurovaa/2/docs/~$Report.docx b/shekurovaa/2/docs/~$Report.docx new file mode 100644 index 0000000..6288f33 Binary files /dev/null and b/shekurovaa/2/docs/~$Report.docx differ