test(simulation): Phase-4 hardening — deferred #953/#963 review gaps (T-964)

The verified-still-open coverage list: per-type attractor reachability
fixtures (LakeShore via enclosed depression, PassEntrance via crafted
saddle, PlainCenter via flat terrain, RiverCrossing via confluence) plus
thin_by_spacing behavior (collision, strict-< boundary, equirectangular
column wrap); heightmap 8-bit decode, sea_level passthrough, downsample
identity and zero-target early-return; drainage area_pct bit-for-bit
determinism plus the isolated-basin-fallback divergence comment (Tyre
N1, citing the pre-#953 behavior it deliberately departs from); the
layer1 mountain-branch pairing test (investigated first — the cascade
test supplies a mountain pool but only ever asserted river counts, a
genuine gap); an importer idempotency test covering atlas_city_names
AND atlas_feature_names plus the Sol exemption, wired into
make test-tooling; and the oasis_water dilation radius scaled by
GRID_W/512 (Tyre N2, hash-stable). One stale item dropped per the
refinement trim (test_sim_determinism wiring — already done).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 18:58:53 +02:00
co-authored by Claude Fable 5
parent e0d120e555
commit f81622bbf0
7 changed files with 626 additions and 3 deletions
+234
View File
@@ -900,4 +900,238 @@ mod tests {
"RiverMouth attractors must survive the cap when mouths exist"
);
}
// -----------------------------------------------------------------------
// Per-type attractor reachability (T-964): crafted heightmaps that
// guarantee at least one attractor of the named type, isolating each
// extraction branch instead of relying on the slope/sine fixtures above
// (which reliably exercise RiverMouth/CoastalAccess/ValleyFloor, but never
// guarantee LakeShore/PassEntrance/PlainCenter/RiverCrossing).
// -----------------------------------------------------------------------
#[test]
fn lake_shore_reachable_via_enclosed_depression() {
// Two separate below-sea-level components: a wide strip along the
// west edge (the largest — becomes ocean) and a small isolated pit
// elsewhere (smaller — becomes an enclosed lake, D-209/compute_lake_mask).
// Land cells 8-adjacent to the pit must classify LakeShore.
let (w, h) = (32usize, 16usize);
let mut data = vec![0.6f32; w * h];
for r in 0..h {
for c in 0..4 {
data[r * w + c] = 0.1; // wide ocean strip
}
}
for r in 6..8 {
for c in 16..18 {
data[r * w + c] = 0.1; // small isolated pit, far from the ocean
}
}
let heightmap = hm(data, w as u32, h as u32, 0.3);
let dr = drainage::analyze(&heightmap.data, w as u32, h as u32, 0.3);
let ta = TerrainAnalysis::analyze(&heightmap, &dr);
assert!(
ta.lake_mask.iter().any(|&x| x),
"fixture sanity: the isolated pit must register as a lake, not ocean"
);
let a = extract_attractors(&heightmap, &dr, &ta);
assert!(
a.iter()
.any(|x| x.attractor_type == AttractorType::LakeShore),
"land adjacent to an enclosed lake must classify LakeShore"
);
}
#[test]
fn pass_entrance_reachable_via_morphological_saddle() {
// Classic saddle: the 8-ring around the center alternates high/low
// going clockwise (N,NE,E,SE,S,SW,W,NW), giving 8 sign transitions
// (is_saddle requires >= 4). Whole grid is high-elevation land so the
// saddle's elev_pct clears the >= 0.5 PassEntrance gate.
let (w, h) = (32usize, 16usize);
let (cr, cc) = (h / 2, w / 2);
let mut data = vec![0.7f32; w * h];
data[cr * w + cc] = 0.75; // the saddle point itself
const RING: [(i32, i32); 8] = [
(-1, 0),
(-1, 1),
(0, 1),
(1, 1),
(1, 0),
(1, -1),
(0, -1),
(-1, -1),
];
let ring_vals = [0.95, 0.55, 0.95, 0.55, 0.95, 0.55, 0.95, 0.55];
for (k, &(dr_off, dc_off)) in RING.iter().enumerate() {
let rr = (cr as i32 + dr_off) as usize;
let cc_ = (cc as i32 + dc_off) as usize;
data[rr * w + cc_] = ring_vals[k];
}
let heightmap = hm(data, w as u32, h as u32, 0.0);
let dr = drainage::analyze(&heightmap.data, w as u32, h as u32, 0.0);
let ta = TerrainAnalysis::analyze(&heightmap, &dr);
assert!(
ta.elev_pct[cr * w + cc] >= 0.5,
"fixture sanity: saddle point must clear the PassEntrance elev_pct gate"
);
let a = extract_attractors(&heightmap, &dr, &ta);
assert!(
a.iter()
.any(|x| x.attractor_type == AttractorType::PassEntrance),
"a genuine morphological saddle at high elevation must classify PassEntrance"
);
}
#[test]
fn plain_center_reachable_via_flat_uniform_terrain() {
// A uniformly flat, non-ocean grid: slope_deg is 0 everywhere (well
// under the < 2.0 PlainCenter gate), so at least one cell survives
// thin_by_spacing as PlainCenter even where ValleyFloor also
// competes for the uniform elev_pct=0.5 rank tie.
let (w, h) = (32usize, 16usize);
let data = vec![0.9f32; w * h];
let heightmap = hm(data, w as u32, h as u32, 0.0);
let dr = drainage::analyze(&heightmap.data, w as u32, h as u32, 0.0);
let ta = TerrainAnalysis::analyze(&heightmap, &dr);
let a = extract_attractors(&heightmap, &dr, &ta);
assert!(
a.iter()
.any(|x| x.attractor_type == AttractorType::PlainCenter),
"flat, non-ocean terrain must produce at least one PlainCenter attractor"
);
}
#[test]
fn river_crossing_reachable_via_confluence() {
// Two V-shaped tributary valleys (west + east branches) converge into
// a single trunk valley at (confluence_row, confluence_col) — the
// trunk cell has 2+ river-cell inflows, so `drainage::analyze` must
// report it as a confluence (drainage.rs's own D8 confluence rule),
// and extract_attractors must tag it RiverCrossing.
let (w, h) = (64usize, 64usize);
let confluence_col = (w / 2) as f32;
let confluence_row = (h / 2) as f32;
let data: Vec<f32> = (0..(w * h))
.map(|i| {
let r = (i / w) as f32;
let c = (i % w) as f32;
if r <= confluence_row {
// Upstream: two separate branches either side of the
// confluence column, each sloping down toward it.
let branch_center = if c < confluence_col {
confluence_col * 0.5
} else {
confluence_col * 1.5
};
let lateral = (c - branch_center).abs() / w as f32;
let downstream = (confluence_row - r) / h as f32;
(0.3 + lateral * 1.5 - downstream * 0.4).clamp(0.0, 1.0)
} else {
// Downstream: single widening trunk valley.
let lateral = (c - confluence_col).abs() / w as f32;
let downstream = (r - confluence_row) / h as f32;
(0.3 + lateral * 1.5 - downstream * 0.6).clamp(0.0, 1.0)
}
})
.collect();
let heightmap = hm(data, w as u32, h as u32, 0.0);
let dr = drainage::analyze(&heightmap.data, w as u32, h as u32, 0.0);
assert!(
!dr.river_network.confluences.is_empty(),
"fixture sanity: the converging-tributary fixture must produce a confluence"
);
let ta = TerrainAnalysis::analyze(&heightmap, &dr);
let a = extract_attractors(&heightmap, &dr, &ta);
assert!(
a.iter()
.any(|x| x.attractor_type == AttractorType::RiverCrossing),
"a genuine D8 confluence must classify RiverCrossing"
);
}
// -----------------------------------------------------------------------
// thin_by_spacing behavior (T-964): spacing collisions + equirectangular
// column wrap.
// -----------------------------------------------------------------------
#[test]
fn thin_by_spacing_drops_close_candidates_keeps_strongest() {
// Three candidates within MIN_SPACING (12) of each other: only the
// strongest should survive; a fourth, far-away candidate is
// independent and must survive alongside it.
let claimed = vec![false; 64 * 64];
let cands = vec![
(10usize, 10usize, 0.5f32),
(10usize, 15usize, 0.9f32), // strongest, within spacing of the other two
(15usize, 10usize, 0.3f32),
(50usize, 50usize, 0.4f32), // far away — independent, must survive
];
let kept = thin_by_spacing(cands, &claimed, 64);
assert_eq!(
kept.len(),
2,
"expected exactly 2 survivors (the strongest of the clustered trio + the \
far-away independent point), got {kept:?}"
);
assert!(
kept.contains(&(10, 15, 0.9)),
"the strongest candidate in the cluster must survive: {kept:?}"
);
assert!(
kept.contains(&(50, 50, 0.4)),
"the far-away independent candidate must survive: {kept:?}"
);
}
#[test]
fn thin_by_spacing_respects_exact_spacing_boundary() {
// Chebyshev distance exactly MIN_SPACING (12) apart must NOT collide
// (the check is `dr.max(dc) < MIN_SPACING`, a strict less-than) — both
// survive. One cell short of that (11) must collide — only the
// stronger survives.
let claimed = vec![false; 64 * 64];
let at_boundary = vec![(0usize, 0usize, 0.5f32), (12usize, 0usize, 0.5f32)];
let kept_boundary = thin_by_spacing(at_boundary, &claimed, 64);
assert_eq!(
kept_boundary.len(),
2,
"cells exactly MIN_SPACING apart must both survive (strict <): {kept_boundary:?}"
);
let inside_spacing = vec![(0usize, 0usize, 0.5f32), (11usize, 0usize, 0.9f32)];
let kept_inside = thin_by_spacing(inside_spacing, &claimed, 64);
assert_eq!(
kept_inside.len(),
1,
"cells 1 short of MIN_SPACING must collide, keeping only the stronger: \
{kept_inside:?}"
);
assert_eq!(kept_inside[0], (11, 0, 0.9));
}
#[test]
fn thin_by_spacing_column_wrap_does_not_collide_across_the_seam() {
// thin_by_spacing itself is a pure Chebyshev-distance thinner over
// (row, col) pairs — it has NO knowledge of the equirectangular
// column wrap (unlike NB8-based neighbor walks elsewhere in this
// file, which wrap explicitly via `wrap_col`). Two candidates at
// opposite ends of a wide grid (col 0 and col w-1) are geographically
// adjacent on the globe but numerically far apart in (row, col)
// space, so thin_by_spacing must NOT treat them as colliding — both
// survive. This pins the current (non-wrap-aware) behavior so a
// future change to make thinning wrap-aware is a deliberate,
// visible decision, not a silent behavior drift.
let w = 64usize;
let claimed = vec![false; w * 64];
let cands = vec![(5usize, 0usize, 0.5f32), (5usize, w - 1, 0.6f32)];
let kept = thin_by_spacing(cands, &claimed, w);
assert_eq!(
kept.len(),
2,
"column-wrap-adjacent candidates are numerically far apart in (row, col) \
space — thin_by_spacing must not collide them: {kept:?}"
);
}
}