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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user