namespace Theriapolis.Core.Util; /// /// 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. /// 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 _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); } /// /// Find the lowest-cost path from (sx,sy) to (gx,gy). /// 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. /// public List<(int X, int Y)>? FindPath( int sx, int sy, int gx, int gy, Func 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; } }