2026-rff_mp/ShulpinIN/maze_lab2/maze.py

532 lines
15 KiB
Python
Raw Normal View History

import sys
from collections import deque
import heapq
import time
import os
from abc import ABC, abstractmethod
from typing import List, Optional, Dict, Any
DATA_PATH = r"C:\Users\User\2026-rff_mp\ShulpinIN\maze_lab2\docs\data"
class Observer(ABC):
@abstractmethod
def update(self, event: str, data: Any = None):
pass
class Observable:
def __init__(self):
self._observers: List[Observer] = []
def attach(self, observer: Observer):
self._observers.append(observer)
def detach(self, observer: Observer):
self._observers.remove(observer)
def notify(self, event: str, data: Any = None):
for observer in self._observers:
observer.update(event, data)
class Tile:
def __init__(self, x: int, y: int):
self._x = x
self._y = y
self._wall = False
self._start = False
self._exit = False
@property
def x(self) -> int:
return self._x
@property
def y(self) -> int:
return self._y
@property
def is_wall(self) -> bool:
return self._wall
@is_wall.setter
def is_wall(self, v: bool):
self._wall = v
@property
def is_start(self) -> bool:
return self._start
@is_start.setter
def is_start(self, v: bool):
self._start = v
@property
def is_exit(self) -> bool:
return self._exit
@is_exit.setter
def is_exit(self, v: bool):
self._exit = v
def passable(self) -> bool:
return not self._wall
def __hash__(self):
return hash((self._x, self._y))
def __eq__(self, other):
if not isinstance(other, Tile):
return False
return self._x == other._x and self._y == other._y
class Maze:
def __init__(self, w: int, h: int):
self._w = w
self._h = h
self._cells = [[Tile(x, y) for x in range(w)] for y in range(h)]
self._start: Optional[Tile] = None
self._exit: Optional[Tile] = None
@property
def width(self) -> int:
return self._w
@property
def height(self) -> int:
return self._h
@property
def start(self) -> Optional[Tile]:
return self._start
@property
def exit(self) -> Optional[Tile]:
return self._exit
def get_cell(self, x: int, y: int) -> Optional[Tile]:
if 0 <= x < self._w and 0 <= y < self._h:
return self._cells[y][x]
return None
def set_cell(self, x: int, y: int, kind: str):
c = self.get_cell(x, y)
if not c:
return
if kind == 'wall':
c.is_wall = True
elif kind == 'start':
if self._start:
self._start.is_start = False
c.is_start = True
c.is_wall = False
self._start = c
elif kind == 'exit':
if self._exit:
self._exit.is_exit = False
c.is_exit = True
c.is_wall = False
self._exit = c
elif kind == 'path':
c.is_wall = False
def neighbours(self, cell: Tile) -> List[Tile]:
result = []
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
nx, ny = cell.x + dx, cell.y + dy
nb = self.get_cell(nx, ny)
if nb and nb.passable():
result.append(nb)
return result
class MazeLoader(ABC):
@abstractmethod
def load(self, filename: str) -> Maze:
pass
class TextMazeLoader(MazeLoader):
def load(self, filename: str) -> Maze:
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
h = len(lines)
w = max(len(line) for line in lines) if h else 0
start_count = 0
exit_count = 0
maze = Maze(w, h)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if ch == '#':
maze.set_cell(x, y, 'wall')
elif ch == 'S':
maze.set_cell(x, y, 'start')
start_count += 1
elif ch == 'E':
maze.set_cell(x, y, 'exit')
exit_count += 1
else:
maze.set_cell(x, y, 'path')
if start_count != 1 or exit_count != 1:
raise ValueError(f"Maze must have one S and one E. Found: S={start_count}, E={exit_count}")
return maze
class PathFinder(ABC):
def __init__(self):
self._visited = 0
@abstractmethod
def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]:
pass
def _reconstruct(self, parent: Dict[Tile, Optional[Tile]], start: Tile, goal: Tile) -> List[Tile]:
path = []
current = goal
while current is not None:
path.append(current)
current = parent.get(current)
path.reverse()
return path if path and path[0] == start else []
@property
def visited_count(self) -> int:
return self._visited
class BFS(PathFinder):
def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]:
queue = deque([start])
parent = {start: None}
visited = {start}
while queue:
current = queue.popleft()
if current == goal:
self._visited = len(visited)
return self._reconstruct(parent, start, goal)
for neighbor in maze.neighbours(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
queue.append(neighbor)
self._visited = len(visited)
return []
class DFS(PathFinder):
def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]:
stack = [start]
parent = {start: None}
visited = {start}
while stack:
current = stack.pop()
if current == goal:
self._visited = len(visited)
return self._reconstruct(parent, start, goal)
for neighbor in maze.neighbours(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
stack.append(neighbor)
self._visited = len(visited)
return []
class AStar(PathFinder):
def _heuristic(self, cell: Tile, goal: Tile) -> int:
return abs(cell.x - goal.x) + abs(cell.y - goal.y)
def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]:
heap = []
counter = 0
start_f = self._heuristic(start, goal)
heapq.heappush(heap, (start_f, counter, start))
counter += 1
parent = {}
g_score = {start: 0}
f_score = {start: start_f}
visited = set()
while heap:
current_f, _, current = heapq.heappop(heap)
visited.add(current)
if current == goal:
self._visited = len(visited)
return self._reconstruct(parent, start, goal)
if current_f > f_score.get(current, float('inf')):
continue
for neighbor in maze.neighbours(current):
tentative_g = g_score[current] + 1
if tentative_g < g_score.get(neighbor, float('inf')):
parent[neighbor] = current
g_score[neighbor] = tentative_g
new_f = tentative_g + self._heuristic(neighbor, goal)
f_score[neighbor] = new_f
heapq.heappush(heap, (new_f, counter, neighbor))
counter += 1
self._visited = len(visited)
return []
class MazeSolver(Observable):
def __init__(self, maze: Maze):
super().__init__()
self._maze = maze
self._algorithm: Optional[PathFinder] = None
def set_algorithm(self, algorithm: PathFinder):
self._algorithm = algorithm
def solve(self) -> Optional[Dict[str, Any]]:
if not self._algorithm:
raise ValueError("Algorithm not set")
start_time = time.perf_counter()
path = self._algorithm.find(self._maze, self._maze.start, self._maze.exit)
end_time = time.perf_counter()
elapsed_ms = (end_time - start_time) * 1000
return {
'time_ms': elapsed_ms,
'visited': self._algorithm.visited_count,
'path_length': len(path),
'path': path
}
class Command(ABC):
@abstractmethod
def execute(self) -> bool:
pass
@abstractmethod
def undo(self) -> bool:
pass
class MoveCommand(Command):
def __init__(self, player: 'Player', dx: int, dy: int, maze: Maze):
self._player = player
self._dx = dx
self._dy = dy
self._maze = maze
self._executed = False
def execute(self) -> bool:
new_x = self._player.position.x + self._dx
new_y = self._player.position.y + self._dy
target = self._maze.get_cell(new_x, new_y)
if target and target.passable():
self._player.move_to(target)
self._executed = True
return True
return False
def undo(self) -> bool:
if self._executed:
self._player.undo()
self._executed = False
return True
return False
class Player:
def __init__(self, start_tile: Tile):
self._position = start_tile
self._previous = None
@property
def position(self) -> Tile:
return self._position
def move_to(self, tile: Tile):
self._previous = self._position
self._position = tile
def undo(self):
if self._previous:
self._position, self._previous = self._previous, None
class ConsoleView(Observer):
def __init__(self, maze: Maze, player: Optional[Player] = None):
self._maze = maze
self._player = player
self._current_path: List[Tile] = []
def update(self, event: str, data: Any = None):
if event == "solving_finished":
self._current_path = data.get('path', [])
self._display_solution(data)
def _display_solution(self, stats: Dict):
os.system('cls' if os.name == 'nt' else 'clear')
print("=" * (self._maze.width * 2 + 4))
print("MAZE SOLUTION")
print("=" * (self._maze.width * 2 + 4))
for y in range(self._maze.height):
print(" ", end='')
for x in range(self._maze.width):
cell = self._maze.get_cell(x, y)
if cell == self._maze.start:
print('S', end=' ')
elif cell == self._maze.exit:
print('E', end=' ')
elif cell.is_wall:
print('#', end=' ')
elif self._current_path and cell in self._current_path:
print('', end=' ')
else:
print('.', end=' ')
print()
print("=" * (self._maze.width * 2 + 4))
print(f"Time: {stats['time_ms']:.3f} ms")
print(f"Visited: {stats['visited']}")
print(f"Path length: {stats['path_length']}")
def display_maze(self):
os.system('cls' if os.name == 'nt' else 'clear')
print("=" * (self._maze.width * 2 + 4))
print("MAZE")
print("=" * (self._maze.width * 2 + 4))
for y in range(self._maze.height):
print(" ", end='')
for x in range(self._maze.width):
cell = self._maze.get_cell(x, y)
if self._player and cell == self._player.position:
print('P', end=' ')
elif cell == self._maze.start:
print('S', end=' ')
elif cell == self._maze.exit:
print('E', end=' ')
elif cell.is_wall:
print('#', end=' ')
else:
print('.', end=' ')
print()
print("=" * (self._maze.width * 2 + 4))
print("S - start E - exit # - wall . - path P - player")
def interactive_mode(maze: Maze):
player = Player(maze.start)
view = ConsoleView(maze, player)
view.display_maze()
solver = MazeSolver(maze)
solver.attach(view)
commands_history: List[Command] = []
print("\nControls:")
print("H (←) J (↓) K (↑) L (→) - move")
print("U - undo")
print("B - BFS")
print("D - DFS")
print("A - A*")
print("Q - quit")
print("\n" + "=" * 50)
while True:
cmd = input("\n> ").lower().strip()
if cmd == 'q':
break
elif cmd == 'b':
solver.set_algorithm(BFS())
result = solver.solve()
if result:
print(f"BFS: {result['time_ms']:.3f} ms, visited={result['visited']}, length={result['path_length']}")
elif cmd == 'd':
solver.set_algorithm(DFS())
result = solver.solve()
if result:
print(f"DFS: {result['time_ms']:.3f} ms, visited={result['visited']}, length={result['path_length']}")
elif cmd == 'a':
solver.set_algorithm(AStar())
result = solver.solve()
if result:
print(f"A*: {result['time_ms']:.3f} ms, visited={result['visited']}, length={result['path_length']}")
elif cmd in ['h', 'j', 'k', 'l']:
dir_map = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)}
dx, dy = dir_map[cmd]
move = MoveCommand(player, dx, dy, maze)
if move.execute():
commands_history.append(move)
view.display_maze()
if player.position == maze.exit:
print("\n*** YOU ESCAPED! ***")
print(f"Total moves: {len(commands_history)}")
break
else:
print("Blocked!")
elif cmd == 'u':
if commands_history:
last_command = commands_history.pop()
last_command.undo()
view.display_maze()
print("Undo successful")
else:
print("Nothing to undo")
else:
print("Unknown command")
def main():
if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
import subprocess
subprocess.run([sys.executable, 'plots.py'])
return
loader = TextMazeLoader()
maze_file = os.path.join(DATA_PATH, "maze1.txt")
if not os.path.exists(maze_file):
print(f"ERROR: Maze file not found: {maze_file}")
print(f"Please create maze1.txt in: {DATA_PATH}")
return
maze = loader.load(maze_file)
interactive_mode(maze)
if __name__ == "__main__":
main()