test(simulation): address PR #165 review (T-1031)
Hoshe review findings — close silent-pass gaps in the §8 drainage tests: - assert_drainage_monotonicity now returns whether it actually checked (chunk had both wet and dry tiles); the three sweep tests (Alluvial/Meander/Braided Delta) assert at least one chunk exercised the law, so a regression that zeroes all channels fails loudly instead of passing vacuously. - The fjord-floor test scans the full 64x64 chunk instead of a single Y=32 row and asserts the deep-water trough exists, so the sea-level floor law can no longer be skipped by a probe that missed the trough. - Remove the unreferenced make_test_terrain_analysis helper (was behind #[allow(dead_code)] with a 'future use' comment — the maintenance trap the review flagged); make_dry_terrain_analysis covers the validation bodies. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -246,16 +246,22 @@ fn golden_seed_determinism_regression() {
|
|||||||
// level; BraidedDelta/CliffCoast/FjordWall floors at sea level."
|
// level; BraidedDelta/CliffCoast/FjordWall floors at sea level."
|
||||||
// Tested by deriving a full 64×64 chunk and checking min(wet_elev) ≤ max(dry_elev).
|
// Tested by deriving a full 64×64 chunk and checking min(wet_elev) ≤ max(dry_elev).
|
||||||
|
|
||||||
|
/// Returns `true` if the monotonicity assertion was actually exercised (the chunk
|
||||||
|
/// produced both wet and dry tiles). A `false` return means the chunk had no wet
|
||||||
|
/// tiles, so the law was trivially satisfied without checking anything — callers
|
||||||
|
/// sweep several chunks and assert at least one returned `true`, so a derivation
|
||||||
|
/// regression that silently zeroes all channels fails loudly instead of passing.
|
||||||
|
#[must_use]
|
||||||
fn assert_drainage_monotonicity(
|
fn assert_drainage_monotonicity(
|
||||||
seed: u64,
|
seed: u64,
|
||||||
body_id: &str,
|
body_id: &str,
|
||||||
label: &str,
|
label: &str,
|
||||||
region: &RegionProfile,
|
region: &RegionProfile,
|
||||||
chunk_pos: (i32, i32),
|
chunk_pos: (i32, i32),
|
||||||
) {
|
) -> bool {
|
||||||
let chunk = derive_chunk_context(seed, body_id, region, chunk_pos);
|
let chunk = derive_chunk_context(seed, body_id, region, chunk_pos);
|
||||||
if !chunk.has_active_channel {
|
if !chunk.has_active_channel {
|
||||||
return; // No channel → monotonicity trivially satisfied.
|
return false; // No channel → monotonicity trivially satisfied.
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut max_dry_elev = i32::MIN;
|
let mut max_dry_elev = i32::MIN;
|
||||||
@@ -295,7 +301,9 @@ fn assert_drainage_monotonicity(
|
|||||||
chunk_pos.0,
|
chunk_pos.0,
|
||||||
chunk_pos.1
|
chunk_pos.1
|
||||||
);
|
);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -311,9 +319,15 @@ fn law_drainage_monotonicity_alluvial_sweep() {
|
|||||||
Some(15.0),
|
Some(15.0),
|
||||||
VegetationClass::Forest,
|
VegetationClass::Forest,
|
||||||
);
|
);
|
||||||
|
let mut checked = false;
|
||||||
for (cx, cy) in [(0, 0), (1, 0), (0, 1), (4, 4), (8, 3)] {
|
for (cx, cy) in [(0, 0), (1, 0), (0, 1), (4, 4), (8, 3)] {
|
||||||
assert_drainage_monotonicity(42, "GJ144d", "AlluvialPlain", ®ion, (cx, cy));
|
checked |= assert_drainage_monotonicity(42, "GJ144d", "AlluvialPlain", ®ion, (cx, cy));
|
||||||
}
|
}
|
||||||
|
assert!(
|
||||||
|
checked,
|
||||||
|
"AlluvialPlain sweep exercised no wet tiles — the monotonicity law was never \
|
||||||
|
actually checked; a derivation regression could silently pass this test."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -329,9 +343,15 @@ fn law_drainage_monotonicity_meander_sweep() {
|
|||||||
Some(14.0),
|
Some(14.0),
|
||||||
VegetationClass::Forest,
|
VegetationClass::Forest,
|
||||||
);
|
);
|
||||||
|
let mut checked = false;
|
||||||
for (cx, cy) in [(0, 0), (2, 1), (5, 5)] {
|
for (cx, cy) in [(0, 0), (2, 1), (5, 5)] {
|
||||||
assert_drainage_monotonicity(99, "GJ447c", "MeanderReach", ®ion, (cx, cy));
|
checked |= assert_drainage_monotonicity(99, "GJ447c", "MeanderReach", ®ion, (cx, cy));
|
||||||
}
|
}
|
||||||
|
assert!(
|
||||||
|
checked,
|
||||||
|
"MeanderReach sweep exercised no wet tiles — the monotonicity law was never \
|
||||||
|
actually checked; a derivation regression could silently pass this test."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -353,30 +373,45 @@ fn law_drainage_monotonicity_fjord_floor_at_sea_level() {
|
|||||||
let mut deep_elevs: Vec<i32> = vec![];
|
let mut deep_elevs: Vec<i32> = vec![];
|
||||||
let mut dry_elevs: Vec<i32> = vec![];
|
let mut dry_elevs: Vec<i32> = vec![];
|
||||||
|
|
||||||
for dx in 0..64i32 {
|
// Scan the full 64×64 chunk, not a single row — the deep-water trough axis is
|
||||||
let col = derive_voxel_column(42, "fjord_body", ®ion, &chunk, dx, 32);
|
// not guaranteed to intersect any fixed Y, so a single-row probe could miss it
|
||||||
match col.water {
|
// entirely and silently pass without ever checking the sea-level claim.
|
||||||
Water::Deep => deep_elevs.push(col.elevation_m),
|
for dy in 0..64i32 {
|
||||||
Water::Dry => dry_elevs.push(col.elevation_m),
|
for dx in 0..64i32 {
|
||||||
_ => {}
|
let col = derive_voxel_column(42, "fjord_body", ®ion, &chunk, dx, dy);
|
||||||
|
match col.water {
|
||||||
|
Water::Deep => deep_elevs.push(col.elevation_m),
|
||||||
|
Water::Dry => dry_elevs.push(col.elevation_m),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !deep_elevs.is_empty() && !dry_elevs.is_empty() {
|
// A glaciated fjord chunk MUST carve a deep-water trough — if it doesn't, the
|
||||||
let max_deep = *deep_elevs.iter().max().unwrap();
|
// derivation regressed and the sea-level law below would never run. Fail loudly.
|
||||||
let min_dry = *dry_elevs.iter().min().unwrap();
|
assert!(
|
||||||
// Fjord floor (deep water) must be below wall elevation.
|
!deep_elevs.is_empty(),
|
||||||
assert!(
|
"§8 FjordWall: no deep-water tiles found in the fjord chunk — the trough \
|
||||||
max_deep <= min_dry,
|
derivation regressed; the sea-level floor law was never exercised."
|
||||||
"§8 FjordWall drainage: deep-water max elev {max_deep} m must be \
|
);
|
||||||
≤ dry wall min elev {min_dry} m"
|
assert!(
|
||||||
);
|
!dry_elevs.is_empty(),
|
||||||
// Fjord floor must be near sea level (D-239 §8).
|
"§8 FjordWall: no dry wall tiles found in the fjord chunk."
|
||||||
assert!(
|
);
|
||||||
max_deep <= 5,
|
|
||||||
"§8 FjordWall: deep-water floor elev {max_deep} m must be near sea level (≤5 m)"
|
let max_deep = *deep_elevs.iter().max().unwrap();
|
||||||
);
|
let min_dry = *dry_elevs.iter().min().unwrap();
|
||||||
}
|
// Fjord floor (deep water) must be below wall elevation.
|
||||||
|
assert!(
|
||||||
|
max_deep <= min_dry,
|
||||||
|
"§8 FjordWall drainage: deep-water max elev {max_deep} m must be \
|
||||||
|
≤ dry wall min elev {min_dry} m"
|
||||||
|
);
|
||||||
|
// Fjord floor must be near sea level (D-239 §8).
|
||||||
|
assert!(
|
||||||
|
max_deep <= 5,
|
||||||
|
"§8 FjordWall: deep-water floor elev {max_deep} m must be near sea level (≤5 m)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -392,9 +427,16 @@ fn law_drainage_monotonicity_braided_delta() {
|
|||||||
Some(18.0),
|
Some(18.0),
|
||||||
VegetationClass::Scrub,
|
VegetationClass::Scrub,
|
||||||
);
|
);
|
||||||
|
let mut checked = false;
|
||||||
for (cx, cy) in [(0, 0), (1, 1)] {
|
for (cx, cy) in [(0, 0), (1, 1)] {
|
||||||
assert_drainage_monotonicity(17, "delta_body", "BraidedDelta", ®ion, (cx, cy));
|
checked |=
|
||||||
|
assert_drainage_monotonicity(17, "delta_body", "BraidedDelta", ®ion, (cx, cy));
|
||||||
}
|
}
|
||||||
|
assert!(
|
||||||
|
checked,
|
||||||
|
"BraidedDelta sweep exercised no wet tiles — the monotonicity law was never \
|
||||||
|
actually checked; a derivation regression could silently pass this test."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── §8 Law 2: Lithology → Landform ──────────────────────────────────────────
|
// ── §8 Law 2: Lithology → Landform ──────────────────────────────────────────
|
||||||
@@ -1433,34 +1475,6 @@ fn make_region(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a minimal flat `TerrainAnalysis` for use in body-level derive calls.
|
|
||||||
/// Same geometry as the region_profile.rs unit tests (64×32 grid, sea_level=0.3).
|
|
||||||
///
|
|
||||||
/// Retained as the gentle-slope counterpart to `make_dry_terrain_analysis` (which
|
|
||||||
/// the validation bodies currently use); kept for future morphology-gate tests that
|
|
||||||
/// need a sloped, partially-oceanic grid rather than the flat all-dry one.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn make_test_terrain_analysis() -> TerrainAnalysis {
|
|
||||||
let (w, h) = (64u32, 32u32);
|
|
||||||
let n = (w * h) as usize;
|
|
||||||
let data: Vec<f32> = (0..n)
|
|
||||||
.map(|i| {
|
|
||||||
let r = (i / w as usize) as f32 / h as f32;
|
|
||||||
let c = (i % w as usize) as f32 / w as f32;
|
|
||||||
(r * 0.6 + c * 0.4).min(1.0)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let hm = BodyHeightmap {
|
|
||||||
body_id: "test".into(),
|
|
||||||
width: w,
|
|
||||||
height: h,
|
|
||||||
data,
|
|
||||||
sea_level: 0.3,
|
|
||||||
};
|
|
||||||
let dr = drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level);
|
|
||||||
TerrainAnalysis::analyze(&hm, &dr)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a flat all-dry `TerrainAnalysis` for validation body derivation.
|
/// Build a flat all-dry `TerrainAnalysis` for validation body derivation.
|
||||||
///
|
///
|
||||||
/// All cells have elevation = 0.5 with sea_level = 0.3 → ocean_fraction_q = 0 at
|
/// All cells have elevation = 0.5 with sea_level = 0.3 → ocean_fraction_q = 0 at
|
||||||
|
|||||||
Reference in New Issue
Block a user