test(measure): the VerticalShafts disconnection is real, not a coarse lie
Our guard was one-directional. FullResolutionPathIsAir catches the false POSITIVE -- coarse says joined, reality says walled -- which is the dangerous direction for safety, so we built it first. The opposite error was unguarded: a corridor narrower than SampleStep falls between sample points and vanishes, so connected regions read as separate. That is exactly what a network of narrow vertical shafts would fake at step 4. Refinement sweep, radius held constant for the controls: step 4 / r256 NOT_CONNECTED 57 comp largest .119 458,752 cells step 4 / r192 NOT_CONNECTED 32 comp largest .220 258,048 step 2 / r192 NOT_CONNECTED 27 comp largest .220 2,064,384 step 4 / r128 NOT_CONNECTED 20 comp largest .291 114,688 step 1 / r128 NOT_CONNECTED 11 comp largest .303 7,340,032 Both fixed-radius pairs stayed negative, so neither the step nor the radius explains it. At one-voxel sampling the arrival and departure mouths are still in different air components. VerticalShafts is a live primordial-law violation: a player descends into it and cannot reach its exit. NotConnected is renamed NotConnectedAtThisResolution and documents its own limit at the declaration, so a bare negative can never again be read as proof. CoarseLied names one direction; this names the other. Sweep split into VoxelForge.Generation.StrateConnectivityRefinement so the fast test stays fast. 4.4 s. The MaxCells refusal path was exercised and still refuses. Build clean, 19 succeeded / 0 warnings / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM
This commit is contained in:
1 parent
efcc8fcb65
commit
cd31dec869
4 files changed
+252
-11
No files matched your search
@@ -3,6 +3,7 @@
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
#include "HAL/PlatformTime.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "VoxelForgeTestFixture.h"
|
||||
@@ -13,6 +14,11 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
"VoxelForge.Generation.StrateConnectivity",
|
||||
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FVoxelForgeStrateConnectivityRefinementTest,
|
||||
"VoxelForge.Generation.StrateConnectivityRefinement",
|
||||
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
namespace
|
||||
{
|
||||
struct FArchetypeReport
|
||||
@@ -99,7 +105,8 @@ namespace
|
||||
switch (Result)
|
||||
{
|
||||
case EVoxelConnectivityResult::Connected: return TEXT("CONNECTED");
|
||||
case EVoxelConnectivityResult::NotConnected: return TEXT("NOT_CONNECTED");
|
||||
case EVoxelConnectivityResult::NotConnectedAtThisResolution:
|
||||
return TEXT("NOT_CONNECTED_AT_THIS_RESOLUTION");
|
||||
case EVoxelConnectivityResult::StartCellSolid: return TEXT("START_CELL_SOLID");
|
||||
case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GOAL_CELL_SOLID");
|
||||
case EVoxelConnectivityResult::OutOfWindow: return TEXT("OUT_OF_WINDOW");
|
||||
@@ -234,6 +241,40 @@ namespace
|
||||
/ static_cast<double>(OutSampleCount))
|
||||
: 0.0f;
|
||||
}
|
||||
|
||||
struct FRefinementSweepRow
|
||||
{
|
||||
int32 SampleStep = 0;
|
||||
int32 RadiusInVoxels = 0;
|
||||
FVoxelStrateMetrics Metrics;
|
||||
FConnectivityProbe Probe;
|
||||
};
|
||||
|
||||
bool FindChainMouths(
|
||||
const TArray<FVoxelPassage>& Passages,
|
||||
int32 StrateIndex,
|
||||
FVector& OutArrivalPoint,
|
||||
FVector& OutDeparturePoint)
|
||||
{
|
||||
int32 ArrivalCount = 0;
|
||||
int32 DepartureCount = 0;
|
||||
for (const FVoxelPassage& Passage : Passages)
|
||||
{
|
||||
if (Passage.LowerStrateIndex == StrateIndex
|
||||
&& Passage.UpperStrateIndex + 1 == StrateIndex)
|
||||
{
|
||||
OutArrivalPoint = Passage.LowerPoint;
|
||||
++ArrivalCount;
|
||||
}
|
||||
if (Passage.UpperStrateIndex == StrateIndex
|
||||
&& Passage.LowerStrateIndex == StrateIndex + 1)
|
||||
{
|
||||
OutDeparturePoint = Passage.UpperPoint;
|
||||
++DepartureCount;
|
||||
}
|
||||
}
|
||||
return ArrivalCount == 1 && DepartureCount == 1;
|
||||
}
|
||||
}
|
||||
|
||||
bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
|
||||
@@ -348,6 +389,25 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
World.DiffLayer->Clear();
|
||||
|
||||
// The refinement test deliberately approaches MaxCells at step 1. Keep a separate,
|
||||
// cheap over-cap control here so a future allocation/refusal regression cannot hide behind
|
||||
// the connectivity API's endpoint result.
|
||||
FVoxelStrateMeasureSettings TooLargeSettings = Settings;
|
||||
TooLargeSettings.SampleStep = 1;
|
||||
TooLargeSettings.RadiusInVoxels = 256;
|
||||
const FVoxelStrateMetrics TooLargeMetrics = VF_MeasureStrate(
|
||||
*World.Generator, *World.StrateManager, 0, TooLargeSettings);
|
||||
if (TooLargeMetrics.bValid
|
||||
|| !TooLargeMetrics.RefusalReason.Contains(TEXT("MaxCells")))
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("HARD FAILURE: the over-cap grid was not refused by MaxCells (valid=%s, "
|
||||
"reason='%s')."),
|
||||
TooLargeMetrics.bValid ? TEXT("true") : TEXT("false"),
|
||||
*TooLargeMetrics.RefusalReason));
|
||||
bAllChecksPassed = false;
|
||||
}
|
||||
|
||||
TArray<FArchetypeReport> ArchetypeReports;
|
||||
const TArray<FStrateSlot>& Layout = World.StrateManager->GetLayout();
|
||||
ArchetypeReports.Reserve(Layout.Num());
|
||||
@@ -448,6 +508,9 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
|
||||
GapAirFraction,
|
||||
GapAirSamples,
|
||||
GapSamples);
|
||||
Summary += FString::Printf(
|
||||
TEXT("MaxCells refusal control: over-cap measurement refused with reason '%s'.\n"),
|
||||
*TooLargeMetrics.RefusalReason);
|
||||
Summary += TEXT("Archetypes (old one-chunk vs derived window; derived InteriorMarginVoxels<0 => 2x BoundarySealThickness, clamped):\n");
|
||||
Summary += TEXT(" name | old margin | old Z span [min,max) | old Air | old Largest | old LargestCells | old >=1% | old Walkable | old FeatureScale vox | old Clearance vox | old Components | old Samples | old Solid | derived margin | derived Z span [min,max) | derived Air | derived Largest | derived LargestCells | derived >=1% | derived Walkable | derived FeatureScale vox | derived Clearance vox | derived Components | derived Samples | derived Solid\n");
|
||||
for (const FArchetypeReport& Report : ArchetypeReports)
|
||||
@@ -496,7 +559,7 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
|
||||
|
||||
int32 NumAnchorProbes = 0;
|
||||
int32 NumAnchorConnected = 0;
|
||||
int32 NumAnchorNotConnected = 0;
|
||||
int32 NumAnchorNotConnectedAtThisResolution = 0;
|
||||
int32 NumAnchorStartCellSolid = 0;
|
||||
int32 NumAnchorGoalCellSolid = 0;
|
||||
int32 NumAnchorOutOfWindow = 0;
|
||||
@@ -552,7 +615,9 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
|
||||
switch (Probe.Result)
|
||||
{
|
||||
case EVoxelConnectivityResult::Connected: ++NumAnchorConnected; break;
|
||||
case EVoxelConnectivityResult::NotConnected: ++NumAnchorNotConnected; break;
|
||||
case EVoxelConnectivityResult::NotConnectedAtThisResolution:
|
||||
++NumAnchorNotConnectedAtThisResolution;
|
||||
break;
|
||||
case EVoxelConnectivityResult::StartCellSolid: ++NumAnchorStartCellSolid; break;
|
||||
case EVoxelConnectivityResult::GoalCellSolid: ++NumAnchorGoalCellSolid; break;
|
||||
case EVoxelConnectivityResult::OutOfWindow: ++NumAnchorOutOfWindow; break;
|
||||
@@ -836,11 +901,11 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
|
||||
NumArrivalDepartureRoutesChecked);
|
||||
Summary += FString::Printf(
|
||||
TEXT("Correct largest-component endpoint probes (%d, including the surface-entry auxiliary): "
|
||||
"connected=%d, not-connected=%d, start-cell-solid=%d, goal-cell-solid=%d, "
|
||||
"connected=%d, not-connected-at-resolution=%d, start-cell-solid=%d, goal-cell-solid=%d, "
|
||||
"out-of-window=%d, coarse-lied=%d, snapped endpoint events=%d.\n"),
|
||||
NumAnchorProbes,
|
||||
NumAnchorConnected,
|
||||
NumAnchorNotConnected,
|
||||
NumAnchorNotConnectedAtThisResolution,
|
||||
NumAnchorStartCellSolid,
|
||||
NumAnchorGoalCellSolid,
|
||||
NumAnchorOutOfWindow,
|
||||
@@ -926,4 +991,176 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
|
||||
return bAllChecksPassed;
|
||||
}
|
||||
|
||||
bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
using namespace VoxelForgeTest;
|
||||
|
||||
FTestWorld World;
|
||||
World.Build(/*InSeed=*/1337, /*InGapChunks=*/2);
|
||||
if (!World.IsValid())
|
||||
{
|
||||
AddError(World.WhyInvalid());
|
||||
return false;
|
||||
}
|
||||
|
||||
const TArray<FStrateSlot>& Layout = World.StrateManager->GetLayout();
|
||||
const int32 VerticalShaftsIndex = FTestWorld::SlotVerticalShafts;
|
||||
if (!Layout.IsValidIndex(VerticalShaftsIndex)
|
||||
|| Layout[VerticalShaftsIndex].Definition == nullptr
|
||||
|| Layout[VerticalShaftsIndex].Definition->GeneratorType
|
||||
!= ECaveGeneratorType::VerticalShafts)
|
||||
{
|
||||
AddError(TEXT("HARD FAILURE: the pinned VerticalShafts fixture slot is missing or changed."));
|
||||
return false;
|
||||
}
|
||||
|
||||
FVector ArrivalPoint = FVector::ZeroVector;
|
||||
FVector DeparturePoint = FVector::ZeroVector;
|
||||
if (!FindChainMouths(
|
||||
World.StrateManager->GetPassages(),
|
||||
VerticalShaftsIndex,
|
||||
ArrivalPoint,
|
||||
DeparturePoint))
|
||||
{
|
||||
AddError(TEXT("HARD FAILURE: VerticalShafts does not have exactly one arrival and one "
|
||||
"departure mouth between consecutive strates."));
|
||||
return false;
|
||||
}
|
||||
|
||||
FVoxelStrateMeasureSettings BaseSettings;
|
||||
BaseSettings.CenterXY = FVector2D::ZeroVector;
|
||||
BaseSettings.MaxCells = 8000000;
|
||||
BaseSettings.HeadroomCells = 2;
|
||||
BaseSettings.InteriorMarginVoxels = -1;
|
||||
|
||||
struct FRefinementCase
|
||||
{
|
||||
int32 SampleStep;
|
||||
int32 RadiusInVoxels;
|
||||
};
|
||||
static constexpr FRefinementCase Cases[] = {
|
||||
{4, 256},
|
||||
{4, 192},
|
||||
{2, 192},
|
||||
{4, 128},
|
||||
{1, 128},
|
||||
};
|
||||
|
||||
bool bAllChecksPassed = true;
|
||||
TArray<FRefinementSweepRow> Rows;
|
||||
Rows.Reserve(UE_ARRAY_COUNT(Cases));
|
||||
|
||||
const double SweepStartSeconds = FPlatformTime::Seconds();
|
||||
for (const FRefinementCase& TestCase : Cases)
|
||||
{
|
||||
FRefinementSweepRow& Row = Rows.AddDefaulted_GetRef();
|
||||
Row.SampleStep = TestCase.SampleStep;
|
||||
Row.RadiusInVoxels = TestCase.RadiusInVoxels;
|
||||
|
||||
FVoxelStrateMeasureSettings RowSettings = BaseSettings;
|
||||
RowSettings.SampleStep = TestCase.SampleStep;
|
||||
RowSettings.RadiusInVoxels = TestCase.RadiusInVoxels;
|
||||
|
||||
Row.Metrics = VF_MeasureStrate(
|
||||
*World.Generator,
|
||||
*World.StrateManager,
|
||||
VerticalShaftsIndex,
|
||||
RowSettings);
|
||||
|
||||
Row.Probe.bChecked = true;
|
||||
Row.Probe.Result = VF_AreConnected(
|
||||
*World.Generator,
|
||||
*World.StrateManager,
|
||||
VerticalShaftsIndex,
|
||||
ArrivalPoint,
|
||||
DeparturePoint,
|
||||
RowSettings,
|
||||
Row.Probe.bStartSnapped,
|
||||
Row.Probe.bGoalSnapped);
|
||||
|
||||
if (!Row.Metrics.bValid
|
||||
|| Row.Metrics.NumSampled <= 0
|
||||
|| Row.Metrics.NumSampled > BaseSettings.MaxCells
|
||||
|| Row.Metrics.NumAir <= 0
|
||||
|| Row.Metrics.NumSolid <= 0)
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("HARD FAILURE: VerticalShafts refinement row step=%d radius=%d did not "
|
||||
"produce a bounded non-vacuous measurement (valid=%s, reason='%s', "
|
||||
"cells=%lld, air=%lld, solid=%lld)."),
|
||||
Row.SampleStep,
|
||||
Row.RadiusInVoxels,
|
||||
Row.Metrics.bValid ? TEXT("true") : TEXT("false"),
|
||||
*Row.Metrics.RefusalReason,
|
||||
Row.Metrics.NumSampled,
|
||||
Row.Metrics.NumAir,
|
||||
Row.Metrics.NumSolid));
|
||||
bAllChecksPassed = false;
|
||||
}
|
||||
|
||||
if (Row.Probe.Result == EVoxelConnectivityResult::OutOfWindow
|
||||
|| Row.Probe.Result == EVoxelConnectivityResult::StartCellSolid
|
||||
|| Row.Probe.Result == EVoxelConnectivityResult::GoalCellSolid)
|
||||
{
|
||||
const FString ProbeText = ConnectivityProbeText(Row.Probe);
|
||||
AddError(FString::Printf(
|
||||
TEXT("HARD FAILURE: VerticalShafts refinement row step=%d radius=%d could "
|
||||
"not query both in-window air mouths: %s."),
|
||||
Row.SampleStep,
|
||||
Row.RadiusInVoxels,
|
||||
*ProbeText));
|
||||
bAllChecksPassed = false;
|
||||
}
|
||||
|
||||
if (Row.Probe.bStartSnapped || Row.Probe.bGoalSnapped)
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("HARD FAILURE: VerticalShafts refinement row step=%d radius=%d snapped "
|
||||
"an endpoint; the controlled comparison is not a mouth-to-mouth query."),
|
||||
Row.SampleStep,
|
||||
Row.RadiusInVoxels));
|
||||
bAllChecksPassed = false;
|
||||
}
|
||||
|
||||
}
|
||||
const double SweepSeconds = FPlatformTime::Seconds() - SweepStartSeconds;
|
||||
|
||||
FString Summary = TEXT(
|
||||
"VerticalShafts arrival->departure refinement (seed 1337; source mouth pair; "
|
||||
"derived interior window):\n");
|
||||
Summary += TEXT(
|
||||
" SampleStep | RadiusInVoxels | verdict | components | largest share | cells | margin | "
|
||||
"Z window | endpoint snaps\n");
|
||||
for (const FRefinementSweepRow& Row : Rows)
|
||||
{
|
||||
const FString VerdictText = ConnectivityProbeText(Row.Probe);
|
||||
FString SnapText = TEXT("none");
|
||||
if (Row.Probe.bStartSnapped || Row.Probe.bGoalSnapped)
|
||||
{
|
||||
SnapText = ConnectivityProbeText(Row.Probe);
|
||||
}
|
||||
Summary += FString::Printf(
|
||||
TEXT(" %d | %d | %s | %d | %.9g | %lld | %d | [%d,%d) | %s\n"),
|
||||
Row.SampleStep,
|
||||
Row.RadiusInVoxels,
|
||||
*VerdictText,
|
||||
Row.Metrics.NumAirComponents,
|
||||
Row.Metrics.LargestComponentShare,
|
||||
Row.Metrics.NumSampled,
|
||||
Row.Metrics.ResolvedMarginVoxels,
|
||||
Row.Metrics.SampledMinZ,
|
||||
Row.Metrics.SampledMaxZ,
|
||||
*SnapText);
|
||||
}
|
||||
Summary += TEXT(
|
||||
"Controlled comparisons hold radius constant: (step 4, radius 192) vs (step 2, "
|
||||
"radius 192), and (step 4, radius 128) vs (step 1, radius 128).\n");
|
||||
Summary += FString::Printf(
|
||||
TEXT("VerticalShafts refinement sweep wall-clock: %.3f seconds.\n"),
|
||||
SweepSeconds);
|
||||
AddInfo(Summary);
|
||||
|
||||
return bAllChecksPassed;
|
||||
}
|
||||
|
||||
#endif // WITH_DEV_AUTOMATION_TESTS
|
||||
@@ -882,7 +882,7 @@ EVoxelConnectivityResult VF_AreConnected(
|
||||
TArray<int32> Path;
|
||||
if (!VoxelStrateMeasurePrivate::FindCoarsePath(Grid, Start, Goal, Path))
|
||||
{
|
||||
return EVoxelConnectivityResult::NotConnected;
|
||||
return EVoxelConnectivityResult::NotConnectedAtThisResolution;
|
||||
}
|
||||
|
||||
// The coarse route is evidence, not a pass. Re-check this one BFS route at one-voxel spacing
|
||||
|
||||
@@ -11,7 +11,7 @@ class UVoxelStrateManager;
|
||||
enum class EVoxelConnectivityResult : uint8
|
||||
{
|
||||
Connected,
|
||||
NotConnected,
|
||||
NotConnectedAtThisResolution, // Evidence only at this grid's resolution; corridors thinner than SampleStep are invisible.
|
||||
StartCellSolid,
|
||||
GoalCellSolid,
|
||||
OutOfWindow,
|
||||
@@ -74,7 +74,9 @@ VOXELFORGE_API FVoxelStrateMetrics VF_MeasureStrate(
|
||||
* If an endpoint's own coarse cell is solid, the query searches the 26 neighbouring cells and
|
||||
* uses the nearest air cell when one exists. The snap flags report those repairs. Solid-cell
|
||||
* results are returned only when no adjacent air cell exists. A coarse route that fails its
|
||||
* full-resolution re-check returns CoarseLied.
|
||||
* full-resolution re-check returns CoarseLied; a missing coarse route returns
|
||||
* NotConnectedAtThisResolution because a corridor thinner than SampleStep is invisible to the
|
||||
* grid. A bare "not connected" result is evidence only at the grid's own resolution.
|
||||
*/
|
||||
VOXELFORGE_API EVoxelConnectivityResult VF_AreConnected(
|
||||
const UVoxelGenerator& Generator,
|
||||
|
||||
Reference in new issue
Block a user