fix(simulation): PR #178 review round — H1-H4, T1, T2

H1: split_edge_at now splits length_cells geometric-distance proportionally (fixed-order f64 polyline sums, halves sum exactly to parent) — subsumes the 2-point-parent all-or-nothing bug. H2: chained-snap test (minor onto minor spur) + determinism assertion actually exercising attach_minors + direct 2-point split unit test. H3: cascade-level RailHeadFacing end-to-end assertion on a plus-shaped landmass fixture (2 deterministic degree-3 settlement junctions). H4+T2: STANDALONE_HQ_JOIN_SQL shared constant, both readers compose it; believability lockstep unit test. T1: HUB_SPACING_DIAG_PX doc cross-refs D-243 elastic seam.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 17:03:30 +02:00
co-authored by Claude Fable 5
parent 5d37d205e1
commit 1fee6fd9da
4 changed files with 404 additions and 38 deletions
+105
View File
@@ -689,4 +689,109 @@ mod tests {
edge_count
);
}
/// PR #178 H3 — the RailHeadFacing wiring end-to-end (T-1076 §4): after the
/// RoadGraph layer runs, the cascade must have mutated
/// `layer3.placements[..].founding_orientation` to `RailHeadFacing` for
/// exactly the settlements that are high-connectivity junctions
/// (degree ≥ 3), and for no others. A regression that drops the
/// `assign_railhead_orientations` call (or runs it before the graph
/// exists) fails here mechanically.
#[test]
fn cascade_assigns_railhead_orientation_at_junctions() {
use crate::atlas::attractor_matching::CityRecord;
use crate::atlas::road_graph::{RoadNodeKind, JUNCTION_DEGREE};
use crate::simulation::generator::{FoundingOrientation, SettlementClass};
// A plus-shaped landmass (arms meeting at the centre, ocean elsewhere):
// settlements string along the arms, so the trunk MST must branch where
// the arms meet — diagnosed to yield exactly 2 degree-3 settlement
// junctions with 8 cities. Deterministic: the junction requirement
// below is a stable fixture property, not flakiness. (The default
// slope fixture never branches — its coastal attractors form a chain,
// and snapped minors raise Junction-node degrees, not settlement
// degrees.)
let (width, height) = (64u32, 32u32);
let mut data = vec![0.05f32; (width * height) as usize]; // ocean
for r in 0..height {
for c in 0..width {
let in_v_arm = (24..40).contains(&c); // vertical arm
let in_h_arm = (12..20).contains(&r); // horizontal arm
if in_v_arm || in_h_arm {
data[(r * width + c) as usize] = 0.6;
}
}
}
let cross_hm = crate::atlas::heightmap::BodyHeightmap {
body_id: "test_body".into(),
width,
height,
data,
sea_level: 0.3,
};
let cities: Vec<CityRecord> = (1..=8u64)
.map(|id| CityRecord {
city_id: id,
name: format!("City{id}"),
settlement_class: SettlementClass::PopulationBudget,
population: 1_000_000 - (id as i64) * 1_000, // hubs = lowest 6 ids
economic_role: "manufacturing".into(),
is_capital: false,
is_standalone_hq: false,
})
.collect();
let snap = run_cascade_from_heightmap(
body_seed(),
cross_hm,
&cities,
Some("independent"),
None,
CascadeLayer::RoadGraph,
);
let graph = snap.road_graph.as_ref().expect("RoadGraph layer ran");
let junction_city_ids: Vec<u64> = graph
.high_connectivity_junctions()
.iter()
.filter_map(|&i| graph.nodes[i].city_id)
.collect();
assert!(
!junction_city_ids.is_empty(),
"fixture must produce at least one degree ≥ {JUNCTION_DEGREE} settlement \
junction — if this fires the fixture changed, not the wiring"
);
// The wiring assertion, both directions: junction settlements carry
// RailHeadFacing; every other settlement does not.
let l3 = snap.layer3.as_ref().expect("Layer 3 ran");
for p in &l3.placements {
let is_junction = junction_city_ids.contains(&p.city_id);
let is_rail = matches!(
p.founding_orientation,
FoundingOrientation::RailHeadFacing { .. }
);
assert_eq!(
is_junction, is_rail,
"city {} junction={} but rail_facing={} — cascade wiring broken",
p.city_id, is_junction, is_rail
);
}
// And the mutated placements are what BodyWorldState carries forward.
let junction_count = junction_city_ids.len();
let state = snap.into_body_world_state();
let rail_count = state
.placements
.iter()
.filter(|p| {
matches!(
p.founding_orientation,
FoundingOrientation::RailHeadFacing { .. }
)
})
.count();
assert_eq!(rail_count, junction_count);
// Silence unused-import warning when the filter above changes.
let _ = RoadNodeKind::Settlement;
}
}