feat: add MazeProject (maze with patterns) to branch

This commit is contained in:
ivantsovma 2026-05-30 09:53:54 +03:00
parent 156aa5d75d
commit 8c2b455ca5
28 changed files with 1348 additions and 0 deletions

4
builders/__init__.py Normal file
View File

@ -0,0 +1,4 @@
from .maze_builder import MazeBuilder
from .text_file_maze_builder import TextFileMazeBuilder
__all__ = ['MazeBuilder', 'TextFileMazeBuilder']

9
builders/maze_builder.py Normal file
View File

@ -0,0 +1,9 @@
from abc import ABC, abstractmethod
class MazeBuilder(ABC):
"""Абстрактный строитель лабиринта"""
@abstractmethod
def build_from_file(self, filename: str):
"""Строит лабиринт из файла"""
pass

View File

@ -0,0 +1,37 @@
from models import Cell, Maze
from .maze_builder import MazeBuilder
class TextFileMazeBuilder(MazeBuilder):
"""Строитель лабиринта из текстового файла"""
def build_from_file(self, filename: str) -> Maze:
"""Читает файл и строит лабиринт"""
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
height = len(lines)
width = len(lines[0]) if height > 0 else 0
maze = Maze(width, height)
for y, line in enumerate(lines):
for x, char in enumerate(line):
is_wall = (char == '#')
is_start = (char == 'S')
is_exit = (char == 'E')
cell = Cell(x, y, is_wall, is_start, is_exit)
maze.set_cell(x, y, cell)
if is_start:
maze.start = cell
if is_exit:
maze.exit = cell
# Валидация
if maze.start is None:
raise ValueError("В лабиринте нет стартовой клетки (S)")
if maze.exit is None:
raise ValueError("В лабиринте нет выходной клетки (E)")
return maze

View File

@ -0,0 +1,87 @@
import sys
import os
import time
import csv
# Добавляем родительскую папку в путь поиска
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from builders import TextFileMazeBuilder
from strategies import BFSStrategy, DFSStrategy, AStarStrategy
def run_experiment(maze_file, strategy, num_runs=5):
"""Запускает эксперимент для одной стратегии"""
builder = TextFileMazeBuilder()
# Корректируем путь к файлу лабиринта
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
maze_path = os.path.join(base_dir, maze_file)
maze = builder.build_from_file(maze_path)
times = []
visited_counts = []
path_length = 0
for _ in range(num_runs):
start_time = time.perf_counter()
path = strategy.find_path(maze, maze.start, maze.exit)
end_time = time.perf_counter()
times.append((end_time - start_time) * 1000) # в миллисекундах
visited_counts.append(strategy.visited_count)
if path:
path_length = len(path)
return {
'maze': os.path.basename(maze_file),
'strategy': strategy.name,
'avg_time_ms': sum(times) / len(times),
'min_time_ms': min(times),
'max_time_ms': max(times),
'avg_visited': sum(visited_counts) / len(visited_counts),
'path_length': path_length
}
def run_all_experiments():
print("ЗАПУСК ЭКСПЕРИМЕНТОВ")
mazes = [
'mazes/simple_maze.txt',
'mazes/small_maze.txt'
]
strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy()]
results = []
for maze_file in mazes:
print(f"\nЛабиринт: {maze_file}")
for strategy in strategies:
print(f" {strategy.name}...", end=" ", flush=True)
result = run_experiment(maze_file, strategy)
results.append(result)
print(f"{result['avg_time_ms']:.2f} мс, посещено: {result['avg_visited']:.0f}")
# Сохраняем результаты
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
results_dir = os.path.join(base_dir, 'docs', 'data')
os.makedirs(results_dir, exist_ok=True)
csv_path = os.path.join(results_dir, 'experiment_results.csv')
with open(csv_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['maze', 'strategy', 'avg_time_ms', 'min_time_ms', 'max_time_ms', 'avg_visited', 'path_length'])
writer.writeheader()
writer.writerows(results)
print(f"Результаты сохранены в {csv_path}")
# Вывод таблицы
print("\nРЕЗУЛЬТАТЫ:")
print(f"{'Лабиринт':<20} {'Стратегия':<10} {'Время(мс)':<12} {'Посещено':<10} {'Длина пути':<10}")
for r in results:
print(f"{r['maze']:<20} {r['strategy']:<10} {r['avg_time_ms']:>8.2f} {r['avg_visited']:>8.0f} {r['path_length']:>8}")
return results
if __name__ == "__main__":
run_all_experiments()

11
mazes/medium_maze.txt Normal file
View File

@ -0,0 +1,11 @@
##########
#S #
# ### ### #
# # # #
### # ### #
# # #
# ### ### #
# # #
####### # #
# E#
##########

