diff --git a/petryaninyas/README.md b/petryaninyas/task1/README.md similarity index 100% rename from petryaninyas/README.md rename to petryaninyas/task1/README.md diff --git a/petryaninyas/bst.py b/petryaninyas/task1/bst.py similarity index 100% rename from petryaninyas/bst.py rename to petryaninyas/task1/bst.py diff --git a/petryaninyas/docs/data/delete.png b/petryaninyas/task1/docs/data/delete.png similarity index 100% rename from petryaninyas/docs/data/delete.png rename to petryaninyas/task1/docs/data/delete.png diff --git a/petryaninyas/docs/data/find.png b/petryaninyas/task1/docs/data/find.png similarity index 100% rename from petryaninyas/docs/data/find.png rename to petryaninyas/task1/docs/data/find.png diff --git a/petryaninyas/docs/data/insert.png b/petryaninyas/task1/docs/data/insert.png similarity index 100% rename from petryaninyas/docs/data/insert.png rename to petryaninyas/task1/docs/data/insert.png diff --git a/petryaninyas/docs/data/results.csv b/petryaninyas/task1/docs/data/results.csv similarity index 100% rename from petryaninyas/docs/data/results.csv rename to petryaninyas/task1/docs/data/results.csv diff --git a/petryaninyas/docs/Отчёт по работе.docx b/petryaninyas/task1/docs/Отчёт по работе.docx similarity index 100% rename from petryaninyas/docs/Отчёт по работе.docx rename to petryaninyas/task1/docs/Отчёт по работе.docx diff --git a/petryaninyas/experiments.py b/petryaninyas/task1/experiments.py similarity index 100% rename from petryaninyas/experiments.py rename to petryaninyas/task1/experiments.py diff --git a/petryaninyas/hash_table.py b/petryaninyas/task1/hash_table.py similarity index 100% rename from petryaninyas/hash_table.py rename to petryaninyas/task1/hash_table.py diff --git a/petryaninyas/linked_list.py b/petryaninyas/task1/linked_list.py similarity index 100% rename from petryaninyas/linked_list.py rename to petryaninyas/task1/linked_list.py diff --git a/petryaninyas/main.py b/petryaninyas/task1/main.py similarity index 100% rename from petryaninyas/main.py rename to petryaninyas/task1/main.py diff --git a/petryaninyas/plot_results.py b/petryaninyas/task1/plot_results.py similarity index 100% rename from petryaninyas/plot_results.py rename to petryaninyas/task1/plot_results.py diff --git a/petryaninyas/requirements.txt b/petryaninyas/task1/requirements.txt similarity index 100% rename from petryaninyas/requirements.txt rename to petryaninyas/task1/requirements.txt diff --git a/petryaninyas/results.csv b/petryaninyas/task1/results.csv similarity index 100% rename from petryaninyas/results.csv rename to petryaninyas/task1/results.csv diff --git a/petryaninyas/utils.py b/petryaninyas/task1/utils.py similarity index 100% rename from petryaninyas/utils.py rename to petryaninyas/task1/utils.py diff --git a/petryaninyas/task2/README.md b/petryaninyas/task2/README.md new file mode 100644 index 0000000..2e6e63f --- /dev/null +++ b/petryaninyas/task2/README.md @@ -0,0 +1,24 @@ +# Maze Solver Project + +ООП-проект для поиска выхода из лабиринта с паттернами: +- Builder +- Strategy +- Observer +- Command + +## Запуск +```bash +python main.py +``` + +## Эксперименты +```bash +python experiment.py +``` + +Результаты сохраняются в папку `experiment_results/`. + +## Требования +```bash +pip install -r requirements.txt +``` diff --git a/petryaninyas/task2/builders/__init__.py b/petryaninyas/task2/builders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petryaninyas/task2/builders/maze_builder.py b/petryaninyas/task2/builders/maze_builder.py new file mode 100644 index 0000000..b055db8 --- /dev/null +++ b/petryaninyas/task2/builders/maze_builder.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod + + +class MazeBuilder(ABC): + @abstractmethod + def buildFromFile(self, filename): + raise NotImplementedError diff --git a/petryaninyas/task2/builders/text_file_maze_builder.py b/petryaninyas/task2/builders/text_file_maze_builder.py new file mode 100644 index 0000000..5e9ca03 --- /dev/null +++ b/petryaninyas/task2/builders/text_file_maze_builder.py @@ -0,0 +1,52 @@ +from core.cell import Cell +from core.maze import Maze +from builders.maze_builder import MazeBuilder + + +class TextFileMazeBuilder(MazeBuilder): + def buildFromFile(self, filename): + with open(filename, "r", encoding="utf-8") as f: + lines = [line.rstrip("\n") for line in f] + + if not lines: + raise ValueError("Maze file is empty") + + width = max(len(line) for line in lines) + height = len(lines) + + cells = [] + startCell = None + exitCell = None + + for y, line in enumerate(lines): + row = [] + for x in range(width): + ch = line[x] if x < len(line) else "#" + + if ch == "#": + cell = Cell(x, y, isWall=True) + elif ch == "S": + if startCell is not None: + raise ValueError("Multiple start cells found") + cell = Cell(x, y, isWall=False, isStart=True) + startCell = cell + elif ch == "E": + if exitCell is not None: + raise ValueError("Multiple exit cells found") + cell = Cell(x, y, isWall=False, isExit=True) + exitCell = cell + elif ch in (" ", "."): + cell = Cell(x, y, isWall=False) + elif ch.isdigit(): + cell = Cell(x, y, isWall=False, weight=max(1, int(ch))) + else: + raise ValueError(f"Unsupported symbol '{ch}' at ({x}, {y})") + row.append(cell) + cells.append(row) + + if startCell is None: + raise ValueError("Start cell 'S' not found") + if exitCell is None: + raise ValueError("Exit cell 'E' not found") + + return Maze(cells, width, height, startCell, exitCell) diff --git a/petryaninyas/task2/commands/__init__.py b/petryaninyas/task2/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petryaninyas/task2/commands/command.py b/petryaninyas/task2/commands/command.py new file mode 100644 index 0000000..71f2dc6 --- /dev/null +++ b/petryaninyas/task2/commands/command.py @@ -0,0 +1,11 @@ +from abc import ABC, abstractmethod + + +class Command(ABC): + @abstractmethod + def execute(self): + raise NotImplementedError + + @abstractmethod + def undo(self): + raise NotImplementedError diff --git a/petryaninyas/task2/commands/move_command.py b/petryaninyas/task2/commands/move_command.py new file mode 100644 index 0000000..e90b7f1 --- /dev/null +++ b/petryaninyas/task2/commands/move_command.py @@ -0,0 +1,37 @@ +from commands.command import Command + + +class MoveCommand(Command): + DIRECTION_TO_DELTA = { + "W": (0, -1), + "A": (-1, 0), + "S": (0, 1), + "D": (1, 0), + } + + def __init__(self, player, maze, direction): + self.player = player + self.maze = maze + self.direction = direction.upper() + self.previousCell = None + + def execute(self): + if self.direction not in self.DIRECTION_TO_DELTA: + return False + + dx, dy = self.DIRECTION_TO_DELTA[self.direction] + current = self.player.currentCell + new_cell = self.maze.getCell(current.x + dx, current.y + dy) + + if new_cell is None or not new_cell.isPassable(): + return False + + self.previousCell = current + self.player.setCell(new_cell) + return True + + def undo(self): + if self.previousCell is None: + return False + self.player.setCell(self.previousCell) + return True diff --git a/petryaninyas/task2/controller/__init__.py b/petryaninyas/task2/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petryaninyas/task2/controller/game_controller.py b/petryaninyas/task2/controller/game_controller.py new file mode 100644 index 0000000..0a4cb39 --- /dev/null +++ b/petryaninyas/task2/controller/game_controller.py @@ -0,0 +1,30 @@ +from commands.move_command import MoveCommand + + +class GameController: + def __init__(self, maze, player, view): + self.maze = maze + self.player = player + self.view = view + self.history = [] + + def move(self, direction): + command = MoveCommand(self.player, self.maze, direction) + if command.execute(): + self.history.append(command) + self.view.update({"type": "move", "direction": direction}) + self.view.render(self.maze, player_position=self.player.currentCell) + return True + print("Cannot move there") + return False + + def undo(self): + if not self.history: + print("Nothing to undo") + return False + command = self.history.pop() + if command.undo(): + self.view.update({"type": "undo"}) + self.view.render(self.maze, player_position=self.player.currentCell) + return True + return False diff --git a/petryaninyas/task2/core/__init__.py b/petryaninyas/task2/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petryaninyas/task2/core/cell.py b/petryaninyas/task2/core/cell.py new file mode 100644 index 0000000..44e2d76 --- /dev/null +++ b/petryaninyas/task2/core/cell.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass + + +@dataclass +class Cell: + x: int + y: int + isWall: bool = False + isStart: bool = False + isExit: bool = False + weight: int = 1 + + def isPassable(self): + return not self.isWall + + def __repr__(self): + parts = [f"Cell({self.x}, {self.y}"] + if self.isWall: + parts.append("WALL") + if self.isStart: + parts.append("START") + if self.isExit: + parts.append("EXIT") + if self.weight != 1: + parts.append(f"w={self.weight}") + return ", ".join(parts) + ")" diff --git a/petryaninyas/task2/core/maze.py b/petryaninyas/task2/core/maze.py new file mode 100644 index 0000000..59c86dd --- /dev/null +++ b/petryaninyas/task2/core/maze.py @@ -0,0 +1,49 @@ +class Maze: + def __init__(self, cells, width, height, startCell=None, exitCell=None): + self.cells = cells + self.width = width + self.height = height + self.startCell = startCell + self.exitCell = exitCell + + def getCell(self, x, y): + if 0 <= x < self.width and 0 <= y < self.height: + return self.cells[y][x] + return None + + def getNeighbors(self, cell): + neighbors = [] + for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)): + nx, ny = cell.x + dx, cell.y + dy + neighbor = self.getCell(nx, ny) + if neighbor is not None and neighbor.isPassable(): + neighbors.append(neighbor) + return neighbors + + def render_lines(self, player_position=None, path=None): + path_set = {(c.x, c.y) for c in path} if path else set() + player_pos = None if player_position is None else (player_position.x, player_position.y) + lines = [] + for y in range(self.height): + row = [] + for x in range(self.width): + cell = self.cells[y][x] + if player_pos == (x, y): + row.append("P") + elif cell.isStart: + row.append("S") + elif cell.isExit: + row.append("E") + elif cell.isWall: + row.append("#") + elif (x, y) in path_set: + row.append("*") + elif cell.weight > 1: + row.append(str(cell.weight)) + else: + row.append(" ") + lines.append("".join(row)) + return lines + + def render(self, player_position=None, path=None): + return "\n".join(self.render_lines(player_position=player_position, path=path)) diff --git a/petryaninyas/task2/core/player.py b/petryaninyas/task2/core/player.py new file mode 100644 index 0000000..b68a0ff --- /dev/null +++ b/petryaninyas/task2/core/player.py @@ -0,0 +1,6 @@ +class Player: + def __init__(self, currentCell): + self.currentCell = currentCell + + def setCell(self, cell): + self.currentCell = cell diff --git a/petryaninyas/task2/core/search_stats.py b/petryaninyas/task2/core/search_stats.py new file mode 100644 index 0000000..5548118 --- /dev/null +++ b/petryaninyas/task2/core/search_stats.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass, field + + +@dataclass +class SearchStats: + timeMs: float + visitedCells: int + pathLength: int + path: list = field(default_factory=list) + found: bool = False + algorithm: str = "" diff --git a/petryaninyas/task2/docs/отчёт по лаб 2.docx b/petryaninyas/task2/docs/отчёт по лаб 2.docx new file mode 100644 index 0000000..8f3d20c Binary files /dev/null and b/petryaninyas/task2/docs/отчёт по лаб 2.docx differ diff --git a/petryaninyas/task2/experiment.py b/petryaninyas/task2/experiment.py new file mode 100644 index 0000000..588f377 --- /dev/null +++ b/petryaninyas/task2/experiment.py @@ -0,0 +1,225 @@ +from pathlib import Path +from statistics import mean +import csv +import random + +import matplotlib.pyplot as plt + +from core.cell import Cell +from core.maze import Maze +from solver.maze_solver import MazeSolver +from strategies.astar_strategy import AStarStrategy +from strategies.bfs_strategy import BFSStrategy +from strategies.dfs_strategy import DFSStrategy +from strategies.dijkstra_strategy import DijkstraStrategy + + +BASE_DIR = Path(__file__).resolve().parent +OUT_DIR = BASE_DIR / "experiment_results" + + +def build_maze_from_symbols(lines): + height = len(lines) + width = max(len(line) for line in lines) + cells = [] + start = None + exit_cell = None + for y, line in enumerate(lines): + row = [] + for x in range(width): + ch = line[x] if x < len(line) else "#" + if ch == "#": + cell = Cell(x, y, isWall=True) + elif ch == "S": + cell = Cell(x, y, isWall=False, isStart=True) + start = cell + elif ch == "E": + cell = Cell(x, y, isWall=False, isExit=True) + exit_cell = cell + elif ch == " " or ch == ".": + cell = Cell(x, y, isWall=False) + elif ch.isdigit(): + cell = Cell(x, y, isWall=False, weight=int(ch)) + else: + raise ValueError(f"Unknown symbol '{ch}' at {x},{y}") + row.append(cell) + cells.append(row) + return Maze(cells, width, height, start, exit_cell) + + +def generate_empty_maze(width, height): + lines = [" " * width for _ in range(height)] + lines = [list(row) for row in lines] + lines[1][1] = "S" + lines[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in lines]) + + +def generate_simple_maze(width, height): + grid = [["#" for _ in range(width)] for _ in range(height)] + for x in range(1, width - 1): + grid[1][x] = " " + for y in range(1, height - 1): + grid[y][width - 2] = " " + grid[1][1] = "S" + grid[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in grid]) + + +def generate_branching_maze(width, height, seed=42, wall_density=0.30): + rng = random.Random(seed) + grid = [["#" for _ in range(width)] for _ in range(height)] + x, y = 1, 1 + grid[y][x] = "S" + while (x, y) != (width - 2, height - 2): + candidates = [] + for dx, dy in [(1, 0), (0, 1)]: + nx, ny = x + dx, y + dy + if 1 <= nx < width - 1 and 1 <= ny < height - 1: + candidates.append((nx, ny)) + if not candidates: + break + x, y = rng.choice(candidates) + grid[y][x] = " " + grid[height - 2][width - 2] = "E" + + # carve extra corridors and dead ends + for yy in range(1, height - 1): + for xx in range(1, width - 1): + if grid[yy][xx] == "#" and rng.random() > wall_density: + grid[yy][xx] = " " + grid[1][1] = "S" + grid[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in grid]) + + +def generate_no_path_maze(width, height): + grid = [[" " for _ in range(width)] for _ in range(height)] + for x in range(width): + grid[height // 2][x] = "#" + grid[1][1] = "S" + grid[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in grid]) + + +def generate_weighted_maze(width, height, seed=123): + rng = random.Random(seed) + grid = [[" " for _ in range(width)] for _ in range(height)] + for y in range(height): + for x in range(width): + r = rng.random() + if r < 0.12: + grid[y][x] = "#" + elif r < 0.25: + grid[y][x] = "3" + elif r < 0.40: + grid[y][x] = "2" + else: + grid[y][x] = "1" + # ensure path-ish + for x in range(width): + grid[1][x] = "1" + for y in range(1, height): + grid[y][width - 2] = "1" + grid[1][1] = "S" + grid[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in grid]) + + +def bench_one_maze(maze_name, maze, strategies, repeats=5): + summary_rows = [] + raw_rows = [] + for strategy_name, strategy_factory in strategies: + times, visiteds, lengths = [], [], [] + for run in range(1, repeats + 1): + solver = MazeSolver(maze) + solver.setStrategy(strategy_factory()) + stats = solver.solve() + raw_rows.append([maze_name, strategy_name, run, f"{stats.timeMs:.6f}", stats.visitedCells, stats.pathLength]) + times.append(stats.timeMs) + visiteds.append(stats.visitedCells) + lengths.append(stats.pathLength) + summary_rows.append([maze_name, strategy_name, f"{mean(times):.6f}", f"{mean(visiteds):.2f}", f"{mean(lengths):.2f}", repeats]) + return summary_rows, raw_rows + + +def save_csv(path, rows): + with open(path, "w", newline="", encoding="utf-8") as f: + csv.writer(f).writerows(rows) + + +def plot_summary(summary_rows): + by_maze = {} + for row in summary_rows[1:]: + maze_name, strategy, avg_time, avg_visited, avg_len, runs = row + by_maze.setdefault(maze_name, []).append((strategy, float(avg_time), float(avg_visited), float(avg_len))) + + for maze_name, items in by_maze.items(): + items.sort(key=lambda t: t[0]) + strategies = [i[0] for i in items] + x = list(range(len(strategies))) + + plt.figure(figsize=(8, 4)) + plt.bar(x, [i[1] for i in items]) + plt.xticks(x, strategies) + plt.ylabel("ms") + plt.title(f"{maze_name} — avg time") + plt.tight_layout() + plt.savefig(OUT_DIR / f"{maze_name}_time.png", dpi=150) + plt.close() + + plt.figure(figsize=(8, 4)) + plt.bar(x, [i[2] for i in items]) + plt.xticks(x, strategies) + plt.ylabel("cells") + plt.title(f"{maze_name} — visited cells") + plt.tight_layout() + plt.savefig(OUT_DIR / f"{maze_name}_visited.png", dpi=150) + plt.close() + + plt.figure(figsize=(8, 4)) + plt.bar(x, [i[3] for i in items]) + plt.xticks(x, strategies) + plt.ylabel("cells") + plt.title(f"{maze_name} — path length") + plt.tight_layout() + plt.savefig(OUT_DIR / f"{maze_name}_length.png", dpi=150) + plt.close() + + +def main(): + OUT_DIR.mkdir(exist_ok=True) + + strategies = [ + ("BFS", BFSStrategy), + ("DFS", DFSStrategy), + ("A*", AStarStrategy), + ("Dijkstra", DijkstraStrategy), + ] + + mazes = [ + ("small_10x10", generate_simple_maze(10, 10)), + ("medium_50x50", generate_branching_maze(50, 50)), + ("large_100x100", generate_branching_maze(100, 100, seed=99, wall_density=0.35)), + ("empty_30x30", generate_empty_maze(30, 30)), + ("no_path_30x30", generate_no_path_maze(30, 30)), + ("weighted_30x30", generate_weighted_maze(30, 30)), + ] + + summary = [["maze", "strategy", "avg_time_ms", "avg_visited_cells", "avg_path_length", "runs"]] + raw = [["maze", "strategy", "run", "time_ms", "visited_cells", "path_length"]] + + for maze_name, maze in mazes: + s_rows, r_rows = bench_one_maze(maze_name, maze, strategies, repeats=5) + summary.extend(s_rows) + raw.extend(r_rows) + + save_csv(OUT_DIR / "summary.csv", summary) + save_csv(OUT_DIR / "raw.csv", raw) + plot_summary(summary) + + print("Saved to", OUT_DIR.resolve()) + + +if __name__ == "__main__": + main() diff --git a/petryaninyas/task2/experiment_results/empty_30x30_length.png b/petryaninyas/task2/experiment_results/empty_30x30_length.png new file mode 100644 index 0000000..53d8f2c Binary files /dev/null and b/petryaninyas/task2/experiment_results/empty_30x30_length.png differ diff --git a/petryaninyas/task2/experiment_results/empty_30x30_time.png b/petryaninyas/task2/experiment_results/empty_30x30_time.png new file mode 100644 index 0000000..c1a3ed2 Binary files /dev/null and b/petryaninyas/task2/experiment_results/empty_30x30_time.png differ diff --git a/petryaninyas/task2/experiment_results/empty_30x30_visited.png b/petryaninyas/task2/experiment_results/empty_30x30_visited.png new file mode 100644 index 0000000..891d00b Binary files /dev/null and b/petryaninyas/task2/experiment_results/empty_30x30_visited.png differ diff --git a/petryaninyas/task2/experiment_results/large_100x100_length.png b/petryaninyas/task2/experiment_results/large_100x100_length.png new file mode 100644 index 0000000..e799e4a Binary files /dev/null and b/petryaninyas/task2/experiment_results/large_100x100_length.png differ diff --git a/petryaninyas/task2/experiment_results/large_100x100_time.png b/petryaninyas/task2/experiment_results/large_100x100_time.png new file mode 100644 index 0000000..812271a Binary files /dev/null and b/petryaninyas/task2/experiment_results/large_100x100_time.png differ diff --git a/petryaninyas/task2/experiment_results/large_100x100_visited.png b/petryaninyas/task2/experiment_results/large_100x100_visited.png new file mode 100644 index 0000000..0e0ca86 Binary files /dev/null and b/petryaninyas/task2/experiment_results/large_100x100_visited.png differ diff --git a/petryaninyas/task2/experiment_results/medium_50x50_length.png b/petryaninyas/task2/experiment_results/medium_50x50_length.png new file mode 100644 index 0000000..1d8cb11 Binary files /dev/null and b/petryaninyas/task2/experiment_results/medium_50x50_length.png differ diff --git a/petryaninyas/task2/experiment_results/medium_50x50_time.png b/petryaninyas/task2/experiment_results/medium_50x50_time.png new file mode 100644 index 0000000..508c196 Binary files /dev/null and b/petryaninyas/task2/experiment_results/medium_50x50_time.png differ diff --git a/petryaninyas/task2/experiment_results/medium_50x50_visited.png b/petryaninyas/task2/experiment_results/medium_50x50_visited.png new file mode 100644 index 0000000..7c91f22 Binary files /dev/null and b/petryaninyas/task2/experiment_results/medium_50x50_visited.png differ diff --git a/petryaninyas/task2/experiment_results/no_path_30x30_length.png b/petryaninyas/task2/experiment_results/no_path_30x30_length.png new file mode 100644 index 0000000..3f92f3e Binary files /dev/null and b/petryaninyas/task2/experiment_results/no_path_30x30_length.png differ diff --git a/petryaninyas/task2/experiment_results/no_path_30x30_time.png b/petryaninyas/task2/experiment_results/no_path_30x30_time.png new file mode 100644 index 0000000..d851257 Binary files /dev/null and b/petryaninyas/task2/experiment_results/no_path_30x30_time.png differ diff --git a/petryaninyas/task2/experiment_results/no_path_30x30_visited.png b/petryaninyas/task2/experiment_results/no_path_30x30_visited.png new file mode 100644 index 0000000..72e6e55 Binary files /dev/null and b/petryaninyas/task2/experiment_results/no_path_30x30_visited.png differ diff --git a/petryaninyas/task2/experiment_results/raw.csv b/petryaninyas/task2/experiment_results/raw.csv new file mode 100644 index 0000000..ad0b80f --- /dev/null +++ b/petryaninyas/task2/experiment_results/raw.csv @@ -0,0 +1,121 @@ +maze,strategy,run,time_ms,visited_cells,path_length +small_10x10,BFS,1,0.086300,15,15 +small_10x10,BFS,2,0.061100,15,15 +small_10x10,BFS,3,0.059300,15,15 +small_10x10,BFS,4,0.058400,15,15 +small_10x10,BFS,5,0.058500,15,15 +small_10x10,DFS,1,0.073400,15,15 +small_10x10,DFS,2,0.063500,15,15 +small_10x10,DFS,3,0.062500,15,15 +small_10x10,DFS,4,0.062700,15,15 +small_10x10,DFS,5,0.070900,15,15 +small_10x10,A*,1,0.110100,15,15 +small_10x10,A*,2,0.089200,15,15 +small_10x10,A*,3,0.087800,15,15 +small_10x10,A*,4,0.087600,15,15 +small_10x10,A*,5,0.087000,15,15 +small_10x10,Dijkstra,1,0.290000,15,15 +small_10x10,Dijkstra,2,0.083300,15,15 +small_10x10,Dijkstra,3,0.091500,15,15 +small_10x10,Dijkstra,4,0.081000,15,15 +small_10x10,Dijkstra,5,0.080400,15,15 +medium_50x50,BFS,1,6.799200,1579,95 +medium_50x50,BFS,2,6.960100,1579,95 +medium_50x50,BFS,3,6.337000,1579,95 +medium_50x50,BFS,4,7.431700,1579,95 +medium_50x50,BFS,5,6.517900,1579,95 +medium_50x50,DFS,1,6.463000,1277,647 +medium_50x50,DFS,2,6.815500,1277,647 +medium_50x50,DFS,3,5.816100,1277,647 +medium_50x50,DFS,4,6.492400,1277,647 +medium_50x50,DFS,5,6.532500,1277,647 +medium_50x50,A*,1,6.940500,927,95 +medium_50x50,A*,2,7.275400,927,95 +medium_50x50,A*,3,7.062500,927,95 +medium_50x50,A*,4,7.727600,927,95 +medium_50x50,A*,5,7.321000,927,95 +medium_50x50,Dijkstra,1,11.483200,1579,95 +medium_50x50,Dijkstra,2,11.194200,1579,95 +medium_50x50,Dijkstra,3,11.255200,1579,95 +medium_50x50,Dijkstra,4,10.512500,1579,95 +medium_50x50,Dijkstra,5,10.696400,1579,95 +large_100x100,BFS,1,25.623500,5566,195 +large_100x100,BFS,2,24.348800,5566,195 +large_100x100,BFS,3,25.452600,5566,195 +large_100x100,BFS,4,30.516900,5566,195 +large_100x100,BFS,5,33.694700,5566,195 +large_100x100,DFS,1,19.415200,3543,1531 +large_100x100,DFS,2,19.919000,3543,1531 +large_100x100,DFS,3,19.104600,3543,1531 +large_100x100,DFS,4,20.000600,3543,1531 +large_100x100,DFS,5,17.840200,3543,1531 +large_100x100,A*,1,7.509300,853,195 +large_100x100,A*,2,7.221200,853,195 +large_100x100,A*,3,6.486700,853,195 +large_100x100,A*,4,6.357600,853,195 +large_100x100,A*,5,6.723800,853,195 +large_100x100,Dijkstra,1,40.782300,5571,195 +large_100x100,Dijkstra,2,41.155000,5571,195 +large_100x100,Dijkstra,3,39.456200,5571,195 +large_100x100,Dijkstra,4,41.388700,5571,195 +large_100x100,Dijkstra,5,40.962500,5571,195 +empty_30x30,BFS,1,4.143200,896,55 +empty_30x30,BFS,2,3.987000,896,55 +empty_30x30,BFS,3,3.777100,896,55 +empty_30x30,BFS,4,3.682300,896,55 +empty_30x30,BFS,5,3.737900,896,55 +empty_30x30,DFS,1,4.024200,842,815 +empty_30x30,DFS,2,4.333900,842,815 +empty_30x30,DFS,3,5.411000,842,815 +empty_30x30,DFS,4,4.677200,842,815 +empty_30x30,DFS,5,5.177400,842,815 +empty_30x30,A*,1,6.603700,784,55 +empty_30x30,A*,2,6.200600,784,55 +empty_30x30,A*,3,6.798400,784,55 +empty_30x30,A*,4,7.178500,784,55 +empty_30x30,A*,5,6.660800,784,55 +empty_30x30,Dijkstra,1,6.396000,896,55 +empty_30x30,Dijkstra,2,6.275200,896,55 +empty_30x30,Dijkstra,3,6.845700,896,55 +empty_30x30,Dijkstra,4,6.531200,896,55 +empty_30x30,Dijkstra,5,6.783400,896,55 +no_path_30x30,BFS,1,2.000100,450,0 +no_path_30x30,BFS,2,1.797900,450,0 +no_path_30x30,BFS,3,1.796200,450,0 +no_path_30x30,BFS,4,1.774100,450,0 +no_path_30x30,BFS,5,1.775200,450,0 +no_path_30x30,DFS,1,2.090400,450,0 +no_path_30x30,DFS,2,2.222600,450,0 +no_path_30x30,DFS,3,2.454300,450,0 +no_path_30x30,DFS,4,2.476200,450,0 +no_path_30x30,DFS,5,2.073700,450,0 +no_path_30x30,A*,1,3.651700,450,0 +no_path_30x30,A*,2,3.495200,450,0 +no_path_30x30,A*,3,3.754200,450,0 +no_path_30x30,A*,4,3.286800,450,0 +no_path_30x30,A*,5,3.335200,450,0 +no_path_30x30,Dijkstra,1,3.050900,450,0 +no_path_30x30,Dijkstra,2,3.109900,450,0 +no_path_30x30,Dijkstra,3,3.292500,450,0 +no_path_30x30,Dijkstra,4,3.418600,450,0 +no_path_30x30,Dijkstra,5,3.212100,450,0 +weighted_30x30,BFS,1,3.418900,788,55 +weighted_30x30,BFS,2,3.368200,788,55 +weighted_30x30,BFS,3,3.516400,788,55 +weighted_30x30,BFS,4,3.224300,788,55 +weighted_30x30,BFS,5,3.131100,788,55 +weighted_30x30,DFS,1,3.291200,693,479 +weighted_30x30,DFS,2,3.362300,693,479 +weighted_30x30,DFS,3,3.523200,693,479 +weighted_30x30,DFS,4,3.521400,693,479 +weighted_30x30,DFS,5,3.332300,693,479 +weighted_30x30,A*,1,1.181000,126,55 +weighted_30x30,A*,2,1.080200,126,55 +weighted_30x30,A*,3,1.368400,126,55 +weighted_30x30,A*,4,1.109800,126,55 +weighted_30x30,A*,5,1.079300,126,55 +weighted_30x30,Dijkstra,1,6.112700,781,55 +weighted_30x30,Dijkstra,2,5.464800,781,55 +weighted_30x30,Dijkstra,3,5.794500,781,55 +weighted_30x30,Dijkstra,4,6.171700,781,55 +weighted_30x30,Dijkstra,5,6.640500,781,55 diff --git a/petryaninyas/task2/experiment_results/small_10x10_length.png b/petryaninyas/task2/experiment_results/small_10x10_length.png new file mode 100644 index 0000000..1094873 Binary files /dev/null and b/petryaninyas/task2/experiment_results/small_10x10_length.png differ diff --git a/petryaninyas/task2/experiment_results/small_10x10_time.png b/petryaninyas/task2/experiment_results/small_10x10_time.png new file mode 100644 index 0000000..d78930d Binary files /dev/null and b/petryaninyas/task2/experiment_results/small_10x10_time.png differ diff --git a/petryaninyas/task2/experiment_results/small_10x10_visited.png b/petryaninyas/task2/experiment_results/small_10x10_visited.png new file mode 100644 index 0000000..2bbc256 Binary files /dev/null and b/petryaninyas/task2/experiment_results/small_10x10_visited.png differ diff --git a/petryaninyas/task2/experiment_results/summary.csv b/petryaninyas/task2/experiment_results/summary.csv new file mode 100644 index 0000000..eaf8e47 --- /dev/null +++ b/petryaninyas/task2/experiment_results/summary.csv @@ -0,0 +1,25 @@ +maze,strategy,avg_time_ms,avg_visited_cells,avg_path_length,runs +small_10x10,BFS,0.064720,15.00,15.00,5 +small_10x10,DFS,0.066600,15.00,15.00,5 +small_10x10,A*,0.092340,15.00,15.00,5 +small_10x10,Dijkstra,0.125240,15.00,15.00,5 +medium_50x50,BFS,6.809180,1579.00,95.00,5 +medium_50x50,DFS,6.423900,1277.00,647.00,5 +medium_50x50,A*,7.265400,927.00,95.00,5 +medium_50x50,Dijkstra,11.028300,1579.00,95.00,5 +large_100x100,BFS,27.927300,5566.00,195.00,5 +large_100x100,DFS,19.255920,3543.00,1531.00,5 +large_100x100,A*,6.859720,853.00,195.00,5 +large_100x100,Dijkstra,40.748940,5571.00,195.00,5 +empty_30x30,BFS,3.865500,896.00,55.00,5 +empty_30x30,DFS,4.724740,842.00,815.00,5 +empty_30x30,A*,6.688400,784.00,55.00,5 +empty_30x30,Dijkstra,6.566300,896.00,55.00,5 +no_path_30x30,BFS,1.828700,450.00,0.00,5 +no_path_30x30,DFS,2.263440,450.00,0.00,5 +no_path_30x30,A*,3.504620,450.00,0.00,5 +no_path_30x30,Dijkstra,3.216800,450.00,0.00,5 +weighted_30x30,BFS,3.331780,788.00,55.00,5 +weighted_30x30,DFS,3.406080,693.00,479.00,5 +weighted_30x30,A*,1.163740,126.00,55.00,5 +weighted_30x30,Dijkstra,6.036840,781.00,55.00,5 diff --git a/petryaninyas/task2/experiment_results/weighted_30x30_length.png b/petryaninyas/task2/experiment_results/weighted_30x30_length.png new file mode 100644 index 0000000..98d4a30 Binary files /dev/null and b/petryaninyas/task2/experiment_results/weighted_30x30_length.png differ diff --git a/petryaninyas/task2/experiment_results/weighted_30x30_time.png b/petryaninyas/task2/experiment_results/weighted_30x30_time.png new file mode 100644 index 0000000..018e5b3 Binary files /dev/null and b/petryaninyas/task2/experiment_results/weighted_30x30_time.png differ diff --git a/petryaninyas/task2/experiment_results/weighted_30x30_visited.png b/petryaninyas/task2/experiment_results/weighted_30x30_visited.png new file mode 100644 index 0000000..e3b871d Binary files /dev/null and b/petryaninyas/task2/experiment_results/weighted_30x30_visited.png differ diff --git a/petryaninyas/task2/main.py b/petryaninyas/task2/main.py new file mode 100644 index 0000000..08f22c7 --- /dev/null +++ b/petryaninyas/task2/main.py @@ -0,0 +1,59 @@ +from builders.text_file_maze_builder import TextFileMazeBuilder +from core.player import Player +from observer.console_view import ConsoleView +from solver.maze_solver import MazeSolver +from strategies.astar_strategy import AStarStrategy +from strategies.bfs_strategy import BFSStrategy +from strategies.dfs_strategy import DFSStrategy +from controller.game_controller import GameController + + +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent + + +def run_demo(): + builder = TextFileMazeBuilder() + maze = builder.buildFromFile(str(BASE_DIR / "mazes" / "maze_small.txt")) + + view = ConsoleView() + view.update({"type": "maze_loaded", "message": "Maze loaded"}) + view.render(maze) + + solver = MazeSolver(maze) + solver.addObserver(view) + + for strategy in (BFSStrategy(), DFSStrategy(), AStarStrategy()): + solver.setStrategy(strategy) + stats = solver.solve() + + print() + print(f"=== {strategy.name} ===") + print(f"Time: {stats.timeMs:.3f} ms") + print(f"Visited cells: {stats.visitedCells}") + print(f"Path length: {stats.pathLength}") + print(f"Path found: {'yes' if stats.found else 'no'}") + + view.render(maze, path=stats.path) + + player = Player(maze.startCell) + controller = GameController(maze, player, view) + + print("Manual mode: W/A/S/D move, Z undo, Q quit") + view.render(maze, player_position=player.currentCell) + + while True: + cmd = input("Command: ").strip().upper() + if cmd == "Q": + break + if cmd == "Z": + controller.undo() + elif cmd in {"W", "A", "S", "D"}: + controller.move(cmd) + else: + print("Unknown command") + + +if __name__ == "__main__": + run_demo() diff --git a/petryaninyas/task2/mazes/maze_empty.txt b/petryaninyas/task2/mazes/maze_empty.txt new file mode 100644 index 0000000..8267fd0 --- /dev/null +++ b/petryaninyas/task2/mazes/maze_empty.txt @@ -0,0 +1,9 @@ +S + + + + + + + + E diff --git a/petryaninyas/task2/mazes/maze_large.txt b/petryaninyas/task2/mazes/maze_large.txt new file mode 100644 index 0000000..eb03326 --- /dev/null +++ b/petryaninyas/task2/mazes/maze_large.txt @@ -0,0 +1,11 @@ +#################################################################################################### +#S # # # # # # # # # # # # # # # E# +# # ### ### # ###### # ### # ## # #### # ####### # #### # # ### ## # ## # # ## # ## # ##### ### ## +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ##### # ######## # ### # ## # #### # ####### ## ### # # #### ####### ## ####### ####### # ### ## +# # # # # # # # # # # # # # # # # # # # # +### # # ###### # ########### ########### ### ####### # ####### ### # # ###### # ### ### # ### #### +# # # # # # # # # # # # # # # # # # # # # # +# ### ###### # ##### # ### # ####### # ### ### ## # ###### # ### # ### ###### # ### # ### ### ## # +# # # # # # # # # +#################################################################################################### diff --git a/petryaninyas/task2/mazes/maze_medium.txt b/petryaninyas/task2/mazes/maze_medium.txt new file mode 100644 index 0000000..67ecd65 --- /dev/null +++ b/petryaninyas/task2/mazes/maze_medium.txt @@ -0,0 +1,11 @@ +################################################## +#S # # # # # # E# +# # ### ### # ###### # ### # ## # #### # ####### ## +# # # # # # # # # # # # # # +# ##### # ######## # ### # ## # #### # ####### ## # +# # # # # # # # # # +### # # ###### # ########### ########### ### ###### +# # # # # # # # # # # +# ### ###### # ##### # ### # ####### # ### ### ## # +# # # # # +################################################## diff --git a/petryaninyas/task2/mazes/maze_no_path.txt b/petryaninyas/task2/mazes/maze_no_path.txt new file mode 100644 index 0000000..9633160 --- /dev/null +++ b/petryaninyas/task2/mazes/maze_no_path.txt @@ -0,0 +1,9 @@ +########## +#S # +# ###### # +# # # +########## +# #E# +# ###### # +# # +########## diff --git a/petryaninyas/task2/mazes/maze_small.txt b/petryaninyas/task2/mazes/maze_small.txt new file mode 100644 index 0000000..e829a58 --- /dev/null +++ b/petryaninyas/task2/mazes/maze_small.txt @@ -0,0 +1,7 @@ +########## +#S #E# +# ## # # ## +# # # +# #### # # +# # # +########## diff --git a/petryaninyas/task2/mazes/maze_weighted.txt b/petryaninyas/task2/mazes/maze_weighted.txt new file mode 100644 index 0000000..be8718d --- /dev/null +++ b/petryaninyas/task2/mazes/maze_weighted.txt @@ -0,0 +1,10 @@ +1111111111111111111111111111 +1S11111111111111111111111111 +1111111111111111111111111111 +1111111111111111111111111111 +1111111111111222222222222111 +1111111111111222222222222111 +1111111111111333333333333111 +1111111111111333333333333111 +111111111111111111111111111E +1111111111111111111111111111 diff --git a/petryaninyas/task2/observer/__init__.py b/petryaninyas/task2/observer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petryaninyas/task2/observer/console_view.py b/petryaninyas/task2/observer/console_view.py new file mode 100644 index 0000000..77248a5 --- /dev/null +++ b/petryaninyas/task2/observer/console_view.py @@ -0,0 +1,26 @@ +import os +from observer.observer import Observer + + +class ConsoleView(Observer): + def update(self, event): + if isinstance(event, str): + print(f"[EVENT] {event}") + elif isinstance(event, dict): + event_type = event.get("type", "unknown") + if event_type == "search_finished": + stats = event.get("stats") + print(f"[EVENT] search finished: {stats}") + else: + print(f"[EVENT] {event_type}: {event}") + else: + print("[EVENT] unknown") + + def clear(self): + os.system("cls" if os.name == "nt" else "clear") + + def render(self, maze, player_position=None, path=None, clear_screen=False): + if clear_screen: + self.clear() + print(maze.render(player_position=player_position, path=path)) + print() diff --git a/petryaninyas/task2/observer/observer.py b/petryaninyas/task2/observer/observer.py new file mode 100644 index 0000000..0ccca59 --- /dev/null +++ b/petryaninyas/task2/observer/observer.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod + + +class Observer(ABC): + @abstractmethod + def update(self, event): + raise NotImplementedError diff --git a/petryaninyas/task2/requirements.txt b/petryaninyas/task2/requirements.txt new file mode 100644 index 0000000..6ccafc3 --- /dev/null +++ b/petryaninyas/task2/requirements.txt @@ -0,0 +1 @@ +matplotlib diff --git a/petryaninyas/task2/solver/__init__.py b/petryaninyas/task2/solver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petryaninyas/task2/solver/maze_solver.py b/petryaninyas/task2/solver/maze_solver.py new file mode 100644 index 0000000..7894661 --- /dev/null +++ b/petryaninyas/task2/solver/maze_solver.py @@ -0,0 +1,50 @@ +import time +from core.search_stats import SearchStats + + +class MazeSolver: + def __init__(self, maze, strategy=None): + self.maze = maze + self.strategy = strategy + self.observers = [] + + def setStrategy(self, strategy): + self.strategy = strategy + + def addObserver(self, observer): + if observer not in self.observers: + self.observers.append(observer) + + def removeObserver(self, observer): + if observer in self.observers: + self.observers.remove(observer) + + def notify(self, event): + for observer in self.observers: + observer.update(event) + + def solve(self): + if self.strategy is None: + raise ValueError("Strategy is not set") + self.notify({"type": "search_started", "strategy": self.strategy.name}) + + start_time = time.perf_counter() + path = self.strategy.findPath(self.maze, self.maze.startCell, self.maze.exitCell) + end_time = time.perf_counter() + + stats = SearchStats( + timeMs=(end_time - start_time) * 1000.0, + visitedCells=getattr(self.strategy, "visitedCount", 0), + pathLength=len(path), + path=path, + found=bool(path), + algorithm=getattr(self.strategy, "name", "") + ) + + if stats.found: + self.notify({"type": "path_found", "strategy": stats.algorithm, "length": stats.pathLength}) + else: + self.notify({"type": "path_not_found", "strategy": stats.algorithm}) + + self.notify({"type": "search_finished", "stats": stats}) + return stats diff --git a/petryaninyas/task2/strategies/__init__.py b/petryaninyas/task2/strategies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petryaninyas/task2/strategies/astar_strategy.py b/petryaninyas/task2/strategies/astar_strategy.py new file mode 100644 index 0000000..4da5535 --- /dev/null +++ b/petryaninyas/task2/strategies/astar_strategy.py @@ -0,0 +1,45 @@ +import heapq +from strategies.pathfinding_strategy import PathFindingStrategy + + +class AStarStrategy(PathFindingStrategy): + name = "A*" + + def heuristic(self, cell, exitCell): + return abs(cell.x - exitCell.x) + abs(cell.y - exitCell.y) + + def findPath(self, maze, start, exitCell): + self.visitedCount = 0 + if start is None or exitCell is None: + return [] + + open_set = [] + heapq.heappush(open_set, (0, 0, start.x, start.y, start)) + parent = {} + g_score = {(start.x, start.y): 0} + closed = set() + + while open_set: + f_score, current_g, _, _, current = heapq.heappop(open_set) + pos = (current.x, current.y) + + if pos in closed: + continue + + closed.add(pos) + self.visitedCount += 1 + + if current.x == exitCell.x and current.y == exitCell.y: + return self._restore_path(parent, start, exitCell) + + for neighbor in maze.getNeighbors(current): + npos = (neighbor.x, neighbor.y) + tentative_g = current_g + getattr(neighbor, "weight", 1) + + if tentative_g < g_score.get(npos, float("inf")): + g_score[npos] = tentative_g + parent[npos] = current + new_f = tentative_g + self.heuristic(neighbor, exitCell) + heapq.heappush(open_set, (new_f, tentative_g, neighbor.x, neighbor.y, neighbor)) + + return [] diff --git a/petryaninyas/task2/strategies/bfs_strategy.py b/petryaninyas/task2/strategies/bfs_strategy.py new file mode 100644 index 0000000..7a98b50 --- /dev/null +++ b/petryaninyas/task2/strategies/bfs_strategy.py @@ -0,0 +1,31 @@ +from collections import deque +from strategies.pathfinding_strategy import PathFindingStrategy + + +class BFSStrategy(PathFindingStrategy): + name = "BFS" + + def findPath(self, maze, start, exitCell): + self.visitedCount = 0 + if start is None or exitCell is None: + return [] + + queue = deque([start]) + visited = {(start.x, start.y)} + parent = {} + + while queue: + current = queue.popleft() + self.visitedCount += 1 + + if current.x == exitCell.x and current.y == exitCell.y: + return self._restore_path(parent, start, exitCell) + + for neighbor in maze.getNeighbors(current): + pos = (neighbor.x, neighbor.y) + if pos not in visited: + visited.add(pos) + parent[pos] = current + queue.append(neighbor) + + return [] diff --git a/petryaninyas/task2/strategies/dfs_strategy.py b/petryaninyas/task2/strategies/dfs_strategy.py new file mode 100644 index 0000000..36451b3 --- /dev/null +++ b/petryaninyas/task2/strategies/dfs_strategy.py @@ -0,0 +1,35 @@ +from strategies.pathfinding_strategy import PathFindingStrategy + + +class DFSStrategy(PathFindingStrategy): + name = "DFS" + + def findPath(self, maze, start, exitCell): + self.visitedCount = 0 + if start is None or exitCell is None: + return [] + + stack = [start] + visited = set() + parent = {} + + while stack: + current = stack.pop() + pos = (current.x, current.y) + if pos in visited: + continue + + visited.add(pos) + self.visitedCount += 1 + + if current.x == exitCell.x and current.y == exitCell.y: + return self._restore_path(parent, start, exitCell) + + neighbors = maze.getNeighbors(current) + for neighbor in reversed(neighbors): + npos = (neighbor.x, neighbor.y) + if npos not in visited: + parent[npos] = current + stack.append(neighbor) + + return [] diff --git a/petryaninyas/task2/strategies/dijkstra_strategy.py b/petryaninyas/task2/strategies/dijkstra_strategy.py new file mode 100644 index 0000000..fd3163f --- /dev/null +++ b/petryaninyas/task2/strategies/dijkstra_strategy.py @@ -0,0 +1,41 @@ +import heapq +from strategies.pathfinding_strategy import PathFindingStrategy + + +class DijkstraStrategy(PathFindingStrategy): + name = "Dijkstra" + + def findPath(self, maze, start, exitCell): + self.visitedCount = 0 + if start is None or exitCell is None: + return [] + + pq = [(0, start.x, start.y, start)] + dist = {(start.x, start.y): 0} + parent = {} + closed = set() + + while pq: + current_cost, _, _, current = heapq.heappop(pq) + pos = (current.x, current.y) + + if pos in closed: + continue + + closed.add(pos) + self.visitedCount += 1 + + if current.x == exitCell.x and current.y == exitCell.y: + return self._restore_path(parent, start, exitCell) + + for neighbor in maze.getNeighbors(current): + npos = (neighbor.x, neighbor.y) + step_cost = getattr(neighbor, "weight", 1) + new_cost = current_cost + step_cost + + if new_cost < dist.get(npos, float("inf")): + dist[npos] = new_cost + parent[npos] = current + heapq.heappush(pq, (new_cost, neighbor.x, neighbor.y, neighbor)) + + return [] diff --git a/petryaninyas/task2/strategies/pathfinding_strategy.py b/petryaninyas/task2/strategies/pathfinding_strategy.py new file mode 100644 index 0000000..17b3ee4 --- /dev/null +++ b/petryaninyas/task2/strategies/pathfinding_strategy.py @@ -0,0 +1,30 @@ +from abc import ABC, abstractmethod + + +class PathFindingStrategy(ABC): + name = "Base" + + def __init__(self): + self.visitedCount = 0 + + @abstractmethod + def findPath(self, maze, start, exitCell): + raise NotImplementedError + + def _restore_path(self, parent, start, exitCell): + if exitCell is None or start is None: + return [] + + path = [] + current = exitCell + + while True: + path.append(current) + if current.x == start.x and current.y == start.y: + break + current = parent.get((current.x, current.y)) + if current is None: + return [] + + path.reverse() + return path