269 lines
8.4 KiB
Python
269 lines
8.4 KiB
Python
|
|
# Основной модуль: оркестратор MazeSolver, Observer для визуализации,
|
|||
|
|
# Player, Command для пошагового управления, интерактивная игра
|
|||
|
|
|
|||
|
|
import sys
|
|||
|
|
import time
|
|||
|
|
import os
|
|||
|
|
from maze_core import TextFileMazeBuilder, Maze
|
|||
|
|
from pathfinding import BFSStrategy, DFSStrategy, AStarStrategy
|
|||
|
|
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Observer:
|
|||
|
|
|
|||
|
|
def update(self, event_type, data):
|
|||
|
|
raise NotImplementedError
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ConsoleView(Observer):
|
|||
|
|
def __init__(self, player=None):
|
|||
|
|
self._last_path = None
|
|||
|
|
self._player = player
|
|||
|
|
|
|||
|
|
def update(self, event_type, data):
|
|||
|
|
if event_type == "maze_loaded":
|
|||
|
|
self.render_maze(data)
|
|||
|
|
elif event_type == "path_found":
|
|||
|
|
self._last_path = data
|
|||
|
|
self.render_path(data)
|
|||
|
|
elif event_type == "player_moved":
|
|||
|
|
self.render_maze_with_player(data)
|
|||
|
|
|
|||
|
|
def render_maze(self, maze):
|
|||
|
|
|
|||
|
|
|
|||
|
|
os.system('cls' if os.name == 'nt' else 'clear')
|
|||
|
|
print("=" * (maze.width * 2 + 4))
|
|||
|
|
print(" ЛАБИРИНТ")
|
|||
|
|
print("=" * (maze.width * 2 + 4))
|
|||
|
|
|
|||
|
|
for y in range(maze.height):
|
|||
|
|
print(" ", end='')
|
|||
|
|
for x in range(maze.width):
|
|||
|
|
cell = maze.get_cell(x, y)
|
|||
|
|
if cell == maze.start:
|
|||
|
|
print('S', end=' ')
|
|||
|
|
elif cell == maze.exit:
|
|||
|
|
print('E', end=' ')
|
|||
|
|
elif cell.is_wall:
|
|||
|
|
print('#', end=' ')
|
|||
|
|
else:
|
|||
|
|
print('.', end=' ')
|
|||
|
|
print()
|
|||
|
|
print("=" * (maze.width * 2 + 4))
|
|||
|
|
print(" S - вход E - выход # - стена . - проход")
|
|||
|
|
|
|||
|
|
def render_maze_with_player(self, maze):
|
|||
|
|
|
|||
|
|
os.system('cls' if os.name == 'nt' else 'clear')
|
|||
|
|
print("=" * (maze.width * 2 + 4))
|
|||
|
|
print(" ЛАБИРИНТ (P - игрок)")
|
|||
|
|
print("=" * (maze.width * 2 + 4))
|
|||
|
|
|
|||
|
|
for y in range(maze.height):
|
|||
|
|
print(" ", end='')
|
|||
|
|
for x in range(maze.width):
|
|||
|
|
cell = maze.get_cell(x, y)
|
|||
|
|
if self._player and cell == self._player.current:
|
|||
|
|
print('P', end=' ')
|
|||
|
|
elif cell == maze.start:
|
|||
|
|
print('S', end=' ')
|
|||
|
|
elif cell == maze.exit:
|
|||
|
|
print('E', end=' ')
|
|||
|
|
elif cell.is_wall:
|
|||
|
|
print('#', end=' ')
|
|||
|
|
else:
|
|||
|
|
print('.', end=' ')
|
|||
|
|
print()
|
|||
|
|
print("=" * (maze.width * 2 + 4))
|
|||
|
|
print(f" Позиция игрока: ({self._player.current.x}, {self._player.current.y})")
|
|||
|
|
print(" S - вход E - выход # - стена . - проход P - игрок")
|
|||
|
|
|
|||
|
|
def render_path(self, path):
|
|||
|
|
|
|||
|
|
if not path:
|
|||
|
|
print("\n Путь не найден!")
|
|||
|
|
return
|
|||
|
|
print(f"\n Путь найден! Длина: {len(path)}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Player:
|
|||
|
|
|
|||
|
|
|
|||
|
|
def __init__(self, start_cell, maze):
|
|||
|
|
self._current = start_cell
|
|||
|
|
self._previous = None
|
|||
|
|
self._maze = maze
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def current(self):
|
|||
|
|
return self._current
|
|||
|
|
|
|||
|
|
def move_to(self, cell):
|
|||
|
|
|
|||
|
|
if cell and cell.is_passable():
|
|||
|
|
self._previous = self._current
|
|||
|
|
self._current = cell
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def undo_move(self):
|
|||
|
|
|
|||
|
|
if self._previous:
|
|||
|
|
self._current, self._previous = self._previous, None
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Command:
|
|||
|
|
|
|||
|
|
def execute(self):
|
|||
|
|
raise NotImplementedError
|
|||
|
|
|
|||
|
|
def undo(self):
|
|||
|
|
raise NotImplementedError
|
|||
|
|
|
|||
|
|
|
|||
|
|
class MoveCommand(Command):
|
|||
|
|
def __init__(self, player, direction, maze):
|
|||
|
|
self._player = player
|
|||
|
|
self._direction = direction
|
|||
|
|
self._maze = maze
|
|||
|
|
self._executed = False
|
|||
|
|
|
|||
|
|
def execute(self):
|
|||
|
|
dx, dy = self._direction
|
|||
|
|
new_x = self._player.current.x + dx
|
|||
|
|
new_y = self._player.current.y + dy
|
|||
|
|
target_cell = self._maze.get_cell(new_x, new_y)
|
|||
|
|
|
|||
|
|
if target_cell and target_cell.is_passable():
|
|||
|
|
self._player.move_to(target_cell)
|
|||
|
|
self._executed = True
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def undo(self):
|
|||
|
|
if self._executed:
|
|||
|
|
self._player.undo_move()
|
|||
|
|
self._executed = False
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
class MazeSolver:
|
|||
|
|
def __init__(self, maze):
|
|||
|
|
self._maze = maze
|
|||
|
|
self._strategy = None
|
|||
|
|
self._observers = []
|
|||
|
|
|
|||
|
|
def attach(self, observer):
|
|||
|
|
self._observers.append(observer)
|
|||
|
|
|
|||
|
|
def notify(self, event_type, data):
|
|||
|
|
for observer in self._observers:
|
|||
|
|
observer.update(event_type, data)
|
|||
|
|
|
|||
|
|
def set_strategy(self, strategy):
|
|||
|
|
self._strategy = strategy
|
|||
|
|
|
|||
|
|
def solve(self):
|
|||
|
|
if self._strategy is None:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
start_time = time.perf_counter()
|
|||
|
|
path = self._strategy.find_path(self._maze, self._maze.start, self._maze.exit)
|
|||
|
|
end_time = time.perf_counter()
|
|||
|
|
time_ms = (end_time - start_time) * 1000
|
|||
|
|
|
|||
|
|
self.notify("path_found", path)
|
|||
|
|
|
|||
|
|
return SearchStats(time_ms, self._strategy.get_visited_count(), len(path))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
|
|||
|
|
builder = TextFileMazeBuilder()
|
|||
|
|
|
|||
|
|
# Загрузка лабиринта из файла
|
|||
|
|
if len(sys.argv) > 1:
|
|||
|
|
maze = builder.build_from_file(sys.argv[1])
|
|||
|
|
else:
|
|||
|
|
maze = builder.build_from_file("maze1.txt")
|
|||
|
|
|
|||
|
|
# Создание игрока и визуализации
|
|||
|
|
player = Player(maze.start, maze)
|
|||
|
|
view = ConsoleView(player)
|
|||
|
|
view.render_maze(maze)
|
|||
|
|
|
|||
|
|
# Создание решателя
|
|||
|
|
solver = MazeSolver(maze)
|
|||
|
|
solver.attach(view)
|
|||
|
|
|
|||
|
|
print("\n УПРАВЛЕНИЕ:")
|
|||
|
|
print(" H (влево) J (вниз) K (вверх) L (вправо)")
|
|||
|
|
print(" U - отменить ход Q - выход")
|
|||
|
|
print("\n АВТО-ПОИСК:")
|
|||
|
|
print(" B - BFS (поиск в ширину)")
|
|||
|
|
print(" D - DFS (поиск в глубину)")
|
|||
|
|
print(" A - A* (звездочка)")
|
|||
|
|
print("\n" + "=" * 50)
|
|||
|
|
|
|||
|
|
command_stack = []
|
|||
|
|
|
|||
|
|
while True:
|
|||
|
|
key = input("\n Команда > ").lower()
|
|||
|
|
|
|||
|
|
if key == 'q':
|
|||
|
|
print("\n До свидания!")
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
elif key == 'b':
|
|||
|
|
solver.set_strategy(BFSStrategy())
|
|||
|
|
stats = solver.solve()
|
|||
|
|
print(f"\n BFS: время={stats.time_ms:.3f}мс, посещено={stats.visited_cells}, длина={stats.path_length}")
|
|||
|
|
|
|||
|
|
elif key == 'd':
|
|||
|
|
solver.set_strategy(DFSStrategy())
|
|||
|
|
stats = solver.solve()
|
|||
|
|
print(f"\n DFS: время={stats.time_ms:.3f}мс, посещено={stats.visited_cells}, длина={stats.path_length}")
|
|||
|
|
|
|||
|
|
elif key == 'a':
|
|||
|
|
solver.set_strategy(AStarStrategy())
|
|||
|
|
stats = solver.solve()
|
|||
|
|
print(f"\n A*: время={stats.time_ms:.3f}мс, посещено={stats.visited_cells}, длина={stats.path_length}")
|
|||
|
|
|
|||
|
|
elif key in ['h', 'j', 'k', 'l']:
|
|||
|
|
dirs = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)}
|
|||
|
|
cmd = MoveCommand(player, dirs[key], maze)
|
|||
|
|
if cmd.execute():
|
|||
|
|
command_stack.append(cmd)
|
|||
|
|
view.render_maze_with_player(maze)
|
|||
|
|
if player.current == maze.exit:
|
|||
|
|
print("\n ПОЗДРАВЛЯЮ! ВЫ НАШЛИ ВЫХОД!")
|
|||
|
|
print(f" Всего ходов: {len(command_stack)}")
|
|||
|
|
break
|
|||
|
|
else:
|
|||
|
|
print("\n Туда нельзя – там стена!")
|
|||
|
|
|
|||
|
|
elif key == 'u':
|
|||
|
|
if command_stack:
|
|||
|
|
cmd = command_stack.pop()
|
|||
|
|
cmd.undo()
|
|||
|
|
view.render_maze_with_player(maze)
|
|||
|
|
print("\n Ход отменён")
|
|||
|
|
else:
|
|||
|
|
print("\n Нечего отменять")
|
|||
|
|
|
|||
|
|
else:
|
|||
|
|
print("\n Неизвестная команда. Используйте H,J,K,L для движения, U для отмены, Q для выхода")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|