forked from UNN/2026-rff_mp
402 lines
12 KiB
Python
402 lines
12 KiB
Python
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Это было долго, но вроде бы все готово. ") #можно конечно сделать многофайловую программу чтобы использовать классы как блоки длч строительства |