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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<Edge>,
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user