fix(simulation): synthetic-overflow placements get the ocean-mask guard (T-1206)
synthetic_attractor now takes the terrain analysis and land-corrects its pure-arithmetic position via a bounded nearest-land ring walk (T-1116's pattern: row-major tie-break, column wrap, row clamp, MAX_LAND_SEARCH_RING=128 sized empirically — real polar ocean bands push nearest land up to 125 cells). Land positions pass through UNTOUCHED — verified by direct before/after scan of all 267 real bodies: 63 land-arithmetic placements byte-identical, and every golden/determinism harness passes unchanged. The gap was real and widespread: 46 of 109 synthetic-overflow placements sat in open water at seed 42 (e.g. GJ903c at a genuine polar ocean cell); post-fix zero, with all 109 preserved (confirmed at a second seed). Degradation is defined and pinned: no land within the bound -> the synthetic attractor is skipped and Phase 5's existing not-placed warning reports it — never a panic, never a fabricated water position (the bound never triggers on any scanned real body). 9 new unit tests; road_graph's anchor comment and the D-210 amendment record the gap CLOSED (validated). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -196,6 +196,8 @@ impl CascadeSnapshot {
|
||||
///
|
||||
/// `territorial_status` (D-212, from the body's `dominant_faction`) and `seed`
|
||||
/// drive the per-settlement spatial-character enrichment (#956, D-213/214/215).
|
||||
/// `terrain` (T-1206) gates the Phase-4 synthetic-overflow path against open
|
||||
/// water — see [`match_cities`]'s doc.
|
||||
fn run_layer3(
|
||||
attractors: &[GeographicAttractor],
|
||||
cities: &[CityRecord],
|
||||
@@ -203,6 +205,7 @@ fn run_layer3(
|
||||
seed: SeedChain,
|
||||
grid_w: u32,
|
||||
grid_h: u32,
|
||||
terrain: Option<&TerrainAnalysis>,
|
||||
) -> Layer3Output {
|
||||
let matrix = CompatibilityMatrix::d195();
|
||||
let placements = match_cities(
|
||||
@@ -214,6 +217,7 @@ fn run_layer3(
|
||||
grid_h,
|
||||
territorial_status,
|
||||
seed,
|
||||
terrain,
|
||||
);
|
||||
Layer3Output { placements }
|
||||
}
|
||||
@@ -321,8 +325,15 @@ pub fn run_cascade_from_heightmap(
|
||||
None => &[],
|
||||
};
|
||||
// cache seam: run_layer3 is a pure, deterministic function of
|
||||
// (attractors, cities, territorial_status, seed) — wrap a persistent
|
||||
// cache here when we add one (build-time bake or local cache; see #1021).
|
||||
// (attractors, cities, territorial_status, seed, terrain) — wrap a
|
||||
// persistent cache here when we add one (build-time bake or local
|
||||
// cache; see #1021).
|
||||
//
|
||||
// T-1206: `snapshot.terrain_analysis` was just populated by the
|
||||
// Topography block above (guaranteed `Some` here — Settlement >
|
||||
// Topography in CascadeLayer's Ord, so the guard above always ran
|
||||
// first) — passed by reference so the DistrictProfile/RoadGraph pass
|
||||
// below still gets to consume (and drop) the same transient value.
|
||||
let l3 = run_layer3(
|
||||
attractors,
|
||||
cities,
|
||||
@@ -330,6 +341,7 @@ pub fn run_cascade_from_heightmap(
|
||||
body_seed,
|
||||
snapshot.heightmap.width,
|
||||
snapshot.heightmap.height,
|
||||
snapshot.terrain_analysis.as_ref(),
|
||||
);
|
||||
snapshot.layer3 = Some(l3);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user