382 lines
14 KiB
Python
382 lines
14 KiB
Python
import abc
|
||
import heapq
|
||
import time
|
||
from collections import deque
|
||
from dataclasses import dataclass
|
||
from typing import List, Optional, Dict, Set, Tuple, Any
|
||
|
||
class Cell:
|
||
#тут что такое клетка
|
||
def __init__(self, x: int, y: int, is_wall: bool = False,
|
||
is_exit: bool = False, is_start: bool = False):
|
||
self.x = x
|
||
self.y = y
|
||
self.is_wall = is_wall
|
||
self.is_exit = is_exit
|
||
self.is_start = is_start
|
||
|
||
def is_passable(self) -> bool: #можно ли пройти через клетку
|
||
return not self.is_wall
|
||
|
||
def __repr__(self) -> str:
|
||
return f"Cell({self.x},{self.y})"
|
||
|
||
|
||
class Maze:
|
||
def __init__(self, width: int, height: int): #что содержит лабиринт, начало конец и тд
|
||
self.width = width
|
||
self.height = height
|
||
self.grid: List[List[Cell]] = []
|
||
self.start_cell: Optional[Cell] = None
|
||
self.exit_cell: Optional[Cell] = None
|
||
|
||
def set_cell(self, x: int, y: int, cell: Cell) -> None: #ставим клетку куда надо или не ставим если в границы не попала
|
||
if not (0 <= x < self.width and 0 <= y < self.height):
|
||
raise IndexError("координаты вне границ лабиринта")
|
||
self.grid[y][x] = cell
|
||
|
||
def get_cell(self, x: int, y: int) -> Optional[Cell]: #тут уже из коррдинат клетку вытаскиваем
|
||
if 0 <= x < self.width and 0 <= y < self.height:
|
||
return self.grid[y][x]
|
||
return None
|
||
|
||
def get_neighbors(self, cell: Cell) -> List[Cell]: #если соседняя клетка проходима - добавляем
|
||
neighbors = []
|
||
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
|
||
nx, ny = cell.x + dx, cell.y + dy
|
||
neighbor = self.get_cell(nx, ny)
|
||
if neighbor and neighbor.is_passable():
|
||
neighbors.append(neighbor)
|
||
return neighbors
|
||
|
||
|
||
class MazeBuilder(abc.ABC):
|
||
@abc.abstractmethod
|
||
def build_from_file(self, filename: str) -> Maze:
|
||
pass
|
||
|
||
|
||
class TextFileMazeBuilder(MazeBuilder):
|
||
def build_from_file(self, filename: str) -> Maze:
|
||
lines = []
|
||
with open(filename, 'r', encoding='utf-8') as f:
|
||
for line in f:
|
||
line = line.rstrip('\n')
|
||
if line: #игнорируем пустые строки
|
||
lines.append(line)
|
||
|
||
if not lines:
|
||
raise ValueError("Файл пуст")
|
||
|
||
height = len(lines)
|
||
width = max(len(line) for line in lines)
|
||
|
||
maze = Maze(width, height)
|
||
#инициализируем сетку пустыми клетками,по умолчанию стенами
|
||
maze.grid = [[Cell(x, y, is_wall=True) for x in range(width)] for y in range(height)]
|
||
|
||
start_cell = None
|
||
exit_cell = None
|
||
|
||
for y, line in enumerate(lines):
|
||
for x, ch in enumerate(line):
|
||
if x >= width:
|
||
continue
|
||
if ch == '#':
|
||
continue
|
||
elif ch == ' ':
|
||
cell = Cell(x, y, is_wall=False)
|
||
elif ch == 'S':
|
||
cell = Cell(x, y, is_wall=False, is_start=True)
|
||
start_cell = cell
|
||
elif ch == 'E':
|
||
cell = Cell(x, y, is_wall=False, is_exit=True)
|
||
exit_cell = cell
|
||
else:
|
||
#любой другой символ считаем проходом
|
||
cell = Cell(x, y, is_wall=False)
|
||
maze.set_cell(x, y, cell)
|
||
|
||
if start_cell is None:
|
||
raise ValueError("отсутствует стартовая клетка (S)") #invalid check
|
||
if exit_cell is None:
|
||
raise ValueError("отсутствует выход (E)")
|
||
|
||
maze.start_cell = start_cell
|
||
maze.exit_cell = exit_cell
|
||
return maze
|
||
|
||
class PathFindingStrategy(abc.ABC):
|
||
@abc.abstractmethod
|
||
def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]:
|
||
pass
|
||
|
||
#дальше скорее математика, методы вроде ещё в том семаке разбирали
|
||
class BFSStrategy(PathFindingStrategy):
|
||
def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]:
|
||
if start is exit_:
|
||
return [start], 1
|
||
|
||
queue = deque([start]) #используйте deque 👍
|
||
visited: Set[Cell] = {start}
|
||
parent: Dict[Cell, Optional[Cell]] = {start: None}
|
||
|
||
while queue:
|
||
current = queue.popleft()
|
||
if current is exit_:
|
||
#восстановление пути
|
||
path = []
|
||
cur = current
|
||
while cur is not None:
|
||
path.append(cur)
|
||
cur = parent[cur]
|
||
path.reverse()
|
||
return path, len(visited)
|
||
|
||
for neighbor in maze.get_neighbors(current):
|
||
if neighbor not in visited:
|
||
visited.add(neighbor)
|
||
parent[neighbor] = current
|
||
queue.append(neighbor)
|
||
|
||
return [], len(visited)
|
||
|
||
|
||
class DFSStrategy(PathFindingStrategy):
|
||
def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]:
|
||
if start is exit_:
|
||
return [start], 1
|
||
|
||
stack = [start]
|
||
visited: Set[Cell] = {start}
|
||
parent: Dict[Cell, Optional[Cell]] = {start: None}
|
||
|
||
while stack:
|
||
current = stack.pop()
|
||
if current is exit_:
|
||
path = []
|
||
cur = current
|
||
while cur is not None:
|
||
path.append(cur)
|
||
cur = parent[cur]
|
||
path.reverse()
|
||
return path, len(visited)
|
||
|
||
for neighbor in maze.get_neighbors(current):
|
||
if neighbor not in visited:
|
||
visited.add(neighbor)
|
||
parent[neighbor] = current
|
||
stack.append(neighbor)
|
||
|
||
return [], len(visited)
|
||
|
||
|
||
class AStarStrategy(PathFindingStrategy):
|
||
@staticmethod
|
||
def _heuristic(cell: Cell, target: Cell) -> int:
|
||
return abs(cell.x - target.x) + abs(cell.y - target.y) #самая простая эвристика
|
||
|
||
def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]:
|
||
if start is exit_:
|
||
return [start], 1
|
||
counter = 0
|
||
open_set = [(0, counter, start)]
|
||
g_score: Dict[Cell, int] = {start: 0}
|
||
f_score: Dict[Cell, int] = {start: self._heuristic(start, exit_)}
|
||
parent: Dict[Cell, Optional[Cell]] = {start: None}
|
||
closed_set: Set[Cell] = set()
|
||
visited_count = 0
|
||
|
||
while open_set:
|
||
_, _, current = heapq.heappop(open_set)
|
||
if current in closed_set:
|
||
continue
|
||
closed_set.add(current)
|
||
visited_count = len(closed_set)
|
||
|
||
if current is exit_:
|
||
path = []
|
||
cur = current
|
||
while cur is not None:
|
||
path.append(cur)
|
||
cur = parent[cur]
|
||
path.reverse()
|
||
return path, visited_count
|
||
|
||
for neighbor in maze.get_neighbors(current):
|
||
if neighbor in closed_set:
|
||
continue
|
||
tentative_g = g_score[current] + 1
|
||
if neighbor not in g_score or tentative_g < g_score[neighbor]:
|
||
parent[neighbor] = current
|
||
g_score[neighbor] = tentative_g
|
||
f = tentative_g + self._heuristic(neighbor, exit_)
|
||
f_score[neighbor] = f
|
||
counter += 1
|
||
heapq.heappush(open_set, (f, counter, neighbor))
|
||
|
||
return [], visited_count
|
||
|
||
@dataclass
|
||
class SearchStats:
|
||
time_ms: float #время выполнения в мс
|
||
visited_cells: int #количество посещённых клеток
|
||
path_length: int #длина найденного пути (0 если пути нет)
|
||
path_found: bool #найден ли путь
|
||
|
||
class Observer(abc.ABC): #я забыл что я там писать хотел после наблюдателя удачи мне завтра разобрать
|
||
@abc.abstractmethod #а я (гугл + хабр) разобрал снова балбесина!!!
|
||
def update(self, event_type: str, data: Any = None) -> None:
|
||
pass
|
||
|
||
|
||
class Subject:
|
||
def __init__(self):
|
||
self._observers: List[Observer] = []
|
||
|
||
def attach(self, observer: Observer) -> None:
|
||
if observer not in self._observers:
|
||
self._observers.append(observer)
|
||
|
||
def detach(self, observer: Observer) -> None:
|
||
if observer in self._observers:
|
||
self._observers.remove(observer)
|
||
|
||
def notify(self, event_type: str, data: Any = None) -> None:
|
||
for observer in self._observers:
|
||
observer.update(event_type, data)
|
||
|
||
class MazeSolver(Subject):
|
||
def __init__(self, maze: Maze, strategy: Optional[PathFindingStrategy] = None):
|
||
super().__init__()
|
||
self.maze = maze
|
||
self._strategy = strategy
|
||
|
||
def set_strategy(self, strategy: PathFindingStrategy) -> None: #смена алгоритма
|
||
self._strategy = strategy
|
||
|
||
def solve(self) -> Optional[SearchStats]:
|
||
if self._strategy is None:
|
||
print("где стратегия?")
|
||
return None
|
||
|
||
self.notify("solving_start", {"strategy": self._strategy.__class__.__name__})
|
||
|
||
start_time = time.perf_counter()
|
||
path, visited = self._strategy.find_path(self.maze, self.maze.start_cell, self.maze.exit_cell)
|
||
end_time = time.perf_counter()
|
||
|
||
time_ms = (end_time - start_time) * 1000.0
|
||
path_found = len(path) > 0
|
||
stats = SearchStats(
|
||
time_ms=time_ms,
|
||
visited_cells=visited,
|
||
path_length=len(path) if path_found else 0,
|
||
path_found=path_found
|
||
)
|
||
|
||
if path_found:
|
||
self.notify("path_found", {"path": path, "stats": stats})
|
||
else:
|
||
self.notify("no_path", {"stats": stats})
|
||
|
||
self.notify("solving_end", {"stats": stats})
|
||
return stats
|
||
|
||
class ConsoleView(Observer): #красиво в консоль выводит лабиринт
|
||
def __init__(self, maze: Maze):
|
||
self.maze = maze
|
||
self.last_path: List[Cell] = []
|
||
|
||
def update(self, event_type: str, data: Any = None) -> None:
|
||
if event_type == "path_found":
|
||
self.last_path = data["path"]
|
||
self.render()
|
||
elif event_type == "no_path":
|
||
self.last_path = []
|
||
self.render(no_path=True)
|
||
elif event_type == "solving_start":
|
||
print(f"\nпоиск пути (алгоритм: {data['strategy']})\n")
|
||
|
||
def render(self, no_path: bool = False) -> None:
|
||
path_set = set(self.last_path) if self.last_path else set()
|
||
|
||
for y in range(self.maze.height):
|
||
row = []
|
||
for x in range(self.maze.width):
|
||
cell = self.maze.get_cell(x, y)
|
||
if cell is None:
|
||
row.append('?')
|
||
continue
|
||
if cell is self.maze.start_cell:
|
||
row.append('S')
|
||
elif cell is self.maze.exit_cell:
|
||
row.append('E')
|
||
elif cell in path_set:
|
||
row.append('*')
|
||
elif cell.is_wall:
|
||
row.append('#')
|
||
else:
|
||
row.append(' ')
|
||
print(''.join(row))
|
||
|
||
if no_path:
|
||
print("\nнет пути (no way)")
|
||
elif self.last_path:
|
||
print(f"\nнайден путь длиной {len(self.last_path)} клеток")
|
||
else:
|
||
print("\nожидание решения")
|
||
|
||
def main():
|
||
import sys
|
||
|
||
if len(sys.argv) < 2:
|
||
print("для запуска: python main.py <имя_лабиринта>.txt")
|
||
return
|
||
|
||
filename = sys.argv[1]
|
||
|
||
#строится лабиринт из файла
|
||
builder = TextFileMazeBuilder()
|
||
try:
|
||
maze = builder.build_from_file(filename)
|
||
except Exception as e:
|
||
print(f"ошибка загрузки лабиринта: {e}")
|
||
return
|
||
|
||
#наблюдатель и решатель прикрепляются
|
||
solver = MazeSolver(maze)
|
||
view = ConsoleView(maze)
|
||
solver.attach(view)
|
||
strategies = {
|
||
"1": BFSStrategy(),
|
||
"2": DFSStrategy(),
|
||
"3": AStarStrategy()
|
||
}
|
||
|
||
print("\nвыберите алгоритм поиска:")
|
||
print("1. BFS")
|
||
print("2. DFS")
|
||
print("3. A*")
|
||
choice = input("введите (1/2/3): ").strip()
|
||
|
||
strategy = strategies.get(choice)
|
||
if not strategy:
|
||
print("неверный выбор, по умолчанию используется BFS.")
|
||
strategy = BFSStrategy()
|
||
|
||
solver.set_strategy(strategy)
|
||
stats = solver.solve()
|
||
|
||
if stats:
|
||
print("\nстатистика по поиску пути в данном лабиринте")
|
||
print(f"выбранный алгоритм: {strategy.__class__.__name__}")
|
||
print(f"время выполнения: {stats.time_ms:.3f} мс")
|
||
print(f"посещено клеток: {stats.visited_cells}")
|
||
print(f"путь найден?: {'да' if stats.path_found else 'нет'}")
|
||
if stats.path_found:
|
||
print(f"длина пути: {stats.path_length}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |