test(opstack): three tile scans sampled <1 lattice period; fix the CLASS this time
The "a sampler must cover at least one period" bug was found in the tunnel test (+/-32 vs RoomSpacing 80) and again in the shaft test (+/-48 vs ShaftSpacing 55). Both were fixed. The class was not: three box-verdict scans still ran the original RandRange(-6,6)*Extent at Step 1 / Cells 8, i.e. +/-48 voxels. Island +/-48 vs IslandSpacing 95 = 0.51 periods (worse than either fix) Slab +/-48 vs ColumnSpacing 60 = 0.80 periods Maze +/-48 vs CellSize 40 = 1.20 periods Verified the fixture does not override any of those spacings, so the header defaults are what these tests really ran against. This matters now specifically:7dbdf51changed the island and maze verdicts andeaa44bfchanged the slab column verdict, and these are the tests that guard them. Widening before the build is what makes that build's green mean anything. The rule the fixed tests already encoded, now explicit: Extent = 8, so SpanCells = the lattice spacing gives exactly 8 periods -- which is where the shaft test's 55 came from. Island 95, Slab 60, Maze 40, each printing the ratio computed LIVE from the params struct so a future narrowing cannot be silent. No assertion, tolerance, AddError, brute-force loop, seed, Step, Cells or tile count was touched: this changes what the measurement looks at, never what it demands. Proved counts will move (expected); NumUnsound must stay 0. Test-only -- git diff lists three files under Private/Tests/. Not built. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
|||||||
|
# Codex task 005 — three box-verdict tile scans sample less than one lattice period
|
||||||
|
|
||||||
|
**Owner:** Codex (Model Luna, xHigh) · **Orchestrator:** Claude · **Branch:** `experimental`
|
||||||
|
**Status:** specified, not started
|
||||||
|
**Kind:** test coverage. **Zero risk to the game — no non-test file may change.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why this exists
|
||||||
|
|
||||||
|
This project has already found and fixed this exact defect **twice**:
|
||||||
|
|
||||||
|
> *"A sampler must cover at least one period of what it samples. The tunnel test drew tile XY from
|
||||||
|
> ±32 voxels with `RoomSpacing 80` — it measured the spine hub and called it the world. The shaft
|
||||||
|
> test had the identical bug (±48 against `ShaftSpacing 55`)."* — `OPSTACK-HANDOFF.md`
|
||||||
|
|
||||||
|
Both were fixed to `SpanCells = 55` ⇒ **±440 voxels**, and both now print their own extent in units
|
||||||
|
of the pattern's period so it cannot silently regress.
|
||||||
|
|
||||||
|
**Three box-verdict tile scans were never fixed**, because the fix was applied where the bug was
|
||||||
|
noticed rather than to the class. All three still use the original `Rng.RandRange(-6, 6) * Extent`
|
||||||
|
with `Step = 1, Cells = 8` ⇒ `Extent = 8` ⇒ **half-extent ±48 voxels**:
|
||||||
|
|
||||||
|
| test | half-extent | lattice period (default, **unchanged by the fixture**) | coverage |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `VoxelForgeOpStackIslandTest.cpp` ~244 | ±48 | `IslandSpacing` **95** | **0.51 periods** ⛔ worse than either bug already fixed |
|
||||||
|
| `VoxelForgeOpStackSlabTest.cpp` ~289 | ±48 | `ColumnSpacing` **60** | **0.80 periods** ⛔ |
|
||||||
|
| `VoxelForgeOpStackMazeTest.cpp` ~276 | ±48 | `CellSize` **40** | 1.20 periods ⚠️ marginal |
|
||||||
|
|
||||||
|
Verified: none of `EnableIslandFeatures` / the slab tuning / the maze setup overrides the spacing, so
|
||||||
|
the header defaults are what these tests actually run against.
|
||||||
|
|
||||||
|
**Why it matters right now, specifically.** Two commits just changed the box verdicts these very
|
||||||
|
tests are supposed to guard — `7dbdf51` (the `ExtraReach` bound, which touches **islands**, maze and
|
||||||
|
shafts) and `eaa44bf` (the radius envelope, which touches **slab columns** and shafts). The tests
|
||||||
|
that would catch a mistake in those changes currently sample about half a lattice cell.
|
||||||
|
|
||||||
|
## The rule the two fixed tests already encode
|
||||||
|
|
||||||
|
`Extent = Step * Cells = 8` voxels, so `SpanVoxels = SpanCells * 8`. To get **8 periods** of
|
||||||
|
half-extent you set:
|
||||||
|
|
||||||
|
> **`SpanCells` = the lattice spacing** (`55` for `ShaftSpacing 55` — that is where the shaft test's
|
||||||
|
> `55` comes from, and it is not a coincidence).
|
||||||
|
|
||||||
|
Apply the same:
|
||||||
|
|
||||||
|
| test | `SpanCells` | resulting half-extent | periods |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Island | `95` | ±760 | 8.0 |
|
||||||
|
| Slab | `60` | ±480 | 8.0 |
|
||||||
|
| Maze | `40` | ±320 | 8.0 |
|
||||||
|
|
||||||
|
## What to build
|
||||||
|
|
||||||
|
For each of the three tests, mirror **exactly** what `VoxelForgeOpStackShaftTest.cpp` (~212) does:
|
||||||
|
|
||||||
|
1. Hoist `const int32 SpanCells` and `const int32 SpanVoxels = SpanCells * 8;` **outside the tile
|
||||||
|
loop** — the report needs them and `Extent` is loop-local. (That scoping slip has already happened
|
||||||
|
once in this file family; the shaft test's comment records it.)
|
||||||
|
2. Draw `Rng.RandRange(-SpanCells, SpanCells) * Extent` for X and Y. **Leave the Z draw exactly as
|
||||||
|
it is** — it is clamped to the strate slot and is not part of this defect.
|
||||||
|
3. Extend the existing report line to print `SpanVoxels`, the live ratio
|
||||||
|
`(float)SpanVoxels / FMath::Max(<the spacing param>, 1.0f)`, and the spacing itself — so the
|
||||||
|
extent is stated in units of the pattern's own period and a future narrowing is visible.
|
||||||
|
|
||||||
|
## ⚠️ Invariants
|
||||||
|
|
||||||
|
1. **This is NOT "widen until it passes."** The comment already in the tunnel test says it best and
|
||||||
|
the same reasoning applies here: *every proved tile is still brute-forced voxel by voxel below, so
|
||||||
|
a wider sampler that produced a FALSE verdict fails exactly as before. We are changing what the
|
||||||
|
measurement **looks at**, not what it **demands**.* Do not touch the brute-force loop, its
|
||||||
|
tolerance, or any `AddError`.
|
||||||
|
2. **Do not adjust an assertion to accommodate a moved number.** Widening will change the proved /
|
||||||
|
Mixed counts — that is the point. If an existing assertion would now fail, **report it and stop**;
|
||||||
|
do not retune it. ("Don't assert a number you want to improve" is a written lesson here.)
|
||||||
|
3. **No file outside `Source/VoxelForge/Private/Tests/` may change.** `git diff --stat` must list
|
||||||
|
only those three test files.
|
||||||
|
4. Do not change `Step`, `Cells`, the tile count (`60`), or the RNG seeds — a changed seed makes the
|
||||||
|
before/after incomparable, and comparability is the whole point of touching this now.
|
||||||
|
5. Comments are French + English; match the surrounding file.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
- Three test files changed, nothing else.
|
||||||
|
- Each of the three now prints its extent **and** that extent in periods of its own spacing param.
|
||||||
|
- After the build, each ratio line reads **≥ 8 periods**.
|
||||||
|
- The proved counts will move. **That is expected.** What must NOT move: `violations` / `NumUnsound`
|
||||||
|
stays **0** in all three. If it becomes non-zero, the wider sampler has found a genuine hole that
|
||||||
|
the narrow one was hiding — which would be this task paying for itself immediately, and must be
|
||||||
|
reported loudly rather than tuned away.
|
||||||
|
|
||||||
|
## Notes for the reviewer (Claude)
|
||||||
|
|
||||||
|
- Confirm `SpanCells`/`SpanVoxels` are outside the tile loop in all three.
|
||||||
|
- Confirm the Z draw is untouched.
|
||||||
|
- Confirm the ratio is computed **live** from the params struct, not hardcoded — a hardcoded "8.0
|
||||||
|
periods" in a format string would be a success message that asserts coverage while measuring
|
||||||
|
nothing, which is a named failure mode in this project.
|
||||||
@@ -3697,3 +3697,49 @@ the same file** — `VF_PerlinAbsBound` for the first, `FIslandBlobSource` for t
|
|||||||
|
|
||||||
⇒ When adding a bound, the question is not "what is the max parameter" but **"what is the supremum
|
⇒ When adding a bound, the question is not "what is the max parameter" but **"what is the supremum
|
||||||
of the thing `Eval` can actually produce, and where in this file has that already been worked out?"**
|
of the thing `Eval` can actually produce, and where in this file has that already been worked out?"**
|
||||||
|
|
||||||
|
## 2026-08-16 (g) — the sampler bug was fixed twice and never as a CLASS. Three more instances.
|
||||||
|
|
||||||
|
`CODEX-TASK-005`. Test-only; no non-test file changed.
|
||||||
|
|
||||||
|
The handoff records: *"A sampler must cover at least one period of what it samples"* — found in the
|
||||||
|
tunnel test (±32 vs `RoomSpacing 80`) and again in the shaft test (±48 vs `ShaftSpacing 55`). Both
|
||||||
|
were widened to ±440 and taught to print their extent in periods. **The fix was applied where the
|
||||||
|
bug was noticed, never to the class.** Three box-verdict tile scans still ran the original
|
||||||
|
`RandRange(-6, 6) * Extent` with `Step 1, Cells 8` ⇒ **±48 voxels**:
|
||||||
|
|
||||||
|
| test | half-extent | period (default — verified NOT overridden by the fixture) | coverage |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Island | ±48 | `IslandSpacing` **95** | **0.51 periods** — worse than either bug already fixed |
|
||||||
|
| Slab | ±48 | `ColumnSpacing` **60** | **0.80 periods** |
|
||||||
|
| Maze | ±48 | `CellSize` **40** | 1.20 periods (marginal) |
|
||||||
|
|
||||||
|
**Why this mattered *today* rather than in general:** the two commits immediately before it changed
|
||||||
|
the very verdicts these tests guard — `7dbdf51` touches **islands** and maze, `eaa44bf` touches
|
||||||
|
**slab columns**. The tests that would catch a mistake in those fixes were sampling half a lattice
|
||||||
|
cell. Widening them before the build is what makes that build's green mean something.
|
||||||
|
|
||||||
|
### The rule the two fixed tests already encoded, now written down
|
||||||
|
|
||||||
|
`Extent = Step · Cells = 8`, so `SpanVoxels = SpanCells · 8`. For **8 periods** of half-extent:
|
||||||
|
**`SpanCells` = the lattice spacing.** That is where the shaft test's `55` comes from — it equals
|
||||||
|
`ShaftSpacing`, and it is not a coincidence. Applied: Island `95`, Slab `60`, Maze `40`, each giving
|
||||||
|
exactly 8.0 periods, each printing the ratio **computed live from the params struct** so a future
|
||||||
|
narrowing is visible instead of silent.
|
||||||
|
|
||||||
|
### What was deliberately NOT done
|
||||||
|
|
||||||
|
- **No assertion, tolerance, `AddError` or brute-force loop touched.** Every proved tile is still
|
||||||
|
verified voxel by voxel; a wider sampler producing a false verdict fails exactly as before. This
|
||||||
|
changes what the measurement *looks at*, never what it *demands*.
|
||||||
|
- **No seed, `Step`, `Cells` or tile count changed** — a moved seed makes the before/after
|
||||||
|
incomparable, which is the only reason to do this now rather than later.
|
||||||
|
- The ratio is never a hardcoded "8.0" in a format string. That would be a success message asserting
|
||||||
|
coverage while measuring nothing — a named failure mode here.
|
||||||
|
|
||||||
|
### What to read after the build
|
||||||
|
|
||||||
|
The proved counts **will move**; that is the point, not a regression. **`NumUnsound` / `violations`
|
||||||
|
must stay 0 in all three.** If one goes non-zero, the wider sampler has found a real hole the narrow
|
||||||
|
one was hiding — in which case this task paid for itself on its first run, and the number must be
|
||||||
|
reported rather than tuned away.
|
||||||
|
|||||||
@@ -235,14 +235,18 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
|||||||
{
|
{
|
||||||
int32 NumProvedSolid = 0, NumProvedAir = 0, NumMixed = 0, NumUnsound = 0;
|
int32 NumProvedSolid = 0, NumProvedAir = 0, NumMixed = 0, NumUnsound = 0;
|
||||||
FRandomStream Rng(24680);
|
FRandomStream Rng(24680);
|
||||||
|
// Hors de la boucle : la ligne de rapport en a besoin. Une étendue d'échantillonnage qu'on
|
||||||
|
// ne peut pas citer dans le rapport est une étendue que personne ne surveille.
|
||||||
|
const int32 SpanCells = 95;
|
||||||
|
const int32 SpanVoxels = SpanCells * 8; // Extent = Step * Cells = 1 * 8
|
||||||
|
|
||||||
for (int32 t = 0; t < 60; ++t)
|
for (int32 t = 0; t < 60; ++t)
|
||||||
{
|
{
|
||||||
const int32 Step = 1, Cells = 8;
|
const int32 Step = 1, Cells = 8;
|
||||||
const int32 Extent = Step * Cells;
|
const int32 Extent = Step * Cells;
|
||||||
const FIntVector Origin(
|
const FIntVector Origin(
|
||||||
Rng.RandRange(-6, 6) * Extent,
|
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
||||||
Rng.RandRange(-6, 6) * Extent,
|
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
||||||
FMath::Clamp(Rng.RandRange(BottomVoxelZ / Extent, TopVoxelZ / Extent), -4096, 4096) * Extent);
|
FMath::Clamp(Rng.RandRange(BottomVoxelZ / Extent, TopVoxelZ / Extent), -4096, 4096) * Extent);
|
||||||
|
|
||||||
const int32 GridDim = Cells + 1;
|
const int32 GridDim = Cells + 1;
|
||||||
@@ -289,10 +293,12 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
|||||||
TestEqual(TEXT("every box verdict the island stack emits survives brute force"), NumUnsound, 0);
|
TestEqual(TEXT("every box verdict the island stack emits survives brute force"), NumUnsound, 0);
|
||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Box verdicts over 60 FloatingIslands tiles: %d proved AllSolid, %d proved AllAir, ")
|
TEXT("Box verdicts over 60 FloatingIslands tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||||
|
TEXT("IslandSpacing %.0f): %d proved AllSolid, %d proved AllAir, ")
|
||||||
TEXT("%d Mixed. Today's ClassifyTile proves ZERO of these. The AllAir count is the new ")
|
TEXT("%d Mixed. Today's ClassifyTile proves ZERO of these. The AllAir count is the new ")
|
||||||
TEXT("thing: no cave archetype has ever been able to prove 'all air', and a floating-")
|
TEXT("thing: no cave archetype has ever been able to prove 'all air', and a floating-")
|
||||||
TEXT("island strate is mostly exactly that (OPSTACK-DECOMPOSITION 7)."),
|
TEXT("island strate is mostly exactly that (OPSTACK-DECOMPOSITION 7)."),
|
||||||
|
SpanVoxels, (float)SpanVoxels / FMath::Max(P.IslandSpacing, 1.0f), P.IslandSpacing,
|
||||||
NumProvedSolid, NumProvedAir, NumMixed));
|
NumProvedSolid, NumProvedAir, NumMixed));
|
||||||
|
|
||||||
if (NumProvedAir == 0)
|
if (NumProvedAir == 0)
|
||||||
|
|||||||
@@ -267,14 +267,18 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
{
|
{
|
||||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0;
|
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0;
|
||||||
FRandomStream Rng(24680);
|
FRandomStream Rng(24680);
|
||||||
|
// Hors de la boucle : la ligne de rapport en a besoin. Une étendue d'échantillonnage qu'on
|
||||||
|
// ne peut pas citer dans le rapport est une étendue que personne ne surveille.
|
||||||
|
const int32 SpanCells = 40;
|
||||||
|
const int32 SpanVoxels = SpanCells * 8; // Extent = Step * Cells = 1 * 8
|
||||||
|
|
||||||
for (int32 t = 0; t < 60; ++t)
|
for (int32 t = 0; t < 60; ++t)
|
||||||
{
|
{
|
||||||
const int32 Step = 1, Cells = 8; // petites tuiles : force brute tenable
|
const int32 Step = 1, Cells = 8; // petites tuiles : force brute tenable
|
||||||
const int32 Extent = Step * Cells;
|
const int32 Extent = Step * Cells;
|
||||||
const FIntVector Origin(
|
const FIntVector Origin(
|
||||||
Rng.RandRange(-6, 6) * Extent,
|
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
||||||
Rng.RandRange(-6, 6) * Extent,
|
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
||||||
FMath::Clamp(Rng.RandRange(BottomVoxelZ / Extent, TopVoxelZ / Extent), -4096, 4096) * Extent);
|
FMath::Clamp(Rng.RandRange(BottomVoxelZ / Extent, TopVoxelZ / Extent), -4096, 4096) * Extent);
|
||||||
|
|
||||||
const int32 GridDim = Cells + 1; // le MÊME treillis que le mesher, marge ±1 comprise
|
const int32 GridDim = Cells + 1; // le MÊME treillis que le mesher, marge ±1 comprise
|
||||||
@@ -319,9 +323,11 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
NumUnsound, 0);
|
NumUnsound, 0);
|
||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Box verdicts over 60 Maze tiles: %d proved uniform, %d Mixed. Today's ClassifyTile ")
|
TEXT("Box verdicts over 60 Maze tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||||
|
TEXT("CellSize %.0f): %d proved uniform, %d Mixed. Today's ClassifyTile ")
|
||||||
TEXT("proves ZERO of these -- every cave archetype falls through to \"pas prouvable en ")
|
TEXT("proves ZERO of these -- every cave archetype falls through to \"pas prouvable en ")
|
||||||
TEXT("v1\". Any number above zero here is tile-skipping Maze has never had."),
|
TEXT("v1\". Any number above zero here is tile-skipping Maze has never had."),
|
||||||
|
SpanVoxels, (float)SpanVoxels / FMath::Max(MazeParams.CellSize, 1.0f), MazeParams.CellSize,
|
||||||
NumProved, NumMixed));
|
NumProved, NumMixed));
|
||||||
|
|
||||||
if (NumProved == 0)
|
if (NumProved == 0)
|
||||||
|
|||||||
@@ -280,14 +280,18 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
|
|||||||
{
|
{
|
||||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0;
|
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0;
|
||||||
FRandomStream Rng(24680 + SlotIndex);
|
FRandomStream Rng(24680 + SlotIndex);
|
||||||
|
// Hors de la boucle : la ligne de rapport en a besoin. Une étendue d'échantillonnage qu'on
|
||||||
|
// ne peut pas citer dans le rapport est une étendue que personne ne surveille.
|
||||||
|
const int32 SpanCells = 60;
|
||||||
|
const int32 SpanVoxels = SpanCells * 8; // Extent = Step * Cells = 1 * 8
|
||||||
|
|
||||||
for (int32 t = 0; t < NumSlabTiles; ++t)
|
for (int32 t = 0; t < NumSlabTiles; ++t)
|
||||||
{
|
{
|
||||||
const int32 Step = 1, Cells = 8;
|
const int32 Step = 1, Cells = 8;
|
||||||
const int32 Extent = Step * Cells;
|
const int32 Extent = Step * Cells;
|
||||||
const FIntVector Origin(
|
const FIntVector Origin(
|
||||||
Rng.RandRange(-6, 6) * Extent,
|
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
||||||
Rng.RandRange(-6, 6) * Extent,
|
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
||||||
FMath::Clamp(Rng.RandRange(BottomVoxelZ / Extent, TopVoxelZ / Extent), -4096, 4096) * Extent);
|
FMath::Clamp(Rng.RandRange(BottomVoxelZ / Extent, TopVoxelZ / Extent), -4096, 4096) * Extent);
|
||||||
|
|
||||||
const int32 GridDim = Cells + 1; // le MÊME treillis que le mesher, marge ±1 comprise
|
const int32 GridDim = Cells + 1; // le MÊME treillis que le mesher, marge ±1 comprise
|
||||||
@@ -335,10 +339,13 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
|
|||||||
NumUnsound, 0);
|
NumUnsound, 0);
|
||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("%s box verdicts over %d tiles: %d proved uniform, %d Mixed. Today's ")
|
TEXT("%s box verdicts over %d tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||||
|
TEXT("ColumnSpacing %.0f): %d proved uniform, %d Mixed. Today's ")
|
||||||
TEXT("ClassifyTile proves ZERO of these. This number is the whole point of making ")
|
TEXT("ClassifyTile proves ZERO of these. This number is the whole point of making ")
|
||||||
TEXT("the slab surfaces XY-pure (OPSTACK-DECOMPOSITION 3.1)."),
|
TEXT("the slab surfaces XY-pure (OPSTACK-DECOMPOSITION 3.1)."),
|
||||||
SlotName, NumSlabTiles, NumProved, NumMixed));
|
SlotName, NumSlabTiles, SpanVoxels,
|
||||||
|
(float)SpanVoxels / FMath::Max(SlabParams.ColumnSpacing, 1.0f), SlabParams.ColumnSpacing,
|
||||||
|
NumProved, NumMixed));
|
||||||
|
|
||||||
if (NumProved == 0)
|
if (NumProved == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user