Files
paper-racing-gpi/ProgramAStar.cs
2025-10-20 19:35:38 +05:00

650 lines
29 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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");
}
}
}
}