D-222 renamed the 512m generation cell from District to Quarter (District is now a new 2048m tier above it). Align the generation skeleton code to the canonical vocabulary. Pure naming — no behavior change; all 1296 lib tests + integration tests pass unchanged. Renamed (spatial-cell identifiers): - DistrictSkeleton → QuarterSkeleton, DistrictWorldState → QuarterWorldState - DistrictId → QuarterId, DistrictContext/DistrictBoundaries → Quarter* - field district_id → quarter_id, district_type → quarter_type - BodyWorldState.districts map → .quarters - generate_skeleton → generate_quarter_skeleton Deliberately left as-is (these name functional ZONING, not the spatial tier — orthogonal to D-222): DistrictType, DistrictLayoutMode, the district_mix module (DistrictMix/compute_district_mix), and the GenWorkItem::GenerateSkeleton / GenCompletion::SkeletonGenerated variants. Also aligned the perception "sim tile" → "subtile" vocabulary (D-222: Subtile = 0.5m) in decisions/perception.md and the generation-cascade code comments. Historical D-066/D-094/D-201/D-220 decision bodies keep their existing D-222 amendment notes (not rewritten in place); the public max_offset_sim_tiles fn name is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
120 lines
4.2 KiB
Rust
120 lines
4.2 KiB
Rust
//! 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
|
||
);
|
||
}
|
||
}
|
||
}
|