diff --git a/anikinvd/docs/data/2-nd-exercise/main.py b/anikinvd/docs/data/2-nd-exercise/main.py new file mode 100644 index 0000000..deb8308 --- /dev/null +++ b/anikinvd/docs/data/2-nd-exercise/main.py @@ -0,0 +1,490 @@ +import sys +from collections import deque +import heapq +import time +import os + + +# ---------- Модель лабиринта ---------- +class Tile: + def __init__(self, column, row): + self.col = column + self.row = row + self.blocked = False + self.is_start = False + self.is_exit = False + + @property + def x(self): + return self.col + + @property + def y(self): + return self.row + + @property + def is_wall(self): + return self.blocked + + @is_wall.setter + def is_wall(self, value): + self.blocked = value + + def can_step(self): + return not self.blocked + + +class Labyrinth: + def __init__(self, width, height): + self._w = width + self._h = height + self._grid = [[Tile(x, y) for x in range(width)] for y in range(height)] + self._start_tile = None + self._exit_tile = None + + @property + def width(self): + return self._w + + @property + def height(self): + return self._h + + @property + def start(self): + return self._start_tile + + @property + def exit(self): + return self._exit_tile + + def get_tile(self, x, y): + if 0 <= x < self._w and 0 <= y < self._h: + return self._grid[y][x] + return None + + def set_tile_type(self, x, y, kind): + tile = self.get_tile(x, y) + if tile is None: + return + + if kind == 'wall': + tile.blocked = True + elif kind == 'start': + if self._start_tile: + self._start_tile.is_start = False + tile.is_start = True + tile.blocked = False + self._start_tile = tile + elif kind == 'exit': + if self._exit_tile: + self._exit_tile.is_exit = False + tile.is_exit = True + tile.blocked = False + self._exit_tile = tile + elif kind == 'path': + tile.blocked = False + + def neighbours(self, tile): + """Возвращает список проходимых соседей""" + result = [] + directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] # вверх, вниз, влево, вправо + for dx, dy in directions: + nx, ny = tile.x + dx, tile.y + dy + nb = self.get_tile(nx, ny) + if nb and nb.can_step(): + result.append(nb) + return result + + +# ---------- Загрузка лабиринта ---------- +class MazeLoader: + def load(self, filename): + raise NotImplementedError + + +class TxtMazeLoader(MazeLoader): + def load(self, filename): + with open(filename, 'r') as f: + lines = [line.rstrip('\n') for line in f.readlines()] + h = len(lines) + w = max(len(line) for line in lines) if h > 0 else 0 + start_cnt = 0 + exit_cnt = 0 + lab = Labyrinth(w, h) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + if ch == "#": + lab.set_tile_type(x, y, "wall") + elif ch == "S": + lab.set_tile_type(x, y, "start") + start_cnt += 1 + elif ch == "E": + lab.set_tile_type(x, y, "exit") + exit_cnt += 1 + else: + lab.set_tile_type(x, y, 'path') + + if start_cnt != 1 or exit_cnt != 1: + raise ValueError(f"Maze error: S={start_cnt}, E={exit_cnt} (need exactly one each)") + return lab + + +# ---------- Стратегии поиска пути ---------- +class SearchStrategy: + def find_path(self, lab, start, goal): + raise NotImplementedError + + def _rebuild_path(self, came_from, start, goal): + path = [] + cur = goal + while cur is not None: + path.append(cur) + cur = came_from.get(cur) + path.reverse() + return path + + def visited_cells(self): + return getattr(self, '_visited', 0) + + +class BFS(SearchStrategy): + def find_path(self, lab, start, goal): + q = deque() + q.append(start) + parent = {start: None} + visited = {start} + + while q: + cur = q.popleft() + if cur == goal: + self._visited = len(visited) + return self._rebuild_path(parent, start, goal) + for nb in lab.neighbours(cur): + if nb not in visited: + visited.add(nb) + parent[nb] = cur + q.append(nb) + self._visited = len(visited) + return [] + + +class DFS(SearchStrategy): + def find_path(self, lab, start, goal): + stack = [start] + parent = {start: None} + visited = {start} + + while stack: + cur = stack.pop() + if cur == goal: + self._visited = len(visited) + return self._rebuild_path(parent, start, goal) + for nb in lab.neighbours(cur): + if nb not in visited: + visited.add(nb) + parent[nb] = cur + stack.append(nb) + self._visited = len(visited) + return [] + + +class AStar(SearchStrategy): + def _heuristic(self, a, b): + return abs(a.x - b.x) + abs(a.y - b.y) + + def find_path(self, lab, start, goal): + heap = [] + counter = 0 + start_f = self._heuristic(start, goal) + heapq.heappush(heap, (start_f, counter, start)) + counter += 1 + + parent = {} + g = {start: 0} + f = {start: start_f} + visited = set() + + while heap: + cur_f, _, cur = heapq.heappop(heap) + visited.add(cur) + if cur == goal: + self._visited = len(visited) + return self._rebuild_path(parent, start, goal) + if cur_f > f.get(cur, float('inf')): + continue + for nb in lab.neighbours(cur): + new_g = g[cur] + 1 + if new_g < g.get(nb, float('inf')): + parent[nb] = cur + g[nb] = new_g + new_f = new_g + self._heuristic(nb, goal) + f[nb] = new_f + heapq.heappush(heap, (new_f, counter, nb)) + counter += 1 + self._visited = len(visited) + return [] + + +# ---------- Статистика ---------- +class SearchStats: + def __init__(self, time_ms, visited, path_len): + self.time_ms = time_ms + self.visited_cells = visited + self.path_length = path_len + + +# ---------- Наблюдатель ---------- +class Observer: + def notify(self, event, data): + raise NotImplementedError + + +class ConsoleDisplay(Observer): + def __init__(self, player=None): + self._last_path = None + self._player = player + + def notify(self, event, data): + if event == "maze_loaded": + self._draw_maze(data) + elif event == "path_found": + self._last_path = data + self._show_path(data) + elif event == "player_moved": + self._draw_maze_with_player(data) + + def _draw_maze(self, lab): + os.system('cls' if os.name == 'nt' else 'clear') + print("=" * (lab.width * 2 + 4)) + print(" LABYRINTH") + print("=" * (lab.width * 2 + 4)) + + for y in range(lab.height): + print(" ", end='') + for x in range(lab.width): + cell = lab.get_tile(x, y) + if cell == lab.start: + print('S', end=' ') + elif cell == lab.exit: + print('E', end=' ') + elif cell.is_wall: + print('#', end=' ') + else: + print('.', end=' ') + print() + print("=" * (lab.width * 2 + 4)) + print(" S - start E - exit # - wall . - free") + + def _draw_maze_with_player(self, lab): + os.system('cls' if os.name == 'nt' else 'clear') + print("=" * (lab.width * 2 + 4)) + print(" LABYRINTH (P = player)") + print("=" * (lab.width * 2 + 4)) + + for y in range(lab.height): + print(" ", end='') + for x in range(lab.width): + cell = lab.get_tile(x, y) + if self._player and cell == self._player.position: + print('P', end=' ') + elif cell == lab.start: + print('S', end=' ') + elif cell == lab.exit: + print('E', end=' ') + elif cell.is_wall: + print('#', end=' ') + else: + print('.', end=' ') + print() + print("=" * (lab.width * 2 + 4)) + print(f" Player at: ({self._player.position.x}, {self._player.position.y})") + print(" S - start E - exit # - wall . - free P - player") + + def _show_path(self, path): + if not path: + print("\n No route found!") + return + print(f"\n Route found! Length = {len(path)}") + + +# ---------- Игрок и команды ---------- +class Player: + def __init__(self, start_cell, lab): + self._pos = start_cell + self._prev = None + self._lab = lab + + @property + def position(self): + return self._pos + + def move(self, target): + if target and target.can_step(): + self._prev = self._pos + self._pos = target + return True + return False + + def undo(self): + if self._prev: + self._pos, self._prev = self._prev, None + return True + return False + + +class Action: + def execute(self): + raise NotImplementedError + + def revert(self): + raise NotImplementedError + + +class MoveAction(Action): + def __init__(self, player, direction, lab): + self._player = player + self._dx, self._dy = direction + self._lab = lab + self._done = False + + def execute(self): + new_x = self._player.position.x + self._dx + new_y = self._player.position.y + self._dy + target = self._lab.get_tile(new_x, new_y) + if target and target.can_step(): + self._player.move(target) + self._done = True + return True + return False + + def revert(self): + if self._done: + self._player.undo() + self._done = False + return True + return False + + +# ---------- Решатель лабиринта ---------- +class LabyrinthSolver: + def __init__(self, lab): + self._lab = lab + self._strategy = None + self._watchers = [] + + def attach(self, observer): + self._watchers.append(observer) + + def _broadcast(self, event, data): + for obs in self._watchers: + obs.notify(event, data) + + def set_algorithm(self, strategy): + self._strategy = strategy + + def solve(self): + if self._strategy is None: + return None + + t0 = time.perf_counter() + path = self._strategy.find_path(self._lab, self._lab.start, self._lab.exit) + t1 = time.perf_counter() + elapsed_ms = (t1 - t0) * 1000 + + self._broadcast("path_found", path) + return SearchStats(elapsed_ms, self._strategy.visited_cells(), len(path)) + + +def run_experiment(maze_file, algorithm, runs=5): + loader = TxtMazeLoader() + lab = loader.load(maze_file) + + total_time = 0.0 + total_visited = 0 + total_length = 0 + + for _ in range(runs): + solver = LabyrinthSolver(lab) + solver.set_algorithm(algorithm) + stats = solver.solve() + if stats: + total_time += stats.time_ms + total_visited += stats.visited_cells + total_length += stats.path_length + + return { + 'time_ms': total_time / runs, + 'visited_cells': total_visited / runs, + 'path_length': total_length / runs + } + + +# ---------- Точка входа ---------- +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == 'experiment': + print("Running experiments...") + sys.exit(0) + + loader = TxtMazeLoader() + lab = loader.load("maze1.txt") + + player = Player(lab.start, lab) + view = ConsoleDisplay(player) + view.notify("maze_loaded", lab) + + solver = LabyrinthSolver(lab) + solver.attach(view) + + print("\n CONTROLS:") + print(" H (left) J (down) K (up) L (right)") + print(" U - undo Q - quit") + print("\n AUTO SEARCH:") + print(" B - BFS D - DFS A - A*") + print("\n" + "=" * 50) + + history = [] + + while True: + cmd = input("\n Command > ").lower() + + if cmd == 'q': + print("\n Goodbye!") + break + elif cmd == 'b': + solver.set_algorithm(BFS()) + stats = solver.solve() + print(f"\n BFS: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") + elif cmd == 'd': + solver.set_algorithm(DFS()) + stats = solver.solve() + print(f"\n DFS: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") + elif cmd == 'a': + solver.set_algorithm(AStar()) + stats = solver.solve() + print(f"\n A*: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") + elif cmd in ['h', 'j', 'k', 'l']: + dir_map = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)} + action = MoveAction(player, dir_map[cmd], lab) + if action.execute(): + history.append(action) + view.notify("player_moved", lab) + if player.position == lab.exit: + print("\n *** VICTORY! EXIT REACHED ***") + print(f" Moves made: {len(history)}") + break + else: + print("\n Blocked by wall!") + elif cmd == 'u': + if history: + act = history.pop() + act.revert() + view.notify("player_moved", lab) + print("\n Undo successful") + else: + print("\n Nothing to undo") + else: + print("\n Unknown command. Use h,j,k,l to move, u to undo, q to quit") + + print("\n Game over. Thanks for playing!") \ No newline at end of file