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 VisitedCheckpoints { get; init; } public List 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 visitedCheckpoints, List 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 { 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 _obstacles; private readonly Dictionary _checkpoints; private readonly Point _start; private readonly Dictionary _cellTypes; // Тип клетки для каждой точки public RaceTrack(int width, int height, Point start, Dictionary checkpoints, HashSet obstacles, Dictionary 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 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(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? FindSolution() { var openSet = new SortedSet(new GameStateComparer()); var openSetLookup = new Dictionary(); var closedSet = new HashSet(); var initialState = new GameState(_start, new Point(0, 0), new HashSet(), new List { _start }, 0); initialState.HCost = CalculateHeuristic(_start, new Point(0, 0), new HashSet()); 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(currentState.VisitedCheckpoints); foreach (var (id, checkpoint) in _checkpoints) { if (!newCheckpoints.Contains(id) && newPosition.Equals(checkpoint)) { newCheckpoints.Add(id); } } var newPath = new List(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? path = null) { var pathSet = path != null ? new HashSet(path) : new HashSet(); 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 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 path, string filePath) { var accelerations = new List(); 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 checkpoints, HashSet obstacles, Dictionary cellTypes) LoadFromJson(string filePath) { string jsonContent = File.ReadAllText(filePath); var mapData = JsonSerializer.Deserialize(jsonContent); if (mapData?.map == null) throw new Exception("Не удалось загрузить карту из файла"); int height = mapData.map.Length; int width = mapData.map[0].Length; var checkpoints = new Dictionary(); var obstacles = new HashSet(); var cellTypes = new Dictionary(); 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 checkpoints; HashSet obstacles; Dictionary 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 [--output|-o ]\n"); } else { Console.WriteLine("Используется встроенная карта.\n"); } // Создаем поле 210x50 (длинная прямая для 40 чекпоинтов) width = 210; height = 50; // Стартовая позиция start = new Point(2, 2); // Чекпоинты - 40 штук вдоль ОДНОЙ линии (максимально упрощаем) checkpoints = new Dictionary(); for (int i = 1; i <= 40; i++) { // Все чекпоинты вдоль одной линии на расстоянии 5 клеток друг от друга checkpoints[i] = new Point(5 + (i - 1) * 5, 40); } // Препятствия - НЕТ! (для 40 чекпоинтов убираем препятствия для упрощения) obstacles = new HashSet(); // Типы клеток - вся карта обычная дорога (0) cellTypes = new Dictionary(); 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"); } } } }