import os import time import heapq import csv from collections import deque from abc import ABC, abstractmethod class Cell: def __init__(self, x, y, wall=False): self.x = x self.y = y self.wall = wall self.start = False self.exit = False def prohodim(self): return not self.wall # лабиринт class Maze: def __init__(self, w, h): self.w = w self.h = h self.grid = [] for i in range(h): row = [] for j in range(w): row.append(Cell(j, i)) self.grid.append(row) self.start = None self.exit = None def get_cell(self, x, y): if 0 <= x < self.w and 0 <= y < self.h: return self.grid[y][x] return None def set_start(self, x, y): c = self.get_cell(x, y) if c: c.start = True self.start = (x, y) def set_exit(self, x, y): c = self.get_cell(x, y) if c: c.exit = True self.exit = (x, y) def neighbors(self, x, y): res = [] for dx, dy in [(0,1),(1,0),(0,-1),(-1,0)]: nx, ny = x+dx, y+dy c = self.get_cell(nx, ny) if c and c.prohodim(): res.append((nx, ny)) return res class MazeBuilder(ABC): @abstractmethod def load(self, filename): pass class TextMazeBuilder(MazeBuilder): def load(self, filename): f = open(filename, 'r', encoding='utf-8') lines = [] for line in f: lines.append(line.rstrip('\n')) f.close() if not lines: raise Exception("Файл пустой") h = len(lines) w = max([len(l) for l in lines]) maze = Maze(w, h) for y in range(h): line = lines[y] for x in range(len(line)): ch = line[x] if ch == '#': maze.grid[y][x] = Cell(x, y, wall=True) elif ch == 'S': maze.set_start(x, y) elif ch == 'E': maze.set_exit(x, y) return maze class Strategy(ABC): @abstractmethod def find_path(self, maze, start, end): pass # BFS class BFS(Strategy): def find_path(self, maze, start, end): if start is None or end is None: self.visited_count = 0 return None q = deque() q.append(start) parent = {start: None} while q: cur = q.popleft() if cur == end: break for nb in maze.neighbors(cur[0], cur[1]): if nb not in parent: parent[nb] = cur q.append(nb) self.visited_count = len(parent) if end not in parent: return None path = [] c = end while c is not None: path.append(c) c = parent[c] path.reverse() return path # DFS class DFS(Strategy): def find_path(self, maze, start, end): if start is None or end is None: self.visited_count = 0 return None stack = [start] parent = {start: None} while stack: cur = stack.pop() if cur == end: break for nb in maze.neighbors(cur[0], cur[1]): if nb not in parent: parent[nb] = cur stack.append(nb) self.visited_count = len(parent) if end not in parent: return None path = [] c = end while c is not None: path.append(c) c = parent[c] path.reverse() return path # A* class AStar(Strategy): def heur(self, a, b): return abs(a[0]-b[0]) + abs(a[1]-b[1]) def find_path(self, maze, start, end): if start is None or end is None: self.visited_count = 0 return None heap = [(0, start)] came_from = {} g = {start: 0} f = {start: self.heur(start, end)} visited = set([start]) while heap: cur = heapq.heappop(heap)[1] visited.add(cur) if cur == end: break for nb in maze.neighbors(cur[0], cur[1]): newg = g[cur] + 1 if newg < g.get(nb, 999999): came_from[nb] = cur g[nb] = newg f[nb] = newg + self.heur(nb, end) heapq.heappush(heap, (f[nb], nb)) visited.add(nb) self.visited_count = len(visited) if end not in came_from and end != start: return None path = [] c = end while c in came_from: path.append(c) c = came_from[c] path.append(start) path.reverse() return path # решение и статистика class MazeSolver: def __init__(self, maze, strategy): self.maze = maze self.strategy = strategy def solve(self): if self.maze.start is None or self.maze.exit is None: return (0, 0, 0, False) t0 = time.perf_counter() path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) t = (time.perf_counter() - t0) * 1000 if path is None: return (t, 0, 0, False) return (t, self.strategy.visited_count, len(path), True) # Наблюдатель class Observer(ABC): @abstractmethod def update(self, event, data): pass class ConsoleView(Observer): def __init__(self, maze): self.maze = maze self.player_pos = None self.path_set = None def update(self, event, data): if event == 'init': self.draw() elif event == 'move': self.player_pos = data self.draw() elif event == 'path': self.path_set = set(data) if data else None self.draw() def clear(self): os.system('cls' if os.name == 'nt' else 'clear') def draw(self): self.clear() for y in range(self.maze.h): row = '' for x in range(self.maze.w): cell = self.maze.get_cell(x, y) if self.player_pos and (x,y) == self.player_pos: row += 'P' elif cell.start: row += 'S' elif cell.exit: row += 'E' elif cell.wall: row += '#' elif self.path_set and (x,y) in self.path_set: row += '*' else: row += ' ' print(row) print("WASD - ходить, F - BFS путь, G - A* путь, Z - отмена, Q - выход") # игрок class Player: def __init__(self, maze): self.maze = maze self.pos = maze.start self.history = [] def move(self, new_pos): cell = self.maze.get_cell(new_pos[0], new_pos[1]) if cell and cell.prohodim(): self.history.append(self.pos) self.pos = new_pos return True return False def undo(self): if self.history: self.pos = self.history.pop() return True return False class Command(ABC): @abstractmethod def execute(self): pass @abstractmethod def undo(self): pass class MoveCommand(Command): def __init__(self, player, dx, dy): self.player = player self.dx = dx self.dy = dy self.old = None def execute(self): self.old = self.player.pos nx = self.player.pos[0] + self.dx ny = self.player.pos[1] + self.dy return self.player.move((nx, ny)) def undo(self): if self.old: return self.player.move(self.old) return False # эксперимент def run_experiment(maze_files, repeats=5): builder = TextMazeBuilder() results = [] for fname in maze_files: print("Обработка", fname) maze = builder.load(fname) if maze.start is None or maze.exit is None: print(f"Предупреждение: в {fname} нет S или E, пропускаем") continue for algo_class, name in [(BFS, 'BFS'), (DFS, 'DFS'), (AStar, 'A*')]: total_time = 0.0 total_visited = 0 path_len = 0 found = False for _ in range(repeats): alg = algo_class() t0 = time.perf_counter() path = alg.find_path(maze, maze.start, maze.exit) t = (time.perf_counter() - t0) * 1000 total_time += t total_visited += alg.visited_count if path: found = True path_len = len(path) results.append({ 'maze': fname, 'algo': name, 'time': total_time / repeats, 'visited': total_visited / repeats, 'length': path_len, 'found': found }) with open('results.csv', 'w', newline='') as f: writer = csv.DictWriter(f, fieldnames=['maze','algo','time','visited','length','found']) writer.writeheader() writer.writerows(results) print("\nРезультаты:") for r in results: print(f"{r['maze']:15} {r['algo']:5} время={r['time']:6.2f}ms посещено={r['visited']:6.1f} длина={r['length']}") # режим игры def play_game(maze): view = ConsoleView(maze) player = Player(maze) view.update('init', None) while True: cmd = input("> ").strip().upper() if cmd == 'W': c = MoveCommand(player, 0, -1) elif cmd == 'S': c = MoveCommand(player, 0, 1) elif cmd == 'A': c = MoveCommand(player, -1, 0) elif cmd == 'D': c = MoveCommand(player, 1, 0) elif cmd == 'F': solver = MazeSolver(maze, BFS()) t, v, l, ok = solver.solve() if ok: path = BFS().find_path(maze, maze.start, maze.exit) view.update('path', path) print(f"BFS: длина={l} время={t:.2f}ms посещено={v}") else: print("Путь не найден") continue elif cmd == 'G': astar = AStar() path = astar.find_path(maze, maze.start, maze.exit) if path: view.update('path', path) print(f"A*: длина={len(path)} посещено={astar.visited_count}") else: print("Путь не найден") continue elif cmd == 'Z': if player.undo(): view.update('move', player.pos) continue elif cmd == 'Q': break else: print("Неизвестная команда") continue if c.execute(): view.update('move', player.pos) else: print("Стена!") def main(): print("1 - Игра\n2 - Эксперимент") ch = input("> ") builder = TextMazeBuilder() if ch == '1': maze = builder.load('small.txt') #легкая прогулка) play_game(maze) else: files = ['small.txt', 'medium.txt', 'large.txt', 'empty.txt', 'no_exit.txt'] run_experiment(files) if __name__ == '__main__': main() print ("\nЭто было долго, но вроде бы все готово. ") #можно конечно сделать многофайловую программу чтобы использовать классы как блоки длч строительства