refactor(simulation): rename DistrictSkeleton→QuarterSkeleton per D-222 (#950)

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>
This commit is contained in:
2026-06-05 13:06:44 +02:00
co-authored by Claude Opus 4.8
parent f7a21c6b8d
commit c71c26a1a6
10 changed files with 108 additions and 97 deletions
+41 -32
View File
@@ -1,8 +1,8 @@
//! Phase 1 district skeleton generator (D-194, D-196, D-211, D-213, D-214).
//! Phase 1 quarter skeleton generator (D-194, D-196, D-211, D-213, D-214).
//!
//! Entry point: [`generate_skeleton`]. Consumes a [`CityGenerationContext`]
//! Entry point: [`generate_quarter_skeleton`]. Consumes a [`CityGenerationContext`]
//! together with the city's raw population and economic role, and produces a
//! fully classified [`DistrictSkeleton`] with:
//! fully classified [`QuarterSkeleton`] with:
//!
//! - [`SettingType`] derived from the surrounding biome context.
//! - [`ComplexityTier`] derived from population tier × [`WorldTier`].
@@ -22,8 +22,8 @@ use crate::atlas::block_irregularity::block_irregularity;
use crate::atlas::district_mix::{compute_district_mix, population_tier};
use crate::seed::{SeedChain, SeedDomain};
use crate::simulation::generator::{
BlockPlacement, BlockSkeleton, CityGenerationContext, ComplexityTier, DistrictId,
DistrictLayoutMode, DistrictSkeleton, DistrictType, MultiBlockReservation, PoliticalArchetype,
BlockPlacement, BlockSkeleton, CityGenerationContext, ComplexityTier, DistrictLayoutMode,
DistrictType, MultiBlockReservation, PoliticalArchetype, QuarterId, QuarterSkeleton,
ReservationFunction, ReservationId, SettingType, WorldTier, ZoningType,
};
@@ -31,23 +31,23 @@ use crate::simulation::generator::{
// Public API
// ---------------------------------------------------------------------------
/// Generate a Phase 1 [`DistrictSkeleton`] from a city's generation context.
/// Generate a Phase 1 [`QuarterSkeleton`] from a city's generation context.
///
/// # Parameters
/// - `context`: Build-time city context (archetype, world tier, orientation…).
/// - `population`: Raw population count from atlas_city_names.
/// - `economic_role`: Economic role string (one of the 10 canonical values).
/// - `district_id`: Content-addressable identifier for this district.
/// - `quarter_id`: Content-addressable identifier for this district.
/// - `founding_age_years`: Years since founding — controls block irregularity.
/// - `chain`: This district's position in the deterministic seed tree (D-224 SeedChain).
pub fn generate_skeleton(
pub fn generate_quarter_skeleton(
context: &CityGenerationContext,
population: i64,
economic_role: &str,
district_id: DistrictId,
quarter_id: QuarterId,
founding_age_years: u32,
chain: SeedChain,
) -> DistrictSkeleton {
) -> QuarterSkeleton {
// ── 1. SettingType ────────────────────────────────────────────────────
// Pass through the surrounding_biome from context — it already encodes
// the planet/station/wilderness classification established at atlas time.
@@ -103,11 +103,11 @@ pub fn generate_skeleton(
// Reserved blocks carry their own z_levels count.
let z_levels: u8 = 1;
DistrictSkeleton {
district_id,
QuarterSkeleton {
quarter_id,
seed: chain.seed(),
district_type: district_type_from_mix(&primary_district_type),
context: String::new(), // stub — DistrictContext = String
quarter_type: district_type_from_mix(&primary_district_type),
context: String::new(), // stub — QuarterContext = String
world_tier: context.world_tier.clone(),
complexity,
setting,
@@ -242,7 +242,7 @@ fn derive_layout_mode(
/// Generate 4×4 organic block placements seeded deterministically (D-010).
///
/// Uses a seeded LCG; offset range controlled by `irregularity` (0.05–1.0)
/// scaled to the ±16 sim tile maximum from `block_irregularity::max_offset_sim_tiles`.
/// scaled to the ±16 subtile maximum from `block_irregularity::max_offset_sim_tiles`.
fn organic_placements(irregularity: f32, chain: SeedChain) -> [[BlockPlacement; 4]; 4] {
let max_offset = (irregularity * 16.0) as i16;
// id = 0: one placement pass per district. Sibling separation comes from the
@@ -292,7 +292,7 @@ fn zoning_for_district(dt: &DistrictType) -> ZoningType {
}
}
/// Map the primary district type to the DistrictType field on DistrictSkeleton.
/// Map the primary district type to the DistrictType field on QuarterSkeleton.
fn district_type_from_mix(primary: &DistrictType) -> DistrictType {
primary.clone()
}
@@ -437,14 +437,15 @@ mod tests {
fn setting_station_passthrough() {
let mut ctx = make_context(PoliticalArchetype::Commission, WorldTier::Regional);
ctx.surrounding_biome = SettingType::Station;
let sk = generate_skeleton(&ctx, 500_000, "institutional", 1, 200, SeedChain::root(42));
let sk =
generate_quarter_skeleton(&ctx, 500_000, "institutional", 1, 200, SeedChain::root(42));
assert!(matches!(sk.setting, SettingType::Station));
}
#[test]
fn setting_urban_for_city_on_planet() {
let ctx = make_context(PoliticalArchetype::Commission, WorldTier::Regional);
let sk = generate_skeleton(&ctx, 500_000, "financial", 1, 200, SeedChain::root(42));
let sk = generate_quarter_skeleton(&ctx, 500_000, "financial", 1, 200, SeedChain::root(42));
assert!(matches!(sk.setting, SettingType::Urban));
}
@@ -452,14 +453,15 @@ mod tests {
fn complexity_epicenter_high_pop_is_full() {
let ctx = make_context(PoliticalArchetype::Commission, WorldTier::Epicenter);
// 10M pop → pop_tier = 1 → Full on Epicenter
let sk = generate_skeleton(&ctx, 10_000_000, "financial", 1, 200, SeedChain::root(42));
let sk =
generate_quarter_skeleton(&ctx, 10_000_000, "financial", 1, 200, SeedChain::root(42));
assert_eq!(sk.complexity, ComplexityTier::Full);
}
#[test]
fn complexity_waypoint_tiny_pop_is_empty() {
let ctx = make_context(PoliticalArchetype::Pioneer, WorldTier::Waypoint);
let sk = generate_skeleton(&ctx, 1_000, "residential", 1, 50, SeedChain::root(42));
let sk = generate_quarter_skeleton(&ctx, 1_000, "residential", 1, 50, SeedChain::root(42));
assert_eq!(sk.complexity, ComplexityTier::Empty);
}
@@ -467,7 +469,7 @@ mod tests {
fn complexity_backwater_low_pop_is_moderate() {
let ctx = make_context(PoliticalArchetype::Pioneer, WorldTier::Backwater);
// 50_000 pop → pop_tier = 0 → Moderate on Backwater (D-218: not budget-capped)
let sk = generate_skeleton(&ctx, 50_000, "residential", 1, 50, SeedChain::root(42));
let sk = generate_quarter_skeleton(&ctx, 50_000, "residential", 1, 50, SeedChain::root(42));
assert_eq!(sk.complexity, ComplexityTier::Moderate);
}
@@ -475,7 +477,8 @@ mod tests {
fn complexity_backwater_high_pop_is_full() {
let ctx = make_context(PoliticalArchetype::Pioneer, WorldTier::Backwater);
// 10M pop → pop_tier = 1 → Full on Backwater (D-218: not budget-capped)
let sk = generate_skeleton(&ctx, 10_000_000, "residential", 1, 50, SeedChain::root(42));
let sk =
generate_quarter_skeleton(&ctx, 10_000_000, "residential", 1, 50, SeedChain::root(42));
assert_eq!(sk.complexity, ComplexityTier::Full);
}
@@ -483,28 +486,31 @@ mod tests {
fn complexity_passage_low_pop_is_minimal() {
let ctx = make_context(PoliticalArchetype::Commission, WorldTier::Passage);
// 50_000 pop → pop_tier = 0 → Minimal on Passage (transit stop, budget-capped)
let sk = generate_skeleton(&ctx, 50_000, "transit_hub", 1, 100, SeedChain::root(42));
let sk =
generate_quarter_skeleton(&ctx, 50_000, "transit_hub", 1, 100, SeedChain::root(42));
assert_eq!(sk.complexity, ComplexityTier::Minimal);
}
#[test]
fn layout_commission_is_grid() {
let ctx = make_context(PoliticalArchetype::Commission, WorldTier::Regional);
let sk = generate_skeleton(&ctx, 500_000, "institutional", 1, 100, SeedChain::root(99));
let sk =
generate_quarter_skeleton(&ctx, 500_000, "institutional", 1, 100, SeedChain::root(99));
assert!(matches!(sk.layout_mode, DistrictLayoutMode::Grid));
}
#[test]
fn layout_pioneer_is_organic() {
let ctx = make_context(PoliticalArchetype::Pioneer, WorldTier::Regional);
let sk = generate_skeleton(&ctx, 500_000, "residential", 1, 400, SeedChain::root(99));
let sk =
generate_quarter_skeleton(&ctx, 500_000, "residential", 1, 400, SeedChain::root(99));
assert!(matches!(sk.layout_mode, DistrictLayoutMode::Organic { .. }));
}
#[test]
fn block_grid_is_fully_populated() {
let ctx = make_context(PoliticalArchetype::Commission, WorldTier::Regional);
let sk = generate_skeleton(&ctx, 500_000, "financial", 1, 200, SeedChain::root(42));
let sk = generate_quarter_skeleton(&ctx, 500_000, "financial", 1, 200, SeedChain::root(42));
// All 16 blocks must have valid positions.
for row in 0..4 {
for col in 0..4 {
@@ -519,7 +525,7 @@ mod tests {
fn no_reservations_for_small_city() {
let ctx = make_context(PoliticalArchetype::Pioneer, WorldTier::Backwater);
// pop_tier 0, not transit_hub → no reservations.
let sk = generate_skeleton(&ctx, 80_000, "residential", 1, 50, SeedChain::root(42));
let sk = generate_quarter_skeleton(&ctx, 80_000, "residential", 1, 50, SeedChain::root(42));
assert!(sk.reservations.is_empty());
}
@@ -527,7 +533,8 @@ mod tests {
fn park_reservation_for_large_city() {
let ctx = make_context(PoliticalArchetype::Commission, WorldTier::Epicenter);
// 100M pop → pop_tier 2 → park reservation.
let sk = generate_skeleton(&ctx, 100_000_000, "financial", 1, 300, SeedChain::root(42));
let sk =
generate_quarter_skeleton(&ctx, 100_000_000, "financial", 1, 300, SeedChain::root(42));
let has_park = sk
.reservations
.iter()
@@ -539,7 +546,8 @@ mod tests {
fn transit_terminal_for_transit_hub_role() {
let ctx = make_context(PoliticalArchetype::Commission, WorldTier::Regional);
// pop_tier 0 but transit_hub → terminal reservation.
let sk = generate_skeleton(&ctx, 80_000, "transit_hub", 1, 200, SeedChain::root(42));
let sk =
generate_quarter_skeleton(&ctx, 80_000, "transit_hub", 1, 200, SeedChain::root(42));
let has_terminal = sk
.reservations
.iter()
@@ -551,7 +559,8 @@ mod tests {
fn reserved_blocks_linked_in_grid() {
let ctx = make_context(PoliticalArchetype::Commission, WorldTier::Epicenter);
// 100M pop → park at (1,2),(1,3),(2,2),(2,3) with reservation id 1.
let sk = generate_skeleton(&ctx, 100_000_000, "financial", 1, 300, SeedChain::root(42));
let sk =
generate_quarter_skeleton(&ctx, 100_000_000, "financial", 1, 300, SeedChain::root(42));
// All park blocks must reference the park reservation (id=1).
for &(row, col) in &[(1u8, 2u8), (1, 3), (2, 2), (2, 3)] {
let b = &sk.blocks[row as usize][col as usize];
@@ -565,7 +574,7 @@ mod tests {
#[test]
fn determinism_same_seed_same_output() {
let ctx = make_context(PoliticalArchetype::Industrial, WorldTier::Regional);
let sk1 = generate_skeleton(
let sk1 = generate_quarter_skeleton(
&ctx,
2_000_000,
"manufacturing",
@@ -573,7 +582,7 @@ mod tests {
250,
SeedChain::root(12345),
);
let sk2 = generate_skeleton(
let sk2 = generate_quarter_skeleton(
&ctx,
2_000_000,
"manufacturing",