253 lines
8.6 KiB
Python
253 lines
8.6 KiB
Python
import time, os
|
||
from collections import deque
|
||
from abc import ABC, abstractmethod
|
||
import heapq # <-- Добавлен импорт для A*
|
||
|
||
# --- ЭТАП 1: МОДЕЛЬ ---
|
||
class Cell:
|
||
def __init__(self, x, y, is_wall=False):
|
||
self.x = x
|
||
self.y = y
|
||
self.is_wall = is_wall
|
||
def isPassable(self):
|
||
return not self.is_wall
|
||
|
||
class Maze:
|
||
def __init__(self, width, height, grid):
|
||
self.width = width
|
||
self.height = height
|
||
self.grid = grid
|
||
self.start_cell = grid[0][0]
|
||
self.exit_cell = grid[height-1][width-1]
|
||
def getNeighbors(self, cell):
|
||
neighbors = []
|
||
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
|
||
for d in directions:
|
||
nx = cell.x + d[0]
|
||
ny = cell.y + d[1]
|
||
if nx >= 0 and nx < self.width and ny >= 0 and ny < self.height:
|
||
if not self.grid[ny][nx].is_wall:
|
||
neighbors.append(self.grid[ny][nx])
|
||
return neighbors
|
||
|
||
# --- ЭТАП 2: BUILDER ---
|
||
class MazeBuilder:
|
||
def buildFromFile(self, filename):
|
||
path = filename
|
||
if "docs/data/" not in path:
|
||
path = os.path.join("docs", "data", filename)
|
||
|
||
with open(path, 'r') as f:
|
||
lines = []
|
||
for line in f:
|
||
stripped = line.strip()
|
||
if stripped:
|
||
lines.append(stripped)
|
||
|
||
h = len(lines)
|
||
w = len(lines[0])
|
||
grid = []
|
||
for y in range(h):
|
||
row = []
|
||
for x in range(w):
|
||
is_wall = False
|
||
if x < len(lines[y]):
|
||
if lines[y][x] == '#':
|
||
is_wall = True
|
||
row.append(Cell(x, y, is_wall))
|
||
grid.append(row)
|
||
|
||
maze = Maze(w, h, grid)
|
||
for y in range(h):
|
||
for x in range(len(lines[y])):
|
||
if lines[y][x] == 'S':
|
||
maze.start_cell = maze.grid[y][x]
|
||
if lines[y][x] == 'E':
|
||
maze.exit_cell = maze.grid[y][x]
|
||
return maze
|
||
|
||
# --- ЭТАП 3: STRATEGY ---
|
||
class SearchStats:
|
||
def __init__(self, time_ms, visited, length):
|
||
self.time_ms = time_ms
|
||
self.visited = visited
|
||
self.length = length
|
||
|
||
class PathFindingStrategy(ABC):
|
||
@abstractmethod
|
||
def findPath(self, maze, start, exit):
|
||
pass
|
||
|
||
# 1. Поиск в ширину (BFS) - оригинальный алгоритм
|
||
class BFSStrategy(PathFindingStrategy):
|
||
def findPath(self, maze, start, exit):
|
||
queue = deque([start])
|
||
visited = {start: None}
|
||
while len(queue) > 0:
|
||
curr = queue.popleft()
|
||
if curr == exit:
|
||
break
|
||
for n in maze.getNeighbors(curr):
|
||
if n not in visited:
|
||
visited[n] = curr
|
||
queue.append(n)
|
||
path = []
|
||
curr = exit
|
||
while curr is not None:
|
||
path.append(curr)
|
||
curr = visited.get(curr)
|
||
return path[::-1], len(visited)
|
||
|
||
# 2. Поиск в глубину (DFS) - добавлен
|
||
class DFSStrategy(PathFindingStrategy):
|
||
def findPath(self, maze, start, exit):
|
||
stack = [start]
|
||
visited = {start: None}
|
||
while len(stack) > 0:
|
||
curr = stack.pop()
|
||
if curr == exit:
|
||
break
|
||
for n in maze.getNeighbors(curr):
|
||
if n not in visited:
|
||
visited[n] = curr
|
||
stack.append(n)
|
||
path = []
|
||
curr = exit
|
||
while curr is not None:
|
||
path.append(curr)
|
||
curr = visited.get(curr)
|
||
return path[::-1], len(visited)
|
||
|
||
# 3. Алгоритм A*
|
||
class AStarStrategy(PathFindingStrategy):
|
||
def findPath(self, maze, start, exit):
|
||
counter = 0
|
||
queue = [(0, counter, start)]
|
||
came_from = {start: None}
|
||
g_score = {start: 0}
|
||
|
||
while len(queue) > 0:
|
||
_, _, curr = heapq.heappop(queue)
|
||
|
||
if curr == exit:
|
||
break
|
||
|
||
for n in maze.getNeighbors(curr):
|
||
tentative_g_score = g_score[curr] + 1
|
||
if n not in g_score or tentative_g_score < g_score[n]:
|
||
came_from[n] = curr
|
||
g_score[n] = tentative_g_score
|
||
# Эвристика: Манхэттенское расстояние
|
||
f_score = tentative_g_score + abs(n.x - exit.x) + abs(n.y - exit.y)
|
||
counter += 1
|
||
heapq.heappush(queue, (f_score, counter, n))
|
||
|
||
path = []
|
||
curr = exit
|
||
while curr is not None:
|
||
path.append(curr)
|
||
curr = came_from.get(curr)
|
||
return path[::-1], len(came_from)
|
||
|
||
# --- ЭТАП 4: ORCHESTRATOR ---
|
||
class MazeSolver:
|
||
def __init__(self, maze, player=None):
|
||
self.maze = maze
|
||
self.player = player
|
||
self.observers = []
|
||
def attach(self, obs):
|
||
self.observers.append(obs)
|
||
def notify(self, event, data):
|
||
for o in self.observers:
|
||
o.update(event, data)
|
||
def solve(self, strat):
|
||
t0 = time.perf_counter()
|
||
path, visited = strat.findPath(self.maze, self.maze.start_cell, self.maze.exit_cell)
|
||
t1 = time.perf_counter()
|
||
return SearchStats((t1 - t0) * 1000, visited, len(path))
|
||
|
||
# --- ЭТАП 5: OBSERVER & COMMAND ---
|
||
class Player:
|
||
def __init__(self, cell):
|
||
self.current_cell = cell
|
||
|
||
class MoveCommand:
|
||
def __init__(self, player, dx, dy, maze):
|
||
self.player = player
|
||
self.dx = dx
|
||
self.dy = dy
|
||
self.maze = maze
|
||
def execute(self):
|
||
nx = self.player.current_cell.x + self.dx
|
||
ny = self.player.current_cell.y + self.dy
|
||
if nx >= 0 and nx < self.maze.width and ny >= 0 and ny < self.maze.height:
|
||
target = self.maze.grid[ny][nx]
|
||
if target.isPassable():
|
||
self.player.current_cell = target
|
||
return True
|
||
return False
|
||
|
||
class ConsoleView:
|
||
def update(self, event, data):
|
||
print(f"[INFO] {event.upper()}: {data}")
|
||
|
||
# --- ЗАПУСК ---
|
||
if __name__ == "__main__":
|
||
files = ["maze10-10.txt", "maze50-50.txt", "maze100-100.txt", "maze0.txt", "maze777.txt"]
|
||
mode = input("Эксперимент (e) или игра (i)? ").lower()
|
||
|
||
if mode == 'e':
|
||
# Обновленная таблица с колонкой "Метод"
|
||
print(f"{'Файл':<15} | {'Метод':<5} | {'Время(мс)':<10} | {'Посещено':<10} | {'Путь':<6}")
|
||
print("-" * 58)
|
||
|
||
for f in files:
|
||
try:
|
||
m = MazeBuilder().buildFromFile(f)
|
||
|
||
# Словарь с нашими тремя методами
|
||
strategies = {
|
||
"BFS": BFSStrategy(),
|
||
"DFS": DFSStrategy(),
|
||
"A*": AStarStrategy()
|
||
}
|
||
|
||
# Запускаем каждый метод для текущего лабиринта
|
||
for strat_name, strat_obj in strategies.items():
|
||
t_sum, v_sum, l_sum = 0, 0, 0
|
||
for _ in range(10):
|
||
s = MazeSolver(m).solve(strat_obj)
|
||
t_sum += s.time_ms
|
||
v_sum += s.visited
|
||
l_sum += s.length
|
||
|
||
print(f"{f:<15} | {strat_name:<5} | {t_sum/10:<10.2f} | {v_sum/10:<10.1f} | {l_sum/10:<6.1f}")
|
||
|
||
# Линия-разделитель между разными лабиринтами для удобства чтения
|
||
print("-" * 58)
|
||
|
||
except FileNotFoundError:
|
||
print(f"{f:<15} | ОШИБКА: Файл не найден")
|
||
print("-" * 58)
|
||
|
||
elif mode == 'i':
|
||
name = input("Имя файла: ")
|
||
m = MazeBuilder().buildFromFile(name)
|
||
p = Player(m.start_cell)
|
||
s = MazeSolver(m, p)
|
||
s.attach(ConsoleView())
|
||
|
||
while True:
|
||
cmd = input("WASD (q-выход): ").lower()
|
||
if cmd == 'q': break
|
||
dx, dy = 0, 0
|
||
if cmd == 'w': dy = -1
|
||
elif cmd == 'a': dx = -1
|
||
elif cmd == 's': dy = 1
|
||
elif cmd == 'd': dx = 1
|
||
|
||
if dx != 0 or dy != 0:
|
||
if MoveCommand(p, dx, dy, m).execute():
|
||
s.notify("move", f"Направление {cmd}, Координата ({p.current_cell.x}, {p.current_cell.y})")
|
||
else:
|
||
s.notify("error", "Стена!") |