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
+3 -3
View File
@@ -149,7 +149,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
### D-044: Art direction — visual hierarchy (entity > object > structure)
- **Date:** 2026-02-12
- **Decision:** Three-layer visual hierarchy: entities (2px outline, D-033 relationship color, highest saturation) > objects (1px outline, era-appropriate palette, moderate saturation) > structure (minimal/no outline, muted zone palette, lowest saturation). Hard rendering rule: entity always wins visual ties — if an entity and object overlap, the entity's D-033 color must remain visible. This is a readability guarantee, not an aesthetic preference.
- **Entity spec:** 24x32 pixel footprint within 64x64px visual tiles. Entities occupy 1x1 sim tiles (0.5m) but render across a 2x2 sim tile sprite footprint per [D-066](architecture.md#d-066-dual-scale-grid--05m-simulation-1m-visual-2x-retina-factor). D-033 color as primary information signal, silhouette as primary identity signal. One identifying silhouette feature per named NPC (Kael's vest, Lera's apron, Sera's uniform). 3-4 template silhouettes for generic NPCs.
- **Entity spec:** 24x32 pixel footprint within 64x64px visual tiles. Entities occupy 1x1 subtiles (0.5m) but render across a 2x2 subtile sprite footprint per [D-066](architecture.md#d-066-dual-scale-grid--05m-simulation-1m-visual-2x-retina-factor). D-033 color as primary information signal, silhouette as primary identity signal. One identifying silhouette feature per named NPC (Kael's vest, Lera's apron, Sera's uniform). 3-4 template silhouettes for generic NPCs.
- **Canonical reference:** `docs/workshops/art-direction-mood-board/workshop-outcomes.md` §1.4, §1.5
- **Raised by:** Araminta (outline spec), Ozzie (readability priority). Unanimously endorsed.
- **Dissent:** None
@@ -212,7 +212,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
- **Object hover:** Ticks rotate 45° to X-shape. Muted grey #8b8ba0 (amber #e8c547 if flagged). Simpler frame than entity.
- **Weapon aim:** Hard transition. Ticks extend, center gap widens, lines thicken 1→2px. Warm white #f0e8d8. NO bloom. Entity in sights tints to D-033 color — aiming at a friend (green tint) should feel wrong.
- All transitions 150ms linear. z-layer 7. Never changes by zone or narrative state ([D-045](#d-045-art-direction--environmental-neutrality-strict-zero-shift)).
- Cursor changes on LOS, not just proximity. Click interaction range: ~2 sim tiles (= 1m per [D-066](architecture.md#d-066-dual-scale-grid--05m-simulation-1m-visual-2x-retina-factor)). Weapon-selected mode suppresses interaction prompts unless Shift held ("combat intent trumps social intent").
- Cursor changes on LOS, not just proximity. Click interaction range: ~2 subtiles (= 1m per [D-066](architecture.md#d-066-dual-scale-grid--05m-simulation-1m-visual-2x-retina-factor)). Weapon-selected mode suppresses interaction prompts unless Shift held ("combat intent trumps social intent").
- **Diegetic test (Stig):** Interaction labels render on z-layer 6 (insert overlay). If the insert is off, labels disappear. Passes the "is this information from the character's implant?" test.
- **Rationale:** Diegetic — cursor is the insert's own interface element. Consistent with [D-048](#d-048-neural-insert-overlay--visual-design). Entity in weapon sights tinting to D-033 creates moral friction.
- **Cross-reference:** Insert overlay ([D-048](#d-048-neural-insert-overlay--visual-design)), entity color ([D-033](#d-033-entity-color--relationship-to-player)), z-stack ([D-049](#d-049-z-level-rendering-stack-8-layers))
@@ -254,7 +254,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
### D-059: Fog — shader-based, five layers, knowledge-graph-driven
- **Date:** 2026-02-13
- **Decision:** Fog is a shader-driven system (not particles) with five distinct layers:
1. **Clear (vision cone):** Soft gradient edge over 6-8 sim tiles (= 3-4 visual tiles per [D-066](architecture.md#d-066-dual-scale-grid--05m-simulation-1m-visual-2x-retina-factor)), no hard line. Darkwood approach.
1. **Clear (vision cone):** Soft gradient edge over 6-8 subtiles (= 3-4 visual tiles per [D-066](architecture.md#d-066-dual-scale-grid--05m-simulation-1m-visual-2x-retina-factor)), no hard line. Darkwood approach.
2. **Light fog (peripheral):** Desaturated 40-50%, brightness -30%. Animated Perlin noise overlay (8-10s cycle). Entity [D-033](#d-033-entity-color--relationship-to-player) colors visible but reduced.
3. **Deep fog (previously explored):** Near-monochrome with ~10% "zone temperature" tint (bar=warm dark, hub=cool dark, corridor=neutral dark). More pronounced noise (15-20s cycle). Fog breathes.
4. **Unexplored + maps app:** Geometric wireframe outlines #333340. Insert data aesthetic.
+1 -1
View File
@@ -45,7 +45,7 @@ fn archetype_step_bp(archetype: &PoliticalArchetype) -> i32 {
}
}
/// Derive the maximum block offset in sim tiles from `block_irregularity`.
/// 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 {
+4 -4
View File
@@ -14,7 +14,7 @@ use bevy_ecs::prelude::Resource;
use serde::{Deserialize, Serialize};
use crate::atlas::attractor_matching::CityPlacement;
use crate::simulation::generator::{DistrictId, DistrictWorldState, GeographicAttractor};
use crate::simulation::generator::{GeographicAttractor, QuarterId, QuarterWorldState};
/// Simulation tick counter — monotonically increasing u64.
pub type SimTick = u64;
@@ -75,11 +75,11 @@ pub struct BodyWorldState {
/// Settlement placements (D-211, #955). Attractor-matched city positions.
/// Empty until the Layer-3 placement task completes.
pub placements: Vec<CityPlacement>,
/// District-level world state, keyed by `DistrictId` (D-230).
/// Quarter-level world state, keyed by `QuarterId` (D-230).
///
/// Populated by `GenCompletion::SkeletonGenerated` after the plan phase
/// completes for each city. `BTreeMap` for D-010 determinism.
pub districts: BTreeMap<DistrictId, DistrictWorldState>,
pub quarters: BTreeMap<QuarterId, QuarterWorldState>,
/// Last sim tick this entry was read. Used for LRU eviction.
pub last_accessed: SimTick,
}
@@ -201,7 +201,7 @@ mod tests {
drainage_basins: vec![],
attractors: vec![],
placements: vec![],
districts: BTreeMap::new(),
quarters: BTreeMap::new(),
last_accessed: tick,
}
}
+1 -1
View File
@@ -88,7 +88,7 @@ impl CascadeSnapshot {
drainage_basins,
attractors,
placements,
districts: std::collections::BTreeMap::new(),
quarters: std::collections::BTreeMap::new(),
last_accessed: 0,
}
}
+21 -21
View File
@@ -12,7 +12,7 @@
//!
//! **Work item types (D-206):**
//! - `AnalyzeBody`: D8 drainage + attractor extraction for a body.
//! - `GenerateSkeleton`: Phase 1 DistrictSkeleton for a city.
//! - `GenerateSkeleton`: Phase 1 QuarterSkeleton for a city.
//! - `FillChunk`: Phase 2 chunk fill for a pre-loaded district.
//!
//! Completion events are delivered to the main thread via
@@ -31,9 +31,9 @@ use crate::atlas::attractor_matching::CityRecord;
use crate::atlas::body_world_state::BodyWorldState;
use crate::atlas::cascade::{run_cascade_from_heightmap, CascadeLayer};
use crate::atlas::heightmap::{load_heightmap_png, GRID_H, GRID_W};
use crate::atlas::skeleton_gen::generate_skeleton;
use crate::atlas::skeleton_gen::generate_quarter_skeleton;
use crate::seed::SeedChain;
use crate::simulation::generator::{CityGenerationContext, DistrictWorldState};
use crate::simulation::generator::{CityGenerationContext, QuarterWorldState};
// ---------------------------------------------------------------------------
// Priority
@@ -73,7 +73,7 @@ pub enum GenWorkItem {
/// (#955); empty if the body has no settlements (cascade stops at Layer 1).
cities: Vec<CityRecord>,
},
/// Generate a Phase 1 DistrictSkeleton for this city.
/// Generate a Phase 1 QuarterSkeleton for this city.
///
/// `context` is the D-199 economic read-set pre-resolved at dispatch time.
/// All 6 required fields must be populated before this item is submitted
@@ -83,11 +83,11 @@ pub enum GenWorkItem {
/// `body_id` routes the resulting `SkeletonGenerated` completion into the
/// correct `BodyWorldState` cache entry (D-230).
///
/// `district_id` is the stable content-addressable id for the generated
/// `quarter_id` is the stable content-addressable id for the generated
/// district (keyed by city position + world seed).
///
/// `economic_role`, `population`, and `founding_age_years` are D-199 fields
/// carried alongside the context because `generate_skeleton` accepts them as
/// carried alongside the context because `generate_quarter_skeleton` accepts them as
/// separate parameters (its signature is not changed by this ticket).
GenerateSkeleton {
city_id: u64,
@@ -95,17 +95,17 @@ pub enum GenWorkItem {
/// D-199 economic read-set + all other context fields.
context: Box<CityGenerationContext>,
/// Stable content-addressable district id (D-194/D-230).
district_id: u64,
quarter_id: u64,
/// District-level seed chain (D-224).
chain: SeedChain,
// D-199 raw fields passed to generate_skeleton separately.
// D-199 raw fields passed to generate_quarter_skeleton separately.
economic_role: String,
population: i64,
founding_age_years: u32,
},
/// Pre-fill a chunk in an existing district.
FillChunk {
district_id: u64,
quarter_id: u64,
block_pos: (u32, u32),
},
}
@@ -135,14 +135,14 @@ pub enum GenCompletion {
SkeletonGenerated {
city_id: u64,
/// The body this skeleton belongs to — used to route state into
/// `BodyWorldState.districts` (D-230).
/// `BodyWorldState.quarters` (D-230).
body_id: String,
/// District-level world state (skeleton + block tags) produced by the plan phase (D-230).
/// Boxed to keep `GenCompletion` variant sizes balanced (D-230 skeleton is ~2.7 KB).
state: Box<DistrictWorldState>,
state: Box<QuarterWorldState>,
},
ChunkFilled {
district_id: u64,
quarter_id: u64,
block_pos: (u32, u32),
},
/// Work item failed — body_id or city_id for logging.
@@ -385,7 +385,7 @@ fn run_work_item(item: &GenWorkItem) -> GenCompletion {
city_id,
body_id,
context,
district_id,
quarter_id,
chain,
economic_role,
population,
@@ -394,29 +394,29 @@ fn run_work_item(item: &GenWorkItem) -> GenCompletion {
// Build the Phase 1 skeleton from the pre-resolved D-199 context.
// `economic_role`, `population`, and `founding_age_years` are the
// D-199 raw fields carried alongside the context because
// `generate_skeleton` accepts them as separate parameters.
let skeleton = generate_skeleton(
// `generate_quarter_skeleton` accepts them as separate parameters.
let skeleton = generate_quarter_skeleton(
context,
*population,
economic_role,
*district_id,
*quarter_id,
*founding_age_years,
*chain,
);
GenCompletion::SkeletonGenerated {
city_id: *city_id,
body_id: body_id.clone(),
state: Box::new(DistrictWorldState {
state: Box::new(QuarterWorldState {
skeleton,
block_tags: std::collections::BTreeMap::new(),
}),
}
}
GenWorkItem::FillChunk {
district_id,
quarter_id,
block_pos,
} => GenCompletion::ChunkFilled {
district_id: *district_id,
quarter_id: *quarter_id,
block_pos: *block_pos,
},
}
@@ -493,7 +493,7 @@ mod tests {
dominant_bulk_class: BulkClass::NonPhysical,
dominant_production_ubiquity: ProductionUbiquity::Common,
}),
district_id: city_id * 10,
quarter_id: city_id * 10,
chain: SeedChain::root(42 + city_id),
economic_role: "service_mixed".to_string(),
population: 500_000,
@@ -590,7 +590,7 @@ mod tests {
let q = make_queue();
q.submit(
GenWorkItem::FillChunk {
district_id: 99,
quarter_id: 99,
block_pos: (0, 0),
},
GenPriority::High,
+1 -1
View File
@@ -228,7 +228,7 @@ mod tests {
drainage_basins: vec![],
attractors: vec![],
placements: vec![],
districts: std::collections::BTreeMap::new(),
quarters: std::collections::BTreeMap::new(),
last_accessed: 0,
});
let (_db, resolver) = empty_resolver();
+7 -7
View File
@@ -93,16 +93,16 @@ fn drain_generation_completions(
} => {
if !body_id.is_empty() {
if let Some(body_state) = cache.peek_mut(&body_id) {
// Key by state.skeleton.district_id (D-194/D-230): a city has many
// districts, each with its own DistrictId. `city_id` is only the
// Key by state.skeleton.quarter_id (D-194/D-230): a city has many
// quarters, each with its own QuarterId. `city_id` is only the
// dispatch key used in the work item — the canonical insert key is
// the district's own stable id. TODO(#957): the stub GenerateSkeleton
// returns a default skeleton with district_id=0; real gen (#957) will
// the quarter's own stable id. TODO(#957): the stub GenerateSkeleton
// returns a default skeleton with quarter_id=0; real gen (#957) will
// populate it from CityGenerationContext.
let _ = city_id; // used as dispatch key only; district_id is the map key
let _ = city_id; // used as dispatch key only; quarter_id is the map key
body_state
.districts
.insert(state.skeleton.district_id, *state);
.quarters
.insert(state.skeleton.quarter_id, *state);
} else {
tracing::warn!(
city_id,
+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.051.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",
+2 -2
View File
@@ -9,7 +9,7 @@
//! - `NpcBlueprint` — generator output for a single NPC
//!
//! Plus `SpikeOutput` as the top-level binary output container.
//! These are spike-specific and intentionally decoupled from `DistrictSkeleton`.
//! These are spike-specific and intentionally decoupled from `QuarterSkeleton`.
//!
//! ## Content contract
//!
@@ -547,7 +547,7 @@ pub enum RelationshipValence {
/// Top-level output for the generator spike binary.
///
/// Intentionally decoupled from `DistrictSkeleton` — this is a proof-of-life
/// Intentionally decoupled from `QuarterSkeleton` — this is a proof-of-life
/// container, not production architecture.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpikeOutput {
+27 -25
View File
@@ -1,6 +1,6 @@
//! World generator data model — district skeleton and mobile chunk types.
//! World generator data model — quarter skeleton and mobile chunk types.
//!
//! Phase 1 generator output: the `DistrictSkeleton` produced by the world generation
//! Phase 1 generator output: the `QuarterSkeleton` produced by the world generation
//! pipeline. Consumed by Phase 2 (chunk fill) and by the simulation startup path.
//!
//! **D-110 (signed z-levels):** All z-level *position* fields use `i8` (negative values
@@ -24,7 +24,7 @@ use serde::{Deserialize, Serialize};
// ---------------------------------------------------------------------------
/// Unique district identifier (content-addressable via hash of world position + seed).
pub type DistrictId = u64;
pub type QuarterId = u64;
/// Unique multi-block reservation identifier within a district.
pub type ReservationId = u64;
/// Unique social site identifier within a district.
@@ -55,13 +55,13 @@ pub type ChunkLayout = String;
/// Corridor spine connecting district access points. Stub.
pub type CorridorSpine = String;
/// District context — neighboring districts, world position, system. Stub.
pub type DistrictContext = String;
pub type QuarterContext = String;
/// Society profile reference (Miri's cultural ingredients). Stub.
pub type SocietyProfileRef = String;
/// Zone definition — base palette + modifiers + zone name. Stub.
pub type ZoneDefinition = String;
/// District boundary system — edge descriptors with neighbors. Stub.
pub type DistrictBoundaries = String;
pub type QuarterBoundaries = String;
/// Guarantee audit result — tier-appropriate spatial invariant checks. Stub.
pub type GuaranteeAuditResult = String;
/// District access point (entry/exit to neighboring district). Stub.
@@ -939,17 +939,17 @@ pub enum ProductionUbiquity {
MonopolySource,
}
/// District-level world state produced by the plan phase (D-230).
/// Quarter-level world state produced by the plan phase (D-230).
///
/// Output of `GenerateSkeleton` extended to include building-property tags.
/// Stored in `BodyWorldState.districts: BTreeMap<DistrictId, DistrictWorldState>`.
/// Stored in `BodyWorldState.quarters: BTreeMap<QuarterId, QuarterWorldState>`.
/// `BTreeMap` for D-010 determinism.
///
/// D-230: `{ skeleton: DistrictSkeleton, block_tags: BTreeMap<(u8,u8), Vec<BuildingPropertyTag>> }`
/// D-230: `{ skeleton: QuarterSkeleton, block_tags: BTreeMap<(u8,u8), Vec<BuildingPropertyTag>> }`
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
pub struct DistrictWorldState {
pub struct QuarterWorldState {
/// Phase 1 skeleton produced by `GenerateSkeleton` (D-230).
pub skeleton: DistrictSkeleton,
pub skeleton: QuarterSkeleton,
/// Building property tags keyed by block position (row, col) within the 4×4 grid.
/// Each entry is a Vec of one tag per building footprint placed in that block.
/// `BTreeMap` for D-010 determinism (no HashMap non-determinism).
@@ -999,7 +999,7 @@ pub struct CityGenerationContext {
/// Organic layout placement for a single block.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct BlockPlacement {
/// Offset from grid-aligned position (±16 sim tiles per axis max).
/// Offset from grid-aligned position (±16 subtiles per axis max).
pub offset: (i16, i16),
/// Rotation in 15° increments (03, max 45°).
pub rotation_steps: u8,
@@ -1008,8 +1008,8 @@ pub struct BlockPlacement {
pub street_width_bps: u16,
}
/// Single block within a district's 4×4 block grid.
/// Each block = 128×128 sim tiles = 2×2 chunks.
/// Single block within a quarter's 4×4 block grid.
/// Each block = 128×128 subtiles = 2×2 chunks.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
pub struct BlockSkeleton {
/// Grid position (03, 03).
@@ -1160,10 +1160,10 @@ pub struct MultiBlockReservation {
}
// ---------------------------------------------------------------------------
// District skeleton
// Quarter skeleton
// ---------------------------------------------------------------------------
/// Phase 1 generator output for one district.
/// Phase 1 generator output for one quarter.
///
/// Produced by the async background generation pipeline.
/// Consumed by Phase 2 (chunk-by-chunk fill, on demand) and by
@@ -1171,8 +1171,8 @@ pub struct MultiBlockReservation {
///
/// ## Z-level naming convention (D-110)
///
/// - `z_levels: u8` — how many floor levels exist in this district (count, always ≥ 1)
/// - `base_z` does not appear on `DistrictSkeleton` itself — the district's
/// - `z_levels: u8` — how many floor levels exist in this quarter (count, always ≥ 1)
/// - `base_z` does not appear on `QuarterSkeleton` itself — the quarter's
/// ground level is always 0. Sub-ground structures use [`MultiBlockReservation`]
/// with a negative `base_z` field.
///
@@ -1181,14 +1181,16 @@ pub struct MultiBlockReservation {
/// All `Vec<_>` fields are stable-ordered at generation time (sorted by a
/// deterministic key). Generation reproduces identical output for the same seed.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
pub struct DistrictSkeleton {
pub struct QuarterSkeleton {
// ── Identity ──────────────────────────────────────────
pub district_id: DistrictId,
pub quarter_id: QuarterId,
/// Deterministic seed (derived from master seed via SeedChain).
pub seed: u64,
pub district_type: DistrictType,
/// The quarter's primary functional zone (still a `DistrictType` — the
/// zoning vocabulary is orthogonal to the spatial tier, D-222).
pub quarter_type: DistrictType,
/// World context (system, world, neighboring districts).
pub context: DistrictContext,
pub context: QuarterContext,
// ── Classification ────────────────────────────────────
pub world_tier: WorldTier,
@@ -1197,7 +1199,7 @@ pub struct DistrictSkeleton {
pub layout_mode: DistrictLayoutMode,
// ── Spatial Structure ─────────────────────────────────
/// The 4×4 block grid (each block = 128×128 sim tiles = 2×2 chunks).
/// The 4×4 block grid (each block = 128×128 subtiles = 2×2 chunks).
pub blocks: [[BlockSkeleton; 4]; 4],
/// Multi-block reservations (skyscrapers, parks, terminals, plazas).
pub reservations: Vec<MultiBlockReservation>,
@@ -1215,7 +1217,7 @@ pub struct DistrictSkeleton {
pub zone_palette: Vec<ZoneDefinition>,
// ── Boundary System ───────────────────────────────────
pub boundaries: DistrictBoundaries,
pub boundaries: QuarterBoundaries,
// ── Validation ────────────────────────────────────────
/// Guarantee audit result. `None` for Empty districts.
@@ -1367,9 +1369,9 @@ mod tests {
assert_eq!(r.z_levels, 30u8, "z_levels count must remain u8");
}
/// D-110: z_levels (count) remains u8 on DistrictSkeleton.
/// D-110: z_levels (count) remains u8 on QuarterSkeleton.
#[test]
fn district_skeleton_z_levels_is_unsigned() {
fn quarter_skeleton_z_levels_is_unsigned() {
// Verify z_levels is u8 (cannot hold negative value). Static assertion:
// if this compiled with z_levels = 255u8, the type is correct.
fn assert_u8(_: u8) {}