work on pathing

This commit is contained in:
Boki 2026-02-17 13:03:12 -05:00
parent 7d10f1d2a9
commit 3bb0315912
10 changed files with 729 additions and 113 deletions

View file

@ -16,9 +16,15 @@ public class WorldMap : IDisposable
private Mat? _prevWallMask; // for frame deduplication
private readonly PathFinder _pathFinder = new();
// Checkpoint tracking (canvas coordinates)
private readonly List<(Point Pos, long LastSeenMs)> _checkpointsOff = [];
private readonly List<(Point Pos, long LastSeenMs)> _checkpointsOn = [];
private const int CheckpointDedupRadius = 20;
public MapPosition Position => _position;
public bool LastMatchSucceeded { get; private set; }
public int CanvasSize => _canvasSize;
internal List<Point>? LastBfsPath => _pathFinder.LastResult?.Path;
private const int GrowMargin = 500;
private const int GrowAmount = 2000; // 1000px added per side
@ -64,7 +70,8 @@ public class WorldMap : IDisposable
/// Match current wall mask against the accumulated map to find position,
/// then stitch walls and paint explored area.
/// </summary>
public MapPosition MatchAndStitch(Mat classifiedMat, Mat wallMask, MinimapMode mode = MinimapMode.Overlay)
public MapPosition MatchAndStitch(Mat classifiedMat, Mat wallMask, MinimapMode mode = MinimapMode.Overlay,
List<Point>? checkpointsOff = null, List<Point>? checkpointsOn = null)
{
EnsureCapacity();
var sw = Stopwatch.StartNew();
@ -103,6 +110,7 @@ public class WorldMap : IDisposable
StitchWithConfidence(classifiedMat, _position, boosted: true, mode: mode);
PaintExploredCircle(_position);
MergeCheckpoints(_position, classifiedMat.Width, checkpointsOff, checkpointsOn);
LastMatchSucceeded = true; // signal caller to update viewport
if (_consecutiveMatchFails >= 30)
{
@ -140,6 +148,7 @@ public class WorldMap : IDisposable
var stitchStart = sw.Elapsed.TotalMilliseconds;
StitchWithConfidence(classifiedMat, _position, boosted: false, mode: mode);
PaintExploredCircle(_position);
MergeCheckpoints(_position, classifiedMat.Width, checkpointsOff, checkpointsOn);
var stitchMs = sw.Elapsed.TotalMilliseconds - stitchStart;
var posDx = _position.X - prevPos.X;
@ -323,24 +332,23 @@ public class WorldMap : IDisposable
dstRoi.Set(row, col, (byte)MapCell.Explored);
}
// Mark fog on canvas
// Mark fog on canvas (on top of Unknown or Explored — not Wall)
if (isCorner)
{
// Corner minimap: fog is clean, accept all fog pixels
for (var row = 0; row < h; row++)
for (var col = 0; col < w; col++)
{
if (srcRoi.At<byte>(row, col) != (byte)MapCell.Fog) continue;
if (dstRoi.At<byte>(row, col) == (byte)MapCell.Unknown)
var dst = dstRoi.At<byte>(row, col);
if (dst == (byte)MapCell.Unknown || dst == (byte)MapCell.Explored)
dstRoi.Set(row, col, (byte)MapCell.Fog);
}
}
else
{
// Overlay: restrict fog to ring around player (filters spell effect noise)
var fogInner2 = _config.ExploredRadius * _config.ExploredRadius;
var fogOuter = _config.ExploredRadius + 5;
var fogOuter2 = fogOuter * fogOuter;
// Overlay: exclude small area near player center (spell effect noise)
const int fogInner = 30;
const int fogInner2 = fogInner * fogInner;
for (var row = 0; row < h; row++)
for (var col = 0; col < w; col++)
@ -349,10 +357,10 @@ public class WorldMap : IDisposable
var fx = srcX + col - halfSize;
var fy = srcY + row - halfSize;
var dist2 = fx * fx + fy * fy;
if (dist2 < fogInner2 || dist2 > fogOuter2) continue;
if (fx * fx + fy * fy < fogInner2) continue;
if (dstRoi.At<byte>(row, col) == (byte)MapCell.Unknown)
var dst = dstRoi.At<byte>(row, col);
if (dst == (byte)MapCell.Unknown || dst == (byte)MapCell.Explored)
dstRoi.Set(row, col, (byte)MapCell.Fog);
}
}
@ -360,8 +368,10 @@ public class WorldMap : IDisposable
}
/// <summary>
/// Mark explored area: circle around player position, overwrite Unknown and Fog cells.
/// Called by MatchAndStitch after stitch in both bootstrap and match-success branches.
/// Mark explored area: circle around player position.
/// Unknown cells are marked Explored within the full radius.
/// Fog cells are only cleared in the inner portion (fogClearRadius),
/// preserving fog at the edge so it stays visible on the viewport.
/// </summary>
private void PaintExploredCircle(MapPosition position)
{
@ -369,6 +379,8 @@ public class WorldMap : IDisposable
var pcy = (int)Math.Round(position.Y);
var r = _config.ExploredRadius;
var r2 = r * r;
var fogClear = r - 20;
var fogClear2 = fogClear * fogClear;
var y0 = Math.Max(0, pcy - r);
var y1 = Math.Min(_canvasSize - 1, pcy + r);
@ -380,9 +392,12 @@ public class WorldMap : IDisposable
{
var dx = x - pcx;
var dy = y - pcy;
if (dx * dx + dy * dy > r2) continue;
var d2 = dx * dx + dy * dy;
if (d2 > r2) continue;
var cell = _canvas.At<byte>(y, x);
if (cell == (byte)MapCell.Unknown || cell == (byte)MapCell.Fog)
if (cell == (byte)MapCell.Unknown)
_canvas.Set(y, x, (byte)MapCell.Explored);
else if (cell == (byte)MapCell.Fog && d2 <= fogClear2)
_canvas.Set(y, x, (byte)MapCell.Explored);
}
}
@ -430,6 +445,9 @@ public class WorldMap : IDisposable
public (double dirX, double dirY)? FindNearestUnexplored(MapPosition pos, int searchRadius = 400)
=> _pathFinder.FindNearestUnexplored(_canvas, _canvasSize, pos, searchRadius);
public (double dirX, double dirY)? FindPathToTarget(MapPosition pos, Point target, int searchRadius = 400)
=> _pathFinder.FindPathToTarget(_canvas, _canvasSize, pos, target, searchRadius);
public byte[] GetMapSnapshot()
{
Cv2.ImEncode(".png", _canvas, out var buf);
@ -456,7 +474,7 @@ public class WorldMap : IDisposable
else if (v == (byte)MapCell.Wall)
colored.Set(r, c, new Vec3b(26, 45, 61));
else if (v == (byte)MapCell.Fog)
colored.Set(r, c, new Vec3b(55, 40, 28));
colored.Set(r, c, new Vec3b(120, 70, 40));
}
// BFS overlay: frontier cells + direction line
@ -489,6 +507,32 @@ public class WorldMap : IDisposable
var ey = (int)(py2 + bfs.DirY * lineLen);
Cv2.ArrowedLine(colored, new Point(px2, py2), new Point(ex, ey),
new Scalar(0, 220, 0), 2, tipLength: 0.3);
// BFS path polyline (bright green)
if (bfs.Path.Count >= 2)
{
var viewPath = new Point[bfs.Path.Count];
for (var i = 0; i < bfs.Path.Count; i++)
viewPath[i] = new Point(bfs.Path[i].X - x0, bfs.Path[i].Y - y0);
Cv2.Polylines(colored, [viewPath], isClosed: false,
color: new Scalar(0, 255, 0), thickness: 1);
}
}
// Checkpoint markers
foreach (var (cp, _) in _checkpointsOff)
{
var cpx = cp.X - x0;
var cpy = cp.Y - y0;
if (cpx >= 0 && cpx < viewSize && cpy >= 0 && cpy < viewSize)
Cv2.Circle(colored, new Point(cpx, cpy), 5, new Scalar(100, 100, 100), -1); // gray
}
foreach (var (cp, _) in _checkpointsOn)
{
var cpx = cp.X - x0;
var cpy = cp.Y - y0;
if (cpx >= 0 && cpx < viewSize && cpy >= 0 && cpy < viewSize)
Cv2.Circle(colored, new Point(cpx, cpy), 5, new Scalar(220, 200, 0), -1); // bright cyan (BGR)
}
// Player dot (orange, on top)
@ -501,6 +545,90 @@ public class WorldMap : IDisposable
return buf;
}
/// <summary>
/// Convert frame-relative checkpoint positions to canvas coords and merge with existing lists.
/// When a checkpoint-on appears near a checkpoint-off, remove the off entry.
/// </summary>
private void MergeCheckpoints(MapPosition position, int frameSize,
List<Point>? offPoints, List<Point>? onPoints)
{
var halfSize = frameSize / 2;
var canvasX = (int)Math.Round(position.X) - halfSize;
var canvasY = (int)Math.Round(position.Y) - halfSize;
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var r2 = CheckpointDedupRadius * CheckpointDedupRadius;
if (offPoints != null)
{
foreach (var fp in offPoints)
{
var cp = new Point(canvasX + fp.X, canvasY + fp.Y);
if (!IsDuplicate(_checkpointsOff, cp, r2))
_checkpointsOff.Add((cp, now));
}
}
if (onPoints != null)
{
foreach (var fp in onPoints)
{
var cp = new Point(canvasX + fp.X, canvasY + fp.Y);
// Remove matching off-checkpoint (it got activated)
for (var i = _checkpointsOff.Count - 1; i >= 0; i--)
{
var dx = _checkpointsOff[i].Pos.X - cp.X;
var dy = _checkpointsOff[i].Pos.Y - cp.Y;
if (dx * dx + dy * dy <= r2)
{
_checkpointsOff.RemoveAt(i);
break;
}
}
if (!IsDuplicate(_checkpointsOn, cp, r2))
_checkpointsOn.Add((cp, now));
}
}
}
private static bool IsDuplicate(List<(Point Pos, long LastSeenMs)> list, Point pt, int r2)
{
foreach (var (pos, _) in list)
{
var dx = pos.X - pt.X;
var dy = pos.Y - pt.Y;
if (dx * dx + dy * dy <= r2)
return true;
}
return false;
}
/// <summary>
/// Returns the nearest unactivated checkpoint within maxDist canvas pixels, or null.
/// </summary>
public Point? GetNearestCheckpointOff(MapPosition pos, int maxDist = 200)
{
var px = (int)Math.Round(pos.X);
var py = (int)Math.Round(pos.Y);
var maxDist2 = maxDist * maxDist;
Point? best = null;
var bestDist2 = int.MaxValue;
foreach (var (cp, _) in _checkpointsOff)
{
var dx = cp.X - px;
var dy = cp.Y - py;
var d2 = dx * dx + dy * dy;
if (d2 <= maxDist2 && d2 < bestDist2)
{
bestDist2 = d2;
best = cp;
}
}
return best;
}
public void Reset()
{
_canvas.Dispose();
@ -514,6 +642,8 @@ public class WorldMap : IDisposable
_frameCount = 0;
_consecutiveMatchFails = 0;
LastMatchSucceeded = false;
_checkpointsOff.Clear();
_checkpointsOn.Clear();
}
/// <summary>