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:
2026-05-02 18:11:13 +02:00
co-authored by Claude Sonnet 4.6
parent f9fdfb7712
commit b9fd7a3fc8
10 changed files with 3326 additions and 0 deletions
+111
View File
@@ -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);
}
}
}