//! BlockIrregularity derivation from founding_age and PoliticalArchetype (D-216). //! //! `block_irregularity` (0.0–1.0) controls how much a block deviates from //! the district's canonical grid. Minimum 0.05 — no block is perfectly regular. //! //! **Formula (D-216):** //! ```text //! base_irregularity = (founding_age_years / 1000.0).min(1.0) //! archetype_step = Commission|Military → -0.3, Corporate|Academic → -0.1, //! Industrial → 0.0, Pioneer → +0.3 //! block_irregularity = (base + step).clamp(0.05, 1.0) //! ``` //! //! **Determinism (D-010, D-216):** Integer-scaled intermediates; archetype_step //! stored as basis points (i32, 1 bp = 0.001). Final result is f32 from integer //! arithmetic to match the D-216 formula. use crate::simulation::generator::PoliticalArchetype; /// Compute the `block_irregularity` value for one block. /// /// - `founding_age_years`: years since the settlement was founded (integer). /// - `archetype`: the settlement's political archetype. /// /// Returns a value in [0.05, 1.0]. pub fn block_irregularity(founding_age_years: u32, archetype: &PoliticalArchetype) -> f32 { // base_irregularity in integer basis-points (0–1000, where 1000 = 1.0). let base_bp: i32 = (founding_age_years as i32).min(1000); // archetype_step in basis-points. let step_bp: i32 = archetype_step_bp(archetype); // block_irregularity_bp clamped to [50, 1000] (0.05–1.0). let result_bp = (base_bp + step_bp).clamp(50, 1000); result_bp as f32 / 1000.0 } fn archetype_step_bp(archetype: &PoliticalArchetype) -> i32 { match archetype { PoliticalArchetype::Commission | PoliticalArchetype::Military => -300, PoliticalArchetype::Corporate | PoliticalArchetype::Academic => -100, PoliticalArchetype::Industrial => 0, PoliticalArchetype::Pioneer => 300, } } /// Derive the maximum block offset in subtiles from `block_irregularity`. /// /// Used by `DistrictLayoutMode::Organic`: `max_offset = (irregularity × 16.0) as i16`. pub fn max_offset_sim_tiles(irregularity: f32) -> i16 { (irregularity * 16.0) as i16 } #[cfg(test)] mod tests { use super::*; #[test] fn minimum_is_0_05() { // New Commission city (age 0) → base 0, step -300 → clamp to 50bp = 0.05. let v = block_irregularity(0, &PoliticalArchetype::Commission); assert!((v - 0.05).abs() < 1e-6, "expected 0.05, got {v}"); } #[test] fn maximum_is_1_0() { // Old Pioneer city (age 1000+) → base 1000, step +300 → clamp to 1000bp = 1.0. let v = block_irregularity(1500, &PoliticalArchetype::Pioneer); assert!((v - 1.0).abs() < 1e-6, "expected 1.0, got {v}"); } #[test] fn pioneer_more_irregular_than_commission() { let pioneer = block_irregularity(400, &PoliticalArchetype::Pioneer); let commission = block_irregularity(400, &PoliticalArchetype::Commission); assert!( pioneer > commission, "Pioneer ({pioneer}) should be more irregular than Commission ({commission})" ); } #[test] fn age_increases_irregularity() { let young = block_irregularity(50, &PoliticalArchetype::Industrial); let old = block_irregularity(800, &PoliticalArchetype::Industrial); assert!( old > young, "Older settlement ({old}) should be more irregular than young ({young})" ); } #[test] fn max_offset_scales_with_irregularity() { assert_eq!(max_offset_sim_tiles(0.05), 0); // 0.05 × 16 = 0.8 → 0 assert_eq!(max_offset_sim_tiles(1.0), 16); assert_eq!(max_offset_sim_tiles(0.5), 8); } #[test] fn all_archetypes_produce_valid_range() { let archetypes = [ PoliticalArchetype::Commission, PoliticalArchetype::Corporate, PoliticalArchetype::Pioneer, PoliticalArchetype::Military, PoliticalArchetype::Academic, PoliticalArchetype::Industrial, ]; for a in &archetypes { let v = block_irregularity(300, a); assert!( (0.05..=1.0).contains(&v), "archetype {:?} gave {v} out of [0.05, 1.0]", a ); } } }