3
mazes/simple_maze.txt Normal file
View File

@ -0,0 +1,3 @@
#####
#S E#
#####

7
mazes/small_maze.txt Normal file
View File

@ -0,0 +1,7 @@
#######
#S #
# ### #
# # #
### # #
# E#
#######

4
models/__init__.py Normal file
View File

@ -0,0 +1,4 @@
from .cell import Cell
from .maze import Maze
__all__ = ['Cell', 'Maze']

13
models/cell.py Normal file
View File

@ -0,0 +1,13 @@
class Cell:
def __init__(self, x, y, is_wall=False, is_start=False, is_exit=False):
self.x = x
self.y = y
self.is_wall = is_wall
self.is_start = is_start
self.is_exit = is_exit
def is_passable(self):
return not self.is_wall
def __repr__(self):
return f"Cell({self.x}, {self.y}, wall={self.is_wall})"

36
models/maze.py Normal file
View File

@ -0,0 +1,36 @@
from typing import List, Optional
class Maze:
#лабиринт
def __init__(self, width: int, height: int):
self.width = width
self.height = height
self.cells = [[None for _ in range(width)] for _ in range(height)]
self.start = None
self.exit = None
def set_cell(self, x: int, y: int, cell):
#устанавливает клетки в лаб
if 0 <= x < self.width and 0 <= y < self.height:
self.cells[y][x] = cell
def get_cell(self, x: int, y: int):
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[y][x]
return None
def get_neighbors(self, 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
def __repr__(self):
return f"Maze({self.width}x{self.height}, start={self.start}, exit={self.exit})"

93
play_maze.py Normal file
View File

@ -0,0 +1,93 @@
import sys
import os
#Добавляем корневую папку
sys.path.insert(0, r'C:\ivantsovma\docs\MazeProject')
from builders import TextFileMazeBuilder
from strategies import BFSStrategy
from visualization.observer import Event, EventType
from visualization.console_view import ConsoleView
from visualization.game_controller import GameController
def play_maze():
print("НАЙДИ ВЫХОД ИЗ ЛАБИРИНТА")
#Загружаем лабиринт
builder = TextFileMazeBuilder()
#Выбор лабиринта
print("\nВыберите лабиринт:")
print("1. Простой лабиринт (simple_maze.txt)")
print("2. Сложный лабиринт (small_maze.txt)")
choice = input("Ваш выбор (1/2): ").strip()
if choice == "1":
maze_file = "mazes/simple_maze.txt"
else:
maze_file = "mazes/small_maze.txt"
try:
maze = builder.build_from_file(maze_file)
except Exception as e:
print(f"Ошибка загрузки лабиринта: {e}")
return
#Создаём контроллер и отображение
controller = GameController(maze)
view = ConsoleView()
#Подписываем view на события контроллера
controller.attach(view)
#Уведомляем о загрузке лабиринта
controller.notify(Event(EventType.MAZE_LOADED, maze))
#Находим и показываем оптимальный путь (для подсказки)
bfs = BFSStrategy()
optimal_path = bfs.find_path(maze, maze.start, maze.exit)
controller.notify(Event(EventType.PATH_FOUND, optimal_path))
print("\nУправление:")
print(" w - вверх s - вниз a - влево d - вправо")
print(" u - отменить q - выход")
# Игровой цикл
while True:
view.render()
# Проверка победы
if controller.get_player_position() == maze.exit:
view.render()
print("\nВЫ НАШЛИ ВЫХОД!")
break
# Чтение команды
cmd = input("\nВведите команду: ").lower().strip()
if cmd == 'q':
print("Выход из игры...")
break
elif cmd == 'u':
controller.undo()
elif cmd == 'w':
controller.move((0, -1))
elif cmd == 's':
controller.move((0, 1))
elif cmd == 'a':
controller.move((-1, 0))
elif cmd == 'd':
controller.move((1, 0))
else:
print("Неизвестная команда! Используйте w/a/s/d, u или q")
if __name__ == "__main__":
try:
play_maze()
except KeyboardInterrupt:
print("\n\nИгра прервана пользователем")
except Exception as e:
print(f"\nОшибка: {e}")
import traceback
traceback.print_exc()

3
solver/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .maze_solver import MazeSolver, SearchStats
__all__ = ['MazeSolver', 'SearchStats']

127
solver/maze_solver.py Normal file
View File

@ -0,0 +1,127 @@
import time
from dataclasses import dataclass
from typing import List, Optional
@dataclass
#Статистика поиска пути
class SearchStats:
time_ms: float #Время выполнения в миллисекундах
visited_cells: int #Количество посещенных клеток
path_length: int #Длина найденного пути (0 если путь не найден)
path_found: bool #Найден ли путь
def __repr__(self):
status = "Найден" if self.path_found else "Не найден"
return f"Stats({status}, время={self.time_ms:.2f}мс, посещено={self.visited_cells}, длина={self.path_length})"
def to_dict(self):
"""Преобразует статистику в словарь для CSV"""
return {
'time_ms': f"{self.time_ms:.2f}",
'visited_cells': self.visited_cells,
'path_length': self.path_length,
'path_found': self.path_found
}
class MazeSolver:
"""Оркестратор для решения лабиринта"""
#Инициализация решателя лабиринта
def __init__(self, maze, strategy=None):
self.maze = maze
self._strategy = strategy
self._last_path = None
#Динамическая смена стратегии поиска
def set_strategy(self, strategy):
self._strategy = strategy
print(f"Стратегия изменена на: {strategy.name}")
def solve(self) -> SearchStats:
if self._strategy is None:
raise ValueError("Стратегия не установлена! Используйте set_strategy()")
if self.maze.start is None:
raise ValueError("В лабиринте нет стартовой клетки!")
if self.maze.exit is None:
raise ValueError("В лабиринте нет выходной клетки!")
# Замер времени
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
self._last_path = path
return SearchStats(
time_ms=time_ms,
visited_cells=self._strategy.visited_count,
path_length=len(path),
path_found=len(path) > 0
)
def get_path(self) -> Optional[List]:
"""Возвращает последний найденный путь"""
return self._last_path
#Выводит лабиринт с найденным путем
def print_maze_with_path(self):
if not self._last_path:
print("Путь еще не найден. Сначала вызовите solve()")
return
path_set = set(self._last_path)
print(f"\n {' ' * 4}", end="")
for x in range(self.maze.width):
print(f"{x} ", end="")
print()
for y in range(self.maze.height):
print(f" {y}", end="")
for x in range(self.maze.width):
cell = self.maze.get_cell(x, y)
if cell == self.maze.start:
print("S ", end="")
elif cell == self.maze.exit:
print("E ", end="")
elif cell in path_set:
print("", end="")
elif cell.is_wall:
print("# ", end="")
else:
print("· ", end="")
print()
print("\n Условные обозначения:")
print(" # - стена · - проход ● - путь")
print(" S - старт E - выход")
#Сравнивает несколько стратегий на одном лабиринте
def compare_strategies(self, strategies: List) -> dict:
results = {}
print(f"СРАВНЕНИЕ СТРАТЕГИЙ")
print(f"Лабиринт: {self.maze.width}x{self.maze.height}")
for strategy in strategies:
self.set_strategy(strategy)
stats = self.solve()
results[strategy.name] = {
'stats': stats,
'path': self.get_path()
}
# Вывод результатов
status = "OK" if stats.path_found else "BULLSHIT"
print(f"\n {status} {strategy.name}:")
print(f" Время: {stats.time_ms:.2f} мс")
print(f" Посещено клеток: {stats.visited_cells}")
print(f" Длина пути: {stats.path_length}")
return results

6
strategies/__init__.py Normal file
View File

@ -0,0 +1,6 @@
from .path_finding_strategy import PathFindingStrategy
from .bfs_strategy import BFSStrategy
from .dfs_strategy import DFSStrategy
from .astar_strategy import AStarStrategy
__all__ = ['PathFindingStrategy', 'BFSStrategy', 'DFSStrategy', 'AStarStrategy']

View File

@ -0,0 +1,67 @@
import heapq
from typing import List, Dict
from .path_finding_strategy import PathFindingStrategy
class AStarStrategy(PathFindingStrategy):
"""A* поиск с эвристикой (манхэттенское расстояние)"""
def __init__(self):
self._visited_count = 0
@property
def name(self) -> str:
return "AStar"
def _heuristic(self, cell, exit_cell) -> int:
"""Манхэттенское расстояние между клетками"""
return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y)
def find_path(self, maze, start, exit_cell) -> List:
"""Находит путь с помощью A*"""
if not start or not exit_cell:
return []
# Приоритетная очередь (F-значение, ID для уникальности, клетка)
open_set = []
heapq.heappush(open_set, (0, id(start), start))
# Откуда пришли в каждую клетку
came_from = {}
# Стоимость пути от старта до клетки (G-значение)
g_score = {start: 0}
# Оценочная стоимость (F-значение = G + H)
f_score = {start: self._heuristic(start, exit_cell)}
self._visited_count = 0
while open_set:
_, _, current = heapq.heappop(open_set)
self._visited_count += 1
# Нашли выход
if current == exit_cell:
return self._reconstruct_path(came_from, exit_cell)
# Проверяем всех соседей
for neighbor in maze.get_neighbors(current):
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f = tentative_g + self._heuristic(neighbor, exit_cell)
f_score[neighbor] = f
heapq.heappush(open_set, (f, id(neighbor), neighbor))
# Путь не найден
return []
def _reconstruct_path(self, came_from: Dict, current) -> List:
"""Восстанавливает путь от старта до exit"""
path = [current]
while current in came_from:
current = came_from[current]
path.append(current)
return list(reversed(path))

