Files
settled-reach/server/src/simulation/generator.rs
T
jpmschweitzerandClaude Opus 4.7 a61d030b47 refactor(simulation): harden #952 cascade per QA + architecture review
Addresses the Hoshe (QA) + Tyre (architecture) review of the SeedChain/cascade
work:

- Golden was pinning an empty river network (128x64 produced 0 river cells).
  Bumped to 256x128, where GJ1c yields a real network (93 river cells, 19
  mouths) — Layer 1's rivers are now actually guarded, not just attractors.
- SeedChain::for_body(world_seed, body_id) + fnv1a_64: the single canonical
  body_id(String) -> u64 path (FNV-1a, the repo convention), so callers can't
  derive divergent worlds from the same seed via different ad-hoc hashes. The
  golden now uses it.
- Stability guards: seed_domain_discriminants_are_pinned test (CI fails if a
  SeedDomain tag is renumbered); AttractorType gains #[repr(u8)] + explicit
  discriminants (it's cast as a sort key in features.rs).
- Tests: SeedChain::root(0) non-degenerate; run_cascade error path (missing
  file -> Err, not panic).
- Comments: clarified the id=0 derivations (sibling separation is caller-side
  via the per-district/quarter chain; #957 threads the index), tightened the
  all_district_types reachability comment (it pins the seed-0 sequence, not a
  probabilistic claim), and noted the golden's WORLD_SEED is cosmetic at
  Layers 0-1 + the x86_64 f32 capture caveat.

Deferred with reason: the run_cascade -> CascadeInputs struct refactor (Tyre)
is left for #954 — designing Layer-2's context shape now would be later-phase
detail, and there's a single caller to migrate then. EntityRng keeps its
domainless combine (migrating is stream-changing) — noted in D-224.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 13:24:03 +02:00

754 lines
29 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! World generator data model — district skeleton and mobile chunk types.
//!
//! Phase 1 generator output: the `DistrictSkeleton` 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
//! represent basements/sub-levels). Z-level *count* fields use `u8` (always ≥ 1).
//! The distinction:
//! - `base_z: i8` — where the bottom floor starts (can be negative)
//! - `z_levels: u8` — how many floors total (always positive)
//!
//! **D-108 (MobileChunk):** Vessel/vehicle interior chunks attached to mobile entities.
//! Uses the same chunk primitives as static districts in a simpler flat structure
//! (no Phase 1/Phase 2 split, template-stamped interiors).
//!
//! Sources: workshop-outcomes.md, tyre-round4.md
use serde::{Deserialize, Serialize};
// ---------------------------------------------------------------------------
// Concept ID type aliases
// ---------------------------------------------------------------------------
/// Unique district identifier (content-addressable via hash of world position + seed).
pub type DistrictId = u64;
/// Unique multi-block reservation identifier within a district.
pub type ReservationId = u64;
/// Unique social site identifier within a district.
pub type SocialSiteId = u64;
/// Unique role slot identifier within a social site.
pub type RoleSlotId = u64;
// ---------------------------------------------------------------------------
// Stub types for concepts not yet implemented (defined here to allow
// the data model to compile; will be replaced with proper types when
// the respective systems are implemented)
// ---------------------------------------------------------------------------
/// Biome classification for wilderness districts. Stub — full taxonomy deferred.
pub type Biome = String;
/// Water body sub-classification. Stub — full taxonomy deferred.
pub type WaterType = String;
/// Specialized district function (e.g. military, medical, penal). Stub.
pub type SpecializedFunction = String;
/// Cultural / architectural era tag. Stub.
pub type Era = String;
/// Single era modification applied to a block (conversion, overlay, hybrid). Stub.
pub type EraModification = String;
/// Landmark slot description within a block. Stub.
pub type LandmarkSlot = String;
/// Chunk layout specification within a block (how the 2×2 chunks are arranged). Stub.
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;
/// 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;
/// Guarantee audit result — tier-appropriate spatial invariant checks. Stub.
pub type GuaranteeAuditResult = String;
/// District access point (entry/exit to neighboring district). Stub.
pub type AccessPoint = String;
/// Base visual palette for a zone. Stub.
pub type BasePalette = String;
/// Economic modifier on a zone palette. Stub.
pub type EconomicModifier = String;
/// Faction presence modifier on a zone palette. Stub.
pub type FactionModifier = String;
/// Condition modifier on a zone palette (worn, pristine, damaged). Stub.
pub type ConditionModifier = String;
/// Season modifier (affects palette and ambient conditions). Stub.
pub type Season = String;
/// Role slot within a social site template. Stub.
pub type RoleSlot = String;
/// Day phase (morning, afternoon, evening, night, late-night). Stub.
pub type DayPhase = String;
/// Triangle template reference (links to server/content/templates/). Stub.
pub type TriangleTemplate = String;
/// Raw chunk tile data for generator use (64×64 bool grid, true = walkable). Stub.
pub type GeneratorChunkData = Vec<bool>;
/// Tile type identifier (references visual tile set). Stub.
pub type TileId = String;
/// Entity identifier for mutation causality tracking. Stub.
pub type EntityId = u64;
/// Action identifier for player-caused mutations. Stub.
pub type ActionId = String;
/// Object instance identifier within a chunk. Stub.
pub type ObjectId = u64;
/// Placed object (item, furniture, fixture) within a chunk. Stub.
pub type PlacedObject = String;
// ---------------------------------------------------------------------------
// World and complexity tier enums
// ---------------------------------------------------------------------------
/// Network importance of a world in the galaxy.
/// Determines simulation fidelity budget and NPC complexity ceiling.
///
/// Source: D-218, workshop-outcomes.md
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum WorldTier {
/// Hub system. Full simulation, high faction pressure.
Epicenter,
/// Regional system. 1–4 districts, partial full-budget simulation.
Regional,
/// Small community. 1 district. Network-insignificant, NOT budget-capped.
Backwater,
/// Transit stop. Pass-through node. Moderate complexity ceiling.
Passage,
/// Not simulated until player approaches. Minimal complexity ceiling.
Waypoint,
}
/// Generator content budget for a district.
/// Derived from WorldTier + SettingType at Phase 1.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum ComplexityTier {
/// Full social architecture. All Tier 1+2 guarantees. 20-80+ NPCs.
Full,
/// Moderate social architecture. Tier 1 + partial Tier 2. 8-20 NPCs.
Moderate,
/// Minimal social architecture. Tier 1 only. 1-8 NPCs.
Minimal,
/// No social architecture. Pure terrain. 0 NPCs. No guarantees.
Empty,
}
/// Physical setting type for a district.
/// Merged from Gestalt's SettingGeometry and Tyre's TerrainType.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum SettingType {
Station,
Urban,
Agricultural,
Maritime,
Wilderness { biome: Biome },
Water { water_type: WaterType },
Transitional,
Orbital,
Specialized { function: SpecializedFunction },
}
/// Classification of a district (high-level function).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum DistrictType {
LogisticsHub,
Residential,
Commercial,
Industrial,
Administrative,
Entertainment,
MixedUse,
Transit,
Specialized,
}
/// How blocks are placed within the district's 512×512 footprint.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum DistrictLayoutMode {
/// Standard Cartesian grid — perpendicular streets.
Grid,
/// Organic placement with per-block offsets and rotations.
Organic {
placements: [[BlockPlacement; 4]; 4],
},
}
/// Zoning classification for a block or floor zone.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum ZoningType {
Commercial,
Residential,
Industrial,
Administrative,
Transit,
Recreational,
Restricted,
Mixed,
}
/// Reservation function — what purpose a multi-block reservation serves.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum ReservationFunction {
Skyscraper,
Park,
Terminal,
Plaza,
Monument,
IndustrialComplex,
MilitaryBase,
ResearchFacility,
UndergroundComplex,
}
/// Access tier — who is allowed into a zone under normal circumstances.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum AccessTier {
/// Anyone can enter.
Public,
/// Requires employment or residence credential.
Credentialed,
/// Requires explicit invitation or escort.
Restricted,
/// Law enforcement / military only.
Secured,
/// Breach access only — normally sealed.
BreachOnly,
}
/// Vertical corridor type (how floors connect in a multi-level reservation).
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum VerticalCorridorType {
Stairwell,
Elevator,
FreightLift,
AccessHatch,
EmergencyLadder,
}
/// Load state for a z-level within a reservation.
/// Controls lazy loading of floor data during play.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum ZLevelLoadState {
/// Floor is fully generated and in memory.
Loaded(GeneratorChunkData),
/// Floor skeleton only — full data not yet generated.
Skeleton(FloorZone),
/// Floor not yet generated (deferred to Phase 2 on first entry).
Ungenerated,
}
/// Cause of a chunk mutation (for save/load audit trail).
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum MutationCause {
Explosion { radius: u8, source: EntityId },
Fire { spread_from: Option<(u16, u16)> },
Construction { builder: EntityId },
Decay { time_since_maintenance: u32 },
PlayerAction { action: ActionId },
}
/// Type of structural change to a region of chunk tiles.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum StructuralChangeType {
WallDestroyed,
FloorCollapsed,
CeilingBreached,
AreaSealed,
WallConstructed,
}
/// Purpose classification for a triangle within a social site.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum TrianglePurpose {
Investigation,
Economic,
Social,
Political,
Tactical,
Mundane,
}
/// What exists on the far side of a wall tile (for destructible geometry).
/// Every wall tile in a generated chunk is tagged with one of these.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum WallBackside {
/// Another room/corridor exists (tiles already generated).
AdjacentSpace,
/// Solid structural material — 1-2 tiles of fill, then another wall.
StructuralFill,
/// Narrow utility gap (1-3 tiles): pipes, conduits, non-navigable.
ServiceVoid,
/// Edge of chunk — adjacent chunk's boundary tiles on the far side.
ChunkBoundary,
/// Faces outside (hull, exterior wall) — breach has catastrophic consequences.
Exterior,
}
/// Era cause — why a block has the era tag it has.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum EraCause {
Original,
CorporateMerger,
EmergencyExtension,
OrganicGrowth,
InstitutionalIncursion,
EconomicDisruption,
CulturalShift,
}
// ---------------------------------------------------------------------------
// Settlement classification enums (D-196, D-212, D-213, D-214, D-215)
// ---------------------------------------------------------------------------
/// How a settlement enters and exits active simulation.
/// Controls whether generation runs, and at what complexity level.
/// Source: D-196
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum SettlementClass {
/// Named in wiki; always active regardless of population threshold.
NameLocked,
/// Active if pop ≥ 50_000; ghost stub if pop < 5_000.
PopulationBudget,
/// Active only while the triggering economic condition holds.
EconomicTriggered,
/// Emergent settlement not in atlas at generation time; written during simulation.
OrganicGrowth,
}
/// Dominant power structure of a settlement and its physical spatial expression.
/// Derived from TerritorialStatus + economic_role at generation time.
/// Source: D-214
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum PoliticalArchetype {
/// Top-down Commission planning; rectilinear, institutional core, radial-core arrangement.
Commission,
/// Corp-dominated; commercial density, restricted campus blocks, restricted-perimeter adjacent.
Corporate,
/// Self-organized; organic growth, mixed use, ribbon arrangement.
Pioneer,
/// Garrison or fortification origin; defensible geometry, fortified-perimeter arrangement.
Military,
/// University or research origin; campus-quad structure, green space, radial-core arrangement.
Academic,
/// Factory-first; large-footprint industrial blocks, worker residential rings, ribbon arrangement.
Industrial,
}
/// Primary spatial axis of a city's original street grid.
/// Derived from the matched attractor type (D-211). Controls district grid rotation.
/// Source: D-213
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum FoundingOrientation {
/// Street grid perpendicular to coastline. `facing_degrees`: compass bearing toward water (0–359).
Coastal { facing_degrees: u16 },
/// Street grid parallel to founding river. `bearing_degrees`: river flow direction (0–359).
RiverAligned { bearing_degrees: u16 },
/// Grid rotated to follow local contours (valley floor settlements).
TerrainFollowing,
/// Grid aligned to cardinal N/S/E/W (Commission-planned settlements on flat terrain).
Cardinal,
/// Arbitrary bearing (pioneer settlements on open terrain). `bearing_degrees`: 0–359.
Free { bearing_degrees: u16 },
}
/// Territory control status for a province (drainage basin). Priority-ordered derivation.
/// Source: D-212
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum TerritorialStatus {
/// Commission faction_influence ≥ 0.6 in this province.
CommissionControlled,
/// Single corporation faction_influence ≥ 0.5.
CorpTerritory,
/// Two or more factions each ≥ 0.3; no dominant faction.
ContestedZone,
/// No faction with influence ≥ 0.2.
FrontierUnclaimed,
/// Cultural corridor has indigenous autonomy flag.
IndigenousHeld,
/// Population density < 0.01 AND no faction ≥ 0.1.
Derelict,
}
// ---------------------------------------------------------------------------
// Attractor types for settlement placement (D-195, D-209, D-211)
// ---------------------------------------------------------------------------
/// The type of terrain feature that attracts settlement placement.
/// Source: D-195, D-209
// `#[repr(u8)]` with explicit discriminants: `AttractorType` is cast `as u8` as
// the primary sort key for the attractor list (`features.rs`), so the layout is
// load-bearing — reordering would change attractor ordering and the cascade
// golden. Append new variants; never renumber or reorder existing ones.
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum AttractorType {
/// Where a river meets sea level or coastline. Historically high-value.
RiverMouth = 0,
/// Proximity to coast without a river mouth. Port access.
CoastalAccess = 1,
/// Where a river crosses a topographic saddle or confluence point.
RiverCrossing = 2,
/// Local elevation minimum; flat, arable, sheltered.
ValleyFloor = 3,
/// Saddle point between adjacent drainage basins; controls a mountain pass.
PassEntrance = 4,
/// Adjacent to a lake polygon.
LakeShore = 5,
/// Flat terrain away from all other attractors; fallback for plains settlements.
PlainCenter = 6,
}
/// Fine-grained terrain classification carried by each `GeographicAttractor`.
/// Classifies the local terrain more finely than the top-level `SettingType`;
/// drives ZonePalette modifier selection (D-101) and `terrain_modification_cost`.
/// Source: D-210
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
pub enum SubBiomeVariant {
TropicalWet,
TemperateForest,
TemperateGrassland,
BorealForest,
Tundra,
Desert,
Savanna,
Alpine,
Wetland,
CoastalLowland,
/// No heightmap-derivable signal in the Layer-1 inputs (elevation, slope,
/// moisture, latitude); reserved for a future volcanic data source (D-210).
Volcanic,
}
/// A terrain feature at a specific map position that influences city placement scoring.
/// Source: D-195, D-209, D-210
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GeographicAttractor {
/// Pixel position in heightmap space [row, col].
pub position: (u16, u16),
pub attractor_type: AttractorType,
/// Normalized strength 0.0–1.0. Derived from flow accumulation or habitability score.
pub strength: f32,
/// Fine-grained terrain classification at this position (D-210).
pub sub_biome: SubBiomeVariant,
/// Infrastructure-build cost multiplier (1.0 = baseline grassland; higher =
/// more expensive). Derived from `sub_biome` + local slope. Consumed by the
/// attractor-matching pipeline (D-211) to penalize marginal cities. (D-210)
pub terrain_modification_cost: f32,
}
/// Compatibility weights between economic roles and attractor types.
/// A 10×7 matrix (10 economic_role values × 7 AttractorType variants).
/// Each cell is a weight multiplier 0.0–3.0 applied during attractor-matching scoring.
/// Source: D-195
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CompatibilityMatrix {
/// Row order: manufacturing, financial, agricultural, extraction, service_mixed,
/// institutional, transit_hub, research, military, residential.
/// Column order: RiverMouth, CoastalAccess, RiverCrossing, ValleyFloor,
/// PassEntrance, LakeShore, PlainCenter.
pub weights: [[f32; 7]; 10],
}
/// Data contract between build-time (systems.db) and the runtime-background
/// generation tier. Populated from atlas_city_names + bodies at generation
/// dispatch time. All 8 fields are required before a generation task may run.
/// Source: D-200, D-199
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CityGenerationContext {
/// Foreign key into atlas_city_names.id
pub city_id: u64,
pub political_archetype: PoliticalArchetype,
/// Starting economic health seed (0.0–1.0). Derived per D-197.
pub prosperity_baseline: f32,
pub surrounding_biome: SettingType,
/// Compass octants (0=N, 1=NE … 7=NW) where roads enter the city footprint.
pub road_entry_directions: Vec<u8>,
/// City footprint radius in km. Derived from body_radius_km (D-204) + population.
pub footprint_radius_km: f32,
pub founding_orientation: FoundingOrientation,
pub world_tier: WorldTier,
}
// ---------------------------------------------------------------------------
// Supporting structs
// ---------------------------------------------------------------------------
/// Organic layout placement for a single block.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct BlockPlacement {
/// Offset from grid-aligned position (±16 sim tiles per axis max).
pub offset: (i16, i16),
/// Rotation in 15° increments (0–3, max 45°).
pub rotation_steps: u8,
/// Street width in basis points (10000 = 1.0×, range 7500–20000 for 0.75–2.0×).
/// Default 10000 = 4 visual tiles. Uses integer to avoid f32 non-determinism (D-010).
pub street_width_bps: u16,
}
/// Single block within a district's 4×4 block grid.
/// Each block = 128×128 sim tiles = 2×2 chunks.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct BlockSkeleton {
/// Grid position (0–3, 0–3).
pub position: (u8, u8),
pub zoning: ZoningType,
/// Which multi-block reservation this block belongs to (if any).
pub reservation: Option<ReservationId>,
pub chunk_layout: ChunkLayout,
pub hosted_sites: Vec<SocialSiteId>,
pub era: Era,
pub era_modifications: Vec<EraModification>,
pub era_cause: Option<EraCause>,
/// Build density percentage (0 = open/empty, 100 = fully built-up).
/// Integer to avoid f32 non-determinism (D-010).
pub density_pct: u8,
pub landmark: Option<LandmarkSlot>,
}
/// Floor zone within a multi-level reservation.
///
/// **D-110:** `z_level` is `i8` — negative values represent basements
/// (e.g. `z_level = -1` for a sub-basement). This differs from `z_levels: u8`
/// on the parent reservation which counts total floors (always ≥ 1).
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FloorZone {
/// Absolute z-level of this floor. Negative for sub-ground levels (D-110).
pub z_level: i8,
pub zone_type: ZoningType,
pub zone_palette: ZonePalette,
pub access_tier: AccessTier,
}
/// Vertical connection spec within a multi-level reservation.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct VerticalCorridorSpec {
/// Which blocks this vertical corridor passes through.
pub block_coords: Vec<(u8, u8)>,
/// Which z-bands (0-indexed band indices, not absolute z-levels) this
/// corridor connects. See D-110 for signed z-level coordinate system.
pub z_bands_connected: Vec<u8>,
pub access_tier: AccessTier,
pub corridor_type: VerticalCorridorType,
}
/// Visual palette for a zone — base material + contextual modifiers.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ZonePalette {
pub base: BasePalette,
pub modifiers: Vec<PaletteModifier>,
}
/// Contextual modifier applied on top of a base palette.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum PaletteModifier {
EconomicFunction(EconomicModifier),
Era(Era),
FactionPresence(FactionModifier),
Condition(ConditionModifier),
Season(Season),
}
/// Social site placement within a district.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SocialSitePlacement {
pub site_id: SocialSiteId,
/// Which blocks this site spans.
pub blocks: Vec<(u8, u8)>,
pub template_tag: String,
pub access_tier: AccessTier,
pub triangles: Vec<TriangleAssignment>,
pub role_slots: Vec<RoleSlot>,
pub active_phases: Vec<DayPhase>,
}
/// Triangle assignment within a social site.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TriangleAssignment {
pub template: TriangleTemplate,
pub purposes: Vec<TrianglePurpose>,
pub participants: Vec<RoleSlotId>,
/// Which block provides the primary staging area (optional for multi-block sites).
pub staging_block: Option<(u8, u8)>,
}
/// Mutations applied to an already-generated chunk.
/// Stored alongside the chunk in the save file.
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct ChunkMutations {
pub tile_overrides: Vec<TileOverride>,
pub structural_changes: Vec<StructuralChange>,
pub placed_objects: Vec<PlacedObject>,
pub removed_objects: Vec<ObjectId>,
}
/// Single tile override within a chunk mutation.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TileOverride {
/// (x, y, z) within the chunk (z is relative to chunk base, always ≥ 0).
pub position: (u16, u16, u8),
pub new_tile: TileId,
pub cause: MutationCause,
}
/// Rectangular structural change within a chunk.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct StructuralChange {
pub min: (u16, u16, u8),
pub max: (u16, u16, u8),
pub change_type: StructuralChangeType,
}
// ---------------------------------------------------------------------------
// Multi-block reservation
// ---------------------------------------------------------------------------
/// A reservation spanning multiple blocks within a district.
///
/// Used for skyscrapers, parks, transit terminals, plazas, and any structure
/// that requires more than one 128×128 block.
///
/// **D-110:** `base_z: i8` — the lowest floor of this reservation.
/// Negative values represent sub-ground levels (basements, underground complexes).
/// `z_levels: u8` remains unsigned — it counts total floors (always ≥ 1).
///
/// Example: a building with a 3-level basement at ground + 20 floors above:
/// `base_z: -3, z_levels: 24`
/// A deep mine shaft descending 30 floors below ground:
/// `base_z: -30, z_levels: 30`
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct MultiBlockReservation {
/// Which blocks (grid positions) are part of this reservation.
pub blocks: Vec<(u8, u8)>,
pub template_tag: String,
pub function: ReservationFunction,
/// Total number of floors (always ≥ 1, unsigned). D-110.
pub z_levels: u8,
/// Lowest floor's z-level (can be negative for sub-basement). D-110.
pub base_z: i8,
/// Per-floor zone specifications.
pub floor_zones: Vec<FloorZone>,
/// Number of z-bands (logical groupings of floors by zoning).
pub z_band_count: u8,
/// Zone definitions for each z-band.
pub z_band_zones: Vec<ZoneDefinition>,
/// Vertical connection specs (stairs, elevators, ladders).
pub vertical_corridors: Vec<VerticalCorridorSpec>,
pub hosted_sites: Vec<SocialSiteId>,
}
// ---------------------------------------------------------------------------
// District skeleton
// ---------------------------------------------------------------------------
/// Phase 1 generator output for one district.
///
/// Produced by the async background generation pipeline.
/// Consumed by Phase 2 (chunk-by-chunk fill, on demand) and by
/// the production simulation startup path.
///
/// ## 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
/// ground level is always 0. Sub-ground structures use [`MultiBlockReservation`]
/// with a negative `base_z` field.
///
/// ## Determinism (D-010)
///
/// 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)]
pub struct DistrictSkeleton {
// ── Identity ──────────────────────────────────────────
pub district_id: DistrictId,
/// Deterministic seed (derived from master seed via SeedChain).
pub seed: u64,
pub district_type: DistrictType,
/// World context (system, world, neighboring districts).
pub context: DistrictContext,
// ── Classification ────────────────────────────────────
pub world_tier: WorldTier,
pub complexity: ComplexityTier,
pub setting: SettingType,
pub layout_mode: DistrictLayoutMode,
// ── Spatial Structure ─────────────────────────────────
/// The 4×4 block grid (each block = 128×128 sim tiles = 2×2 chunks).
pub blocks: [[BlockSkeleton; 4]; 4],
/// Multi-block reservations (skyscrapers, parks, terminals, plazas).
pub reservations: Vec<MultiBlockReservation>,
/// Corridor spines connecting key access points.
pub corridors: Vec<CorridorSpine>,
/// Number of floor levels in this district (count, unsigned — D-110).
pub z_levels: u8,
// ── Social Structure ──────────────────────────────────
pub social_sites: Vec<SocialSitePlacement>,
pub access_points: Vec<AccessPoint>,
// ── Cultural / World Context ──────────────────────────
pub society_profile: SocietyProfileRef,
pub zone_palette: Vec<ZoneDefinition>,
// ── Boundary System ───────────────────────────────────
pub boundaries: DistrictBoundaries,
// ── Validation ────────────────────────────────────────
/// Guarantee audit result. `None` for Empty districts.
pub guarantee_audit: Option<GuaranteeAuditResult>,
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
/// D-110: FloorZone.z_level must be i8 (signed) to support negative sub-levels.
#[test]
fn floor_zone_z_level_is_signed() {
let fz = FloorZone {
z_level: -2,
zone_type: ZoningType::Restricted,
zone_palette: ZonePalette {
base: String::new(),
modifiers: vec![],
},
access_tier: AccessTier::BreachOnly,
};
assert_eq!(fz.z_level, -2, "sub-basement z_level must round-trip as i8");
}
/// D-110: MultiBlockReservation.base_z must be i8 (signed) for underground buildings.
#[test]
fn reservation_base_z_is_signed() {
let r = MultiBlockReservation {
blocks: vec![(0, 0)],
template_tag: "deep-mine".into(),
function: ReservationFunction::UndergroundComplex,
z_levels: 30,
base_z: -30,
floor_zones: vec![],
z_band_count: 5,
z_band_zones: vec![],
vertical_corridors: vec![],
hosted_sites: vec![],
};
assert_eq!(
r.base_z, -30,
"deep mine base_z must be representable as i8"
);
assert_eq!(r.z_levels, 30u8, "z_levels count must remain u8");
}
/// D-110: z_levels (count) remains u8 on DistrictSkeleton.
#[test]
fn district_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) {}
let z_levels: u8 = 4;
assert_u8(z_levels);
assert!(z_levels > 0, "z_levels is always at least 1");
}
}