using System.Collections.Generic;
using Server.Engines.Pathing.Cache;
using Xunit;
using Xunit.Abstractions;
namespace Server.Tests.Pathfinding;
///
/// Parity coverage for "walkable static surface above a land tile" terrain — sewers,
/// dungeon walkways, bridges, raised foundations, and stacked building floors.
///
/// The original parity tests only queried at the LAND-anchored standing Z and skipped
/// multi-Z fallthroughs, so they never noticed that a query at the REAL walk Z — the static
/// surface a creature actually stands on — returns
/// , because the baker anchored
/// SourceZ at the land average instead of the walkway. In the Britain sewer that's a ~98%
/// cache miss on a known walk-path (confirmed via [PathDiag).
///
/// Method: flood-fill outward from a known-walkable start using
/// — the slow path the cache mirrors. Each
/// reached (x, y, z) is a genuine standing state at its TRUE Z (CheckMovement returns the
/// destination Z it lands on), exactly the set of states A* would query. For every reached
/// state the cache must serve a Hit and agree with the slow path. This naturally follows
/// ramped stairs (each tread at its own Z) and climbs to upper floors, so one start covers
/// the whole connected structure — no fragile fixed-Z assumption.
///
/// A bare test world has no spawned items/mobiles, so CheckMovement reduces to static
/// walkability (no door/dynamic interference). Parity restricted to cardinal directions:
/// the cache stores raw masks and applies the diagonal corner-cut at query time, so a raw
/// diagonal bit legitimately differs from CheckMovement's diagonal result.
///
/// EXPECTED: RED before the standable-surface bake (reached states fall through at their
/// true Z); GREEN after.
///
[Collection("Sequential Pathfinding Tests")]
public class StepCacheStaticSurfaceParityTests
{
private readonly ITestOutputHelper _output;
public StepCacheStaticSurfaceParityTests(ITestOutputHelper output)
{
_output = output;
}
[Theory]
// label, start X, Y, Z (a real in-game walkable tile), max states to explore. Seeds are
// chosen to span the terrain classes the standable-surface bake must get right; the
// flood-fill spreads from each across a wide local area, so a handful of seeds exercises
// thousands of distinct (cell, Z) states without an exhaustive whole-map walk.
// sewer — static walkway @ z=5 over impassable land; covers dungeon walkways + bridges.
// inn — stair foot @ z=10; climbs the stairs onto the 1st & 2nd floors (multi-Z).
// plain — open Britain ground; guards against clearance false-positives on flat land.
// town — Britain cobblestones near the inn; mixed buildings, stairs, raised floors.
[InlineData("brit_sewer_walkway", 6034, 1476, 5, 2500)]
[InlineData("brit_inn_stairs_to_floors", 1495, 1628, 10, 2500)]
[InlineData("trammel_open_plain", 1500, 1600, 10, 2500)]
[InlineData("brit_town_cobblestones", 1494, 1626, 10, 2500)] // plain ground: guards against clearance false-positives
public void CacheServesReachableWalkStates(string label, int sx, int sy, int sz, int maxStates)
{
var cache = StepCache.Instance;
cache.Clear();
cache.MissPromotionThreshold = 1; // eager build — expect the cache to answer every state
var map = Map.Maps[1];
Assert.NotNull(map);
var stub = new ParityStubMobile();
stub.MoveToWorld(new Point3D(sx, sy, sz), map);
// Sanity: the start must itself be a walkable standing state via the slow path.
var startWalkable = false;
for (var d = 0; d < 8; d++)
{
if (Movement.Movement.CheckMovement(stub, map, new Point3D(sx, sy, sz), (Direction)d, out _))
{
startWalkable = true;
break;
}
}
Assert.True(startWalkable, $"[{label}] start ({sx},{sy},{sz}) is not walkable per the slow path — bad waypoint");
var visited = new HashSet<(int x, int y, int z)>();
var queue = new Queue<(int x, int y, int z)>();
visited.Add((sx, sy, sz));
queue.Enqueue((sx, sy, sz));
var states = 0;
var fellThrough = 0;
var disagreements = 0;
const int maxLog = 12;
while (queue.Count > 0)
{
var (x, y, z) = queue.Dequeue();
states++;
var loc = new Point3D(x, y, z);
var lookup = cache.TryGetMask(map, x, y, (sbyte)z);
if (!lookup.IsHit)
{
if (fellThrough < maxLog)
{
_output.WriteLine($"FELL THROUGH @ ({x},{y},{z}) hitKind={lookup.HitKind}");
}
fellThrough++;
}
for (var d = 0; d < 8; d++)
{
var dir = (Direction)d;
var slowOk = Movement.Movement.CheckMovement(stub, map, loc, dir, out var nz);
// Expand the frontier through every legal move (incl. diagonals).
if (slowOk)
{
var nx = x;
var ny = y;
Movement.Movement.Offset(dir, ref nx, ref ny);
var next = (nx, ny, (int)nz);
if (visited.Count < maxStates && visited.Add(next))
{
queue.Enqueue(next);
}
}
// Parity on cardinals only (diagonals carry the query-time corner-cut rule).
if ((d & 1) == 0 && lookup.IsHit)
{
var cacheOk = lookup.IsWalkable(dir);
if (cacheOk != slowOk)
{
if (disagreements < maxLog)
{
_output.WriteLine($"WALK DIFF @ ({x},{y},{z}) dir={dir} slow={slowOk} cache={cacheOk}");
}
disagreements++;
}
else if (slowOk && nz != lookup.GetWalkZ(dir))
{
if (disagreements < maxLog)
{
_output.WriteLine($"Z DIFF @ ({x},{y},{z}) dir={dir} slow={nz} cache={lookup.GetWalkZ(dir)}");
}
disagreements++;
}
}
}
}
stub.Delete();
var fallthroughPct = states == 0 ? 0 : 100.0 * fellThrough / states;
_output.WriteLine($"[{label}] states={states} fellThrough={fellThrough} ({fallthroughPct:F2}%) disagreements={disagreements}");
Assert.True(states > 50, $"[{label}] only explored {states} states — flood-fill stalled, bad waypoint");
// Correctness is strict: where the cache DOES answer, it must agree with the slow path.
Assert.Equal(0, disagreements);
// Coverage: nearly every reachable state should be cache-served. A small residual is
// expected and acceptable — a walkable surface sitting directly under a bridge/stair
// ramp falls through to the slow path (correct, just uncached) because the bake's
// clearance check is intentionally conservative there. A real anchor regression shows
// up as a large fraction (the pre-fix sewer was ~98%), which this still catches.
Assert.True(
fallthroughPct < 1.0,
$"[{label}] cache fell through on {fallthroughPct:F2}% ({fellThrough}/{states}) of reachable states — coverage regression"
);
}
///
/// Default static walker: inherits straight from Mobile so MovementImpl sees no
/// BaseCreature flags (CanSwim/CanFly false, bc==null). Mirrors the existing parity stub.
///
private class ParityStubMobile : Mobile
{
public ParityStubMobile()
{
Body = 0xC9;
}
}
}