View File

@ -0,0 +1,53 @@
from collections import deque
from typing import List, Dict
from .path_finding_strategy import PathFindingStrategy
class BFSStrategy(PathFindingStrategy):
"""Поиск в ширину (BFS) - гарантирует кратчайший путь"""
def __init__(self):
self._visited_count = 0
@property
def name(self) -> str:
return "BFS"
def find_path(self, maze, start, exit_cell) -> List:
"""Находит путь с помощью BFS"""
if not start or not exit_cell:
return []
# Очередь для BFS
queue = deque([start])
# Множество посещенных клеток
visited = {start}
# Словарь для восстановления пути
parent = {start: None}
self._visited_count = 0
while queue:
current = queue.popleft()
self._visited_count += 1
# Нашли выход
if current == exit_cell:
return self._reconstruct_path(parent, 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, end) -> List:
"""Восстанавливает путь от终点 до старта"""
path = []
current = end
while current is not None:
path.append(current)
current = parent.get(current)
return list(reversed(path))

View File

@ -0,0 +1,40 @@
from typing import List
from .path_finding_strategy import PathFindingStrategy
class DFSStrategy(PathFindingStrategy):
"""Поиск в глубину (DFS) - быстрый, но не обязательно кратчайший"""
def __init__(self):
self._visited_count = 0
@property
def name(self) -> str:
return "DFS"
def find_path(self, maze, start, exit_cell) -> List:
"""Находит путь с помощью DFS"""
if not start or not exit_cell:
return []
# Стек для DFS (хранит текущую клетку и путь до неё)
stack = [(start, [start])]
# Множество посещенных клеток
visited = {start}
self._visited_count = 0
while stack:
current, path = stack.pop()
self._visited_count += 1
# Нашли выход
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 []

