From 2d6a6bd3a150bf44300a6b4c3c679052c2a775ac Mon Sep 17 00:00:00 2001 From: Boki Date: Mon, 16 Feb 2026 09:57:32 -0500 Subject: [PATCH] a bit better bfs --- src/Poe2Trade.Navigation/WorldMap.cs | 94 +++++++++++++++++----------- 1 file changed, 57 insertions(+), 37 deletions(-) diff --git a/src/Poe2Trade.Navigation/WorldMap.cs b/src/Poe2Trade.Navigation/WorldMap.cs index 7dcd80e..9b837f8 100644 --- a/src/Poe2Trade.Navigation/WorldMap.cs +++ b/src/Poe2Trade.Navigation/WorldMap.cs @@ -364,9 +364,10 @@ public class WorldMap : IDisposable } /// - /// BFS through walkable (Explored) cells to find the nearest frontier - /// (Explored cell adjacent to Unknown/Fog). Returns direction toward the - /// first step on the shortest path, respecting walls. + /// BFS through walkable (Explored) cells to find the best frontier direction. + /// Instead of stopping at the nearest frontier, runs the full BFS and counts + /// frontier cells reachable per first-step direction. Prefers directions with + /// more frontier cells (corridors) over directions with few (dead-end rooms). /// public (double dirX, double dirY)? FindNearestUnexplored(MapPosition pos, int searchRadius = 400) { @@ -379,24 +380,24 @@ public class WorldMap : IDisposable var rr = searchRadius / step; var gridW = 2 * rr + 1; - // Visited grid + parent tracking (encode parent as single int) var visited = new bool[gridW * gridW]; - var parentX = new short[gridW * gridW]; - var parentY = new short[gridW * gridW]; + // Propagate first step from start during BFS (avoids per-frontier trace-back) + var firstStepX = new short[gridW * gridW]; + var firstStepY = new short[gridW * gridW]; var queue = new Queue<(int gx, int gy)>(4096); var startGx = rr; var startGy = rr; visited[startGy * gridW + startGx] = true; - parentX[startGy * gridW + startGx] = -1; - parentY[startGy * gridW + startGx] = -1; queue.Enqueue((startGx, startGy)); // 8-connected neighbors ReadOnlySpan dxs = [-1, 0, 1, -1, 1, -1, 0, 1]; ReadOnlySpan dys = [-1, -1, -1, 0, 0, 1, 1, 1]; - int foundGx = -1, foundGy = -1; + // Count frontier cells per first-step direction + var frontierCounts = new Dictionary(); + var firstStepCoords = new Dictionary(); while (queue.Count > 0) { @@ -407,8 +408,9 @@ public class WorldMap : IDisposable var wy = cy + (gy - rr) * step; // Check if this explored cell borders Unknown/Fog (= frontier) - if (gx != startGx || gy != startGy) // skip player cell + if (gx != startGx || gy != startGy) { + var cellIdx = gy * gridW + gx; for (var d = 0; d < 8; d++) { var nx = wx + dxs[d] * step; @@ -417,9 +419,15 @@ public class WorldMap : IDisposable var neighbor = _canvas.At(ny, nx); if (neighbor == (byte)MapCell.Unknown || neighbor == (byte)MapCell.Fog) { - foundGx = gx; - foundGy = gy; - goto Found; + var fsKey = firstStepY[cellIdx] * gridW + firstStepX[cellIdx]; + if (frontierCounts.TryGetValue(fsKey, out var cnt)) + frontierCounts[fsKey] = cnt + 1; + else + { + frontierCounts[fsKey] = 1; + firstStepCoords[fsKey] = (firstStepX[cellIdx], firstStepY[cellIdx]); + } + break; // don't double-count this cell } } } @@ -439,43 +447,54 @@ public class WorldMap : IDisposable if (nwx < 0 || nwx >= size || nwy < 0 || nwy >= size) continue; var cell = _canvas.At(nwy, nwx); - if (cell != (byte)MapCell.Explored) continue; // only walk through explored + if (cell != (byte)MapCell.Explored) continue; visited[idx] = true; - parentX[idx] = (short)gx; - parentY[idx] = (short)gy; + // Propagate first step: direct neighbors of start ARE the first step + if (gx == startGx && gy == startGy) + { + firstStepX[idx] = (short)ngx; + firstStepY[idx] = (short)ngy; + } + else + { + var parentIdx = gy * gridW + gx; + firstStepX[idx] = firstStepX[parentIdx]; + firstStepY[idx] = firstStepY[parentIdx]; + } queue.Enqueue((ngx, ngy)); } } - Log.Information("BFS: no reachable frontier within {Radius}px", searchRadius); - return null; - - Found: - // Trace back to first step from start - var traceX = foundGx; - var traceY = foundGy; - while (true) + if (frontierCounts.Count == 0) { - var idx = traceY * gridW + traceX; - var px = parentX[idx]; - var py = parentY[idx]; - if (px == startGx && py == startGy) - break; // traceX/traceY is the first step - traceX = px; - traceY = py; + Log.Information("BFS: no reachable frontier within {Radius}px", searchRadius); + return null; } - var dirX = (double)(traceX - startGx); - var dirY = (double)(traceY - startGy); + // Pick direction with the most frontier cells (prefers corridors over dead ends) + var bestKey = -1; + var bestCount = 0; + foreach (var (key, count) in frontierCounts) + { + if (count > bestCount) + { + bestCount = count; + bestKey = key; + } + } + + var (bestGx, bestGy) = firstStepCoords[bestKey]; + var dirX = (double)(bestGx - startGx); + var dirY = (double)(bestGy - startGy); var len = Math.Sqrt(dirX * dirX + dirY * dirY); - if (len < 0.001) return (1, 0); // shouldn't happen + if (len < 0.001) return (1, 0); dirX /= len; dirY /= len; - var dist = Math.Sqrt((foundGx - startGx) * (foundGx - startGx) + (foundGy - startGy) * (foundGy - startGy)) * step; - Log.Debug("BFS: frontier at {Dist:F0}px, first step dir=({Dx:F2},{Dy:F2})", dist, dirX, dirY); + Log.Debug("BFS: {DirCount} directions, best={Best} frontier cells, dir=({Dx:F2},{Dy:F2})", + frontierCounts.Count, bestCount, dirX, dirY); return (dirX, dirY); } @@ -504,7 +523,8 @@ public class WorldMap : IDisposable colored.Set(r, c, new Vec3b(104, 64, 31)); else if (v == (byte)MapCell.Wall) colored.Set(r, c, new Vec3b(26, 45, 61)); - // Fog not rendered — too noisy from spell effects bleeding through overlay + else if (v == (byte)MapCell.Fog) + colored.Set(r, c, new Vec3b(55, 40, 28)); } var px = cx - x0;