Files
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
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>
2026-04-30 20:40:51 -07:00

121 lines
4.1 KiB
C#
Raw Permalink 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.
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;
}
}