View File

@ -0,0 +1,19 @@
from abc import ABC, abstractmethod
from typing import List
class PathFindingStrategy(ABC):
@abstractmethod
#Находит путь от start до exit_cell
def find_path(self, maze, start, exit_cell) -> List:
pass
@property
@abstractmethod
def name(self) -> str:
#Имя стратегии
pass
@property
def visited_count(self) -> int:
#Количество посещенных клеток (заполняется при поиске)
return getattr(self, '_visited_count', 0)

81
test_builder.py Normal file
View File

@ -0,0 +1,81 @@
import sys
import os
# Добавляем текущую директорию в путь поиска
sys.path.insert(0, os.path.dirname(__file__))
from models import Cell, Maze
from builders import TextFileMazeBuilder
def test_builder():
print("ТЕСТИРОВАНИЕ BUILDER")
# Создаем строителя
builder = TextFileMazeBuilder()
# Загружаем простой лабиринт
print("\n1. Загрузка простого лабиринта (simple_maze.txt):")
try:
maze = builder.build_from_file("mazes/simple_maze.txt")
print(f" Размер: {maze.width}x{maze.height}")
print(f" Старт: {maze.start}")
print(f" Выход: {maze.exit}")
# Визуализация
print("\n Карта лабиринта:")
for y in range(maze.height):
line = ""
for x in range(maze.width):
cell = maze.get_cell(x, y)
if cell.is_wall:
line += "#"
elif cell.is_start:
line += "S"
elif cell.is_exit:
line += "E"
else:
line += " "
print(f" {line}")
print(" Лабиринт загружен успешно!")
except Exception as e:
print(f" Ошибка: {e}")
# Загружаем сложный лабиринт
print("\n2. Загрузка сложного лабиринта (small_maze.txt):")
try:
maze = builder.build_from_file("mazes/small_maze.txt")
print(f" Размер: {maze.width}x{maze.height}")
print(f" Старт: {maze.start}")
print(f" Выход: {maze.exit}")
# Визуализация
print("\n Карта лабиринта:")
for y in range(maze.height):
line = ""
for x in range(maze.width):
cell = maze.get_cell(x, y)
if cell.is_wall:
line += "#"
elif cell.is_start:
line += "S"
elif cell.is_exit:
line += "E"
else:
line += " "
print(f" {line}")
# Проверка соседей
print(f"\n Проверка соседей старта:")
neighbors = maze.get_neighbors(maze.start)
for n in neighbors:
print(f" - Сосед: ({n.x}, {n.y})")
print(" Сложный лабиринт загружен успешно!")
except Exception as e:
print(f" Ошибка: {e}")
print("ТЕСТИРОВАНИЕ ЗАВЕРШЕНО")
if __name__ == "__main__":
test_builder()

