fix(simulation): region-anchor channels and linear landforms (T-1040, T-1041)
derive_chunk_context now derives channel_anchor_m and coast_anchor_m from the Region-or-higher seed path (D-239 §10); all five voxel placement sites measure distance to these anchors in continuous world coordinates: - T-1040: compute_channel_state / compute_meander_reach_channel / in_levee_band subtract the region anchor instead of measuring from the world origin axis — active channels now exist in their chunks anywhere on the body, continuous across chunk boundaries. has_active_channel is honest in both directions (band-crossing gate; floor covers the BraidedDelta belt at short wavelengths). - T-1041: fjord/cliff/gorge/delta drop their rem_euclid(64) chunk-frame folds — one valley/coastline/fan per region instead of one per chunk. D-239 §9 chokepoint widths preserved; two i32 wall-rise overflows on region-scale distances fixed by clamping before multiply. Harness: 6 new believability tests (channel presence + gate honesty at chunk (1000,-750), cross-boundary continuity, one-landform-per-region, braid-belt confinement); drainage sweeps strengthened with anchor-band chunks. Voxel golden regenerated deliberately: case A now pins a genuinely in-channel Shallow voxel on the anchor, case C a Deep trough voxel matching its label; case B byte-identical and L0/L1 cascade golden untouched (no upstream leak). Review: approved, three minors addressed (truncation-convention docs, delta gate floor, case C relocation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,7 @@
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
use settled_reach_server::atlas::chunk_context::derive_chunk_context;
|
||||
use settled_reach_server::atlas::chunk_context::{derive_chunk_context, BasinDirection, ChunkPos};
|
||||
use settled_reach_server::atlas::drainage;
|
||||
use settled_reach_server::atlas::features::TerrainAnalysis;
|
||||
use settled_reach_server::atlas::heightmap::BodyHeightmap;
|
||||
@@ -100,6 +100,37 @@ fn derive_golden(
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the chunk position + tile coordinates sitting ON the region's
|
||||
/// channel anchor at a given along-axis chunk index (T-1040/T-1041): feature
|
||||
/// placement is region-anchored, so the golden pins a voxel on the anchor
|
||||
/// column. Relocates automatically if the anchor derivation changes — which
|
||||
/// flips the golden values anyway.
|
||||
///
|
||||
/// `along_chunk` must stay within region (0, 0) — i.e. in `0..16` — so the
|
||||
/// probed anchor belongs to the same region as the returned chunk.
|
||||
fn anchor_golden_pos(
|
||||
seed: u64,
|
||||
body_id: &str,
|
||||
region: &RegionProfile,
|
||||
along_chunk: i32,
|
||||
) -> ((i32, i32), i32, i32) {
|
||||
assert!(
|
||||
(0..16).contains(&along_chunk),
|
||||
"along_chunk must stay within region (0, 0)"
|
||||
);
|
||||
let probe = derive_chunk_context(seed, body_id, region, (0, 0));
|
||||
let anchor = probe.channel_anchor_m;
|
||||
let along_tile = along_chunk * 64 + 32;
|
||||
match probe.basin_direction {
|
||||
BasinDirection::North | BasinDirection::South => {
|
||||
((anchor.div_euclid(64), along_chunk), anchor, along_tile)
|
||||
}
|
||||
BasinDirection::East | BasinDirection::West => {
|
||||
((along_chunk, anchor.div_euclid(64)), along_tile, anchor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The three fixed golden inputs. Varied families and climate states.
|
||||
fn golden_cases() -> Vec<(
|
||||
&'static str,
|
||||
@@ -110,26 +141,45 @@ fn golden_cases() -> Vec<(
|
||||
i32,
|
||||
i32,
|
||||
)> {
|
||||
// Cases A and C pin voxels on the region channel anchor (T-1040/T-1041):
|
||||
// the channel/trough centreline is region-anchored, not at the world
|
||||
// origin (A) or the chunk centre (C).
|
||||
let alluvial_region = make_region(
|
||||
MorphologyZone::AlluvialPlain,
|
||||
TectonicClass::Active,
|
||||
GlaciationGrade::None,
|
||||
6,
|
||||
22,
|
||||
18,
|
||||
68,
|
||||
Some(12.0),
|
||||
VegetationClass::Forest,
|
||||
);
|
||||
let (alluvial_chunk_pos, alluvial_tx, alluvial_ty) =
|
||||
anchor_golden_pos(0xdeadbeef_cafebabe_u64, "GJ144d", &alluvial_region, 12);
|
||||
let fjord_region = make_region(
|
||||
MorphologyZone::Fjord,
|
||||
TectonicClass::Active,
|
||||
GlaciationGrade::Moderate,
|
||||
55,
|
||||
60,
|
||||
28,
|
||||
55,
|
||||
Some(-8.0),
|
||||
VegetationClass::Barren,
|
||||
);
|
||||
let (fjord_chunk_pos, fjord_tx, fjord_ty) =
|
||||
anchor_golden_pos(0xfeedface_0badc0de_u64, "GJ447c", &fjord_region, 5);
|
||||
vec![
|
||||
// Case A: AlluvialPlain — temperate forest, active channel.
|
||||
// Case A: AlluvialPlain — temperate forest, on the active-channel anchor.
|
||||
(
|
||||
"alluvial_forest_active_channel",
|
||||
0xdeadbeef_cafebabe_u64,
|
||||
"GJ144d",
|
||||
make_region(
|
||||
MorphologyZone::AlluvialPlain,
|
||||
TectonicClass::Active,
|
||||
GlaciationGrade::None,
|
||||
6,
|
||||
22,
|
||||
18,
|
||||
68,
|
||||
Some(12.0),
|
||||
VegetationClass::Forest,
|
||||
),
|
||||
(10, 20),
|
||||
640,
|
||||
1280,
|
||||
alluvial_region,
|
||||
alluvial_chunk_pos,
|
||||
alluvial_tx,
|
||||
alluvial_ty,
|
||||
),
|
||||
// Case B: LavaField — barren volcanic, no channel.
|
||||
(
|
||||
@@ -151,26 +201,16 @@ fn golden_cases() -> Vec<(
|
||||
320,
|
||||
448,
|
||||
),
|
||||
// Case C: FjordWall — glaciated, rocky walls, deep water in trough.
|
||||
// Case C: FjordWall — glaciated, rocky walls; tile on the region-anchored
|
||||
// trough centreline (Deep water unless the warp nudges it onto the floor edge).
|
||||
(
|
||||
"fjord_wall_glaciated",
|
||||
0xfeedface_0badc0de_u64,
|
||||
"GJ447c",
|
||||
make_region(
|
||||
MorphologyZone::Fjord,
|
||||
TectonicClass::Active,
|
||||
GlaciationGrade::Moderate,
|
||||
55,
|
||||
60,
|
||||
28,
|
||||
55,
|
||||
Some(-8.0),
|
||||
VegetationClass::Barren,
|
||||
),
|
||||
(14, 8),
|
||||
// Centre of the chunk (fjord trough) — should be Deep water, Rock.
|
||||
14 * 64 + 32,
|
||||
8 * 64 + 32,
|
||||
fjord_region,
|
||||
fjord_chunk_pos,
|
||||
fjord_tx,
|
||||
fjord_ty,
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -320,8 +360,12 @@ fn law_drainage_monotonicity_alluvial_sweep() {
|
||||
VegetationClass::Forest,
|
||||
);
|
||||
let mut checked = false;
|
||||
for (cx, cy) in [(0, 0), (1, 0), (0, 1), (4, 4), (8, 3)] {
|
||||
checked |= assert_drainage_monotonicity(42, "GJ144d", "AlluvialPlain", ®ion, (cx, cy));
|
||||
// Sweep a spread of region chunks plus the channel-anchor band (T-1040:
|
||||
// the channel is region-anchored, so only band chunks carry wet tiles).
|
||||
let mut positions = vec![(0, 0), (1, 0), (0, 1), (4, 4), (8, 3)];
|
||||
positions.extend(anchor_band_chunks(42, "GJ144d", ®ion, (0, 0)));
|
||||
for pos in positions {
|
||||
checked |= assert_drainage_monotonicity(42, "GJ144d", "AlluvialPlain", ®ion, pos);
|
||||
}
|
||||
assert!(
|
||||
checked,
|
||||
@@ -344,8 +388,10 @@ fn law_drainage_monotonicity_meander_sweep() {
|
||||
VegetationClass::Forest,
|
||||
);
|
||||
let mut checked = false;
|
||||
for (cx, cy) in [(0, 0), (2, 1), (5, 5)] {
|
||||
checked |= assert_drainage_monotonicity(99, "GJ447c", "MeanderReach", ®ion, (cx, cy));
|
||||
let mut positions = vec![(0, 0), (2, 1), (5, 5)];
|
||||
positions.extend(anchor_band_chunks(99, "GJ447c", ®ion, (0, 0)));
|
||||
for pos in positions {
|
||||
checked |= assert_drainage_monotonicity(99, "GJ447c", "MeanderReach", ®ion, pos);
|
||||
}
|
||||
assert!(
|
||||
checked,
|
||||
@@ -369,16 +415,27 @@ fn law_drainage_monotonicity_fjord_floor_at_sea_level() {
|
||||
Some(-8.0),
|
||||
VegetationClass::Barren,
|
||||
);
|
||||
let chunk = derive_chunk_context(42, "fjord_body", ®ion, (0, 0));
|
||||
// The fjord trough is region-anchored (T-1041): scan the chunk whose
|
||||
// cross-range contains the channel anchor — only that chunk column carries
|
||||
// the deep-water trough.
|
||||
let probe = derive_chunk_context(42, "fjord_body", ®ion, (0, 0));
|
||||
let anchor_idx = probe.channel_anchor_m.div_euclid(64);
|
||||
let chunk_pos = match probe.basin_direction {
|
||||
BasinDirection::North | BasinDirection::South => (anchor_idx, 0),
|
||||
BasinDirection::East | BasinDirection::West => (0, anchor_idx),
|
||||
};
|
||||
let chunk = derive_chunk_context(42, "fjord_body", ®ion, chunk_pos);
|
||||
let (base_x, base_y) = (chunk_pos.0 * 64, chunk_pos.1 * 64);
|
||||
let mut deep_elevs: Vec<i32> = vec![];
|
||||
let mut dry_elevs: Vec<i32> = vec![];
|
||||
|
||||
// Scan the full 64×64 chunk, not a single row — the deep-water trough axis is
|
||||
// not guaranteed to intersect any fixed Y, so a single-row probe could miss it
|
||||
// entirely and silently pass without ever checking the sea-level claim.
|
||||
// not guaranteed to intersect any fixed row, so a single-row probe could miss
|
||||
// it entirely and silently pass without ever checking the sea-level claim.
|
||||
for dy in 0..64i32 {
|
||||
for dx in 0..64i32 {
|
||||
let col = derive_voxel_column(42, "fjord_body", ®ion, &chunk, dx, dy);
|
||||
let col =
|
||||
derive_voxel_column(42, "fjord_body", ®ion, &chunk, base_x + dx, base_y + dy);
|
||||
match col.water {
|
||||
Water::Deep => deep_elevs.push(col.elevation_m),
|
||||
Water::Dry => dry_elevs.push(col.elevation_m),
|
||||
@@ -428,9 +485,12 @@ fn law_drainage_monotonicity_braided_delta() {
|
||||
VegetationClass::Scrub,
|
||||
);
|
||||
let mut checked = false;
|
||||
for (cx, cy) in [(0, 0), (1, 1)] {
|
||||
checked |=
|
||||
assert_drainage_monotonicity(17, "delta_body", "BraidedDelta", ®ion, (cx, cy));
|
||||
// The braid belt sits on the region's channel anchor (T-1041) — sweep the
|
||||
// anchor band so the law is exercised on real thread tiles.
|
||||
let mut positions = vec![(0, 0), (1, 1)];
|
||||
positions.extend(anchor_band_chunks(17, "delta_body", ®ion, (0, 0)));
|
||||
for pos in positions {
|
||||
checked |= assert_drainage_monotonicity(17, "delta_body", "BraidedDelta", ®ion, pos);
|
||||
}
|
||||
assert!(
|
||||
checked,
|
||||
@@ -898,6 +958,375 @@ fn law_climate_vegetation_airless_always_absent() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// §2b — Region-anchored feature placement (T-1040 / T-1041, D-239 §10)
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// T-1040: channel centrelines were anchored to the world x=0/y=0 axis — every
|
||||
// chunk of a watered region claimed has_active_channel while channel voxels
|
||||
// existed only near the world origin. T-1041: fjord/cliff/gorge/delta folded
|
||||
// world coordinates into the 64 m chunk frame — region-scale landforms
|
||||
// repeated every chunk. Both are fixed by region-anchored feature axes
|
||||
// (`channel_anchor_m` / `coast_anchor_m`); these tests pin the placement
|
||||
// contract far from the origin, at the ticket's example chunk (1000, −750).
|
||||
|
||||
#[test]
|
||||
fn channel_present_in_active_chunks_far_from_origin() {
|
||||
// T-1040 (a): a has_active_channel chunk at an arbitrary large world
|
||||
// offset contains in-channel voxels — and the gate is honest in both
|
||||
// directions (wet ⇒ gated, ungated ⇒ dry). Sweeps the 16 cross-columns of
|
||||
// the region containing chunk (1000, −750) at that chunk's along index.
|
||||
let region = make_region(
|
||||
MorphologyZone::AlluvialPlain,
|
||||
TectonicClass::Stable,
|
||||
GlaciationGrade::None,
|
||||
5,
|
||||
20,
|
||||
18,
|
||||
60,
|
||||
Some(15.0),
|
||||
VegetationClass::Forest,
|
||||
);
|
||||
let (seed, body) = (42u64, "GJ144d");
|
||||
let ns = basin_is_ns(seed, body, ®ion, (1000, -750));
|
||||
let cross_base = if ns {
|
||||
(1000 >> 4) << 4
|
||||
} else {
|
||||
(-750i32 >> 4) << 4
|
||||
};
|
||||
let mut any_gated_wet = false;
|
||||
let mut any_gate_off = false;
|
||||
let mut total_wet = 0usize;
|
||||
for i in 0..16 {
|
||||
let pos: ChunkPos = if ns {
|
||||
(cross_base + i, -750)
|
||||
} else {
|
||||
(1000, cross_base + i)
|
||||
};
|
||||
let chunk = derive_chunk_context(seed, body, ®ion, pos);
|
||||
let (bx, by) = (pos.0 * 64, pos.1 * 64);
|
||||
let mut wet = 0usize;
|
||||
for dy in 0..64i32 {
|
||||
for dx in 0..64i32 {
|
||||
let col = derive_voxel_column(seed, body, ®ion, &chunk, bx + dx, by + dy);
|
||||
if col.water != Water::Dry {
|
||||
wet += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
total_wet += wet;
|
||||
if chunk.has_active_channel {
|
||||
any_gated_wet |= wet > 0;
|
||||
} else {
|
||||
any_gate_off = true;
|
||||
assert_eq!(
|
||||
wet, 0,
|
||||
"T-1040: chunk {pos:?} has no active channel but contains {wet} wet voxels \
|
||||
— the chunk gate and the voxel placement disagree"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
total_wet > 0,
|
||||
"T-1040: the region at chunk (1000, −750) must contain channel voxels \
|
||||
(pre-fix: zero — channels existed only near the world-origin axis)"
|
||||
);
|
||||
assert!(
|
||||
any_gated_wet,
|
||||
"T-1040: at least one has_active_channel chunk must contain in-channel voxels"
|
||||
);
|
||||
assert!(
|
||||
any_gate_off,
|
||||
"T-1040: the channel band must not blanket the region — some chunks must gate off"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_continuous_across_chunk_boundary_far_from_origin() {
|
||||
// T-1040 (a): channel position is continuous across adjacent chunk pairs.
|
||||
// Every tile derives under its PRODUCTION covering chunk; the wet band's
|
||||
// midpoint may not jump at a 64 m along-boundary (chunk-frame dependence
|
||||
// would jump by up to a chunk width or drop out entirely).
|
||||
let region = make_region(
|
||||
MorphologyZone::AlluvialPlain,
|
||||
TectonicClass::Stable,
|
||||
GlaciationGrade::None,
|
||||
5,
|
||||
20,
|
||||
18,
|
||||
60,
|
||||
Some(15.0),
|
||||
VegetationClass::Forest,
|
||||
);
|
||||
let (seed, body) = (42u64, "GJ144d");
|
||||
let probe = derive_chunk_context(seed, body, ®ion, (1000, -750));
|
||||
let ns = matches!(
|
||||
probe.basin_direction,
|
||||
BasinDirection::North | BasinDirection::South
|
||||
);
|
||||
let anchor = probe.channel_anchor_m;
|
||||
// Wet-band midpoint of one world cross-row, sampling the full swept band.
|
||||
let row_mid = |along: i32| -> Option<i32> {
|
||||
let wet: Vec<i32> = (anchor - 220..anchor + 220)
|
||||
.filter(|&c| {
|
||||
derive_at_cross_along(seed, body, ®ion, ns, c, along).water != Water::Dry
|
||||
})
|
||||
.collect();
|
||||
wet.first().map(|f| (f + wet.last().unwrap()) / 2)
|
||||
};
|
||||
// An interior along-boundary of the region containing chunk (1000, −750):
|
||||
// between along-chunks −744 and −743 (N/S) or 1004 and 1005 (E/W).
|
||||
let boundary = if ns { -743 * 64 } else { 1005 * 64 };
|
||||
let mut prev: Option<i32> = None;
|
||||
for along in boundary - 4..boundary + 4 {
|
||||
let mid = row_mid(along).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"T-1040: row along={along} contains no wet tiles — the channel \
|
||||
dropped out at the chunk boundary {boundary}"
|
||||
)
|
||||
});
|
||||
if let Some(p) = prev {
|
||||
assert!(
|
||||
(mid - p).abs() <= 12,
|
||||
"T-1040: wet-band midpoint jumped {} m between adjacent rows \
|
||||
{} and {} (boundary {boundary}) — channel is not continuous",
|
||||
(mid - p).abs(),
|
||||
along - 1,
|
||||
along
|
||||
);
|
||||
}
|
||||
prev = Some(mid);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fjord_region_has_one_valley_spanning_chunks() {
|
||||
// T-1041 (b): a FjordWall region contains ONE deep-water trough spanning
|
||||
// its chunks — pre-fix a complete fjord cross-section repeated in every
|
||||
// 64 m chunk. Representative sweep: two far regions × three along rows.
|
||||
let region = make_region(
|
||||
MorphologyZone::Fjord,
|
||||
TectonicClass::Active,
|
||||
GlaciationGrade::Moderate,
|
||||
55,
|
||||
60,
|
||||
28,
|
||||
55,
|
||||
Some(-8.0),
|
||||
VegetationClass::Barren,
|
||||
);
|
||||
let (seed, body) = (42u64, "fjord_body");
|
||||
for region_chunk in [(640, -480), (-336, 992)] {
|
||||
let probe = derive_chunk_context(seed, body, ®ion, region_chunk);
|
||||
let ns = matches!(
|
||||
probe.basin_direction,
|
||||
BasinDirection::North | BasinDirection::South
|
||||
);
|
||||
let (cross_chunk, along_chunk) = if ns {
|
||||
(region_chunk.0, region_chunk.1)
|
||||
} else {
|
||||
(region_chunk.1, region_chunk.0)
|
||||
};
|
||||
let cross_base = (cross_chunk >> 4) * 1024;
|
||||
let along_base = (along_chunk >> 4) * 1024;
|
||||
let positions: Vec<i32> = (cross_base..cross_base + 1024).collect();
|
||||
for along in [along_base + 32, along_base + 512, along_base + 992] {
|
||||
let clusters = count_feature_clusters(
|
||||
&positions,
|
||||
|c| derive_at_cross_along(seed, body, ®ion, ns, c, along).water == Water::Deep,
|
||||
16,
|
||||
);
|
||||
assert_eq!(
|
||||
clusters, 1,
|
||||
"T-1041: FjordWall region {region_chunk:?} must contain exactly ONE \
|
||||
deep-water trough across its 1024 m cross extent at along={along} \
|
||||
(got {clusters}; pre-fix: one per 64 m chunk)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gorge_region_has_one_valley_spanning_chunks() {
|
||||
// T-1041 (b): an IncisedGorge region contains ONE shallow-floor gorge
|
||||
// spanning its chunks — not one per chunk.
|
||||
let region = make_region(
|
||||
MorphologyZone::MountainPass,
|
||||
TectonicClass::Active,
|
||||
GlaciationGrade::None,
|
||||
50,
|
||||
65,
|
||||
5,
|
||||
40,
|
||||
Some(5.0),
|
||||
VegetationClass::Scrub,
|
||||
);
|
||||
let (seed, body) = (42u64, "gorge_body");
|
||||
for region_chunk in [(640, -480), (-336, 992)] {
|
||||
let probe = derive_chunk_context(seed, body, ®ion, region_chunk);
|
||||
let ns = matches!(
|
||||
probe.basin_direction,
|
||||
BasinDirection::North | BasinDirection::South
|
||||
);
|
||||
let (cross_chunk, along_chunk) = if ns {
|
||||
(region_chunk.0, region_chunk.1)
|
||||
} else {
|
||||
(region_chunk.1, region_chunk.0)
|
||||
};
|
||||
let cross_base = (cross_chunk >> 4) * 1024;
|
||||
let along_base = (along_chunk >> 4) * 1024;
|
||||
let positions: Vec<i32> = (cross_base..cross_base + 1024).collect();
|
||||
for along in [along_base + 32, along_base + 512, along_base + 992] {
|
||||
let clusters = count_feature_clusters(
|
||||
&positions,
|
||||
|c| {
|
||||
derive_at_cross_along(seed, body, ®ion, ns, c, along).water == Water::Shallow
|
||||
},
|
||||
16,
|
||||
);
|
||||
assert_eq!(
|
||||
clusters, 1,
|
||||
"T-1041: IncisedGorge region {region_chunk:?} must contain exactly ONE \
|
||||
shallow gorge floor across its 1024 m cross extent at along={along} \
|
||||
(got {clusters}; pre-fix: one per 64 m chunk)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cliff_coast_one_continuous_coastline_per_region() {
|
||||
// T-1041 (b): a CliffCoast region has ONE continuous (warp-displaced)
|
||||
// coast line on the region-anchored face — pre-fix the cliff face sat at
|
||||
// intra-chunk offset 48–55 in every chunk, sawtoothing the coast at 64 m
|
||||
// pitch. Transect runs along the seaward (basin) axis across the region.
|
||||
let region = make_region(
|
||||
MorphologyZone::CliffCoast,
|
||||
TectonicClass::Active,
|
||||
GlaciationGrade::None,
|
||||
60,
|
||||
40,
|
||||
20,
|
||||
30,
|
||||
Some(10.0),
|
||||
VegetationClass::Scrub,
|
||||
);
|
||||
let (seed, body) = (42u64, "cliff_body");
|
||||
for region_chunk in [(656, -464), (-256, 768)] {
|
||||
let probe = derive_chunk_context(seed, body, ®ion, region_chunk);
|
||||
let coast = probe.coast_anchor_m;
|
||||
let ns = matches!(
|
||||
probe.basin_direction,
|
||||
BasinDirection::North | BasinDirection::South
|
||||
);
|
||||
let (cross_chunk, along_chunk) = if ns {
|
||||
(region_chunk.0, region_chunk.1)
|
||||
} else {
|
||||
(region_chunk.1, region_chunk.0)
|
||||
};
|
||||
let along_base = (along_chunk >> 4) * 1024;
|
||||
let cross_fixed = cross_chunk * 64 + 32;
|
||||
let positions: Vec<i32> = (along_base..along_base + 1024).collect();
|
||||
// Exactly one ocean cluster (the seaward side of the one coast line).
|
||||
let clusters = count_feature_clusters(
|
||||
&positions,
|
||||
|a| derive_at_cross_along(seed, body, ®ion, ns, cross_fixed, a).water == Water::Deep,
|
||||
16,
|
||||
);
|
||||
assert_eq!(
|
||||
clusters, 1,
|
||||
"T-1041: CliffCoast region {region_chunk:?} must have exactly ONE ocean \
|
||||
side (got {clusters} Deep clusters; pre-fix: one 64 m sawtooth per chunk)"
|
||||
);
|
||||
// Monotone coast: well inland of the face line → Dry; well seaward →
|
||||
// Deep. Margins absorb the ±8 m domain warp (face 0..8, ledge 8..13).
|
||||
for &a in &positions {
|
||||
// Signed seaward distance — mirrors generate_cliff_coast.
|
||||
let d = match probe.basin_direction {
|
||||
BasinDirection::North | BasinDirection::West => coast - a,
|
||||
BasinDirection::South | BasinDirection::East => a - coast,
|
||||
};
|
||||
let col = derive_at_cross_along(seed, body, ®ion, ns, cross_fixed, a);
|
||||
if d <= -9 {
|
||||
assert_eq!(
|
||||
col.water,
|
||||
Water::Dry,
|
||||
"T-1041: tile {} m inland of the coast line must be Dry (along={a})",
|
||||
-d
|
||||
);
|
||||
} else if d >= 21 {
|
||||
assert_eq!(
|
||||
col.water,
|
||||
Water::Deep,
|
||||
"T-1041: tile {d} m seaward of the coast line must be Deep (along={a})"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn braided_threads_confined_to_region_belt() {
|
||||
// T-1041: braided threads anastomose across the region-anchored fan belt
|
||||
// (anchor ± 32 m + thread width + warp) — pre-fix the same three threads
|
||||
// restarted in every 64 m chunk, spreading thread water across the whole
|
||||
// region's cross extent.
|
||||
let region = make_region(
|
||||
MorphologyZone::Delta,
|
||||
TectonicClass::Active,
|
||||
GlaciationGrade::None,
|
||||
3,
|
||||
8,
|
||||
25,
|
||||
50,
|
||||
Some(18.0),
|
||||
VegetationClass::Scrub,
|
||||
);
|
||||
let (seed, body) = (17u64, "delta_body");
|
||||
for region_chunk in [(800, -592)] {
|
||||
let probe = derive_chunk_context(seed, body, ®ion, region_chunk);
|
||||
let ns = matches!(
|
||||
probe.basin_direction,
|
||||
BasinDirection::North | BasinDirection::South
|
||||
);
|
||||
let anchor = probe.channel_anchor_m;
|
||||
let (cross_chunk, along_chunk) = if ns {
|
||||
(region_chunk.0, region_chunk.1)
|
||||
} else {
|
||||
(region_chunk.1, region_chunk.0)
|
||||
};
|
||||
let cross_base = (cross_chunk >> 4) * 1024;
|
||||
let along = along_chunk * 64 + 32;
|
||||
let wet: Vec<i32> = (cross_base..cross_base + 1024)
|
||||
.filter(|&c| {
|
||||
derive_at_cross_along(seed, body, ®ion, ns, c, along).water == Water::Shallow
|
||||
})
|
||||
.collect();
|
||||
assert!(
|
||||
!wet.is_empty(),
|
||||
"T-1041: BraidedDelta region {region_chunk:?} must contain thread water"
|
||||
);
|
||||
// Belt confinement: thread centres ∈ anchor ± 32, half-width ≤ 4,
|
||||
// warp ≤ 8 → all thread water within anchor ± 44.
|
||||
for &c in &wet {
|
||||
assert!(
|
||||
(c - anchor).abs() <= 44,
|
||||
"T-1041: thread water at cross={c} is {} m from the fan axis {anchor} \
|
||||
— outside the region belt (pre-fix: threads repeated every chunk)",
|
||||
(c - anchor).abs()
|
||||
);
|
||||
}
|
||||
// Braided, not single-thread: 1–3 thread clusters within the belt
|
||||
// (three threads, possibly merged where centres overlap; warp-scale
|
||||
// gap tolerance — the ±8 m warp punches small holes in a thread).
|
||||
let cluster_count = count_feature_clusters(&wet, |_| true, 8);
|
||||
assert!(
|
||||
(1..=3).contains(&cluster_count),
|
||||
"T-1041: expected 1–3 braided thread clusters in the belt, got {cluster_count}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// §3 — Per-family <5 ms/chunk budget (D-239 §10)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1446,6 +1875,92 @@ fn validation_gruenfeld_skipped_not_in_wiki() {
|
||||
// Shared helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Whether the region's basin runs north/south (cross axis = x). Region-scale
|
||||
/// property — identical for every chunk of the region containing `region_chunk`.
|
||||
fn basin_is_ns(seed: u64, body_id: &str, region: &RegionProfile, region_chunk: ChunkPos) -> bool {
|
||||
let probe = derive_chunk_context(seed, body_id, region, region_chunk);
|
||||
matches!(
|
||||
probe.basin_direction,
|
||||
BasinDirection::North | BasinDirection::South
|
||||
)
|
||||
}
|
||||
|
||||
/// The chunk positions of the channel-anchor band (anchor column ± 1) across
|
||||
/// the full along-extent of the region containing `region_chunk` (T-1040).
|
||||
///
|
||||
/// The channel/landform centreline is region-anchored: it lives in the anchor
|
||||
/// chunk column, swinging up to one meander amplitude sideways. The meander
|
||||
/// wavelength (≤ ~650 m) fits inside the region's 1024 m along-extent, so the
|
||||
/// centreline crosses the anchor column at least once — sweeping this band
|
||||
/// guarantees wet tiles are exercised somewhere in it.
|
||||
fn anchor_band_chunks(
|
||||
seed: u64,
|
||||
body_id: &str,
|
||||
region: &RegionProfile,
|
||||
region_chunk: ChunkPos,
|
||||
) -> Vec<ChunkPos> {
|
||||
let probe = derive_chunk_context(seed, body_id, region, region_chunk);
|
||||
let anchor_idx = probe.channel_anchor_m.div_euclid(64);
|
||||
let ns = matches!(
|
||||
probe.basin_direction,
|
||||
BasinDirection::North | BasinDirection::South
|
||||
);
|
||||
// Region base index on the along axis (16-chunk regions; arithmetic shift
|
||||
// = floor division, correct for negative chunks).
|
||||
let along_base = if ns {
|
||||
(region_chunk.1 >> 4) << 4
|
||||
} else {
|
||||
(region_chunk.0 >> 4) << 4
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
for cross in anchor_idx - 1..=anchor_idx + 1 {
|
||||
for j in along_base..along_base + 16 {
|
||||
out.push(if ns { (cross, j) } else { (j, cross) });
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Derive one voxel at world cross/along coordinates under the PRODUCTION
|
||||
/// covering chunk's context (per-tile chunk lookup — exactly what a consumer
|
||||
/// streaming the world does). Basis-aware: cross ⊥ basin, along ∥ basin.
|
||||
fn derive_at_cross_along(
|
||||
seed: u64,
|
||||
body_id: &str,
|
||||
region: &RegionProfile,
|
||||
ns: bool,
|
||||
cross: i32,
|
||||
along: i32,
|
||||
) -> settled_reach_server::atlas::voxel::VoxelColumn {
|
||||
let (tx, ty) = if ns { (cross, along) } else { (along, cross) };
|
||||
let chunk_pos = (tx.div_euclid(64), ty.div_euclid(64));
|
||||
let chunk = derive_chunk_context(seed, body_id, region, chunk_pos);
|
||||
derive_voxel_column(seed, body_id, region, &chunk, tx, ty)
|
||||
}
|
||||
|
||||
/// Count clusters of positions where `pred` holds across a cross/along
|
||||
/// transect, merging runs separated by gaps ≤ `gap_tolerance` (domain-warp
|
||||
/// jitter can fragment a single feature by a few metres; distinct per-chunk
|
||||
/// repeats are ≥ ~50 m apart and never merge).
|
||||
fn count_feature_clusters(
|
||||
positions: &[i32],
|
||||
hits: impl Fn(i32) -> bool,
|
||||
gap_tolerance: i32,
|
||||
) -> usize {
|
||||
let mut clusters = 0usize;
|
||||
let mut last_hit: Option<i32> = None;
|
||||
for &p in positions {
|
||||
if hits(p) {
|
||||
match last_hit {
|
||||
Some(prev) if p - prev <= gap_tolerance => {}
|
||||
_ => clusters += 1,
|
||||
}
|
||||
last_hit = Some(p);
|
||||
}
|
||||
}
|
||||
clusters
|
||||
}
|
||||
|
||||
/// Construct a `RegionProfile` directly from parameters, deriving the
|
||||
/// dependent fields (precipitation_class, river_threshold) consistently.
|
||||
fn make_region(
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
"label": "alluvial_forest_active_channel",
|
||||
"seed": 16045690984503098046,
|
||||
"body_id": "GJ144d",
|
||||
"tile_x": 640,
|
||||
"tile_y": 1280,
|
||||
"tile_x": 223,
|
||||
"tile_y": 800,
|
||||
"terrain": 0,
|
||||
"vegetation": 1,
|
||||
"water": 0,
|
||||
"elevation_m": 11,
|
||||
"vegetation": 4,
|
||||
"water": 1,
|
||||
"elevation_m": 8,
|
||||
"cover": 0
|
||||
},
|
||||
{
|
||||
@@ -27,12 +27,12 @@
|
||||
"label": "fjord_wall_glaciated",
|
||||
"seed": 18369614217980264670,
|
||||
"body_id": "GJ447c",
|
||||
"tile_x": 928,
|
||||
"tile_y": 544,
|
||||
"tile_x": 228,
|
||||
"tile_y": 352,
|
||||
"terrain": 3,
|
||||
"vegetation": 0,
|
||||
"water": 2,
|
||||
"elevation_m": 1,
|
||||
"elevation_m": 0,
|
||||
"cover": 1
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user