diff --git a/volkovim/task2/builders/maze_builder.py b/volkovim/task2/builders/maze_builder.py new file mode 100644 index 0000000..b5894f4 --- /dev/null +++ b/volkovim/task2/builders/maze_builder.py @@ -0,0 +1,8 @@ +from abc import ABC, abstractmethod + + +class MazeBuilder(ABC): + + @abstractmethod + def buildFromFile(self, filename): + pass \ No newline at end of file diff --git a/volkovim/task2/builders/text_file_builder.py b/volkovim/task2/builders/text_file_builder.py new file mode 100644 index 0000000..91f6823 --- /dev/null +++ b/volkovim/task2/builders/text_file_builder.py @@ -0,0 +1,102 @@ +from builders.maze_builder import MazeBuilder +from core.cell import Cell +from core.maze import Maze + + +class TextFileMazeBuilder(MazeBuilder): + + def buildFromFile(self, filename): + + with open(filename, "r", encoding="utf-8") as source: + raw_lines = [line.rstrip("\n") for line in source] + + if not raw_lines: + raise ValueError("Maze file is empty") + + expected_width = len(raw_lines[0]) + + blueprint = [] + start_cell = None + exit_cell = None + + for y_index, raw in enumerate(raw_lines): + + if len(raw) != expected_width: + raise ValueError( + f"Broken maze shape at line {y_index + 1}" + ) + + row_pack = [] + + for x_index, symbol in enumerate(raw): + + current = None + + if symbol == "#": + current = Cell( + x_index, + y_index, + isWall=True + ) + + elif symbol == " ": + current = Cell( + x_index, + y_index + ) + + elif symbol == "S": + + if start_cell: + raise ValueError( + "Multiple start cells detected" + ) + + current = Cell( + x_index, + y_index, + isStart=True + ) + + start_cell = current + + elif symbol == "E": + + if exit_cell: + raise ValueError( + "Multiple exit cells detected" + ) + + current = Cell( + x_index, + y_index, + isExit=True + ) + + exit_cell = current + + else: + raise ValueError( + f"Unsupported symbol '{symbol}' " + f"at ({x_index}, {y_index})" + ) + + row_pack.append(current) + + blueprint.append(row_pack) + + if start_cell is None: + raise ValueError( + "Start cell S not found" + ) + + if exit_cell is None: + raise ValueError( + "Exit cell E not found" + ) + + return Maze( + blueprint, + start_cell=start_cell, + exit_cell=exit_cell + ) \ No newline at end of file diff --git a/volkovim/task2/command/command.py b/volkovim/task2/command/command.py new file mode 100644 index 0000000..9dbbf3f --- /dev/null +++ b/volkovim/task2/command/command.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod + + +class Command(ABC): + + @abstractmethod + def execute(self): + pass + + @abstractmethod + def undo(self): + pass \ No newline at end of file diff --git a/volkovim/task2/command/move_command.py b/volkovim/task2/command/move_command.py new file mode 100644 index 0000000..6f96288 --- /dev/null +++ b/volkovim/task2/command/move_command.py @@ -0,0 +1,65 @@ +from command.command import Command + + +class MoveCommand(Command): + + def __init__( + self, + player, + maze, + direction + ): + self.player = player + self.maze = maze + self.direction = direction + self.previous = None + + def _targetCell(self): + + offsets = { + "W": (0, -1), + "S": (0, 1), + "A": (-1, 0), + "D": (1, 0) + } + + dx, dy = offsets.get( + self.direction.upper(), + (0, 0) + ) + + x, y = self.player.getPosition() + + return self.maze.getCell( + x + dx, + y + dy + ) + + def execute(self): + + destination = self._targetCell() + + if destination is None: + return False + + if not destination.isPassable(): + return False + + self.previous = self.player.current + + self.player.place( + destination + ) + + return True + + def undo(self): + + if self.previous is None: + return False + + self.player.place( + self.previous + ) + + return True \ No newline at end of file diff --git a/volkovim/task2/command/player.py b/volkovim/task2/command/player.py new file mode 100644 index 0000000..9f8249b --- /dev/null +++ b/volkovim/task2/command/player.py @@ -0,0 +1,13 @@ +class Player: + def __init__(self, start_cell): + self.current = start_cell + + def place(self, cell): + self.current = cell + + def getPosition(self): + return self.current.getPosition() + + def __str__(self): + x, y = self.getPosition() + return f"Player({x}, {y})" \ No newline at end of file diff --git a/volkovim/task2/core/cell.py b/volkovim/task2/core/cell.py new file mode 100644 index 0000000..d642eda --- /dev/null +++ b/volkovim/task2/core/cell.py @@ -0,0 +1,40 @@ +class Cell: + def __init__( + self, + x: int, + y: int, + isWall: bool = False, + isStart: bool = False, + isExit: bool = False + ): + self.x = x + self.y = y + self.isWall = isWall + self.isStart = isStart + self.isExit = isExit + + def isPassable(self) -> bool: + return self.isWall is False + + def getPosition(self): + return self.x, self.y + + def __str__(self): + if self.isStart: + return "S" + + if self.isExit: + return "E" + + if self.isWall: + return "#" + + return " " + + def __repr__(self): + return ( + f"Cell(x={self.x}, y={self.y}, " + f"wall={self.isWall}, " + f"start={self.isStart}, " + f"exit={self.isExit})" + ) \ No newline at end of file diff --git a/volkovim/task2/core/maze.py b/volkovim/task2/core/maze.py new file mode 100644 index 0000000..6a60931 --- /dev/null +++ b/volkovim/task2/core/maze.py @@ -0,0 +1,59 @@ +from core.cell import Cell + + +class Maze: + def __init__(self, cells_map, start_cell=None, exit_cell=None): + self.cells = cells_map + self.start = start_cell + self.exit = exit_cell + + self.height = len(cells_map) + self.width = len(cells_map[0]) if self.height else 0 + + def getCell(self, x: int, y: int): + if y < 0 or y >= self.height: + return None + + if x < 0 or x >= self.width: + return None + + return self.cells[y][x] + + def getNeighbors(self, current: Cell): + reachable = [] + + top = self.getCell(current.x, current.y - 1) + right = self.getCell(current.x + 1, current.y) + bottom = self.getCell(current.x, current.y + 1) + left = self.getCell(current.x - 1, current.y) + + for candidate in (top, right, bottom, left): + if candidate is None: + continue + + if candidate.isPassable(): + reachable.append(candidate) + + return reachable + + def hasStart(self): + return self.start is not None + + def hasExit(self): + return self.exit is not None + + def size(self): + return self.width, self.height + + def __str__(self): + rows = [] + + for line in self.cells: + visual = "" + + for cell in line: + visual += str(cell) + + rows.append(visual) + + return "\n".join(rows) \ No newline at end of file diff --git a/volkovim/task2/core/search_stats.py b/volkovim/task2/core/search_stats.py new file mode 100644 index 0000000..9407acb --- /dev/null +++ b/volkovim/task2/core/search_stats.py @@ -0,0 +1,22 @@ +class SearchStats: + def __init__( + self, + strategy_name, + elapsed_ms, + visited_cells, + path_length + ): + self.strategy_name = strategy_name + self.elapsed_ms = elapsed_ms + self.visited_cells = visited_cells + self.path_length = path_length + + def __str__(self): + lines = [ + f"Strategy: {self.strategy_name}", + f"Time: {self.elapsed_ms:.3f} ms", + f"Visited: {self.visited_cells}", + f"Path length: {self.path_length}" + ] + + return "\n".join(lines) \ No newline at end of file diff --git a/volkovim/task2/experiments/benchmark.py b/volkovim/task2/experiments/benchmark.py new file mode 100644 index 0000000..e563600 --- /dev/null +++ b/volkovim/task2/experiments/benchmark.py @@ -0,0 +1,93 @@ +import csv + +from solver.maze_solver import MazeSolver + + +class BenchmarkRunner: + + def __init__( + self, + maze, + strategies, + cycles=5 + ): + self.maze = maze + self.strategies = strategies + self.cycles = cycles + + def launch(self): + + report = [] + + for strategy in self.strategies: + + solver = MazeSolver( + self.maze, + strategy + ) + + total_time = 0 + total_visited = 0 + total_path = 0 + + for _ in range(self.cycles): + + _, stats = solver.solve() + + total_time += stats.time_ms + total_visited += stats.visited_cells + total_path += stats.path_length + + report.append( + { + "maze": "", + "strategy": + strategy.__class__.__name__, + "time_ms": + round( + total_time / self.cycles, + 4 + ), + "visited_cells": + round( + total_visited / self.cycles, + 2 + ), + "path_length": + round( + total_path / self.cycles, + 2 + ) + } + ) + + return report + + def exportCSV( + self, + filename, + results + ): + + with open( + filename, + "w", + newline="", + encoding="utf-8" + ) as file: + + writer = csv.DictWriter( + file, + fieldnames=[ + "maze", + "strategy", + "time_ms", + "visited_cells", + "path_length" + ] + ) + + writer.writeheader() + + for row in results: + writer.writerow(row) \ No newline at end of file diff --git a/volkovim/task2/experiments/plots.py b/volkovim/task2/experiments/plots.py new file mode 100644 index 0000000..b0da4d2 --- /dev/null +++ b/volkovim/task2/experiments/plots.py @@ -0,0 +1,161 @@ +import csv +import matplotlib.pyplot as plt + + +class ChartBuilder: + + def __init__( + self, + csv_file + ): + self.csv_file = csv_file + + def _read(self): + + rows = [] + + with open( + self.csv_file, + "r", + encoding="utf-8" + ) as file: + + reader = csv.DictReader(file) + + for row in reader: + rows.append(row) + + return rows + + def buildTimeChart(self): + + rows = self._read() + + labels = [] + values = [] + + for row in rows: + + labels.append( + f"{row['maze']}\n" + f"{row['strategy']}" + ) + + values.append( + float( + row["time_ms"] + ) + ) + + plt.figure() + + plt.bar( + labels, + values + ) + + plt.title( + "Search Time" + ) + + plt.ylabel( + "Milliseconds" + ) + + plt.xticks( + rotation=45 + ) + + plt.tight_layout() + + plt.show() + + def buildVisitedChart(self): + + rows = self._read() + + labels = [] + values = [] + + for row in rows: + + labels.append( + f"{row['maze']}\n" + f"{row['strategy']}" + ) + + values.append( + float( + row[ + "visited_cells" + ] + ) + ) + + plt.figure() + + plt.bar( + labels, + values + ) + + plt.title( + "Visited Cells" + ) + + plt.ylabel( + "Cells" + ) + + plt.xticks( + rotation=45 + ) + + plt.tight_layout() + + plt.show() + + def buildPathChart(self): + + rows = self._read() + + labels = [] + values = [] + + for row in rows: + + labels.append( + f"{row['maze']}\n" + f"{row['strategy']}" + ) + + values.append( + float( + row[ + "path_length" + ] + ) + ) + + plt.figure() + + plt.bar( + labels, + values + ) + + plt.title( + "Path Length" + ) + + plt.ylabel( + "Cells" + ) + + plt.xticks( + rotation=45 + ) + + plt.tight_layout() + + plt.show() \ No newline at end of file diff --git a/volkovim/task2/experiments/results.csv b/volkovim/task2/experiments/results.csv new file mode 100644 index 0000000..230672b --- /dev/null +++ b/volkovim/task2/experiments/results.csv @@ -0,0 +1,16 @@ +maze,strategy,time_ms,visited_cells,path_length +small.txt,BFSStrategy,0.0676,53.0,23.0 +small.txt,DFSStrategy,0.0527,31.0,31.0 +small.txt,AStarStrategy,0.0682,46.0,23.0 +medium.txt,BFSStrategy,0.7144,717.0,431.0 +medium.txt,DFSStrategy,0.6878,737.0,431.0 +medium.txt,AStarStrategy,0.6968,591.0,431.0 +large.txt,BFSStrategy,2.103,2491.0,1171.0 +large.txt,DFSStrategy,2.7719,3019.0,1243.0 +large.txt,AStarStrategy,2.5953,1995.0,1171.0 +empty.txt,BFSStrategy,0.027,19.0,8.0 +empty.txt,DFSStrategy,0.0115,8.0,8.0 +empty.txt,AStarStrategy,0.0151,8.0,8.0 +blocked.txt,BFSStrategy,0.002,1.0,0.0 +blocked.txt,DFSStrategy,0.0013,1.0,0.0 +blocked.txt,AStarStrategy,0.0019,1.0,0.0 diff --git a/volkovim/task2/main.py b/volkovim/task2/main.py new file mode 100644 index 0000000..8f7bb39 --- /dev/null +++ b/volkovim/task2/main.py @@ -0,0 +1,70 @@ +from builders.text_file_builder import TextFileMazeBuilder + +from strategies.bfs import BFSStrategy +from strategies.dfs import DFSStrategy +from strategies.astar import AStarStrategy + +from experiments.benchmark import BenchmarkRunner +from experiments.plots import ChartBuilder + + +builder = TextFileMazeBuilder() + +maze_files = [ + "small.txt", + "medium.txt", + "large.txt", + "empty.txt", + "blocked.txt" +] + +all_results = [] + +for maze_file in maze_files: + + print() + print("Loading:", maze_file) + + maze = builder.buildFromFile( + f"mazes/{maze_file}" + ) + + runner = BenchmarkRunner( + maze, + [ + BFSStrategy(), + DFSStrategy(), + AStarStrategy() + ], + cycles=10 + ) + + results = runner.launch() + + for row in results: + row["maze"] = maze_file + + all_results.extend(results) + +runner.exportCSV( + "experiments/results.csv", + all_results +) + +print() +print("CSV created") + +charts = ChartBuilder( + "experiments/results.csv" +) + +print("Time chart...") +charts.buildTimeChart() + +print("Visited chart...") +charts.buildVisitedChart() + +print("Path chart...") +charts.buildPathChart() + +print("Done") \ No newline at end of file diff --git a/volkovim/task2/mazes/blocked.txt b/volkovim/task2/mazes/blocked.txt new file mode 100644 index 0000000..179e497 --- /dev/null +++ b/volkovim/task2/mazes/blocked.txt @@ -0,0 +1,3 @@ +########## +#S#####E## +########## \ No newline at end of file diff --git a/volkovim/task2/mazes/empty.txt b/volkovim/task2/mazes/empty.txt new file mode 100644 index 0000000..d502d43 --- /dev/null +++ b/volkovim/task2/mazes/empty.txt @@ -0,0 +1,5 @@ +########## +#S E# +# # +# # +########## \ No newline at end of file diff --git a/volkovim/task2/mazes/large.txt b/volkovim/task2/mazes/large.txt new file mode 100644 index 0000000..0ce5102 --- /dev/null +++ b/volkovim/task2/mazes/large.txt @@ -0,0 +1,100 @@ +S # # # # # # # # # # # # # # + # # ####### ### # ##### # # # # # # # # ### # # ### # # ### # ##### # ##### ### ### ### ######### # + # # # # # # # # # # # # # # # # # # # # # # # # # # + # ##### ##### ### ### ##### ##### ####### # ##### ####### # # # ####### ##### # ######### ### ### # + # # # # # # # # # # # # # # # # # # # # # # # # # + # # ##### # # ##### # # ####### # ### ##### # # # # ### ### ######### ##### # ##### # # ### ### # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + ##### # # # ### ### ####### # # ### ######### ### ### ### ### # # # # # ### ##### # # ### # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # + ### # ### # ##### ##### ######### ######### # ##### ### # ####### # ##### ### ####### # ### # ### # + # # # # # # # # # # # # # # # # # # # # # + # ################# # ### # # # # # ########### ############# # ##### ##### ##### # ##### ### # ### + # # # # # # # # # # # # # # # # # # # # # # + ### # ### # ### ####### # ##### ######### ### # ### ### # ####### # ### ####### ##### # ### # # # # + # # # # # # # # # # # # # # # # # # # # # # # + ######### ### ### # ### # ####### ##### # # # ####### ##### ####### # ########### ####### ######### + # # # # # # # # # # # # # # # # # # # # # +## # ####### ### ##### # ####### ### # # # # ##### ### ########### # # # ### # ##### # # ### ##### # + # # # # # # # # # # # # # # # # # # # # # # # # # + ### # # ########### ######### # ### # # ### ### ####### ### # # # # ##### # ### ##### # # ### ### # + # # # # # # # # # # # # # # # # # # # # # # # # # # # +## ##### # # # # # ### # ### ##### # # # ##### # ### # ### # # # # # # # ##### ### ####### # ### ### + # # # # # # # # # # # # # # # # # # # # # # # # # # # # + ##### ### # # # # # ######### ### ### ### # ##### ##### ### # ##### # ######### ############# ### # + # # # # # # # # # # # # # # # # # # # # # # # # + # ##### # # ### ### ##### # ### ### ##### # # ### # ##### ##### ### ### # ##### ### # # ### # # ### + # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # ####### ####### ### ### ######### ### # # ##### # ### ########### ### # ### ####### ### # # # + # # # # # # # # # # # # # # # # # # # # # # + ################### # # # # ### # # # ######### # # ### ############### # ### ##### ### ### ##### # + # # # # # # # # # # # # # # # # # # # # # + ##### # # ### # # ### # ####### # # ##### # # ### ##### # ####### # # # ######### # ######### ##### + # # # # # # # # # # # # # # # # # # # # # # # # # # # # +#### # # ### # # ### # ### ####### ##### # # ### # # ##### # # ####### # # # ##### ### # # ####### # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +## # # # # ### ### # ### ### # # # ### # ####### ### # ########### # ##### ##### # # ### ### ##### # + # # # # # # # # # # # # # # # # # # # # # # # # # # + ########### # # ########### ### # # # # # ### # # ### # ####### # ######### # # ### # ######### ### + # # # # # # # # # # # # # # # # # # # # # # # # +######## # # # ### # ########### # # ####### ### # # # ####### ### # # # ### # ### ######### # # # # + # # # # # # # # # # # # # # # # # # # # # # # + ##### ##### ### # # # ####### ### ### # # # # ############# # ##### # ### ##### ######### ### ### # + # # # # # # # # # # # # # # # # # # # # # # # # # + ### ### # ### # ##### # # # ### # # # ### # ### ### # # # # ########### ####### # ##### ####### ### + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +## ####### # ##### # ### ### # # # # # # # ####### ### # # # # # # # # ### # # ##### # # # # ##### # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + ### # # # ##### ####### # ### # ### ### ####### # # ### ####### ### ####### # ### ####### # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # + # ##### ##### # # ### ### # ######### ### ### ### ### ####### ##### # ######### ##### # ### # ### # + # # # # # # # # # # # # # # # # # # # # # # # # # + # # ####### # ##### ### # # # # # ### ### # ### ### ######### # ####### # ####### ### ######### ### + # # # # # # # # # # # # # # # # # # # # # # # # # # # + ######### # ### ### # # # # # # ####### ##### ### # # ##### # ### # # ####### # # # ### # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # +## # # # ### ##### # ######### # # ####### ### ### # ##### ######### # # # ####### ### # # # # ### # + # # # # # # # # # # # # # # # # # # # # # # # + ### ######### ####### # # ######### # # ### ### ### ##### ### # ########### ######### # # # ### ### + # # # # # # # # # # # # # # # # # # # # # # # # +## ######### # ### # ####### ### # ### ### # # ####### # ### ##### ### # # ### # ### ##### # # ### # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # ### ### ### # # # ##### # ### # ### ### # # # ####### ##### ### ### # ##### # ##### # ### # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # ### ### # ### # ######### # ######### # ### # # # ### ##### ### # ### # # ### # ##### ##### # + # # # # # # # # # # # # # # # # # # # # # # # # # # + ##### # ### ##### ####### ### # ##### # # # ### ### # ##### # # ### # ####### ##### # # # ### # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # + ### # ### ######### # ##### ### # ### # # # ############# ### ### # ##### # ### ######### # ### # # + # # # # # # # # # # # # # # # # # # # # # # # # +## # # # ### ##### # ##### # ### ### # # # ### # # ######### # # ##### ### ####### ### # ######### # + # # # # # # # # # # # # # # # # # # # # # # # # # # # + # ############### ### ####### ### # ### ####### # # # # ######### # ### # # # ##### ##### # # ##### + # # # # # # # # # # # # # # # # # # # # # # + ##### # # ### # ### ######### # ######### # # ### # # ####### # ########### # # # ##### ####### # # + # # # # # # # # # # # # # # # # # # # # # # # +## # ####### ### # # # ############# ### ### ### ### ##### # # ### ########### ##### # ### ### # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # + ### ### # # # ##### ### # # ##### ####### # # ### ### # # ##### # # ####### ##### # # # # # ##### # + # # # # # # # # # # # # # # # # # # # # # # # # # # # +###### ##### ### # ### ### # # ########### # # # ##### # ### ##### # # ### ######### ### ##### # ### + # # # # # # # # # # # # # # # # # # # # # # # + # # # # # ### ####### ######### # # ######### ### # ##### ##### ##### ### # # # # # # ### ####### # + # # # # # # # # # # # # # # # # # # # # # # # # +#### # ##### # # # ##### ### # ### # ##### ### # ##### ### ######### ### ### ########### # # ##### # + # # # # # # # # # # # # # # # # # # # # # # # # # # # + ######### ##### # ####### # # # ##### # ### # # # # ####### ##### # # ### ### # # # # ### # # ### # + # # # # # # # # # # # # # # # # # # # # # # # # # # + # ### # ##### # # # ############### ### ######### ### # ##### # # ######### # ### # ### # # # # ### + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # ####### # ### # ##### ##### # # ### # ### # # ##### # ### # # # ####### # ##### # # # ### # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # + ##### # ### # # # ##### ##### # # # ### ### # # # # ######### # ########### # # ##### ####### # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # +## # # # # ##### # ### ####### ########### ### # # ####### # # ##### # # # ### ##### ##### # ##### # + # # # # # # # # # # # # # # # # # # # # # # + ### ##### ########### # ### # # ####### # # # ############# # ### ### # ### ######### # ### ### # # + # # # # # # # # # # # # # # # # # # # # # # # # # + ##### # # # ##### ####### # ### # # ##### # ### ####### ######### # ########### ### # ######### # # + # # # # # # # # # +################################################################################################## E \ No newline at end of file diff --git a/volkovim/task2/mazes/medium.txt b/volkovim/task2/mazes/medium.txt new file mode 100644 index 0000000..6eba40a --- /dev/null +++ b/volkovim/task2/mazes/medium.txt @@ -0,0 +1,50 @@ +S # # # # + ####### ### # # ########### ##### # ##### ##### # + # # # # # # # # # # +## # # ### ####### ##### ####### # ### ######### # + # # # # # # # # # # # # # + ####### # # # # ### ##### # ### ### # # # # ### # + # # # # # # # # # # # # # # # # # + # ### # ### # ### ### # # ### ### ####### # # ### + # # # # # # # # # # # # # # + # # ############# # ### ### # ######### # # ### # + # # # # # # # # # + ########### ########### # ##### ### ### # # # ### + # # # # # # # # # # # # # # + # # ####### # ### # ##### ### ### ### ### # # # # + # # # # # # # # # # # # # # # # +###### ### ### # # # # # # # ### ### ##### # # # # + # # # # # # # # # # # # # # # + ### # # ######### ### # # # # ####### ##### # # # + # # # # # # # # # # # # # # + ### ### # ##### # # ######### # # # # ##### # # # + # # # # # # # # # # # # +## # ######### # # ### ### # ### ######### ##### # + # # # # # # # # # # # # + ##### # ### # ### ##### # # # ####### ##### # # # + # # # # # # # # # # # # # # # +## # ##### # # ##### ##### ### ### # ### # # # ### + # # # # # # # # # # # # # + ##### # ### # # ##### ### # ### ######### # ##### + # # # # # # # # # # # + # ####### ######### ### ####### # # ####### ### # + # # # # # # # # # # # # # # + # # ####### # # ##### # # ### ### # # # # ##### # + # # # # # # # # # # # # # # # # + # ##### # ####### # # # # # ### # ### # # # ### # + # # # # # # # # # # # # # # # + # ########### # ### ####### ### # ### # # # # # # + # # # # # # # # # # # # # + # # ####### ##### ########### ##### # # ##### # # + # # # # # # # # # # # + ### ### ### # ############### # # # ##### ### ### + # # # # # # # # # # # # # # + # ### ### # ### ##### # # # # # ##### # ### # # # + # # # # # # # # # # # # # # + # # ####### # ### ######### ######### ### # # # # + # # # # # # # # # # # # # # # + ##### # ####### # # # ### # # # # # ### ### # # # + # # # # # # # # # # # # # # # +## ### ##### ####### ### # # ### ##### # ### ### # + # # # # +################################################ E \ No newline at end of file diff --git a/volkovim/task2/mazes/small.txt b/volkovim/task2/mazes/small.txt new file mode 100644 index 0000000..439365c --- /dev/null +++ b/volkovim/task2/mazes/small.txt @@ -0,0 +1,10 @@ +S # + ### ### # + # # # +## # # ### + # # # + ####### # + # # +## # ##### + +######## E \ No newline at end of file diff --git a/volkovim/task2/observer/console_view.py b/volkovim/task2/observer/console_view.py new file mode 100644 index 0000000..05f31fc --- /dev/null +++ b/volkovim/task2/observer/console_view.py @@ -0,0 +1,64 @@ +from observer.observer import Observer + + +class ConsoleView(Observer): + + def update(self, event): + + event_type = event.get("type") + + if event_type == "maze_loaded": + print("[VIEW] Maze loaded") + + elif event_type == "search_started": + print("[VIEW] Search started") + + elif event_type == "search_finished": + print("[VIEW] Search completed") + + elif event_type == "path_found": + print( + f"[VIEW] Path length: " + f"{event.get('length')}" + ) + + def render( + self, + maze, + path=None + ): + route_marks = set() + + if path: + for cell in path: + route_marks.add( + cell.getPosition() + ) + + screen = [] + + for row in maze.cells: + + visual_row = "" + + for cell in row: + + position = cell.getPosition() + + if ( + position in route_marks + and not cell.isStart + and not cell.isExit + ): + visual_row += "*" + + else: + visual_row += str(cell) + + screen.append( + visual_row + ) + + print( + "\n".join(screen) + ) \ No newline at end of file diff --git a/volkovim/task2/observer/observer.py b/volkovim/task2/observer/observer.py new file mode 100644 index 0000000..3a8886f --- /dev/null +++ b/volkovim/task2/observer/observer.py @@ -0,0 +1,8 @@ +from abc import ABC, abstractmethod + + +class Observer(ABC): + + @abstractmethod + def update(self, event): + pass \ No newline at end of file diff --git a/volkovim/task2/report/report_2.docx b/volkovim/task2/report/report_2.docx new file mode 100644 index 0000000..1143cd7 Binary files /dev/null and b/volkovim/task2/report/report_2.docx differ diff --git a/volkovim/task2/requirements.txt b/volkovim/task2/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/volkovim/task2/solver/maze_solver.py b/volkovim/task2/solver/maze_solver.py new file mode 100644 index 0000000..cbee4ca --- /dev/null +++ b/volkovim/task2/solver/maze_solver.py @@ -0,0 +1,73 @@ +import time + +from solver.search_stats import SearchStats + + +class MazeSolver: + + def __init__( + self, + maze, + strategy + ): + self.maze = maze + self.strategy = strategy + self.observers = [] + + def addObserver( + self, + observer + ): + self.observers.append( + observer + ) + + def notify( + self, + event + ): + for observer in self.observers: + observer.update(event) + + def setStrategy( + self, + strategy + ): + self.strategy = strategy + + def solve(self): + + self.notify( + "search_started" + ) + + start_time = time.perf_counter() + + path, visited_cells = ( + self.strategy.findPath( + self.maze, + self.maze.start, + self.maze.exit + ) + ) + + finish_time = ( + time.perf_counter() + ) + + elapsed_ms = ( + finish_time + - start_time + ) * 1000 + + stats = SearchStats( + elapsed_ms, + visited_cells, + len(path) + ) + + self.notify( + "search_finished" + ) + + return path, stats \ No newline at end of file diff --git a/volkovim/task2/solver/search_stats.py b/volkovim/task2/solver/search_stats.py new file mode 100644 index 0000000..9553063 --- /dev/null +++ b/volkovim/task2/solver/search_stats.py @@ -0,0 +1,22 @@ +class SearchStats: + + def __init__( + self, + time_ms, + visited_cells, + path_length + ): + self.time_ms = time_ms + self.visited_cells = visited_cells + self.path_length = path_length + + def __str__(self): + + return ( + f"Time: " + f"{self.time_ms:.4f} ms | " + f"Visited: " + f"{self.visited_cells} | " + f"Path length: " + f"{self.path_length}" + ) \ No newline at end of file diff --git a/volkovim/task2/strategies/astar.py b/volkovim/task2/strategies/astar.py new file mode 100644 index 0000000..0907a75 --- /dev/null +++ b/volkovim/task2/strategies/astar.py @@ -0,0 +1,107 @@ +import heapq +from itertools import count +from strategies.strategy import PathFindingStrategy + + +class AStarStrategy(PathFindingStrategy): + + def _estimate(self, current, target): + return ( + abs(current.x - target.x) + + abs(current.y - target.y) + ) + + def findPath(self, maze, start, exit): + + frontier = [] + + sequence = count() + + heapq.heappush( + frontier, + ( + self._estimate(start, exit), + next(sequence), + 0, + start + ) + ) + + ancestry = {} + + travel_cost = { + start.getPosition(): 0 + } + + explored = set() + + explored_count = 0 + + while frontier: + + _, _, spent, current = heapq.heappop( + frontier + ) + + current_mark = current.getPosition() + + if current_mark in explored: + continue + + explored.add(current_mark) + explored_count += 1 + + if current == exit: + break + + for neighbor in maze.getNeighbors(current): + + mark = neighbor.getPosition() + + new_cost = spent + 1 + + if ( + mark not in travel_cost + or new_cost < travel_cost[mark] + ): + + travel_cost[mark] = new_cost + ancestry[mark] = current + + priority = ( + new_cost + + self._estimate( + neighbor, + exit + ) + ) + + heapq.heappush( + frontier, + ( + priority, + next(sequence), + new_cost, + neighbor + ) + ) + + if ( + exit.getPosition() not in ancestry + and exit != start + ): + return [], explored_count + + route = [] + cursor = exit + + while cursor != start: + route.append(cursor) + cursor = ancestry[ + cursor.getPosition() + ] + + route.append(start) + route.reverse() + + return route, explored_count \ No newline at end of file diff --git a/volkovim/task2/strategies/bfs.py b/volkovim/task2/strategies/bfs.py new file mode 100644 index 0000000..0860126 --- /dev/null +++ b/volkovim/task2/strategies/bfs.py @@ -0,0 +1,51 @@ +from collections import deque +from strategies.strategy import PathFindingStrategy + + +class BFSStrategy(PathFindingStrategy): + + def findPath(self, maze, start, exit): + + frontier = deque([start]) + + visited = { + start.getPosition() + } + + ancestry = {} + + explored_count = 0 + + while frontier: + + current = frontier.popleft() + explored_count += 1 + + if current == exit: + break + + for neighbor in maze.getNeighbors(current): + + mark = neighbor.getPosition() + + if mark in visited: + continue + + visited.add(mark) + ancestry[mark] = current + frontier.append(neighbor) + + if exit.getPosition() not in visited: + return [], explored_count + + route = [] + cursor = exit + + while cursor != start: + route.append(cursor) + cursor = ancestry[cursor.getPosition()] + + route.append(start) + route.reverse() + + return route, explored_count \ No newline at end of file diff --git a/volkovim/task2/strategies/dfs.py b/volkovim/task2/strategies/dfs.py new file mode 100644 index 0000000..3b12bdb --- /dev/null +++ b/volkovim/task2/strategies/dfs.py @@ -0,0 +1,52 @@ +from strategies.strategy import PathFindingStrategy + + +class DFSStrategy(PathFindingStrategy): + + def findPath(self, maze, start, exit): + + frontier = [start] + + visited = { + start.getPosition() + } + + ancestry = {} + + explored_count = 0 + + while frontier: + + current = frontier.pop() + explored_count += 1 + + if current == exit: + break + + neighbors = maze.getNeighbors(current) + + for neighbor in reversed(neighbors): + + point = neighbor.getPosition() + + if point in visited: + continue + + visited.add(point) + ancestry[point] = current + frontier.append(neighbor) + + if exit.getPosition() not in visited: + return [], explored_count + + route = [] + cursor = exit + + while cursor != start: + route.append(cursor) + cursor = ancestry[cursor.getPosition()] + + route.append(start) + route.reverse() + + return route, explored_count \ No newline at end of file diff --git a/volkovim/task2/strategies/dijkstra.py b/volkovim/task2/strategies/dijkstra.py new file mode 100644 index 0000000..e69de29 diff --git a/volkovim/task2/strategies/strategy.py b/volkovim/task2/strategies/strategy.py new file mode 100644 index 0000000..ef3376e --- /dev/null +++ b/volkovim/task2/strategies/strategy.py @@ -0,0 +1,8 @@ +from abc import ABC, abstractmethod + + +class PathFindingStrategy(ABC): + + @abstractmethod + def findPath(self, maze, start, exit): + pass \ No newline at end of file