This commit is contained in:
2025-10-20 19:35:38 +05:00
commit 023ccd03d8
42 changed files with 10007 additions and 0 deletions

649
ProgramAStar.cs Normal file
View File

@@ -0,0 +1,649 @@
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");
}
}
}
}