31
test_maze.py Normal file
View File

@ -0,0 +1,31 @@
from models import Cell, Maze
# Создаем лабиринт 3x3
maze = Maze(3, 3)
# Создаем клетки
for y in range(3):
for x in range(3):
cell = Cell(x, y, is_wall=False)
maze.set_cell(x, y, cell)
# Устанавливаем старт и выход
maze.start = maze.get_cell(0, 0)
maze.start.is_start = True
maze.exit = maze.get_cell(2, 2)
maze.exit.is_exit = True
#Создаем стену в центре
center = maze.get_cell(1, 1)
center.is_wall = True
print(f"Лабиринт: {maze}")
print(f"Старт: {maze.start}")
print(f"Выход: {maze.exit}")
# Проверяем соседей
neighbors = maze.get_neighbors(maze.start)
print(f"Соседи старта: {neighbors}")
# Проверяем проходимость
print(f"Центр проходим? {center.is_passable()}")

164
test_solver.py Normal file
View File

@ -0,0 +1,164 @@
import sys
import os
# Добавляем корневую папку в путь поиска
sys.path.insert(0, r'C:\ivantsovma\docs\MazeProject')
from builders import TextFileMazeBuilder
from strategies import BFSStrategy, DFSStrategy, AStarStrategy
from solver import MazeSolver
def test_solver_basic():
print("БАЗОВАЯ РАБОТА MAZESOLVER")
# Загружаем лабиринт
builder = TextFileMazeBuilder()
maze = builder.build_from_file("mazes/small_maze.txt")
print(f"\nЛабиринт загружен: {maze.width}x{maze.height}")
print(f"Старт: ({maze.start.x}, {maze.start.y})")
print(f"Выход: ({maze.exit.x}, {maze.exit.y})")
# Создаем решатель с BFS стратегией
solver = MazeSolver(maze, BFSStrategy())
# Решаем лабиринт
print("\nРешение лабиринта (BFS)")
stats = solver.solve()
print(f"\nРезультат:")
print(f" {stats}")
# Показываем путь на карте
print("\nВизуализация пути:")
solver.print_maze_with_path()
return solver, stats
#Тест динамической смены стратегии
def test_solver_dynamic_strategy():
print("ДИНАМИЧЕСКАЯ СМЕНА СТРАТЕГИИ")
# Загружаем лабиринт
builder = TextFileMazeBuilder()
maze = builder.build_from_file("mazes/small_maze.txt")
# Создаем решатель без стратегии
solver = MazeSolver(maze)
# Пробуем разные стратегии
strategies = [
BFSStrategy(),
DFSStrategy(),
AStarStrategy()
]
for strategy in strategies:
print(f"\nУстановка стратегии: {strategy.name}")
solver.set_strategy(strategy)
stats = solver.solve()
print(f" {stats}")
return solver
#Сравнение
def test_solver_comparison():
print("СРАВНЕНИЕ ВСЕХ СТРАТЕГИЙ")
# Загружаем лабиринт
builder = TextFileMazeBuilder()
maze = builder.build_from_file("mazes/small_maze.txt")
# Создаем решатель
solver = MazeSolver(maze)
# Сравниваем стратегии
strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy()]
results = solver.compare_strategies(strategies)
#Сводную таблица
print("СВОДНАЯ ТАБЛИЦА")
print(f"\n {'Стратегия':<10} {'Время(мс)':<10} {'Посещено':<10} {'Длина пути':<10} {'Статус':<10}")
for name, data in results.items():
stats = data['stats']
print(f" {name:<10} {stats.time_ms:<10.2f} {stats.visited_cells:<10} {stats.path_length:<10} {'OK' if stats.path_found else 'BULLSHIT'}")
return results
#Тест на разных лабиринтах
def test_multiple_mazes():
print("РАЗНЫЕ ЛАБИРИНТЫ")
mazes_files = [
("mazes/simple_maze.txt", "Простой (5x3)"),
("mazes/small_maze.txt", "Средний (7x7)")
]
strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy()]
for maze_file, maze_name in mazes_files:
print(f"\n{maze_name}")
try:
builder = TextFileMazeBuilder()
maze = builder.build_from_file(maze_file)
solver = MazeSolver(maze)
for strategy in strategies:
solver.set_strategy(strategy)
stats = solver.solve()
status = "OK" if stats.path_found else "BULLSHIT"
print(f" {status} {strategy.name}: {stats.time_ms:.2f}мс | {stats.visited_cells} клеток | длина={stats.path_length}")
except Exception as e:
print(f" Ошибка загрузки {maze_file}: {e}")
def test_no_exit_maze():
print("ЛАБИРИНТ БЕЗ ВЫХОДА")
#Создаем простой лабиринт без выхода
from models import Maze, Cell
maze = Maze(5, 5)
#Заполняем проходами
for y in range(5):
for x in range(5):
cell = Cell(x, y, is_wall=False)
maze.set_cell(x, y, cell)
#Устанавливаем старт, но НЕ устанавливаем выход!
start = maze.get_cell(0, 0)
start.is_start = True
maze.start = start
# Выход не устанавливаем (maze.exit = None)
# Создаем стену, чтобы заблокировать путь
for x in range(5):
wall = maze.get_cell(x, 4)
wall.is_wall = True
print(f"\nЛабиринт: {maze.width}x{maze.height}")
print(f"Старт: ({maze.start.x}, {maze.start.y})")
print(f"Выход: отсутствует (None)")
# Пытаемся найти выход
solver = MazeSolver(maze, BFSStrategy())
try:
stats = solver.solve()
print(f"\nРезультат:")
print(f" {stats}")
except ValueError as e:
print(f"\nРезультат:")
print(f"Ошибка: {e}")
print(f"\nКорректная обработка: программа обнаружила отсутствие выхода")
if __name__ == "__main__":
# Запускаем все тесты
test_solver_basic()
test_solver_dynamic_strategy()
test_solver_comparison()
test_multiple_mazes()
test_no_exit_maze()
print("ВСЕ ТЕСТЫ ЗАВЕРШЕНЫ")

