add maze_solver
This commit is contained in:
parent
82e988c965
commit
ae812457fb
18
SobolevNS/docs/data/task2_maze/maze_solver/__init__.py
Normal file
18
SobolevNS/docs/data/task2_maze/maze_solver/__init__.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
"""Пакет maze_solver."""
|
||||||
|
from .model import Cell, Maze
|
||||||
|
from .builder import MazeBuilder, TextFileMazeBuilder
|
||||||
|
from .strategies import (
|
||||||
|
PathFindingStrategy, BFSStrategy, DFSStrategy,
|
||||||
|
AStarStrategy, DijkstraStrategy, STRATEGIES,
|
||||||
|
)
|
||||||
|
from .solver import MazeSolver, Observer, ConsoleView, SearchStats
|
||||||
|
from .command import Player, Command, MoveCommand, CommandHistory
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Cell", "Maze",
|
||||||
|
"MazeBuilder", "TextFileMazeBuilder",
|
||||||
|
"PathFindingStrategy", "BFSStrategy", "DFSStrategy",
|
||||||
|
"AStarStrategy", "DijkstraStrategy", "STRATEGIES",
|
||||||
|
"MazeSolver", "Observer", "ConsoleView", "SearchStats",
|
||||||
|
"Player", "Command", "MoveCommand", "CommandHistory",
|
||||||
|
]
|
||||||
92
SobolevNS/docs/data/task2_maze/maze_solver/builder.py
Normal file
92
SobolevNS/docs/data/task2_maze/maze_solver/builder.py
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
"""
|
||||||
|
maze_solver/builder.py - паттерн Builder для создания лабиринтов.
|
||||||
|
|
||||||
|
Зачем Builder: процесс построения лабиринта сложный (чтение файла, парсинг,
|
||||||
|
валидация символов, простановка флагов, поиск старта и выхода). Builder
|
||||||
|
изолирует эти подробности от клиента; для нового формата (JSON, бинарный)
|
||||||
|
достаточно реализовать ещё один builder с тем же интерфейсом.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from .model import Cell, Maze
|
||||||
|
|
||||||
|
|
||||||
|
class MazeBuilder(ABC):
|
||||||
|
"""Абстрактный билдер лабиринта."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def build_from_file(self, filename) -> Maze:
|
||||||
|
"""Возвращает готовый Maze."""
|
||||||
|
|
||||||
|
|
||||||
|
class TextFileMazeBuilder(MazeBuilder):
|
||||||
|
"""Билдер из текстового формата.
|
||||||
|
|
||||||
|
Символы:
|
||||||
|
'#' - стена
|
||||||
|
' ' - проход (вес 1)
|
||||||
|
'S' - старт (проходим)
|
||||||
|
'E' - выход (проходим)
|
||||||
|
'.' - асфальт (вес 1) - то же, что пробел
|
||||||
|
',' - песок (вес 2)
|
||||||
|
'~' - болото (вес 3)
|
||||||
|
|
||||||
|
Лишние пробельные символы в начале/конце файла игнорируются,
|
||||||
|
но внутри строки пробелы значимы (это проходы).
|
||||||
|
"""
|
||||||
|
|
||||||
|
WEIGHT_MAP = {'.': 1, ',': 2, '~': 3}
|
||||||
|
|
||||||
|
def build_from_file(self, filename) -> Maze:
|
||||||
|
with open(filename, encoding="utf-8") as f:
|
||||||
|
raw = f.read().splitlines()
|
||||||
|
|
||||||
|
# отбрасываем пустые строки в конце - частая мелочь
|
||||||
|
while raw and raw[-1] == "":
|
||||||
|
raw.pop()
|
||||||
|
if not raw:
|
||||||
|
raise ValueError(f"Файл лабиринта {filename!r} пуст.")
|
||||||
|
|
||||||
|
height = len(raw)
|
||||||
|
width = max(len(line) for line in raw)
|
||||||
|
|
||||||
|
# выравниваем строки по ширине пробелами (если строки разной длины)
|
||||||
|
lines = [line.ljust(width, '#') for line in raw]
|
||||||
|
|
||||||
|
maze = Maze(width, height)
|
||||||
|
start_count = 0
|
||||||
|
exit_count = 0
|
||||||
|
|
||||||
|
for y, line in enumerate(lines):
|
||||||
|
for x, ch in enumerate(line):
|
||||||
|
cell = self._parse_char(x, y, ch)
|
||||||
|
maze.grid[y][x] = cell
|
||||||
|
if cell.is_start:
|
||||||
|
maze.start = cell
|
||||||
|
start_count += 1
|
||||||
|
if cell.is_exit:
|
||||||
|
maze.exit_ = cell
|
||||||
|
exit_count += 1
|
||||||
|
|
||||||
|
# валидация
|
||||||
|
if start_count != 1:
|
||||||
|
raise ValueError(
|
||||||
|
f"В лабиринте {filename!r} ожидался ровно 1 'S', нашли {start_count}.")
|
||||||
|
if exit_count != 1:
|
||||||
|
raise ValueError(
|
||||||
|
f"В лабиринте {filename!r} ожидался ровно 1 'E', нашли {exit_count}.")
|
||||||
|
|
||||||
|
return maze
|
||||||
|
|
||||||
|
def _parse_char(self, x, y, ch):
|
||||||
|
if ch == '#':
|
||||||
|
return Cell(x, y, is_wall=True)
|
||||||
|
if ch == 'S':
|
||||||
|
return Cell(x, y, is_start=True, weight=1)
|
||||||
|
if ch == 'E':
|
||||||
|
return Cell(x, y, is_exit=True, weight=1)
|
||||||
|
if ch in self.WEIGHT_MAP:
|
||||||
|
return Cell(x, y, weight=self.WEIGHT_MAP[ch])
|
||||||
|
if ch == ' ':
|
||||||
|
return Cell(x, y, weight=1)
|
||||||
|
raise ValueError(f"Неизвестный символ {ch!r} в позиции ({x},{y}).")
|
||||||
87
SobolevNS/docs/data/task2_maze/maze_solver/command.py
Normal file
87
SobolevNS/docs/data/task2_maze/maze_solver/command.py
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
"""
|
||||||
|
maze_solver/command.py - паттерн Command.
|
||||||
|
|
||||||
|
Player хранит текущую клетку. MoveCommand двигает игрока в выбранном
|
||||||
|
направлении и помнит предыдущую позицию для undo. Менеджер CommandHistory
|
||||||
|
держит стек выполненных команд.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class Player:
|
||||||
|
"""Игрок в лабиринте."""
|
||||||
|
|
||||||
|
def __init__(self, cell):
|
||||||
|
self.cell = cell
|
||||||
|
|
||||||
|
@property
|
||||||
|
def x(self): return self.cell.x
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y(self): return self.cell.y
|
||||||
|
|
||||||
|
|
||||||
|
class Command(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def execute(self): ...
|
||||||
|
@abstractmethod
|
||||||
|
def undo(self): ...
|
||||||
|
|
||||||
|
|
||||||
|
class MoveCommand(Command):
|
||||||
|
"""Команда перемещения игрока на одну клетку.
|
||||||
|
|
||||||
|
direction: одна из 'W','A','S','D' (вверх, влево, вниз, вправо).
|
||||||
|
"""
|
||||||
|
|
||||||
|
DELTAS = {
|
||||||
|
'W': (0, -1),
|
||||||
|
'S': (0, 1),
|
||||||
|
'A': (-1, 0),
|
||||||
|
'D': (1, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, maze, player, direction):
|
||||||
|
self.maze = maze
|
||||||
|
self.player = player
|
||||||
|
self.direction = direction.upper()
|
||||||
|
self._prev_cell = None
|
||||||
|
self._executed = False
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
if self.direction not in self.DELTAS:
|
||||||
|
return False
|
||||||
|
dx, dy = self.DELTAS[self.direction]
|
||||||
|
target = self.maze.get_cell(self.player.x + dx, self.player.y + dy)
|
||||||
|
if target is None or not target.is_passable():
|
||||||
|
return False
|
||||||
|
self._prev_cell = self.player.cell
|
||||||
|
self.player.cell = target
|
||||||
|
self._executed = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if not self._executed:
|
||||||
|
return False
|
||||||
|
self.player.cell = self._prev_cell
|
||||||
|
self._executed = False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class CommandHistory:
|
||||||
|
"""Стек выполненных команд (для общего undo)."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._stack = []
|
||||||
|
|
||||||
|
def do(self, cmd):
|
||||||
|
if cmd.execute():
|
||||||
|
self._stack.append(cmd)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if not self._stack:
|
||||||
|
return False
|
||||||
|
return self._stack.pop().undo()
|
||||||
92
SobolevNS/docs/data/task2_maze/maze_solver/model.py
Normal file
92
SobolevNS/docs/data/task2_maze/maze_solver/model.py
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
"""
|
||||||
|
maze_solver/model.py - модель лабиринта (этап 1, без паттернов).
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Cell:
|
||||||
|
"""Клетка лабиринта.
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
x, y - координаты
|
||||||
|
is_wall - стена ли
|
||||||
|
is_start - стартовая клетка
|
||||||
|
is_exit - клетка выхода
|
||||||
|
weight - стоимость прохода (по умолчанию 1, для взвешенного режима >1)
|
||||||
|
"""
|
||||||
|
__slots__ = ("x", "y", "is_wall", "is_start", "is_exit", "weight")
|
||||||
|
|
||||||
|
def __init__(self, x, y, is_wall=False, is_start=False, is_exit=False, weight=1):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.is_wall = is_wall
|
||||||
|
self.is_start = is_start
|
||||||
|
self.is_exit = is_exit
|
||||||
|
self.weight = weight
|
||||||
|
|
||||||
|
def is_passable(self):
|
||||||
|
return not self.is_wall
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"Cell({self.x},{self.y},wall={self.is_wall})"
|
||||||
|
|
||||||
|
|
||||||
|
class Maze:
|
||||||
|
"""Лабиринт как двумерный массив клеток.
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
width, height - размеры
|
||||||
|
grid - список списков клеток [y][x]
|
||||||
|
start, exit_ - ссылки на клетки старта и выхода (могут быть None при ошибке)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.grid = [[Cell(x, y, is_wall=True) for x in range(width)]
|
||||||
|
for y in range(height)]
|
||||||
|
self.start = None
|
||||||
|
self.exit_ = None
|
||||||
|
|
||||||
|
def get_cell(self, x, y):
|
||||||
|
if 0 <= x < self.width and 0 <= y < self.height:
|
||||||
|
return self.grid[y][x]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_neighbors(self, cell):
|
||||||
|
"""Соседи (вверх, вниз, влево, вправо), только проходимые и в пределах поля."""
|
||||||
|
out = []
|
||||||
|
for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)):
|
||||||
|
nb = self.get_cell(cell.x + dx, cell.y + dy)
|
||||||
|
if nb is not None and nb.is_passable():
|
||||||
|
out.append(nb)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def render_text(self, path=None, player=None):
|
||||||
|
"""Возвращает текстовое представление лабиринта.
|
||||||
|
|
||||||
|
'#' стена, ' ' проход, 'S' старт, 'E' выход,
|
||||||
|
'.' клетка пути, '@' игрок.
|
||||||
|
"""
|
||||||
|
path_set = set()
|
||||||
|
if path:
|
||||||
|
for c in path:
|
||||||
|
path_set.add((c.x, c.y))
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for y in range(self.height):
|
||||||
|
row = []
|
||||||
|
for x in range(self.width):
|
||||||
|
cell = self.grid[y][x]
|
||||||
|
ch = ' '
|
||||||
|
if cell.is_wall:
|
||||||
|
ch = '#'
|
||||||
|
elif cell.is_start:
|
||||||
|
ch = 'S'
|
||||||
|
elif cell.is_exit:
|
||||||
|
ch = 'E'
|
||||||
|
elif (x, y) in path_set:
|
||||||
|
ch = '.'
|
||||||
|
if player is not None and player.x == x and player.y == y:
|
||||||
|
ch = '@'
|
||||||
|
row.append(ch)
|
||||||
|
lines.append("".join(row))
|
||||||
|
return "\n".join(lines)
|
||||||
102
SobolevNS/docs/data/task2_maze/maze_solver/solver.py
Normal file
102
SobolevNS/docs/data/task2_maze/maze_solver/solver.py
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
"""
|
||||||
|
maze_solver/solver.py - оркестратор MazeSolver + паттерн Observer.
|
||||||
|
|
||||||
|
MazeSolver знает лабиринт и текущую стратегию (Strategy). Перед поиском
|
||||||
|
он уведомляет наблюдателей (Observer) о старте, после поиска - о результате.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Observer ----------
|
||||||
|
|
||||||
|
class Observer(ABC):
|
||||||
|
"""Интерфейс наблюдателя."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def update(self, event):
|
||||||
|
"""event - dict с ключом 'type' и сопровождающими данными."""
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleView(Observer):
|
||||||
|
"""Простой текстовый наблюдатель."""
|
||||||
|
|
||||||
|
def __init__(self, verbose=True):
|
||||||
|
self.verbose = verbose
|
||||||
|
|
||||||
|
def update(self, event):
|
||||||
|
if not self.verbose:
|
||||||
|
return
|
||||||
|
t = event["type"]
|
||||||
|
if t == "maze_loaded":
|
||||||
|
m = event["maze"]
|
||||||
|
print(f"[ConsoleView] лабиринт {m.width}x{m.height} загружен")
|
||||||
|
elif t == "search_start":
|
||||||
|
print(f"[ConsoleView] старт поиска: {event['strategy']}")
|
||||||
|
elif t == "search_end":
|
||||||
|
stats = event["stats"]
|
||||||
|
print(f"[ConsoleView] поиск окончен: путь={stats['path_length']}, "
|
||||||
|
f"посещено={stats['visited']}, время={stats['elapsed_ms']:.3f} мс")
|
||||||
|
elif t == "move":
|
||||||
|
print(f"[ConsoleView] игрок -> ({event['x']},{event['y']})")
|
||||||
|
elif t == "path_found":
|
||||||
|
print("[ConsoleView] путь найден")
|
||||||
|
elif t == "no_path":
|
||||||
|
print("[ConsoleView] пути нет")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- MazeSolver ----------
|
||||||
|
|
||||||
|
class SearchStats(dict):
|
||||||
|
"""Простой dict-подобный контейнер статистики поиска."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MazeSolver:
|
||||||
|
def __init__(self, maze, strategy=None):
|
||||||
|
self.maze = maze
|
||||||
|
self.strategy = strategy
|
||||||
|
self._observers = []
|
||||||
|
|
||||||
|
def set_strategy(self, strategy):
|
||||||
|
self.strategy = strategy
|
||||||
|
|
||||||
|
def attach(self, observer):
|
||||||
|
self._observers.append(observer)
|
||||||
|
|
||||||
|
def detach(self, observer):
|
||||||
|
self._observers.remove(observer)
|
||||||
|
|
||||||
|
def _notify(self, event):
|
||||||
|
for obs in self._observers:
|
||||||
|
obs.update(event)
|
||||||
|
|
||||||
|
def solve(self):
|
||||||
|
if self.strategy is None:
|
||||||
|
raise RuntimeError("Стратегия не задана")
|
||||||
|
if self.maze.start is None or self.maze.exit_ is None:
|
||||||
|
raise RuntimeError("В лабиринте нет старта или выхода")
|
||||||
|
|
||||||
|
self._notify({"type": "search_start", "strategy": self.strategy.name})
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
result = self.strategy.find_path(self.maze,
|
||||||
|
self.maze.start,
|
||||||
|
self.maze.exit_)
|
||||||
|
elapsed = (time.perf_counter() - t0) * 1000.0
|
||||||
|
|
||||||
|
path = result["path"]
|
||||||
|
stats = SearchStats(
|
||||||
|
strategy=self.strategy.name,
|
||||||
|
elapsed_ms=elapsed,
|
||||||
|
visited=result["visited"],
|
||||||
|
path_length=len(path),
|
||||||
|
path=path,
|
||||||
|
)
|
||||||
|
self._notify({"type": "search_end", "stats": stats})
|
||||||
|
if path:
|
||||||
|
self._notify({"type": "path_found"})
|
||||||
|
else:
|
||||||
|
self._notify({"type": "no_path"})
|
||||||
|
return stats
|
||||||
179
SobolevNS/docs/data/task2_maze/maze_solver/strategies.py
Normal file
179
SobolevNS/docs/data/task2_maze/maze_solver/strategies.py
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
"""
|
||||||
|
maze_solver/strategies.py - паттерн Strategy.
|
||||||
|
|
||||||
|
Каждая стратегия реализует один и тот же интерфейс PathFindingStrategy
|
||||||
|
с методом find_path(maze, start, exit_), возвращающим:
|
||||||
|
{'path': [Cell, ...], 'visited': int}
|
||||||
|
|
||||||
|
Стратегии не модифицируют сам лабиринт.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from collections import deque
|
||||||
|
import heapq
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- интерфейс стратегии ----------
|
||||||
|
|
||||||
|
class PathFindingStrategy(ABC):
|
||||||
|
name = "Strategy"
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def find_path(self, maze, start, exit_):
|
||||||
|
"""Возвращает dict с ключами 'path' (list[Cell]) и 'visited' (int).
|
||||||
|
Если пути нет - path = []."""
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- общая утилита: восстановление пути ----------
|
||||||
|
|
||||||
|
def _reconstruct(parents, start, end):
|
||||||
|
"""Восстанавливает путь по словарю предшественников {(x,y): Cell|None}."""
|
||||||
|
path = []
|
||||||
|
cur = end
|
||||||
|
while cur is not None:
|
||||||
|
path.append(cur)
|
||||||
|
cur = parents.get((cur.x, cur.y))
|
||||||
|
path.reverse()
|
||||||
|
if path and path[0] is start:
|
||||||
|
return path
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- BFS ----------
|
||||||
|
|
||||||
|
class BFSStrategy(PathFindingStrategy):
|
||||||
|
"""Поиск в ширину. Гарантирует кратчайший путь по числу шагов
|
||||||
|
(когда веса всех клеток равны)."""
|
||||||
|
name = "BFS"
|
||||||
|
|
||||||
|
def find_path(self, maze, start, exit_):
|
||||||
|
queue = deque([start])
|
||||||
|
parents = {(start.x, start.y): None}
|
||||||
|
visited = 1
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
cell = queue.popleft()
|
||||||
|
if cell is exit_:
|
||||||
|
return {"path": _reconstruct(parents, start, exit_),
|
||||||
|
"visited": visited}
|
||||||
|
for nb in maze.get_neighbors(cell):
|
||||||
|
key = (nb.x, nb.y)
|
||||||
|
if key not in parents:
|
||||||
|
parents[key] = cell
|
||||||
|
visited += 1
|
||||||
|
queue.append(nb)
|
||||||
|
return {"path": [], "visited": visited}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- DFS ----------
|
||||||
|
|
||||||
|
class DFSStrategy(PathFindingStrategy):
|
||||||
|
"""Поиск в глубину. Не гарантирует кратчайший путь, но прост и быстр."""
|
||||||
|
name = "DFS"
|
||||||
|
|
||||||
|
def find_path(self, maze, start, exit_):
|
||||||
|
stack = [start]
|
||||||
|
parents = {(start.x, start.y): None}
|
||||||
|
visited = 1
|
||||||
|
|
||||||
|
while stack:
|
||||||
|
cell = stack.pop()
|
||||||
|
if cell is exit_:
|
||||||
|
return {"path": _reconstruct(parents, start, exit_),
|
||||||
|
"visited": visited}
|
||||||
|
for nb in maze.get_neighbors(cell):
|
||||||
|
key = (nb.x, nb.y)
|
||||||
|
if key not in parents:
|
||||||
|
parents[key] = cell
|
||||||
|
visited += 1
|
||||||
|
stack.append(nb)
|
||||||
|
return {"path": [], "visited": visited}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- A* ----------
|
||||||
|
|
||||||
|
def _manhattan(a, b):
|
||||||
|
return abs(a.x - b.x) + abs(a.y - b.y)
|
||||||
|
|
||||||
|
|
||||||
|
class AStarStrategy(PathFindingStrategy):
|
||||||
|
"""A*-поиск с манхэттенской эвристикой. Учитывает вес клеток (weight)."""
|
||||||
|
name = "A*"
|
||||||
|
|
||||||
|
def find_path(self, maze, start, exit_):
|
||||||
|
# f = g + h; в куче храним (f, tie, cell)
|
||||||
|
g_score = {(start.x, start.y): 0}
|
||||||
|
parents = {(start.x, start.y): None}
|
||||||
|
tie = 0
|
||||||
|
heap = [(_manhattan(start, exit_), tie, start)]
|
||||||
|
visited = 0
|
||||||
|
closed = set()
|
||||||
|
|
||||||
|
while heap:
|
||||||
|
f, _, cell = heapq.heappop(heap)
|
||||||
|
key = (cell.x, cell.y)
|
||||||
|
if key in closed:
|
||||||
|
continue
|
||||||
|
closed.add(key)
|
||||||
|
visited += 1
|
||||||
|
|
||||||
|
if cell is exit_:
|
||||||
|
return {"path": _reconstruct(parents, start, exit_),
|
||||||
|
"visited": visited}
|
||||||
|
|
||||||
|
for nb in maze.get_neighbors(cell):
|
||||||
|
nb_key = (nb.x, nb.y)
|
||||||
|
tentative_g = g_score[key] + nb.weight
|
||||||
|
if tentative_g < g_score.get(nb_key, float("inf")):
|
||||||
|
g_score[nb_key] = tentative_g
|
||||||
|
parents[nb_key] = cell
|
||||||
|
tie += 1
|
||||||
|
heapq.heappush(heap,
|
||||||
|
(tentative_g + _manhattan(nb, exit_), tie, nb))
|
||||||
|
return {"path": [], "visited": visited}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Дейкстра ----------
|
||||||
|
|
||||||
|
class DijkstraStrategy(PathFindingStrategy):
|
||||||
|
"""Дейкстра - оптимальный путь с учётом веса клеток.
|
||||||
|
На немодифицированном лабиринте (все веса = 1) совпадает с BFS."""
|
||||||
|
name = "Dijkstra"
|
||||||
|
|
||||||
|
def find_path(self, maze, start, exit_):
|
||||||
|
dist = {(start.x, start.y): 0}
|
||||||
|
parents = {(start.x, start.y): None}
|
||||||
|
tie = 0
|
||||||
|
heap = [(0, tie, start)]
|
||||||
|
visited = 0
|
||||||
|
closed = set()
|
||||||
|
|
||||||
|
while heap:
|
||||||
|
d, _, cell = heapq.heappop(heap)
|
||||||
|
key = (cell.x, cell.y)
|
||||||
|
if key in closed:
|
||||||
|
continue
|
||||||
|
closed.add(key)
|
||||||
|
visited += 1
|
||||||
|
|
||||||
|
if cell is exit_:
|
||||||
|
return {"path": _reconstruct(parents, start, exit_),
|
||||||
|
"visited": visited}
|
||||||
|
|
||||||
|
for nb in maze.get_neighbors(cell):
|
||||||
|
nb_key = (nb.x, nb.y)
|
||||||
|
nd = d + nb.weight
|
||||||
|
if nd < dist.get(nb_key, float("inf")):
|
||||||
|
dist[nb_key] = nd
|
||||||
|
parents[nb_key] = cell
|
||||||
|
tie += 1
|
||||||
|
heapq.heappush(heap, (nd, tie, nb))
|
||||||
|
return {"path": [], "visited": visited}
|
||||||
|
|
||||||
|
|
||||||
|
STRATEGIES = {
|
||||||
|
"BFS": BFSStrategy,
|
||||||
|
"DFS": DFSStrategy,
|
||||||
|
"A*": AStarStrategy,
|
||||||
|
"Dijkstra": DijkstraStrategy,
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user