feat(simulation): add server/src/atlas/ — full Phase 1 generation pipeline
Ten-module atlas package implementing the D-194–D-218 district generation stack: heightmap loader, BodyWorldState LRU cache, D8 drainage routing, background generation queue, five-phase attractor-matching, three-component district mix, block irregularity, tile condition thresholds, and the Phase 1 skeleton generator that wires them into DistrictSkeleton. Closes #916 #917 #918 #919 #920 #922 #923 #924 #899. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
//! 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 sim tiles 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!(v >= 0.05 && v <= 1.0, "archetype {:?} gave {v} out of [0.05, 1.0]", a);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user