Тестирование программы (успех)

This commit is contained in:
Dima 2026-05-24 19:17:11 +03:00
parent 9c066a3e03
commit 9395b9807c

View File

@ -298,7 +298,7 @@ class MazeSolver:
def solve(self): def solve(self):
""" """
решение лабиринта с использованием текущей стратегии. решение лабиринта с использованием текущей стратегии.
возвращает SearchStats (время, посещённые клетки, длина пути) возвращает время, посещённые клетки, длина пути
""" """
if self._strategy is None: if self._strategy is None:
raise ValueError("Стратегия не установлена. Используйте setStrategy()") raise ValueError("Стратегия не установлена. Используйте setStrategy()")
@ -321,18 +321,17 @@ class MazeSolver:
) )
return path, stats return path, stats
# ==================== ЭТАП 5: НАБЛЮДАТЕЛЬ (OBSERVER) ====================
class Observer: class Observer:
"""Интерфейс наблюдателя""" """интерфейс наблюдателя"""
def update(self, event, data): def update(self, event, data):
raise NotImplementedError raise NotImplementedError
class ConsoleDisplay(Observer): class ConsoleDisplay(Observer):
"""Консольная визуализация - наблюдатель""" """консольная визуализация - наблюдатель"""
def __init__(self): def __init__(self):
self._last_path = None self._last_path = None
@ -372,7 +371,7 @@ class ConsoleDisplay(Observer):
print("S - старт E - выход # - стена . - проход") print("S - старт E - выход # - стена . - проход")
def _draw_maze_with_player(self, game_state): def _draw_maze_with_player(self, game_state):
"""Отрисовка лабиринта с игроком""" """отрисовка лабиринта с игроком"""
maze = game_state['maze'] maze = game_state['maze']
player = game_state['player'] player = game_state['player']
@ -411,7 +410,6 @@ class ConsoleDisplay(Observer):
print(f"\n Путь найден! Длина: {len(path)} клеток") print(f"\n Путь найден! Длина: {len(path)} клеток")
# ==================== ЭТАП 5: КОМАНДА (COMMAND) ====================
class Command: class Command:
"""Интерфейс команды""" """Интерфейс команды"""
@ -505,7 +503,6 @@ class CommandInvoker:
return len(self._history) return len(self._history)
# ==================== ЭТАП 5: ИГРОК ====================
class Player: class Player:
"""Игрок, перемещающийся по лабиринту""" """Игрок, перемещающийся по лабиринту"""
@ -546,7 +543,7 @@ class GameController:
self.view.update("maze_loaded", self.maze) self.view.update("maze_loaded", self.maze)
print("УПРАВЛЕНИЕ:") print("УПРАВЛЕНИЕ:")
print(" H/J/K/L или ←/↓/↑/→ - движение") print(" H/J/K/Ll - движение")
print(" U - отменить ход") print(" U - отменить ход")
print(" R - повторить ход") print(" R - повторить ход")
print(" B - BFS поиск пути") print(" B - BFS поиск пути")
@ -562,7 +559,7 @@ class GameController:
cmd = input("\nКоманда > ").lower() cmd = input("\nКоманда > ").lower()
if cmd == 'q': if cmd == 'q':
print("До свидания!") print("До встречи!")
break break
elif cmd in ['h', 'j', 'k', 'l']: elif cmd in ['h', 'j', 'k', 'l']:
@ -665,4 +662,596 @@ class GameController:
self.view.update("player_moved", { self.view.update("player_moved", {
'maze': self.maze, 'maze': self.maze,
'player': self.player 'player': self.player
}) })
class MazeGenerator:
"""генератор тестовых лабиринтов различной сложности"""
@staticmethod
def create_empty_maze(width, height):
"""пустой лабиринт без стен"""
maze = Maze(width, height)
for y in range(height):
for x in range(width):
maze.get_cell(x, y).is_wall = False
maze.set_start(0, 0)
maze.set_exit(width - 1, height - 1)
return maze
@staticmethod
def create_simple_maze(width, height):
"""простой лабиринт с прямым путём"""
maze = Maze(width, height)
# Заполняем стенами
for y in range(height):
for x in range(width):
maze.get_cell(x, y).is_wall = True
# Создаём прямой путь
for i in range(min(width, height)):
maze.get_cell(i, i).is_wall = False
if i + 1 < width:
maze.get_cell(i + 1, i).is_wall = False
if i + 1 < height:
maze.get_cell(i, i + 1).is_wall = False
maze.set_start(0, 0)
maze.set_exit(width - 1, height - 1)
return maze
@staticmethod
def generate_dfs_maze(width, height):
"""генерация запутанного лабиринта алгоритмом DFS"""
maze = Maze(width, height)
# заполняем стенами
for y in range(height):
for x in range(width):
maze.get_cell(x, y).is_wall = True
start_x, start_y = 1, 1
maze.get_cell(start_x, start_y).is_wall = False
stack = [(start_x, start_y)]
visited = {(start_x, start_y)}
directions = [(0, -2), (0, 2), (-2, 0), (2, 0)]
while stack:
x, y = stack[-1]
neighbors = []
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 < nx < width - 1 and 0 < ny < height - 1 and (nx, ny) not in visited:
neighbors.append((nx, ny, dx, dy))
if neighbors:
import random
nx, ny, dx, dy = random.choice(neighbors)
maze.get_cell(x + dx // 2, y + dy // 2).is_wall = False
maze.get_cell(nx, ny).is_wall = False
visited.add((nx, ny))
stack.append((nx, ny))
else:
stack.pop()
maze.set_start(start_x, start_y)
# ищем дальнюю точку для выхода
farthest = (start_x, start_y)
max_dist = 0
for y in range(height):
for x in range(width):
cell = maze.get_cell(x, y)
if not cell.is_wall:
dist = abs(x - start_x) + abs(y - start_y)
if dist > max_dist:
max_dist = dist
farthest = (x, y)
maze.set_exit(farthest[0], farthest[1])
return maze
@staticmethod
def create_no_exit_maze(width, height):
"""лабиринт без выхода"""
maze = MazeGenerator.generate_dfs_maze(width, height)
if maze.exit:
maze.exit.is_wall = True
maze.exit.is_exit = False
maze.exit = None
return maze
@staticmethod
def save_to_file(maze, filename):
"""сохранение лабиринта в файл"""
with open(filename, 'w', encoding='utf-8') as f:
for y in range(maze.height):
line = ""
for x in range(maze.width):
cell = maze.get_cell(x, y)
if cell.is_start:
line += 'S'
elif cell.is_exit:
line += 'E'
elif cell.is_wall:
line += '#'
else:
line += '.'
f.write(line + '\n')
class ExperimentRunner:
"""запуск экспериментов и сбор статистики"""
def __init__(self, runs_per_experiment=5):
self.runs_per_experiment = runs_per_experiment
self.results = []
def run_experiment(self, maze, strategy, maze_name):
"""запуск одного эксперимента"""
times = []
visited = []
path_lengths = []
for _ in range(self.runs_per_experiment):
start_time = time.perf_counter()
path, visited_count = strategy.findPath(maze, maze.start, maze.exit)
end_time = time.perf_counter()
times.append((end_time - start_time) * 1000)
visited.append(visited_count)
path_lengths.append(len(path) if path else 0)
return {
'maze_name': maze_name,
'strategy': strategy.get_name(),
'avg_time_ms': sum(times) / len(times),
'min_time_ms': min(times),
'max_time_ms': max(times),
'avg_visited_cells': sum(visited) / len(visited),
'min_visited_cells': min(visited),
'max_visited_cells': max(visited),
'avg_path_length': sum(path_lengths) / len(path_lengths),
'min_path_length': min(path_lengths),
'max_path_length': max(path_lengths),
'path_found': any(pl > 0 for pl in path_lengths),
'runs': self.runs_per_experiment
}
def run_all_experiments(self):
"""запуск всех экспериментов на всех лабиринтах"""
print("ГЕНЕРАЦИЯ ТЕСТОВЫХ ЛАБИРИНТОВ")
# Создаём тестовые лабиринты
test_mazes = {
'tiny_simple (10x10)': MazeGenerator.create_simple_maze(10, 10),
'small_empty (20x20)': MazeGenerator.create_empty_maze(20, 20),
'medium_dfs (30x30)': MazeGenerator.generate_dfs_maze(30, 30),
'medium_complex (40x40)': MazeGenerator.generate_dfs_maze(40, 40),
'large_dfs (50x50)': MazeGenerator.generate_dfs_maze(50, 50),
'very_large_dfs (100x100)': MazeGenerator.generate_dfs_maze(100, 100),
'no_exit (20x20)': MazeGenerator.create_no_exit_maze(20, 20)
}
# Сохраняем лабиринты в файлы
for name, maze in test_mazes.items():
filename = f"test_{name.replace(' ', '_').replace('(', '').replace(')', '')}.txt"
MazeGenerator.save_to_file(maze, filename)
print(f" Создан: {filename}")
# Стратегии для тестирования
strategies = [
BFSStrategy(),
DFSStrategy(),
AStarStrategy(),
DijkstraStrategy()
]
print("ЗАПУСК ЭКСПЕРИМЕНТОВ")
for maze_name, maze in test_mazes.items():
print(f"\n Лабиринт: {maze_name}")
print(f" Размер: {maze.width}x{maze.height}")
print(f" Старт: ({maze.start.x}, {maze.start.y})")
print(f" Выход: ({maze.exit.x}, {maze.exit.y})")
for strategy in strategies:
print(f"{strategy.get_name()}...", end=" ", flush=True)
result = self.run_experiment(maze, strategy, maze_name)
self.results.append(result)
status = "" if result['path_found'] else ""
print(f"{status} {result['avg_time_ms']:.2f}мс, "
f"{result['avg_visited_cells']:.0f} клеток, "
f"{result['avg_path_length']:.1f} шагов")
def save_to_csv(self, filename="experiment_results.csv"):
import csv
with open(filename, 'w', newline='', encoding='utf-8') as csvfile:
fieldnames = [
'maze_name', 'strategy', 'runs',
'avg_time_ms', 'min_time_ms', 'max_time_ms',
'avg_visited_cells', 'min_visited_cells', 'max_visited_cells',
'avg_path_length', 'min_path_length', 'max_path_length',
'path_found'
]
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for result in self.results:
writer.writerow(result)
print(f"\n Результаты сохранены в {filename}")
def print_summary(self):
print("СВОДНАЯ СТАТИСТИКА ЭКСПЕРИМЕНТОВ")
# Группировка по лабиринтам
grouped = {}
for result in self.results:
name = result['maze_name']
if name not in grouped:
grouped[name] = []
grouped[name].append(result)
for maze_name, results in grouped.items():
print(f"\n {maze_name}")
print(f"{'Стратегия':<25} {'Время(мс)':<12} {'Посещено':<12} {'Длина пути':<12} {'Найден':<8}")
for result in sorted(results, key=lambda x: x['avg_time_ms']):
status = "" if result['path_found'] else ""
print(f"{result['strategy']:<25} "
f"{result['avg_time_ms']:<12.2f} "
f"{result['avg_visited_cells']:<12.0f} "
f"{result['avg_path_length']:<12.1f} "
f"{status:<8}")
print("ОБЩАЯ СТАТИСТИКА ПО СТРАТЕГИЯМ")
strategy_stats = {}
for result in self.results:
name = result['strategy']
if name not in strategy_stats:
strategy_stats[name] = {'times': [], 'visited': [], 'lengths': []}
strategy_stats[name]['times'].append(result['avg_time_ms'])
strategy_stats[name]['visited'].append(result['avg_visited_cells'])
strategy_stats[name]['lengths'].append(result['avg_path_length'])
print(f"\n{'Стратегия':<25} {'Ср.время(мс)':<15} {'Ср.посещено':<15} {'Ср.длина':<12}")
for name, stats in strategy_stats.items():
avg_time = sum(stats['times']) / len(stats['times'])
avg_visited = sum(stats['visited']) / len(stats['visited'])
avg_length = sum(stats['lengths']) / len(stats['lengths'])
print(f"{name:<25} {avg_time:<15.2f} {avg_visited:<15.0f} {avg_length:<12.1f}")
def print_conclusions(self):
print("ВЫВОДЫ И РЕКОМЕНДАЦИИ")
# Находим лучшие стратегии
bfs_results = [r for r in self.results if r['strategy'] == "BFS (Поиск в ширину)" and r['path_found']]
dfs_results = [r for r in self.results if r['strategy'] == "DFS (Поиск в глубину)" and r['path_found']]
astar_results = [r for r in self.results if r['strategy'] == "A* (A Star)" and r['path_found']]
dijkstra_results = [r for r in self.results if r['strategy'] == "Дейкстра (Dijkstra)" and r['path_found']]
conclusions = []
if bfs_results:
avg_bfs_time = sum(r['avg_time_ms'] for r in bfs_results) / len(bfs_results)
avg_bfs_length = sum(r['avg_path_length'] for r in bfs_results) / len(bfs_results)
conclusions.append(f" • BFS: среднее время {avg_bfs_time:.2f}мс, длина пути {avg_bfs_length:.1f}")
if dfs_results:
avg_dfs_time = sum(r['avg_time_ms'] for r in dfs_results) / len(dfs_results)
avg_dfs_length = sum(r['avg_path_length'] for r in dfs_results) / len(dfs_results)
conclusions.append(f" • DFS: среднее время {avg_dfs_time:.2f}мс, длина пути {avg_dfs_length:.1f}")
if astar_results:
avg_astar_time = sum(r['avg_time_ms'] for r in astar_results) / len(astar_results)
avg_astar_length = sum(r['avg_path_length'] for r in astar_results) / len(astar_results)
conclusions.append(f" • A*: среднее время {avg_astar_time:.2f}мс, длина пути {avg_astar_length:.1f}")
if dijkstra_results:
avg_dijkstra_time = sum(r['avg_time_ms'] for r in dijkstra_results) / len(dijkstra_results)
avg_dijkstra_length = sum(r['avg_path_length'] for r in dijkstra_results) / len(dijkstra_results)
conclusions.append(f" • Дейкстра: среднее время {avg_dijkstra_time:.2f}мс, длина пути {avg_dijkstra_length:.1f}")
print("\n РЕЗУЛЬТАТЫ АНАЛИЗА:\n")
for c in conclusions:
print(c)
print("\n РЕКОМЕНДАЦИИ:\n")
print(" 1. Для маленьких лабиринтов - любой алгоритм работает быстро")
print(" 2. Для больших лабиринтов - A* даёт лучший компромисс скорость/качество")
print(" 3. BFS гарантирует кратчайший путь, но медленнее на больших картах")
print(" 4. DFS самый быстрый, но путь может быть неоптимальным")
print(" 5. Если путь не существует - BFS и A* эффективнее обнаруживают это")
def plot_experiment_results(csv_filename="experiment_results.csv"):
"""построение графиков по результатам экспериментов"""
try:
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
except ImportError:
print("Установите: pip install matplotlib pandas")
return
if not os.path.exists(csv_filename):
print("Файл результатов не найден")
return
df = pd.read_csv(csv_filename)
fig = plt.figure(figsize=(16, 12))
fig.suptitle('Сравнение алгоритмов поиска в лабиринте', fontsize=16)
# Время выполнения
ax1 = fig.add_subplot(2, 2, 1)
for strategy in df['strategy'].unique():
data = df[df['strategy'] == strategy]
ax1.bar(data['maze_name'], data['avg_time_ms'], alpha=0.7, label=strategy)
ax1.set_xlabel('Лабиринт')
ax1.set_ylabel('Время (мс)')
ax1.set_title('Время выполнения алгоритмов')
ax1.legend(loc='upper left', fontsize=8)
ax1.tick_params(axis='x', rotation=45, labelsize=8)
# Посещённые клетки
ax2 = fig.add_subplot(2, 2, 2)
for strategy in df['strategy'].unique():
data = df[df['strategy'] == strategy]
ax2.bar(data['maze_name'], data['avg_visited_cells'], alpha=0.7, label=strategy)
ax2.set_xlabel('Лабиринт')
ax2.set_ylabel('Посещено клеток')
ax2.set_title('Эффективность поиска')
ax2.legend(loc='upper left', fontsize=8)
ax2.tick_params(axis='x', rotation=45, labelsize=8)
# Длина пути
ax3 = fig.add_subplot(2, 2, 3)
for strategy in df['strategy'].unique():
data = df[df['strategy'] == strategy]
ax3.bar(data['maze_name'], data['avg_path_length'], alpha=0.7, label=strategy)
ax3.set_xlabel('Лабиринт')
ax3.set_ylabel('Длина пути')
ax3.set_title('Оптимальность пути')
ax3.legend(loc='upper left', fontsize=8)
ax3.tick_params(axis='x', rotation=45, labelsize=8)
# Радар-диаграмма
ax4 = fig.add_subplot(2, 2, 4, projection='polar')
strategies = df['strategy'].unique()
metrics = ['avg_time_ms', 'avg_visited_cells', 'avg_path_length']
metric_labels = ['Время', 'Посещено', 'Длина пути']
for strategy in strategies:
data = df[df['strategy'] == strategy]
values = []
for metric in metrics:
val = data[metric].mean()
max_val = df[metric].max()
normalized = 1 - (val / max_val) if max_val > 0 else 0.5
values.append(normalized)
values.append(values[0])
angles = np.linspace(0, 2 * np.pi, len(metrics), endpoint=False).tolist()
angles += angles[:1]
ax4.plot(angles, values, 'o-', linewidth=2, label=strategy)
ax4.fill(angles, values, alpha=0.25)
ax4.set_xticks(angles[:-1])
ax4.set_xticklabels(metric_labels)
ax4.set_ylim(0, 1)
ax4.set_title('Радар-диаграмма (дальше от центра = лучше)')
ax4.legend(loc='upper right', fontsize=7, bbox_to_anchor=(1.3, 1.0))
plt.tight_layout()
plt.savefig('algorithm_comparison.png', dpi=150, bbox_inches='tight')
plt.show()
print("График сохранён: algorithm_comparison.png")
def plot_by_maze(csv_filename="experiment_results.csv"):
"""построение графиков для каждого лабиринта"""
try:
import matplotlib.pyplot as plt
import pandas as pd
except ImportError:
return
if not os.path.exists(csv_filename):
return
df = pd.read_csv(csv_filename)
mazes = df['maze_name'].unique()
fig, axes = plt.subplots(len(mazes), 3, figsize=(15, 4 * len(mazes)))
fig.suptitle('Детальный анализ по каждому лабиринту', fontsize=14)
if len(mazes) == 1:
axes = axes.reshape(1, -1)
for i, maze_name in enumerate(mazes):
maze_data = df[df['maze_name'] == maze_name]
axes[i, 0].bar(maze_data['strategy'], maze_data['avg_time_ms'])
axes[i, 0].set_title(f'{maze_name}\nВремя (мс)')
axes[i, 0].tick_params(axis='x', rotation=45, labelsize=8)
axes[i, 1].bar(maze_data['strategy'], maze_data['avg_visited_cells'])
axes[i, 1].set_title('Посещено клеток')
axes[i, 1].tick_params(axis='x', rotation=45, labelsize=8)
bars = axes[i, 2].bar(maze_data['strategy'], maze_data['avg_path_length'])
axes[i, 2].set_title('Длина пути')
axes[i, 2].tick_params(axis='x', rotation=45, labelsize=8)
for j, (bar, found) in enumerate(zip(bars, maze_data['path_found'])):
if not found:
bar.set_color('red')
plt.tight_layout()
plt.savefig('maze_detailed_analysis.png', dpi=150, bbox_inches='tight')
plt.show()
print("Детальные графики сохранены: maze_detailed_analysis.png")
def print_analysis(csv_filename="experiment_results.csv"):
"""вывод анализа эффективности алгоритмов"""
try:
import pandas as pd
except ImportError:
return
if not os.path.exists(csv_filename):
return
df = pd.read_csv(csv_filename)
print("АНАЛИЗ ЭФФЕКТИВНОСТИ АЛГОРИТМОВ")
print("\nОбщая статистика по алгоритмам:")
print(f"{'Алгоритм':<25} {'Время(мс)':<12} {'Посещено':<12} {'Длина пути':<12} {'Найден %':<10}")
for strategy in df['strategy'].unique():
data = df[df['strategy'] == strategy]
avg_time = data['avg_time_ms'].mean()
avg_visited = data['avg_visited_cells'].mean()
avg_length = data['avg_path_length'].mean()
found_rate = data['path_found'].mean() * 100
print(f"{strategy:<25} {avg_time:<12.2f} {avg_visited:<12.0f} {avg_length:<12.1f} {found_rate:<10.1f}%")
print("АНАЛИЗ ПО ТИПАМ ЛАБИРИНТОВ")
for maze_name in df['maze_name'].unique():
maze_data = df[df['maze_name'] == maze_name]
print(f"\nЛабиринт: {maze_name}")
if 'tiny' in maze_name:
maze_type = "Маленький (10x10)"
elif 'small' in maze_name:
maze_type = "Небольшой (20x20)"
elif 'medium' in maze_name:
maze_type = "Средний (30x30-40x40)"
elif 'large' in maze_name:
maze_type = "Большой (50x50)"
elif 'no_exit' in maze_name:
maze_type = "Без выхода"
else:
maze_type = "Обычный"
print(f" Тип: {maze_type}")
best_time = maze_data.loc[maze_data['avg_time_ms'].idxmin()]
print(f" Самый быстрый: {best_time['strategy']} ({best_time['avg_time_ms']:.2f} мс)")
best_visited = maze_data.loc[maze_data['avg_visited_cells'].idxmin()]
print(f" Самый экономный: {best_visited['strategy']} ({best_visited['avg_visited_cells']:.0f} клеток)")
path_data = maze_data[maze_data['path_found'] == True]
if not path_data.empty:
best_path = path_data.loc[path_data['avg_path_length'].idxmin()]
print(f" Самый короткий путь: {best_path['strategy']} ({best_path['avg_path_length']:.1f} шагов)")
else:
print(" Путь не найден ни одним алгоритмом")
print("ВЫВОДЫ И РЕКОМЕНДАЦИИ")
print("""
1. BFS (Поиск в ширину):
- Гарантирует кратчайший путь
- Медленнее на больших лабиринтах
- Рекомендуется для маленьких лабиринтов (до 20x20)
2. DFS (Поиск в глубину):
- Самый быстрый по времени
- Не гарантирует кратчайший путь
- Рекомендуется когда важна скорость, а не оптимальность
3. A* (A Star):
- Лучший компромисс между скоростью и оптимальностью
- Рекомендуется для больших и сложных лабиринтов (40x40+)
4. Дейкстра:
- Гарантирует оптимальный путь
- Работает с взвешенными графами
- Медленнее A* на больших лабиринтах
ИТОГОВЫЕ РЕКОМЕНДАЦИИ:
- Маленькие лабиринты (до 20x20): BFS
- Средние лабиринты (20x20 - 40x40): A*
- Большие лабиринты (40x40+): A* или DFS
- Когда нужен кратчайший путь: BFS или A*
- Когда важна только скорость: DFS
- Лабиринты без выхода: BFS или A* (быстрее обнаруживают)
""")
def run_full_analysis():
"""Запуск полного анализа с построением графиков"""
if not os.path.exists("experiment_results.csv"):
print("Результаты не найдены. Запускаем эксперименты...")
runner = ExperimentRunner(runs_per_experiment=5)
runner.run_all_experiments()
runner.save_to_csv()
runner.print_summary()
runner.print_conclusions()
plot_experiment_results()
plot_by_maze()
print_analysis()
def run_experiments():
"""Запуск экспериментов с построением графиков и анализом"""
runner = ExperimentRunner(runs_per_experiment=5)
runner.run_all_experiments()
runner.save_to_csv()
runner.print_summary()
runner.print_conclusions()
run_full_analysis()
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
run_experiments()
else:
# Интерактивный режим
sample = """###############
#S #
# ### ####### #
# # # #
### # ### # # #
# # # # #
# ### # ### ###
# # E #
###############"""
with open("maze.txt", "w") as f:
f.write(sample)
builder = TextFileMazeBuilder()
maze = builder.buildFromFile("maze.txt")
print(f"Лабиринт загружен: {maze.width}x{maze.height}")
print(f"Старт: ({maze.start.x}, {maze.start.y})")
print(f"Выход: ({maze.exit.x}, {maze.exit.y})")
game = GameController(maze)
game.run()