2026-05-25 18:17:56 +00:00
|
|
|
|
import time, os
|
2026-05-25 00:30:40 +00:00
|
|
|
|
from collections import deque
|
2026-05-25 01:50:25 +00:00
|
|
|
|
from abc import ABC, abstractmethod
|
2026-05-25 18:17:56 +00:00
|
|
|
|
import heapq # <-- Добавлен импорт для A*
|
2026-05-25 00:30:40 +00:00
|
|
|
|
|
2026-05-25 18:17:56 +00:00
|
|
|
|
# --- ЭТАП 1: МОДЕЛЬ ---
|
2026-05-24 23:27:34 +00:00
|
|
|
|
class Cell:
|
2026-05-25 18:17:56 +00:00
|
|
|
|
def __init__(self, x, y, is_wall=False):
|
2026-05-25 05:17:44 +00:00
|
|
|
|
self.x = x
|
|
|
|
|
|
self.y = y
|
|
|
|
|
|
self.is_wall = is_wall
|
2026-05-25 18:17:56 +00:00
|
|
|
|
def isPassable(self):
|
2026-05-25 05:17:44 +00:00
|
|
|
|
return not self.is_wall
|
2026-05-25 03:35:06 +00:00
|
|
|
|
|
2026-05-24 23:27:34 +00:00
|
|
|
|
class Maze:
|
2026-05-25 18:17:56 +00:00
|
|
|
|
def __init__(self, width, height, grid):
|
2026-05-25 05:17:44 +00:00
|
|
|
|
self.width = width
|
|
|
|
|
|
self.height = height
|
2026-05-25 18:17:56 +00:00
|
|
|
|
self.grid = grid
|
|
|
|
|
|
self.start_cell = grid[0][0]
|
|
|
|
|
|
self.exit_cell = grid[height-1][width-1]
|
|
|
|
|
|
def getNeighbors(self, cell):
|
2026-05-25 05:17:44 +00:00
|
|
|
|
neighbors = []
|
|
|
|
|
|
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
|
2026-05-25 18:17:56 +00:00
|
|
|
|
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])
|
2026-05-25 05:17:44 +00:00
|
|
|
|
return neighbors
|
|
|
|
|
|
|
2026-05-25 18:17:56 +00:00
|
|
|
|
# --- ЭТАП 2: BUILDER ---
|
2026-05-24 23:27:34 +00:00
|
|
|
|
class MazeBuilder:
|
2026-05-25 05:17:44 +00:00
|
|
|
|
def buildFromFile(self, filename):
|
2026-05-25 18:17:56 +00:00
|
|
|
|
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]
|
2026-05-25 05:17:44 +00:00
|
|
|
|
return maze
|
|
|
|
|
|
|
2026-05-25 18:17:56 +00:00
|
|
|
|
# --- ЭТАП 3: STRATEGY ---
|
|
|
|
|
|
class SearchStats:
|
|
|
|
|
|
def __init__(self, time_ms, visited, length):
|
|
|
|
|
|
self.time_ms = time_ms
|
|
|
|
|
|
self.visited = visited
|
|
|
|
|
|
self.length = length
|
2026-05-25 05:17:44 +00:00
|
|
|
|
|
2026-05-25 18:17:56 +00:00
|
|
|
|
class PathFindingStrategy(ABC):
|
2026-05-25 03:35:06 +00:00
|
|
|
|
@abstractmethod
|
2026-05-25 18:17:56 +00:00
|
|
|
|
def findPath(self, maze, start, exit):
|
|
|
|
|
|
pass
|
2026-05-25 00:30:40 +00:00
|
|
|
|
|
2026-05-25 18:17:56 +00:00
|
|
|
|
# 1. Поиск в ширину (BFS) - оригинальный алгоритм
|
2026-05-25 00:30:40 +00:00
|
|
|
|
class BFSStrategy(PathFindingStrategy):
|
2026-05-25 05:17:44 +00:00
|
|
|
|
def findPath(self, maze, start, exit):
|
|
|
|
|
|
queue = deque([start])
|
2026-05-25 18:17:56 +00:00
|
|
|
|
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)
|
2026-05-25 00:30:40 +00:00
|
|
|
|
|
2026-05-25 18:17:56 +00:00
|
|
|
|
# 3. Алгоритм A*
|
2026-05-25 00:30:40 +00:00
|
|
|
|
class AStarStrategy(PathFindingStrategy):
|
2026-05-25 05:17:44 +00:00
|
|
|
|
def findPath(self, maze, start, exit):
|
2026-05-25 18:17:56 +00:00
|
|
|
|
counter = 0
|
|
|
|
|
|
queue = [(0, counter, start)]
|
|
|
|
|
|
came_from = {start: None}
|
2026-05-25 05:17:44 +00:00
|
|
|
|
g_score = {start: 0}
|
2026-05-25 00:52:35 +00:00
|
|
|
|
|
2026-05-25 18:17:56 +00:00
|
|
|
|
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))
|
2026-05-25 01:50:25 +00:00
|
|
|
|
|
2026-05-25 18:17:56 +00:00
|
|
|
|
path = []
|
|
|
|
|
|
curr = exit
|
|
|
|
|
|
while curr is not None:
|
|
|
|
|
|
path.append(curr)
|
|
|
|
|
|
curr = came_from.get(curr)
|
|
|
|
|
|
return path[::-1], len(came_from)
|
2026-05-25 01:50:25 +00:00
|
|
|
|
|
2026-05-25 18:17:56 +00:00
|
|
|
|
# --- ЭТАП 4: ORCHESTRATOR ---
|
2026-05-25 00:52:35 +00:00
|
|
|
|
class MazeSolver:
|
2026-05-25 18:17:56 +00:00
|
|
|
|
def __init__(self, maze, player=None):
|
2026-05-25 00:52:35 +00:00
|
|
|
|
self.maze = maze
|
2026-05-25 03:35:06 +00:00
|
|
|
|
self.player = player
|
2026-05-25 18:17:56 +00:00
|
|
|
|
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):
|
2026-05-25 05:17:44 +00:00
|
|
|
|
t0 = time.perf_counter()
|
2026-05-25 18:17:56 +00:00
|
|
|
|
path, visited = strat.findPath(self.maze, self.maze.start_cell, self.maze.exit_cell)
|
2026-05-25 05:17:44 +00:00
|
|
|
|
t1 = time.perf_counter()
|
2026-05-25 18:17:56 +00:00
|
|
|
|
return SearchStats((t1 - t0) * 1000, visited, len(path))
|
2026-05-25 01:06:25 +00:00
|
|
|
|
|
2026-05-25 18:17:56 +00:00
|
|
|
|
# --- ЭТАП 5: OBSERVER & COMMAND ---
|
|
|
|
|
|
class Player:
|
|
|
|
|
|
def __init__(self, cell):
|
|
|
|
|
|
self.current_cell = cell
|
2026-05-25 03:35:06 +00:00
|
|
|
|
|
2026-05-25 18:17:56 +00:00
|
|
|
|
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()
|
2026-05-25 03:35:06 +00:00
|
|
|
|
|
2026-05-25 18:17:56 +00:00
|
|
|
|
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", "Стена!")
|