2026-rff_mp/KolbasovPD/docs/data/2-nd_exercise/main.py

546 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import heapq
from collections import deque
from abc import ABC, abstractmethod
import time
import csv
import os
from typing import List, Tuple, Optional, Dict, Set
class Cell:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
self.is_wall = False
self.is_start = False
self.is_exit = False
self.weight = 1
def is_passable(self) -> bool:
return not self.is_wall
class Maze:
def __init__(self, width: int = 0, height: int = 0):
self.width = width
self.height = height
self.cells: List[List[Cell]] = []
self.start: Optional[Cell] = None
self.exit: Optional[Cell] = None
def get_cell(self, x: int, y: int) -> Optional[Cell]:
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[y][x]
return None
def get_neighbors(self, cell: Cell) -> List[Cell]:
neighbors = []
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
for dx, dy in directions:
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):
@abstractmethod
def build_from_file(self, filename: str) -> Maze:
pass
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename: str) -> Maze:
with open(filename, 'r', encoding='utf-8') as f:
lines = f.readlines()
height = len(lines)
width = len(lines[0].strip()) if height > 0 else 0
maze = Maze(width, height)
maze.cells = [[Cell(x, y) for x in range(width)] for y in range(height)]
for y, line in enumerate(lines):
line = line.rstrip('\n')
for x, ch in enumerate(line):
if x < width:
cell = maze.cells[y][x]
if ch == '#':
cell.is_wall = True
elif ch == 'S':
cell.is_start = True
maze.start = cell
elif ch == 'E':
cell.is_exit = True
maze.exit = cell
elif ch.isdigit():
cell.weight = int(ch)
cell.is_wall = False
if not maze.start or not maze.exit:
raise ValueError("Лабиринт должен содержать старт (S) и выход (E)")
return maze
class PathFindingStrategy(ABC):
@abstractmethod
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
pass
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
queue = deque([start])
visited = {start}
parent = {start: None}
while queue:
current = queue.popleft()
if current == exit_cell:
return self.reconstruct_path(parent, start, exit_cell)
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
queue.append(neighbor)
return []
def reconstruct_path(self, parent: Dict, start: Cell, exit_cell: Cell) -> List[Cell]:
path = []
current = exit_cell
while current is not None:
path.append(current)
current = parent[current]
path.reverse()
return path
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
stack = [(start, [start])]
visited = {start}
while stack:
current, path = stack.pop()
if current == exit_cell:
return path
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
stack.append((neighbor, path + [neighbor]))
return []
class AStarStrategy(PathFindingStrategy):
def heuristic(self, a: Cell, b: Cell) -> int:
return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
open_set = [(0, start)]
came_from = {}
g_score = {start: 0}
f_score = {start: self.heuristic(start, exit_cell)}
while open_set:
_, current = heapq.heappop(open_set)
if current == exit_cell:
return self.reconstruct_path(came_from, start, exit_cell)
for neighbor in maze.get_neighbors(current):
tentative_g = g_score[current] + neighbor.weight
if neighbor not in g_score or tentative_g < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score[neighbor] = tentative_g + self.heuristic(neighbor, exit_cell)
heapq.heappush(open_set, (f_score[neighbor], neighbor))
return []
def reconstruct_path(self, came_from: Dict, start: Cell, exit_cell: Cell) -> List[Cell]:
path = []
current = exit_cell
while current != start:
path.append(current)
current = came_from[current]
path.append(start)
path.reverse()
return path
class DijkstraStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
pq = [(0, start)]
distances = {start: 0}
parent = {start: None}
while pq:
dist, current = heapq.heappop(pq)
if current == exit_cell:
return self.reconstruct_path(parent, start, exit_cell)
if dist > distances[current]:
continue
for neighbor in maze.get_neighbors(current):
new_dist = dist + neighbor.weight
if neighbor not in distances or new_dist < distances[neighbor]:
distances[neighbor] = new_dist
parent[neighbor] = current
heapq.heappush(pq, (new_dist, neighbor))
return []
def reconstruct_path(self, parent: Dict, start: Cell, exit_cell: Cell) -> List[Cell]:
path = []
current = exit_cell
while current is not None:
path.append(current)
current = parent[current]
path.reverse()
return path
class SearchStats:
def __init__(self, time_ms: float, visited_cells: int, path_length: int, path: List[Cell] = None):
self.time_ms = time_ms
self.visited_cells = visited_cells
self.path_length = path_length
self.path = path
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
self.maze = maze
self.strategy = strategy
self.observers = []
def set_strategy(self, strategy: PathFindingStrategy):
self.strategy = strategy
def add_observer(self, observer):
self.observers.append(observer)
def notify_observers(self, event: str, data=None):
for observer in self.observers:
observer.update(event, data)
def solve(self) -> SearchStats:
self.notify_observers("search_started")
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
visited_cells = self.count_visited_cells()
path_length = len(path)
stats = SearchStats(time_ms, visited_cells, path_length, path)
self.notify_observers("search_finished", stats)
return stats
def count_visited_cells(self) -> int:
if isinstance(self.strategy, BFSStrategy):
return len(self.bfs_visited)
elif isinstance(self.strategy, DFSStrategy):
return len(self.dfs_visited)
return 0
class Observer(ABC):
@abstractmethod
def update(self, event: str, data=None):
pass
class ConsoleView(Observer):
def __init__(self):
self.maze = None
self.current_path = None
def set_maze(self, maze: Maze):
self.maze = maze
def update(self, event: str, data=None):
if event == "search_finished":
self.display_path(data.path)
elif event == "search_started":
print("\nПоиск пути начат...")
def display_path(self, path: List[Cell]):
if not path:
print("\nПуть не найден!")
return
print(f"\nПуть найден! Длина: {len(path)} шагов")
self.render(path)
def render(self, path: List[Cell] = None):
if not self.maze:
return
path_set = set(path) if path else set()
for y in range(self.maze.height):
for x in range(self.maze.width):
cell = self.maze.get_cell(x, y)
if cell in path_set:
print('*', end='')
elif cell.is_start:
print('S', end='')
elif cell.is_exit:
print('E', end='')
elif cell.is_wall:
print('#', end='')
else:
print(' ', end='')
print()
class Player:
def __init__(self, start_cell: Cell):
self.current = start_cell
self.history = []
def move_to(self, cell: Cell):
if cell and cell.is_passable():
self.history.append(self.current)
self.current = cell
return True
return False
def undo(self):
if self.history:
self.current = self.history.pop()
return True
return False
class Command(ABC):
@abstractmethod
def execute(self) -> bool:
pass
@abstractmethod
def undo(self):
pass
class MoveCommand(Command):
def __init__(self, player: Player, direction: str, maze: Maze):
self.player = player
self.direction = direction
self.maze = maze
self.previous_position = None
def execute(self) -> bool:
self.previous_position = self.player.current
dx, dy = 0, 0
if self.direction == 'W':
dy = -1
elif self.direction == 'S':
dy = 1
elif self.direction == 'A':
dx = -1
elif self.direction == 'D':
dx = 1
new_cell = self.maze.get_cell(self.player.current.x + dx, self.player.current.y + dy)
if new_cell and new_cell.is_passable():
return self.player.move_to(new_cell)
return False
def undo(self):
if self.previous_position:
self.player.current = self.previous_position
def generate_test_mazes():
test_mazes = {
"tiny": [
"########",
"#S #",
"# #### #",
"# E #",
"########"
],
"empty": [
"########",
"#S #",
"# #",
"# E#",
"########"
],
"no_exit": [
"########",
"#S #",
"# #### #",
"# # #",
"########"
],
"weighted": [
"########",
"#S2 #",
"# 5#3 #",
"# 2 E #",
"########"
]
}
os.makedirs("mazes", exist_ok=True)
for name, maze_data in test_mazes.items():
filename = f"mazes/{name}.txt"
with open(filename, 'w', encoding='utf-8') as f:
f.write('\n'.join(maze_data))
print(f"Создан лабиринт: {filename}")
def run_experiments():
strategies = {
"BFS": BFSStrategy(),
"DFS": DFSStrategy(),
"A*": AStarStrategy(),
"Dijkstra": DijkstraStrategy()
}
mazes_list = ["tiny", "empty", "no_exit", "weighted"]
results = []
for maze_name in mazes_list:
filename = f"mazes/{maze_name}.txt"
try:
builder = TextFileMazeBuilder()
maze = builder.build_from_file(filename)
print(f"\nТестирование лабиринта: {maze_name}")
for strategy_name, strategy in strategies.items():
print(f" Стратегия: {strategy_name}")
times = []
visited_counts = []
path_lengths = []
for i in range(5):
solver = MazeSolver(maze, strategy)
stats = solver.solve()
times.append(stats.time_ms)
visited_counts.append(stats.visited_cells if stats.visited_cells else 0)
path_lengths.append(stats.path_length)
avg_time = sum(times) / len(times)
avg_visited = sum(visited_counts) / len(visited_counts)
avg_path_len = sum(path_lengths) / len(path_lengths)
results.append([
maze_name, strategy_name, avg_time, avg_visited, avg_path_len
])
print(f" Время: {avg_time:.3f} мс, Посещено: {avg_visited:.1f}, Путь: {avg_path_len:.1f}")
except Exception as e:
print(f"Ошибка загрузки {maze_name}: {e}")
with open("maze_results.csv", 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(["Лабиринт", "Стратегия", "Время_мс", "Посещено_клеток", "Длина_пути"])
writer.writerows(results)
print("\nРезультаты сохранены в maze_results.csv")
def interactive_mode():
print("\n=== ИНТЕРАКТИВНЫЙ РЕЖИМ ===")
filename = input("Введите имя файла с лабиринтом: ")
try:
builder = TextFileMazeBuilder()
maze = builder.build_from_file(filename)
print("\nВыберите стратегию:")
print("1. BFS (кратчайший путь)")
print("2. DFS (быстрый, не обязательно кратчайший)")
print("3. A* (оптимальный с эвристикой)")
print("4. Dijkstra (для взвешенных лабиринтов)")
choice = input("Ваш выбор (1-4): ")
strategies = {
'1': BFSStrategy(),
'2': DFSStrategy(),
'3': AStarStrategy(),
'4': DijkstraStrategy()
}
if choice not in strategies:
print("Неверный выбор!")
return
solver = MazeSolver(maze, strategies[choice])
view = ConsoleView()
view.set_maze(maze)
solver.add_observer(view)
stats = solver.solve()
print(f"\nСтатистика:")
print(f"Время выполнения: {stats.time_ms:.3f} мс")
print(f"Длина пути: {stats.path_length}")
input("\nНажмите Enter для ручного режима...")
player = Player(maze.start)
while player.current != maze.exit:
os.system('cls' if os.name == 'nt' else 'clear')
view.render()
print(f"\nТекущая позиция: ({player.current.x}, {player.current.y})")
print("Управление: W/A/S/D для движения, Z для отмены, Q для выхода")
cmd = input("> ").upper()
if cmd == 'Q':
break
elif cmd == 'Z':
command = MoveCommand(player, 'U', maze)
command.undo()
print("Отмена последнего хода")
elif cmd in ['W', 'A', 'S', 'D']:
command = MoveCommand(player, cmd, maze)
if command.execute():
print("Перемещение выполнено")
else:
print("Нельзя пройти в этом направлении")
else:
print("Неизвестная команда")
if player.current == maze.exit:
print("\nПОЗДРАВЛЯЮ! ВЫ НАШЛИ ВЫХОД!")
break
except Exception as e:
print(f"Ошибка: {e}")
def main():
print("="*80)
print("ПОИСК ВЫХОДА ИЗ ЛАБИРИНТА")
print("Применённые паттерны: Builder, Strategy, Observer, Command")
print("="*80)
generate_test_mazes()
print("\n1. Запустить эксперименты")
print("2. Интерактивный режим")
choice = input("\nВыберите режим (1-2): ")
if choice == '1':
run_experiments()
elif choice == '2':
interactive_mode()
else:
print("Неверный выбор!")
if __name__ == "__main__":
main()