From f880635b24da00ad4952bab15d4518c98dad3c1d Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sat, 6 Jun 2026 10:55:07 +0200 Subject: [PATCH] fix(simulation): decorrelate per-footprint tag fields + test gaps (#957) PR #153 review (Hoshe H1): zone_type/era/floors/flavor all shared one fp_chain and each took splitmix64(seed) of the same value, correlating the fields (era tracking floor height across a city). Derive a distinct sub-chain per field so they draw independent entropy. Block_tags aren't serialized, so no fixture change; behavior stays deterministic. Also (review): n=1 corridor test (empty, no panic), Frontier-density waterfront test, and a comment on the reserved `_morphology` param (Tyre). Co-Authored-By: Claude Opus 4.8 (1M context) --- server/src/atlas/skeleton_gen.rs | 53 +++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/server/src/atlas/skeleton_gen.rs b/server/src/atlas/skeleton_gen.rs index 7d122df01..4c1a1e9b2 100644 --- a/server/src/atlas/skeleton_gen.rs +++ b/server/src/atlas/skeleton_gen.rs @@ -798,6 +798,9 @@ fn block_on_quarter_edge(row: u8, col: u8, edge: Edge) -> bool { fn subdivide_block_footprints( density_pct: u8, bulk: &BulkClass, + // Reserved for morphology-specific lot shaping (narrow canyon lots, + // pier-oriented delta lots) once that detail is designed; the waterfront + // rule already consumes the water-facing geometry via `waterfront`. _morphology: &MorphologyZone, waterfront: Option, seed: SeedChain, @@ -877,17 +880,31 @@ fn assign_block_tags( .into_iter() .enumerate() .map(|(i, footprint)| { + // Distinct per-field sub-chains so zone / era / floors / flavor draw + // independent entropy — sharing one `fp_chain` made every field a + // modulo of the same `splitmix64(seed)` value and correlated them + // (e.g. construction era tracking floor height across the city). let fp_chain = block_chain.derive(SeedDomain::Block, i as u64 + 1); - let zone_type_id = zone_type_for(&block.zoning, economic_role, setting, fp_chain); + let zone_type_id = zone_type_for( + &block.zoning, + economic_role, + setting, + fp_chain.derive(SeedDomain::Block, 0), + ); let entry_class = building_entry_class(&block.zoning, &skeleton.layout_mode, prosperity); - let (era, era_cause) = construction_era(founding_age_years, prosperity, fp_chain); + let (era, era_cause) = construction_era( + founding_age_years, + prosperity, + fp_chain.derive(SeedDomain::Block, 1), + ); let initial = initial_condition(prosperity, &era_cause); - let extent = floor_extent(block.density_pct, fp_chain); + let extent = floor_extent(block.density_pct, fp_chain.derive(SeedDomain::Block, 2)); // D-232: deterministic (seed + zone_type) → flavor index into the // body's K-tag trait selection. + let flavor_seed = fp_chain.derive(SeedDomain::Block, 3).seed(); let flavor_index = - (splitmix64(fp_chain.seed() ^ zone_type_hash(&zone_type_id)) % flavor_n) as u8; + (splitmix64(flavor_seed ^ zone_type_hash(&zone_type_id)) % flavor_n) as u8; BuildingPropertyTag { zone_type_id, footprint, @@ -1724,6 +1741,13 @@ mod tests { assert_eq!(derive_corridors(&mesh, &MorphologyZone::Delta).len(), 3); // hub-spoke } + #[test] + fn corridors_single_node_returns_empty() { + let aps = derive_access_points(&[], &[]); + assert_eq!(aps.len(), 1); + assert!(derive_corridors(&aps, &MorphologyZone::AlluvialPlain).is_empty()); + } + #[test] fn hub_spoke_shares_a_common_node() { let aps = derive_access_points(&[0, 2, 4, 6], &[]); @@ -1769,6 +1793,27 @@ mod tests { ); } + #[test] + fn waterfront_applies_at_frontier_density() { + // A coastal frontier settlement (few large lots) still presents to the quay. + let inland = subdivide_block_footprints( + 10, + &BulkClass::BulkSolid, + &MorphologyZone::CoastalLowland, + None, + SeedChain::root(11), + ); + let quay = subdivide_block_footprints( + 10, + &BulkClass::BulkSolid, + &MorphologyZone::CoastalLowland, + Some(Edge::North), + SeedChain::root(11), + ); + let min_y = |v: &[TileRect]| v.iter().map(|r| r.origin.1).min().unwrap_or(u8::MAX); + assert!(min_y(&quay) < min_y(&inland)); + } + #[test] fn skeleton_has_streets_and_local_lattice() { let ctx = make_context(PoliticalArchetype::Commission, WorldTier::Regional);