117
test_strategy.py Normal file
View File

@ -0,0 +1,117 @@
import sys
import os
# Добавляем корневую папку в путь поиска
sys.path.insert(0, r'C:\ivantsovma\docs\MazeProject')
from builders import TextFileMazeBuilder
from strategies import BFSStrategy, DFSStrategy, AStarStrategy
def test_strategies():
print("ТЕСТИРОВАНИЕ STRATEGY ПАТТЕРНА")
# Загружаем лабиринт
builder = TextFileMazeBuilder()
maze = builder.build_from_file("mazes/small_maze.txt")
print(f"\nЛабиринт: {maze.width}x{maze.height}")
print(f"Старт: ({maze.start.x}, {maze.start.y})")
print(f"Выход: ({maze.exit.x}, {maze.exit.y})")
# Создаем стратегии
strategies = [
BFSStrategy(),
DFSStrategy(),
AStarStrategy()
]
print("РЕЗУЛЬТАТЫ ПОИСКА ПУТИ")
for strategy in strategies:
print(f"\n--- {strategy.name} ---")
# Ищем путь
path = strategy.find_path(maze, maze.start, maze.exit)
if path:
print(f"Путь найден!")
print(f"Посещено клеток: {strategy.visited_count}")
print(f"Длина пути: {len(path)} шагов")
print(f"Путь: ", end="")
for i, cell in enumerate(path[:5]):
print(f"({cell.x},{cell.y})", end="")
if i < len(path[:5]) - 1:
print("", end="")
if len(path) > 5:
print(f" ... → ({path[-1].x},{path[-1].y})")
else:
print()
else:
print(f"Путь не найден!")
# Визуализация
print("ВИЗУАЛИЗАЦИЯ ЛАБИРИНТА С ПУТЕМ (BFS)")
# Находим путь BFS
bfs = BFSStrategy()
path = bfs.find_path(maze, maze.start, maze.exit)
path_set = set(path)
print("\n " + " " * 4 + "0 1 2 3 4 5 6")
for y in range(maze.height):
line = f" {y}"
for x in range(maze.width):
cell = maze.get_cell(x, y)
if cell in path_set and cell != maze.start and cell != maze.exit:
line += ""
elif cell == maze.start:
line += "S "
elif cell == maze.exit:
line += "E "
elif cell.is_wall:
line += "# "
else:
line += "· "
print(line)
print("\n Условные обозначения:")
print(" # - стена")
print(" · - проход")
print(" ● - путь")
print(" S - старт")
print(" E - выход")
def test_simple_maze():
print("ТЕСТ НА ПРОСТОМ ЛАБИРИНТЕ")
builder = TextFileMazeBuilder()
maze = builder.build_from_file("mazes/simple_maze.txt")
print(f"\nЛабиринт 5x3:")
for y in range(maze.height):
line = ""
for x in range(maze.width):
cell = maze.get_cell(x, y)
if cell.is_wall:
line += "#"
elif cell.is_start:
line += "S"
elif cell.is_exit:
line += "E"
else:
line += " "
print(f" {line}")
strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy()]
for strategy in strategies:
path = strategy.find_path(maze, maze.start, maze.exit)
if path:
print(f"\n {strategy.name}: путь найден за {len(path)} шагов")
else:
print(f"\n {strategy.name}: путь НЕ найден")
if __name__ == "__main__":
test_strategies()
test_simple_maze()
print("ТЕСТИРОВАНИЕ ЗАВЕРШЕНО")

