b451f83174
Captures the pre-Godot-port state of the codebase. This is the rollback anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md). All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
121 lines
4.1 KiB
C#
121 lines
4.1 KiB
C#
namespace Theriapolis.Core.Util;
|
||
|
||
/// <summary>
|
||
/// 8-directional A* pathfinder on the 1024×1024 world tile grid.
|
||
/// Uses pre-allocated arrays and a generation counter to avoid clearing between queries.
|
||
/// The cost function receives (fromX, fromY, toX, toY, entryDir) and returns the cost
|
||
/// of moving to the 'to' tile, or float.PositiveInfinity if impassable.
|
||
/// </summary>
|
||
public sealed class AStarPathfinder
|
||
{
|
||
private const int W = C.WORLD_WIDTH_TILES;
|
||
private const int H = C.WORLD_HEIGHT_TILES;
|
||
private const int TileCount = W * H;
|
||
|
||
// Persistent arrays reused across queries (generation counter avoids clearing)
|
||
private readonly float[] _gScore = new float[TileCount];
|
||
private readonly int[] _cameFrom = new int[TileCount]; // -1 = none
|
||
private readonly byte[] _generation = new byte[TileCount]; // current run's gen tag
|
||
private byte _currentGen;
|
||
|
||
private readonly BinaryHeap<int> _open = new(4096);
|
||
|
||
// 8-directional deltas: N, NE, E, SE, S, SW, W, NW
|
||
private static readonly (int dx, int dy)[] Dirs =
|
||
{
|
||
( 0,-1), ( 1,-1), ( 1, 0), ( 1, 1),
|
||
( 0, 1), (-1, 1), (-1, 0), (-1,-1),
|
||
};
|
||
|
||
private static float Diagonal(int dx, int dy)
|
||
=> (dx != 0 && dy != 0) ? 1.4142f : 1f; // sqrt(2) for diagonals
|
||
|
||
private static float OctileHeuristic(int x0, int y0, int x1, int y1)
|
||
{
|
||
int dx = Math.Abs(x1 - x0);
|
||
int dy = Math.Abs(y1 - y0);
|
||
return Math.Max(dx, dy) + (1.4142f - 1f) * Math.Min(dx, dy);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Find the lowest-cost path from (sx,sy) to (gx,gy).
|
||
/// <paramref name="costFn"/> receives (fromX, fromY, toX, toY, entryDir) and returns
|
||
/// the cost of entering the 'to' tile from this direction, or PositiveInfinity if impassable.
|
||
/// Returns null if no path exists within the tile grid or if maxExpansions is exceeded.
|
||
/// </summary>
|
||
public List<(int X, int Y)>? FindPath(
|
||
int sx, int sy,
|
||
int gx, int gy,
|
||
Func<int, int, int, int, byte, float> costFn,
|
||
int maxExpansions = 600_000)
|
||
{
|
||
if (sx == gx && sy == gy) return new List<(int, int)> { (sx, sy) };
|
||
int expansions = 0;
|
||
|
||
// Increment generation (wrap around)
|
||
_currentGen = (byte)((_currentGen + 1) == 0 ? 1 : _currentGen + 1);
|
||
_open.Clear();
|
||
|
||
int startIdx = sy * W + sx;
|
||
_gScore[startIdx] = 0f;
|
||
_cameFrom[startIdx] = -1;
|
||
_generation[startIdx] = _currentGen;
|
||
|
||
float h0 = OctileHeuristic(sx, sy, gx, gy);
|
||
_open.Insert(startIdx, h0);
|
||
|
||
while (!_open.IsEmpty)
|
||
{
|
||
if (++expansions > maxExpansions) return null; // safety cap
|
||
|
||
int curIdx = _open.ExtractMin();
|
||
int curX = curIdx % W;
|
||
int curY = curIdx / W;
|
||
|
||
if (curX == gx && curY == gy)
|
||
return ReconstructPath(curIdx);
|
||
|
||
float curG = _gScore[curIdx];
|
||
|
||
for (int d = 0; d < 8; d++)
|
||
{
|
||
int nx = curX + Dirs[d].dx;
|
||
int ny = curY + Dirs[d].dy;
|
||
if ((uint)nx >= W || (uint)ny >= H) continue;
|
||
|
||
byte entryDir = (byte)d;
|
||
float moveCost = costFn(curX, curY, nx, ny, entryDir);
|
||
if (float.IsPositiveInfinity(moveCost)) continue;
|
||
|
||
float newG = curG + Diagonal(Dirs[d].dx, Dirs[d].dy) + moveCost;
|
||
int nIdx = ny * W + nx;
|
||
|
||
if (_generation[nIdx] == _currentGen && newG >= _gScore[nIdx])
|
||
continue; // already found a better path
|
||
|
||
_gScore[nIdx] = newG;
|
||
_cameFrom[nIdx] = curIdx;
|
||
_generation[nIdx] = _currentGen;
|
||
|
||
float f = newG + OctileHeuristic(nx, ny, gx, gy);
|
||
_open.Insert(nIdx, f);
|
||
}
|
||
}
|
||
|
||
return null; // no path found
|
||
}
|
||
|
||
private List<(int X, int Y)> ReconstructPath(int goalIdx)
|
||
{
|
||
var path = new List<(int, int)>();
|
||
int cur = goalIdx;
|
||
while (cur != -1)
|
||
{
|
||
path.Add((cur % W, cur / W));
|
||
cur = _cameFrom[cur];
|
||
}
|
||
path.Reverse();
|
||
return path;
|
||
}
|
||
}
|