650 lines
29 KiB
C#
650 lines
29 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.IO;
|
||
using System.Text.Json;
|
||
|
||
namespace PaperRacing.AStar
|
||
{
|
||
// Представляет точку на поле
|
||
public record Point(int X, int Y)
|
||
{
|
||
public static Point operator +(Point a, Point b) => new(a.X + b.X, a.Y + b.Y);
|
||
public static Point operator -(Point a, Point b) => new(a.X - b.X, a.Y - b.Y);
|
||
|
||
public double DistanceTo(Point other)
|
||
{
|
||
return Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2));
|
||
}
|
||
|
||
public int ManhattanDistance(Point other)
|
||
{
|
||
return Math.Abs(X - other.X) + Math.Abs(Y - other.Y);
|
||
}
|
||
}
|
||
|
||
// Состояние игры с поддержкой A*
|
||
public class GameState
|
||
{
|
||
public Point Position { get; init; }
|
||
public Point Velocity { get; init; }
|
||
public HashSet<int> VisitedCheckpoints { get; init; }
|
||
public List<Point> Path { get; init; }
|
||
public int GCost { get; init; } // Фактическая стоимость (количество шагов)
|
||
public double HCost { get; set; } // Эвристическая стоимость
|
||
public double FCost => GCost + HCost; // Полная стоимость
|
||
|
||
public GameState(Point position, Point velocity, HashSet<int> visitedCheckpoints, List<Point> path, int gCost)
|
||
{
|
||
Position = position;
|
||
Velocity = velocity;
|
||
VisitedCheckpoints = visitedCheckpoints;
|
||
Path = path;
|
||
GCost = gCost;
|
||
}
|
||
|
||
public string GetKey()
|
||
{
|
||
var checkpointsMask = string.Join(",", VisitedCheckpoints.OrderBy(x => x));
|
||
return $"{Position.X},{Position.Y}|{Velocity.X},{Velocity.Y}|{checkpointsMask}";
|
||
}
|
||
}
|
||
|
||
// Компаратор для очереди с приоритетом
|
||
public class GameStateComparer : IComparer<GameState>
|
||
{
|
||
public int Compare(GameState? x, GameState? y)
|
||
{
|
||
if (x == null || y == null) return 0;
|
||
int fCostCompare = x.FCost.CompareTo(y.FCost);
|
||
if (fCostCompare != 0) return fCostCompare;
|
||
return y.GCost.CompareTo(x.GCost); // При равных FCost предпочитаем больший GCost
|
||
}
|
||
}
|
||
|
||
// Игровое поле с алгоритмом A*
|
||
public class RaceTrack
|
||
{
|
||
private readonly int _width;
|
||
private readonly int _height;
|
||
private readonly HashSet<Point> _obstacles;
|
||
private readonly Dictionary<int, Point> _checkpoints;
|
||
private readonly Point _start;
|
||
private readonly Dictionary<Point, int> _cellTypes; // Тип клетки для каждой точки
|
||
|
||
public RaceTrack(int width, int height, Point start, Dictionary<int, Point> checkpoints, HashSet<Point> obstacles, Dictionary<Point, int> cellTypes)
|
||
{
|
||
_width = width;
|
||
_height = height;
|
||
_start = start;
|
||
_checkpoints = checkpoints;
|
||
_obstacles = obstacles;
|
||
_cellTypes = cellTypes;
|
||
}
|
||
|
||
private bool IsInBounds(Point p) => p.X >= 0 && p.X < _width && p.Y >= 0 && p.Y < _height;
|
||
|
||
// Получить диапазон допустимых ускорений в зависимости от типа клетки
|
||
private (int minAccel, int maxAccel) GetAccelerationRange(Point position)
|
||
{
|
||
if (_cellTypes.TryGetValue(position, out int cellType))
|
||
{
|
||
return cellType switch
|
||
{
|
||
2 => (-1, 1), // Снег: ускорение от -1 до +1
|
||
3 => (0, 0), // Лёд: ускорение нельзя менять
|
||
_ => (-2, 2) // Обычная дорога, чекпоинт, старт: ускорение от -2 до +2
|
||
};
|
||
}
|
||
return (-2, 2); // По умолчанию как обычная дорога
|
||
}
|
||
|
||
// Эвристическая функция: оценка оставшегося расстояния
|
||
private double CalculateHeuristic(Point position, Point velocity, HashSet<int> visitedCheckpoints)
|
||
{
|
||
// Находим непосещенные чекпоинты
|
||
var unvisited = _checkpoints.Where(kv => !visitedCheckpoints.Contains(kv.Key)).ToList();
|
||
|
||
if (unvisited.Count == 0)
|
||
return 0;
|
||
|
||
// Простая эвристика: расстояние до ближайшего + сумма расстояний между оставшимися
|
||
if (unvisited.Count == 1)
|
||
{
|
||
// Console.WriteLine($"Last checkpoint!");
|
||
double distToCheckpoint = position.DistanceTo(unvisited[0].Value);
|
||
double currentSpeed = Math.Sqrt(velocity.X * velocity.X + velocity.Y * velocity.Y);
|
||
double maxAcceleration = Math.Sqrt(2); // Максимальное ускорение по диагонали
|
||
|
||
// Оценка: сколько шагов нужно для достижения с учетом текущей скорости
|
||
return EstimateStepsToReach(distToCheckpoint, currentSpeed, maxAcceleration, true);
|
||
}
|
||
|
||
// Для нескольких чекпоинтов используем жадную эвристику TSP
|
||
double totalCost = 0;
|
||
var current = position;
|
||
var remaining = new List<Point>(unvisited.Select(kv => kv.Value));
|
||
double speed = Math.Sqrt(velocity.X * velocity.X + velocity.Y * velocity.Y);
|
||
|
||
while (remaining.Count > 0)
|
||
{
|
||
// Находим ближайший непосещенный чекпоинт
|
||
var nearest = remaining.OrderBy(p => current.DistanceTo(p)).First();
|
||
double dist = current.DistanceTo(nearest);
|
||
totalCost += EstimateStepsToReach(dist, speed, Math.Sqrt(2));
|
||
|
||
current = nearest;
|
||
remaining.Remove(nearest);
|
||
speed = 4.0; // Предполагаем более высокую среднюю скорость для следующих сегментов
|
||
}
|
||
|
||
// Агрессивная эвристика для 40 чекпоинтов - множитель 0.7
|
||
return totalCost;
|
||
}
|
||
|
||
// Оценка количества шагов для достижения расстояния
|
||
private double EstimateStepsToReach(double distance, double currentSpeed, double maxAcceleration, bool isLastCheckpoint = false)
|
||
{
|
||
if (distance <= 0)
|
||
return 0;
|
||
|
||
// Упрощенная физическая модель: можно ускоряться на maxAcceleration каждый шаг
|
||
// v = v0 + a*t, s = v0*t + 0.5*a*t^2
|
||
// Решаем квадратное уравнение: 0.5*a*t^2 + v0*t - s = 0
|
||
|
||
double a = maxAcceleration;
|
||
double v0 = currentSpeed;
|
||
double s = distance;
|
||
|
||
// t = (-v0 + sqrt(v0^2 + 2*a*s)) / a
|
||
double discriminant = v0 * v0 + 2 * a * s;
|
||
if (discriminant < 0)
|
||
return distance / (v0 + 0.1); // Fallback
|
||
|
||
double steps = (-v0 + Math.Sqrt(discriminant)) / a;
|
||
|
||
if (isLastCheckpoint)
|
||
{
|
||
return steps;
|
||
}
|
||
|
||
// Учитываем, что нужно еще замедлиться
|
||
double brakingSteps = currentSpeed > 0 ? currentSpeed / maxAcceleration : 0;
|
||
|
||
return Math.Max(1, steps + brakingSteps * 0.5);
|
||
}
|
||
|
||
// A* алгоритм
|
||
public List<Point>? FindSolution()
|
||
{
|
||
var openSet = new SortedSet<GameState>(new GameStateComparer());
|
||
var openSetLookup = new Dictionary<string, GameState>();
|
||
var closedSet = new HashSet<string>();
|
||
|
||
var initialState = new GameState(_start, new Point(0, 0), new HashSet<int>(), new List<Point> { _start }, 0);
|
||
initialState.HCost = CalculateHeuristic(_start, new Point(0, 0), new HashSet<int>());
|
||
|
||
openSet.Add(initialState);
|
||
openSetLookup[initialState.GetKey()] = initialState;
|
||
|
||
int iterations = 0;
|
||
const int maxIterations = 5000000; // Увеличено для 40 чекпоинтов
|
||
int maxOpenSetSize = 0;
|
||
|
||
Console.WriteLine($"Начальная эвристика: {initialState.HCost:F2}");
|
||
|
||
while (openSet.Count > 0 && iterations < maxIterations)
|
||
{
|
||
iterations++;
|
||
maxOpenSetSize = Math.Max(maxOpenSetSize, openSet.Count);
|
||
|
||
if (iterations % 10000 == 0)
|
||
{
|
||
var current = openSet.Min!;
|
||
Console.WriteLine($"Итерация {iterations}: OpenSet={openSet.Count}, FCost={current.FCost:F2}, GCost={current.GCost}, Посещено={current.VisitedCheckpoints.Count}/{_checkpoints.Count}");
|
||
}
|
||
|
||
var currentState = openSet.Min!;
|
||
openSet.Remove(currentState);
|
||
openSetLookup.Remove(currentState.GetKey());
|
||
|
||
// Проверяем, собрали ли все чекпоинты
|
||
if (currentState.VisitedCheckpoints.Count == _checkpoints.Count)
|
||
{
|
||
Console.WriteLine($"\n=== Решение найдено ===");
|
||
Console.WriteLine($"Итераций: {iterations}");
|
||
Console.WriteLine($"Максимальный размер открытого множества: {maxOpenSetSize}");
|
||
Console.WriteLine($"Количество ходов: {currentState.GCost}");
|
||
Console.WriteLine($"Финальная стоимость: {currentState.FCost:F2}");
|
||
return currentState.Path;
|
||
}
|
||
|
||
closedSet.Add(currentState.GetKey());
|
||
|
||
// Генерируем все возможные ускорения в зависимости от типа клетки
|
||
var (minAccel, maxAccel) = GetAccelerationRange(currentState.Position);
|
||
|
||
for (int dx = minAccel; dx <= maxAccel; dx++)
|
||
{
|
||
for (int dy = minAccel; dy <= maxAccel; dy++)
|
||
{
|
||
var acceleration = new Point(dx, dy);
|
||
var newVelocity = currentState.Velocity + acceleration;
|
||
var newPosition = currentState.Position + newVelocity;
|
||
|
||
if (!IsInBounds(newPosition))
|
||
continue;
|
||
|
||
// Можно проезжать через препятствия, но нельзя на них останавливаться
|
||
if (_obstacles.Contains(newPosition))
|
||
continue;
|
||
|
||
// Проверяем чекпоинты
|
||
var newCheckpoints = new HashSet<int>(currentState.VisitedCheckpoints);
|
||
foreach (var (id, checkpoint) in _checkpoints)
|
||
{
|
||
if (!newCheckpoints.Contains(id) && newPosition.Equals(checkpoint))
|
||
{
|
||
newCheckpoints.Add(id);
|
||
}
|
||
}
|
||
|
||
var newPath = new List<Point>(currentState.Path) { newPosition };
|
||
var newState = new GameState(newPosition, newVelocity, newCheckpoints, newPath, currentState.GCost + 1);
|
||
newState.HCost = CalculateHeuristic(newPosition, newVelocity, newCheckpoints);
|
||
|
||
var key = newState.GetKey();
|
||
|
||
if (closedSet.Contains(key))
|
||
continue;
|
||
|
||
// Проверяем, есть ли уже такое состояние в открытом множестве
|
||
if (openSetLookup.TryGetValue(key, out var existingState))
|
||
{
|
||
// Если новый путь лучше, обновляем
|
||
if (newState.GCost < existingState.GCost)
|
||
{
|
||
openSet.Remove(existingState);
|
||
openSet.Add(newState);
|
||
openSetLookup[key] = newState;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
openSet.Add(newState);
|
||
openSetLookup[key] = newState;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Console.WriteLine($"\nРешение не найдено после {iterations} итераций");
|
||
Console.WriteLine($"Максимальный размер открытого множества: {maxOpenSetSize}");
|
||
return null;
|
||
}
|
||
|
||
public void Visualize(List<Point>? path = null)
|
||
{
|
||
var pathSet = path != null ? new HashSet<Point>(path) : new HashSet<Point>();
|
||
|
||
for (int y = _height - 1; y >= 0; y--)
|
||
{
|
||
Console.Write($"{y:00}|");
|
||
for (int x = 0; x < _width; x++)
|
||
{
|
||
var point = new Point(x, y);
|
||
|
||
if (point.Equals(_start))
|
||
Console.Write("S ");
|
||
else if (_checkpoints.Values.Contains(point))
|
||
{
|
||
int checkpointId = _checkpoints.First(kv => kv.Value.Equals(point)).Key;
|
||
// Для чисел > 9 показываем символ
|
||
if (checkpointId < 10)
|
||
Console.Write($"{checkpointId} ");
|
||
else
|
||
Console.Write("● "); // Точка для всех чекпоинтов >= 10
|
||
}
|
||
else if (_obstacles.Contains(point))
|
||
Console.Write("# ");
|
||
else if (pathSet.Contains(point))
|
||
Console.Write(". ");
|
||
else if (_cellTypes.TryGetValue(point, out int cellType))
|
||
{
|
||
// Показываем тип поверхности
|
||
switch (cellType)
|
||
{
|
||
case 2: // Снег
|
||
Console.Write("~ ");
|
||
break;
|
||
case 3: // Лёд
|
||
Console.Write("= ");
|
||
break;
|
||
default:
|
||
Console.Write(" ");
|
||
break;
|
||
}
|
||
}
|
||
else
|
||
Console.Write(" ");
|
||
}
|
||
Console.WriteLine();
|
||
}
|
||
|
||
// Ось X
|
||
Console.Write(" ");
|
||
for (int x = 0; x < _width; x++)
|
||
{
|
||
if (x % 5 == 0)
|
||
Console.Write($"{x / 10}");
|
||
else
|
||
Console.Write(" ");
|
||
}
|
||
Console.WriteLine();
|
||
Console.Write(" ");
|
||
for (int x = 0; x < _width; x++)
|
||
{
|
||
Console.Write($"{x % 10}");
|
||
}
|
||
Console.WriteLine();
|
||
}
|
||
|
||
public void ShowPath(List<Point> path)
|
||
{
|
||
Console.WriteLine("\nДетальный путь решения:");
|
||
Point prevVelocity = new Point(0, 0);
|
||
|
||
for (int i = 0; i < path.Count; i++)
|
||
{
|
||
Point velocity = i > 0 ? path[i] - path[i - 1] : new Point(0, 0);
|
||
Point acceleration = velocity - prevVelocity;
|
||
|
||
string checkpoint = "";
|
||
foreach (var (id, pos) in _checkpoints)
|
||
{
|
||
if (pos.Equals(path[i]))
|
||
{
|
||
checkpoint = $" ✓ ЧЕКПОИНТ #{id}";
|
||
break;
|
||
}
|
||
}
|
||
|
||
Console.WriteLine($"Шаг {i,3}: Поз=({path[i].X,2},{path[i].Y,2}) Скор=({velocity.X,2},{velocity.Y,2}) Ускор=({acceleration.X,2},{acceleration.Y,2}){checkpoint}");
|
||
prevVelocity = velocity;
|
||
}
|
||
}
|
||
|
||
public void ExportSolutionToJson(List<Point> path, string filePath)
|
||
{
|
||
var accelerations = new List<int[]>();
|
||
Point prevVelocity = new Point(0, 0);
|
||
|
||
for (int i = 0; i < path.Count; i++)
|
||
{
|
||
Point velocity = i > 0 ? path[i] - path[i - 1] : new Point(0, 0);
|
||
Point acceleration = velocity - prevVelocity;
|
||
|
||
accelerations.Add(new int[] { acceleration.X, -acceleration.Y });
|
||
prevVelocity = velocity;
|
||
}
|
||
|
||
var solution = new { solution = accelerations };
|
||
var options = new JsonSerializerOptions
|
||
{
|
||
WriteIndented = true,
|
||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||
};
|
||
|
||
string jsonContent = JsonSerializer.Serialize(solution, options);
|
||
File.WriteAllText(filePath, jsonContent);
|
||
|
||
Console.WriteLine($"\n✓ Решение экспортировано в файл: {filePath}");
|
||
Console.WriteLine($" Количество шагов: {accelerations.Count}");
|
||
}
|
||
}
|
||
|
||
// Классы для десериализации JSON
|
||
public class MapData
|
||
{
|
||
public int[][]? map { get; set; }
|
||
}
|
||
|
||
// Загрузчик карт из JSON
|
||
public class MapLoader
|
||
{
|
||
public static (int width, int height, Point start, Dictionary<int, Point> checkpoints, HashSet<Point> obstacles, Dictionary<Point, int> cellTypes)
|
||
LoadFromJson(string filePath)
|
||
{
|
||
string jsonContent = File.ReadAllText(filePath);
|
||
var mapData = JsonSerializer.Deserialize<MapData>(jsonContent);
|
||
|
||
if (mapData?.map == null)
|
||
throw new Exception("Не удалось загрузить карту из файла");
|
||
|
||
int height = mapData.map.Length;
|
||
int width = mapData.map[0].Length;
|
||
|
||
var checkpoints = new Dictionary<int, Point>();
|
||
var obstacles = new HashSet<Point>();
|
||
var cellTypes = new Dictionary<Point, int>();
|
||
Point? start = null;
|
||
int checkpointId = 1;
|
||
int snowCount = 0;
|
||
int iceCount = 0;
|
||
|
||
// Проходим по карте (JSON карта идет сверху вниз, поэтому инвертируем Y)
|
||
for (int jsonY = 0; jsonY < height; jsonY++)
|
||
{
|
||
for (int x = 0; x < width; x++)
|
||
{
|
||
int cellType = mapData.map[jsonY][x];
|
||
// Инвертируем Y координату для правильного отображения
|
||
int y = height - 1 - jsonY;
|
||
var point = new Point(x, y);
|
||
|
||
// Сохраняем тип клетки
|
||
cellTypes[point] = cellType;
|
||
|
||
switch (cellType)
|
||
{
|
||
case 0: // Дорога
|
||
// Первая дорога становится стартом, если старт еще не задан и нет явного старта (тип 5)
|
||
if (start == null)
|
||
start = point;
|
||
break;
|
||
case 1: // Камень (препятствие)
|
||
obstacles.Add(point);
|
||
break;
|
||
case 2: // Снег
|
||
snowCount++;
|
||
break;
|
||
case 3: // Лёд
|
||
iceCount++;
|
||
break;
|
||
case 4: // Чекпоинт
|
||
checkpoints[checkpointId++] = point;
|
||
break;
|
||
case 5: // Старт (приоритетнее чем тип 0)
|
||
start = point;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (start == null)
|
||
throw new Exception("Не найдена стартовая позиция (ячейка типа 0 или 5)");
|
||
|
||
Console.WriteLine($"Загружена карта: {width}x{height}");
|
||
Console.WriteLine($"Старт: ({start.X}, {start.Y})");
|
||
Console.WriteLine($"Чекпоинтов: {checkpoints.Count}");
|
||
Console.WriteLine($"Препятствий: {obstacles.Count}");
|
||
Console.WriteLine($"Снег: {snowCount} клеток");
|
||
Console.WriteLine($"Лёд: {iceCount} клеток");
|
||
|
||
return (width, height, start, checkpoints, obstacles, cellTypes);
|
||
}
|
||
}
|
||
|
||
class Program
|
||
{
|
||
static void Main(string[] args)
|
||
{
|
||
Console.WriteLine("╔════════════════════════════════════════╗");
|
||
Console.WriteLine("║ Гонки на бумаге - Алгоритм A* ║");
|
||
Console.WriteLine("╚════════════════════════════════════════╝\n");
|
||
|
||
// Обработка аргументов командной строки
|
||
string? mapFilePath = null;
|
||
string? outputFilePath = null;
|
||
|
||
for (int i = 0; i < args.Length; i++)
|
||
{
|
||
if (args[i] == "--output" || args[i] == "-o")
|
||
{
|
||
if (i + 1 < args.Length)
|
||
{
|
||
outputFilePath = args[i + 1];
|
||
i++;
|
||
}
|
||
}
|
||
else if (mapFilePath == null && File.Exists(args[i]))
|
||
{
|
||
mapFilePath = args[i];
|
||
}
|
||
}
|
||
|
||
int width, height;
|
||
Point start;
|
||
Dictionary<int, Point> checkpoints;
|
||
HashSet<Point> obstacles;
|
||
Dictionary<Point, int> cellTypes;
|
||
|
||
// Проверяем, передан ли путь к файлу карты
|
||
if (mapFilePath != null)
|
||
{
|
||
Console.WriteLine($"Загрузка карты из файла: {mapFilePath}\n");
|
||
try
|
||
{
|
||
(width, height, start, checkpoints, obstacles, cellTypes) = MapLoader.LoadFromJson(mapFilePath);
|
||
Console.WriteLine();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"❌ Ошибка загрузки карты: {ex.Message}");
|
||
return;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Используем встроенную карту по умолчанию
|
||
if (args.Length > 0 && !args.Any(a => a == "--output" || a == "-o"))
|
||
{
|
||
Console.WriteLine($"⚠️ Файл карты не найден. Используется встроенная карта.\n");
|
||
}
|
||
else if (args.Length == 0)
|
||
{
|
||
Console.WriteLine("Используется встроенная карта.\n");
|
||
Console.WriteLine("Использование:");
|
||
Console.WriteLine(" racing-astar <map-file.json> [--output|-o <output-file.json>]\n");
|
||
}
|
||
else
|
||
{
|
||
Console.WriteLine("Используется встроенная карта.\n");
|
||
}
|
||
|
||
// Создаем поле 210x50 (длинная прямая для 40 чекпоинтов)
|
||
width = 210;
|
||
height = 50;
|
||
|
||
// Стартовая позиция
|
||
start = new Point(2, 2);
|
||
|
||
// Чекпоинты - 40 штук вдоль ОДНОЙ линии (максимально упрощаем)
|
||
checkpoints = new Dictionary<int, Point>();
|
||
for (int i = 1; i <= 40; i++)
|
||
{
|
||
// Все чекпоинты вдоль одной линии на расстоянии 5 клеток друг от друга
|
||
checkpoints[i] = new Point(5 + (i - 1) * 5, 40);
|
||
}
|
||
|
||
// Препятствия - НЕТ! (для 40 чекпоинтов убираем препятствия для упрощения)
|
||
obstacles = new HashSet<Point>();
|
||
|
||
// Типы клеток - вся карта обычная дорога (0)
|
||
cellTypes = new Dictionary<Point, int>();
|
||
for (int y = 0; y < height; y++)
|
||
{
|
||
for (int x = 0; x < width; x++)
|
||
{
|
||
cellTypes[new Point(x, y)] = 0; // Обычная дорога
|
||
}
|
||
}
|
||
}
|
||
|
||
// Создаем трек
|
||
var track = new RaceTrack(width, height, start, checkpoints, obstacles, cellTypes);
|
||
|
||
Console.WriteLine("Начальное поле:");
|
||
Console.WriteLine($"S - старт, 1-{checkpoints.Count} - чекпоинты ({checkpoints.Count} шт.)");
|
||
Console.WriteLine("# - препятствия (можно проезжать, нельзя останавливаться)");
|
||
Console.WriteLine("~ - снег (ускорение ±1), = - лёд (ускорение нельзя менять)\n");
|
||
track.Visualize();
|
||
|
||
Console.WriteLine("\n" + new string('═', 50));
|
||
Console.WriteLine("Запуск алгоритма A* с эвристикой...");
|
||
Console.WriteLine(new string('═', 50) + "\n");
|
||
|
||
var startTime = DateTime.Now;
|
||
var solution = track.FindSolution();
|
||
var elapsed = DateTime.Now - startTime;
|
||
|
||
if (solution != null)
|
||
{
|
||
Console.WriteLine($"\nВремя работы: {elapsed.TotalSeconds:F2} сек");
|
||
Console.WriteLine("\n" + new string('═', 50));
|
||
Console.WriteLine("ВИЗУАЛИЗАЦИЯ РЕШЕНИЯ:");
|
||
Console.WriteLine(new string('═', 50) + "\n");
|
||
track.Visualize(solution);
|
||
track.ShowPath(solution);
|
||
|
||
// Статистика
|
||
Console.WriteLine("\n" + new string('═', 50));
|
||
Console.WriteLine("СТАТИСТИКА:");
|
||
Console.WriteLine(new string('═', 50));
|
||
Console.WriteLine($"Всего ходов: {solution.Count - 1}");
|
||
Console.WriteLine($"Чекпоинтов собрано: {checkpoints.Count}");
|
||
|
||
// Расчет максимальной скорости
|
||
int maxSpeed = 0;
|
||
for (int i = 1; i < solution.Count; i++)
|
||
{
|
||
var velocity = solution[i] - solution[i - 1];
|
||
int speed = Math.Abs(velocity.X) + Math.Abs(velocity.Y);
|
||
maxSpeed = Math.Max(maxSpeed, speed);
|
||
}
|
||
Console.WriteLine($"Максимальная скорость: {maxSpeed}");
|
||
|
||
// Экспорт решения в JSON, если указан файл выгрузки
|
||
if (outputFilePath != null)
|
||
{
|
||
try
|
||
{
|
||
track.ExportSolutionToJson(solution, outputFilePath);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"\n❌ Ошибка экспорта решения: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Console.WriteLine($"\nВремя работы: {elapsed.TotalSeconds:F2} сек");
|
||
Console.WriteLine("\n❌ Решение не найдено!");
|
||
Console.WriteLine("Попробуйте упростить задачу или увеличить maxIterations");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|