81
test_visualization.py Normal file
View File

@ -0,0 +1,81 @@
import sys
import os
sys.path.insert(0, r'C:\ivantsovma\docs\MazeProject')
from builders import TextFileMazeBuilder
from strategies import BFSStrategy
from visualization import ConsoleView, GameController
def test_observer():
print("ПАТТЕРН OBSERVER")
# Загружаем лабиринт
builder = TextFileMazeBuilder()
maze = builder.build_from_file("mazes/small_maze.txt")
#создаём наблюдателя
view = ConsoleView()
#уведомления о событии
view.update("maze_loaded", maze)
view.update("search_start", None)
view.update("path_found", None)
print("\nObserver работает!")
def test_game_controller():
print("ПАТТЕРН COMMAND (УПРАВЛЕНИЕ ИГРОКОМ)")
#pагружаем простой лабиринт
builder = TextFileMazeBuilder()
maze = builder.build_from_file("mazes/simple_maze.txt")
#cоздаём контроллер
controller = GameController(maze)
print(f"Начальная позиция: ({controller.get_player_position().x}, {controller.get_player_position().y})")
#движение вправо
controller.move((1, 0)) #Вправо
print(f"После движения вправо: ({controller.get_player_position().x}, {controller.get_player_position().y})")
controller.move((1, 0)) #Вправо
print(f"После движения вправо: ({controller.get_player_position().x}, {controller.get_player_position().y})")
#отменяем последние движения
controller.undo()
print(f"После отмены: ({controller.get_player_position().x}, {controller.get_player_position().y})")
controller.undo()
print(f"После второй отмены: ({controller.get_player_position().x}, {controller.get_player_position().y})")
print("\nCommand работает!")
def test_integration():
print("ИНТЕГРАЦИЯ ВСЕХ КОМПОНЕНТОВ")
# Загружаем лабиринт
builder = TextFileMazeBuilder()
maze = builder.build_from_file("mazes/small_maze.txt")
# Находим путь
bfs = BFSStrategy()
path = bfs.find_path(maze, maze.start, maze.exit)
# Создаём отображение
view = ConsoleView()
view.maze = maze
view.path = path
view.render()
print("\nИнтеграция работает!")
def run_interactive_game():
print("ИНТЕРАКТИВНАЯ ИГРА")
print("Для запуска интерактивной игры используйте:")
print("python play_maze.py")
if __name__ == "__main__":
test_observer()
test_game_controller()
test_integration()
run_interactive_game()

View File

@ -0,0 +1,6 @@
from .observer import Observable
from .console_view import ConsoleView
from .command import MoveCommand, Player
from .game_controller import GameController
__all__ = ['Observer', 'Observable', 'ConsoleView', 'Command', 'MoveCommand', 'Player', 'GameController']

54
visualization/command.py Normal file
View File

@ -0,0 +1,54 @@
from abc import ABC, abstractmethod
class Command(ABC):
#Выполняет команду
@abstractmethod
def execute(self):
pass
#Отменяет команду
@abstractmethod
def undo(self):
pass
class MoveCommand(Command):
def __init__(self, player, direction, maze, notifier=None):
self.player = player
self.direction = direction # (dx, dy)
self.maze = maze
self.notifier = notifier
self.previous_cell = player.current_cell
#Перемещает игрока в указанном направлении
def execute(self):
dx, dy = self.direction
new_x = self.player.current_cell.x + dx
new_y = self.player.current_cell.y + dy
new_cell = self.maze.get_cell(new_x, new_y)
# Проверяем, можно ли пройти
if new_cell and new_cell.is_passable():
self.player.move_to(new_cell)
return True
return False
#возвращает на предыдущую клетку
def undo(self):
if self.previous_cell:
self.player.move_to(self.previous_cell)
return True
return False
class Player:
#Игрок в лабиринте
def __init__(self, start_cell):
self.current_cell = start_cell
self.start_cell = start_cell
#Перемещает игрока на новую клетку
def move_to(self, cell):
self.current_cell = cell
#Сбрасывает игрока на стартовую позицию
def reset(self):
self.current_cell = self.start_cell

View File

@ -0,0 +1,118 @@
import os
from .observer import Observer, Event, EventType
class ConsoleView(Observer):
"""Консольное отображение лабиринта"""
def __init__(self):
self.maze = None
self.player_pos = None
self.path = []
self.current_strategy = None
self.messages = []
def update(self, event: Event):
event_type = event.event_type
data = event.data
if event_type == EventType.MAZE_LOADED:
self.maze = data
self._log(f"Лабиринт загружен: {self.maze.width}x{self.maze.height}")
elif event_type == EventType.PATH_FOUND:
self.path = data if data else []
self._log(f"Путь найден! Длина: {len(self.path)} шагов")
elif event_type == EventType.PATH_NOT_FOUND:
self.path = []
self._log(f"Путь не найден!")
elif event_type == EventType.PLAYER_MOVED:
self.player_pos = data
self._log(f"Игрок переместился на ({data.x}, {data.y})")
elif event_type == EventType.SEARCH_START:
self._log(f"Начинаем поиск пути...")
elif event_type == EventType.SEARCH_END:
self._log(f"Поиск завершён")
elif event_type == EventType.ERROR:
self._log(f"Ошибка: {data}")
elif event_type == EventType.UNDO:
self._log(f"Отмена последнего действия")
# После каждого события перерисовываем
self.render()
def render(self):
"""Отрисовывает лабиринт в консоли"""
if not self.maze:
print("Лабиринт не загружен")
return
# Очищаем консоль
os.system('cls' if os.name == 'nt' else 'clear')
print("=" * 60)
print("ЛАБИРИНТ")
if self.current_strategy:
print(f"Алгоритм: {self.current_strategy}")
print("=" * 60)
# Создаём множество клеток пути для быстрого доступа
path_set = set(self.path) if self.path else set()
# Верхняя граница с координатами
print(" " + " ".join(f"{x:2}" for x in range(self.maze.width)))
for y in range(self.maze.height):
# Номер строки
line = f"{y:2}"
for x in range(self.maze.width):
cell = self.maze.get_cell(x, y)
# Определяем символ для отображения
if self.player_pos and cell == self.player_pos:
line += "🎮 "
elif cell == self.maze.start:
line += "🚩 "
elif cell == self.maze.exit:
line += "🏁 "
elif cell in path_set:
line += ""
elif cell.is_wall:
line += "██ "
else:
line += "· "
print(line)
print("Условные обозначения:")
print(" ██ - стена · - проход ● - путь")
print(" 🚩 - старт 🏁 - выход 🎮 - игрок")
if self.current_strategy:
print(f" Алгоритм: {self.current_strategy}")
# Выводим сообщения
if self.messages:
print("\nСООБЩЕНИЯ:")
for msg in self.messages[-5:]: # Показываем последние 5 сообщений
print(f" {msg}")
print("Команды: W/A/S/D - движение, U - отмена, Q - выход")
def set_strategy(self, strategy_name: str):
"""Устанавливает имя текущей стратегии для отображения"""
self.current_strategy = strategy_name
def _log(self, message: str):
"""Добавляет сообщение в лог"""
self.messages.append(message)
if len(self.messages) > 10:
self.messages.pop(0)
def clear_messages(self):
"""Очищает сообщения"""
self.messages = []

View File

@ -0,0 +1,38 @@
from .observer import Observable, Event, EventType
from .command import MoveCommand, Player
class GameController(Observable):
def __init__(self, maze):
super().__init__()
self.maze = maze
self.player = Player(maze.start)
self.command_history = []
def move(self, direction):
"""Перемещает игрока в направлении"""
cmd = MoveCommand(self.player, direction, self.maze, self)
if cmd.execute():
self.command_history.append(cmd)
# Правильный вызов notify с одним аргументом Event
self.notify(Event(EventType.PLAYER_MOVED, self.player.current_cell))
return True
return False
def undo(self):
"""Отменяет последнее действие"""
if self.command_history:
cmd = self.command_history.pop()
cmd.undo()
self.notify(Event(EventType.UNDO, None))
return True
return False
def reset(self):
"""Сбрасывает игру"""
self.player.reset()
self.command_history.clear()
self.notify(Event(EventType.PLAYER_MOVED, self.player.current_cell))
def get_player_position(self):
"""Возвращает позицию игрока"""
return self.player.current_cell

39
visualization/observer.py Normal file
View File

@ -0,0 +1,39 @@
from abc import ABC, abstractmethod
from typing import Any
from enum import Enum, auto
class EventType(Enum):
MAZE_LOADED = auto()
PATH_FOUND = auto()
PATH_NOT_FOUND = auto()
PLAYER_MOVED = auto()
SEARCH_START = auto()
SEARCH_END = auto()
ERROR = auto()
UNDO = auto()
class Event:
def __init__(self, event_type: EventType, data: Any = None):
self.event_type = event_type
self.data = data
class Observer(ABC):
@abstractmethod
def update(self, event: Event):
pass
class Observable:
def __init__(self):
self._observers = []
def attach(self, observer: Observer):
if observer not in self._observers:
self._observers.append(observer)
def detach(self, observer: Observer):
if observer in self._observers:
self._observers.remove(observer)
def notify(self, event: Event):
for observer in self._observers:
observer.update(event)