Standardized YAML frontmatter on all 38 files with title, description, type, workshop, agent, and round fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
50 KiB
title, description, type, status, workshop, agent, round, created
| title | description | type | status | workshop | agent | round | created |
|---|---|---|---|---|---|---|---|
| Tyre Round 4 — Architecture Finalization | Final MobileChunk specification, architecture finalization, and D-record accuracy review | workshop | archived | generator-architecture | tyre | 4 | 2026-02-27 |
Round 4: Tyre — Architecture Finalization
Workshop: Generator Architecture (#562) Agent: Tyre (Technical Architect) Date: 2026-02-27
Round 4 scope: Final convergence. Five specific assignments from lead. This is the version that goes into the D-record.
1. OQ-R4-A: Final MobileChunk Specification
Lead mandate: entity-carried chunks for trains, ships, spaceships, traincars. My MobileChunk model wins structurally. The question from Nigel: can vessel interiors be simpler than full districts while using the same entity-carried architecture?
1.1 The Answer: YES — Emphatically
Nigel's concern is valid and important. His instanced-district model was attractive because it was simple — vessel as a temporary district instance, same data path as everything else. My entity-carried model is more powerful, but Nigel is right to ask: does "more powerful" mean "more complex generation"?
No. The entity-carried architecture describes how the chunk exists in the world (attached to an entity, with movement states). It says nothing about how complex the chunk's interior is. These are orthogonal.
cracks knuckles — Let me be explicit about the simplification layers.
1.2 Vessel Interior Simplification
Vessel interiors are NOT full districts. They don't use the district generation pipeline. They're template-stamped chunks with NPC role slots:
| Property | Full District | Vessel Interior |
|---|---|---|
| Generation pipeline | Phase 1 → Phase 2 (skeleton → fill) | Template stamp only (no skeleton stage) |
| Spatial archetype guarantees | 3-11 checks per complexity tier | None. Vessel templates are hand-authored. |
| Block/quarter system | 4×4 blocks, quarter-based fill | None. Single template per vessel class. |
| NPC generation | Full 10-axis generation + entanglement | Roster seeding only. Crew = fixed roles from template. Passengers = seeded from route's NPC pool at departure. |
| Triangle system | Multi-triangle per social site | One triangle max per vessel (the confined-social-pressure triangle) |
| ComplexityTier | Full / Moderate / Minimal / Empty | Not applicable. Vessel interiors don't have a ComplexityTier — they have a VesselClass. |
| Zone palette | Full modifier stack | Single palette per vessel class (luxury liner vs cargo hauler vs patrol boat) |
The point: a MobileChunk's entity-carrying architecture (docking, transit states, world persistence) is infrastructure that costs ~9.5 dev-days once. The interior content is as simple or as complex as the template demands. A cargo train car interior is 32×8 sim tiles with a crew NPC and some crates. That's trivially cheap to generate.
1.3 Vessel Classes
Instead of ComplexityTier, vessels have a VesselClass that determines interior scope:
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
enum VesselClass {
/// Small vehicle: shuttle, skiff, small boat.
/// Interior: 16×8 to 32×16 sim tiles. 0-2 NPCs (crew only).
/// No social sites. No triangles. Pure transit.
Small,
/// Standard vehicle: train car, medium ship, orbital shuttle.
/// Interior: 32×16 to 64×32 sim tiles. 2-8 NPCs (crew + passengers).
/// One social site (the common area). Optional triangle.
Standard,
/// Large vehicle: passenger liner, freighter, long-haul train.
/// Interior: 64×64 sim tiles (full chunk). 8-30 NPCs.
/// Multiple social sites (dining, bar, deck, cabins). 1-2 triangles.
/// This is the "social pressure cooker" Nigel described.
Large,
/// Capital vessel: warship, colony ship, station-scale transport.
/// Interior: 2×2 chunks (128×128 sim tiles — a mobile "block").
/// 30+ NPCs. Full social site complement. Multiple triangles.
/// v0.5+ feature. Deferred.
Capital,
}
1.4 The Final MobileChunk Struct
/// A chunk attached to a mobile entity instead of fixed coordinates.
/// Companion struct to DistrictSkeleton — NOT a district, but a
/// first-class world entity with its own generation and streaming rules.
#[derive(Serialize, Deserialize, Clone, Debug)]
struct MobileChunk {
// ── Identity ──────────────────────────────────────────
/// Entity this chunk is attached to
entity_id: EntityId,
/// Vessel class determines interior scope
vessel_class: VesselClass,
/// Template tag for interior generation
template_tag: VesselTemplateTag,
/// Seed for cosmetic variation (different cargo, wear, graffiti)
interior_seed: u64,
// ── Chunk Data ────────────────────────────────────────
/// The chunk data itself — same format as static chunks.
/// None if not yet generated (generated on first player entry).
data: Option<ChunkData>,
/// Interior dimensions in sim tiles (may be smaller than 64×64).
/// Actual ChunkData is always 64×64; unused tiles are impassable.
interior_size: (u16, u16),
// ── World Presence ────────────────────────────────────
/// Current movement state
movement: MobileMovementState,
/// Connection points to the outside world (doors, airlocks, gangways).
/// Active only when the entity is docked/stopped.
access_points: Vec<MobileAccessPoint>,
// ── Social Content ────────────────────────────────────
/// Crew roster (fixed roles from vessel template)
crew_slots: Vec<CrewSlot>,
/// Passenger manifest (seeded at journey departure, empty when idle)
passenger_manifest: Vec<PassengerId>,
/// Social site placements within the vessel interior
social_sites: Vec<VesselSocialSite>,
/// Triangle assignments (max 2 for Large, 0 for Small)
triangles: Vec<TriangleAssignment>,
// ── Mutations ─────────────────────────────────────────
/// Post-generation modifications (same as static chunks)
mutations: ChunkMutations,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
enum MobileMovementState {
/// Docked at a fixed position — accessible from the world.
/// Vessel is visible on the map. Interior persists in save state.
Docked {
dock_position: WorldPosition,
/// Which static chunk the door connects to (None if free-floating dock)
connected_chunk: Option<ChunkCoord>,
},
/// In transit along a route — interior accessible, exterior scrolls.
/// Client renders scrolling background through window tiles.
InTransit {
route: RouteId,
progress: f32, // 0.0–1.0 along route
speed: f32, // sim tiles per tick
},
/// Between star systems — only the interior exists.
/// Maximum social confinement. No exit until arrival.
InterSystem {
origin: SystemId,
destination: SystemId,
progress: f32,
},
/// Idle at a location — not docked to infrastructure, not in transit.
/// Vessel is parked (anchored ship, grounded shuttle).
/// Visible on map. Access requires approach (swimming, walking to it).
Idle {
world_position: WorldPosition,
},
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct MobileAccessPoint {
/// Position within the mobile chunk (interior coordinates)
interior_position: (u16, u16),
/// Direction the access point faces (relative to entity heading)
facing: CardinalDirection,
/// What kind of connection (gangway, airlock, cargo door)
connection_type: MobileConnectionType,
/// Is this access point currently active?
active: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
enum MobileConnectionType {
/// Standard door/gangway — pedestrian access
Gangway,
/// Airlock — required for station-to-vessel in vacuum
Airlock,
/// Cargo door — large, ground-level, vehicle-accessible
CargoDoor,
/// Emergency hatch — always available but triggers alarm
EmergencyHatch,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct CrewSlot {
role: CrewRole,
/// NPC assigned to this slot (None if position unfilled)
assigned_npc: Option<NpcId>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
enum CrewRole {
Captain, Pilot, Engineer, Navigator,
Steward, Security, Cook, Medic,
Deckhand, // generic crew
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct VesselSocialSite {
site_type: VesselSiteType,
/// Tile region within the interior
bounds: TileRect,
/// NPC role slots for this site
role_slots: Vec<RoleSlot>,
/// Active day-phases (D-031)
active_phases: Vec<DayPhase>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
enum VesselSiteType {
/// Where passengers and crew gather (dining, bar, common room)
CommonArea,
/// Bridge/helm — captain and officers
Bridge,
/// Passenger cabins — private, low-traffic
Cabins,
/// Cargo hold — crew-only, storage, potential informal zone
CargoHold,
/// Deck/observation — open area with exterior view
Observation,
}
1.5 Persistent Docked Vessels — The Key Advantage
This is what Nigel's instanced model could NOT do, and why the lead chose entity-carried:
A ship docked at Port Sova is a world entity. The player walks along the dock and sees the vessel sprite. NPCs know it's there — dock workers are loading cargo, a crew member is smoking at the gangway. The ship has been there since Tuesday and will depart Thursday. This is world presence, not an instance that pops into existence when you buy a ticket.
The Docked state enables:
- Investigation opportunity: The player can board a docked vessel before departure. The crew is aboard. The cargo manifest is discoverable. The ship has history (mutations from previous voyages).
- Tycoon opportunity: The docked vessel represents cargo capacity. Its departure schedule is a trade window.
- Assassination opportunity: A target boarding a vessel tomorrow means the player has until departure to act — or they follow the target aboard and the vessel becomes a sealed environment.
- World texture: Ports feel alive because vessels arrive and depart. The dock district's NPC density varies with the vessel schedule.
Vessel lifecycle:
- Spawned at system generation (seeded: which vessels exist, which routes they serve)
- Docked at a port (idle between voyages — world entity, visible, boardable)
- Passengers board at departure time (manifest seeded from departure location's NPC pool)
- InTransit (interior accessible, exterior scrolls)
- Arrives at destination (passengers disembark, vessel enters Docked/Idle at new port)
- Cycle repeats (scheduled routes) or vessel enters Idle (unscheduled)
The vessel persists across the entire lifecycle. Its interior state, crew, and mutations carry forward. A ship the player damaged three voyages ago still has the scar.
1.6 Addressing Nigel's Cost Concern
Nigel's instanced district model was cheaper. He's right — the instanced model is simpler to implement. But the cost delta is smaller than it appears:
| Component | Entity-carried | Instanced district | Delta |
|---|---|---|---|
| Data structure | MobileChunk (~100 LOC) | DistrictSkeleton reuse (~0 LOC) | +100 LOC |
| Docked state | 2 dev-days | Not applicable | +2 dev-days |
| InTransit scroll | 3 dev-days | 3 dev-days (same visual requirement) | 0 |
| InterSystem | 0.5 dev-days | 0.5 dev-days | 0 |
| Templates | 2 dev-days | 2 dev-days | 0 |
| NPC routines | 1 dev-day | 1 dev-day | 0 |
| World persistence | 1 dev-day | Not applicable | +1 dev-day |
| Total | ~9.5 dev-days | ~6.5 dev-days | +3 dev-days |
Three dev-days buys us: persistent docked vessels, vessel history/mutations across voyages, world-present ships visible at ports, investigation/boarding before departure, dock NPCs that interact with specific vessels.
That's an excellent trade. The simplifications in §1.2 already addressed the complexity concern for generation. The only additional cost is the world-presence plumbing, and that's infrastructure the game needs regardless.
1.7 Streaming Integration
One mobile chunk loaded per player at a time (v0.1–v0.4 constraint). The loading rules:
- Player outside vessel: Vessel is a sprite. Interior not loaded.
- Player enters docked vessel: MobileChunk loads. Surrounding static chunks remain loaded (dock area).
- Vessel departs (InTransit): Static chunks around departure dock unload as they leave the loading grid. MobileChunk stays loaded. Exterior transitions to scrolling visual buffer.
- Vessel arrives: Destination dock static chunks load. Access points activate. Player can exit.
- Player exits vessel: MobileChunk drops from active loading grid (but persists in world state — crew continues reduced-fidelity simulation).
2. OQ-R4-B: WorldTier Rename
Lead decision: rename SignificanceTier to WorldTier. Adopting Nigel's naming and tier definitions.
2.1 The Renamed Enum
/// How important this location is in the galaxy network.
/// Set at system generation from seed + galaxy topology.
/// Constrains the maximum achievable ComplexityTier.
/// IMMUTABLE after generation.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
enum WorldTier {
/// Maximum connectivity, major faction presence, historically significant.
/// Multiple districts. Hub of trade, politics, culture.
/// Can support: Full or Moderate complexity.
Epicenter,
/// Meaningful connectivity, notable faction presence, relevant to the wider network.
/// 1-4 districts. Significant but not central.
/// Can support: Full, Moderate, or Minimal complexity.
Regional,
/// Transit-relevant primarily. Light faction footprint.
/// Functionally important for travel but not socially deep.
/// 1-2 districts. Pass-through with some infrastructure.
/// Can support: Moderate or Minimal complexity.
Passage,
/// Low external connectivity, weak external faction presence.
/// Self-contained community. 1 district.
/// Can support: Full (dense isolated community), Moderate, or Minimal.
Backwater,
/// Minimal or no social complexity. Geography/transit function only.
/// Fuel stop, relay station, uninhabited.
/// Can support: Minimal or Empty complexity.
Waypoint,
}
2.2 The Constraint Matrix
WorldTier constrains the maximum ComplexityTier. ComplexityTier constrains the maximum DramaDensity. The chain:
WorldTier → constrains → ComplexityTier → constrains → DramaDensity (runtime)
| WorldTier | Max ComplexityTier | Rationale |
|---|---|---|
| Epicenter | Full | Has the population and infrastructure |
| Regional | Full | Can sustain full complexity in main district |
| Passage | Moderate | Transit-focused; infrastructure serves movement, not residence |
| Backwater | Full | KEY INSIGHT: a Backwater can be Full. Dense isolated community. |
| Waypoint | Minimal | Not enough social structure for more |
| ComplexityTier | Max DramaDensity | Rationale |
|---|---|---|
| Full | Flashpoint | Has enough NPCs and social fabric for maximum drama |
| Moderate | High | Enough for multi-thread drama, not enough for full Flashpoint |
| Minimal | Low | A few NPCs, one thread at most |
| Empty | Zero | No social infrastructure, no drama possible |
2.3 The Backwater + Full Case
This is the single most important combination in the matrix and deserves emphasis.
A Backwater world with Full complexity is a dense, isolated community rich with human drama. Think: fishing village of 200 people who've known each other for 40 years. No galactic significance. No faction presence. But every NPC has a history with every other NPC. The information asymmetry inverts (Miri's insight): the player can't be anonymous. Everyone knows their name within hours. The conspiracy here is intimate — not cargo smuggling or political assassination, but decades of personal entanglement.
This is a fundamentally different game experience from an Epicenter + Full district. The generator produces it by allowing WorldTier and ComplexityTier to be independent. The combination space is the game's variety.
2.4 What Changes in the Codebase
The rename is purely nominal. Everywhere the codebase references SignificanceTier, replace with WorldTier. Everywhere it says significance:, replace with world_tier:. The enum values change:
| Old (SignificanceTier) | New (WorldTier) |
|---|---|
| CenterStage | Epicenter |
| Regional | Regional |
| (no equivalent) | Passage |
| Backwater | Backwater |
| Waypoint | Waypoint |
| Insignificant | (absorbed into Waypoint) |
The addition of Passage (between Regional and Backwater) and the absorption of Insignificant into Waypoint are the substantive changes. A transit hub that has some social structure but isn't a residential community was awkwardly represented as either Regional (too much) or Backwater (wrong connotation). Passage captures this correctly.
3. OQ-R4-F: Gas Explosion — XOR vs. Structured Re-Generation
Gestalt proposed original_seed XOR event_seed for large-scale events that affect too many tiles for the overlay model. The question: does XOR produce results that look caused or random?
Let me be honest about what this means technically.
3.1 The Concrete Scenario
Setup: Block (2,1) of a logistics district. Chunk (4,2) — a warehouse zone adjacent to a gas main. The chunk has been visited and is cached. The warehouse has:
- A 20×30 floor area with cargo racks (rows of crates, 2-tile aisles)
- A loading dock on the south face (8 tiles wide)
- An office in the northeast corner (8×6 tiles, desk, terminal, filing)
- A maintenance corridor along the west wall (2 tiles wide, pipe runs)
- Gas main runs under the maintenance corridor
Event: Gas main rupture → explosion. Blast radius: 12 sim tiles from the rupture point (midpoint of west wall). Force: High.
3.2 Approach A: XOR Re-Seeded Re-Generation
original_chunk_seed = 0xA7B3C1D4E5F60718
event_seed = 0x00000000DEADBEEF (gas_explosion event)
new_chunk_seed = 0xA7B3C1D4BA530EA7 (XOR result)
Phase 2 re-runs with new_chunk_seed. The template stamper generates a completely new chunk from this seed. Result:
ORIGINAL (seed A7B3...): XOR RE-GENERATED (seed A7B3...BA53...):
┌─────────────────────┐ ┌─────────────────────┐
│ ┌──────┐ │ │ │
│ │Office│ Cargo │ │ Open floor plan │
│ │ │ racks │ │ (different layout) │
│ └──────┘ ║║║║║║ │ │ ┌─────┐ ┌──────┐│
│ pipes ║║║║║║║║║║ │ │ │Break │ │Tool ││
│ ║ ║║║║║║║║║║ │ │ │room │ │shop ││
│ ║ ║║║║║║║║║║ │ │ └─────┘ └──────┘│
│ ║ ║║║║║║║║║║ │ │ │
│ ║ ── aisles ── │ │ ┌────────────────┐│
│ ║ │ │ │ Storage bay ││
│ ════loading dock════ │ │ └────────────────┘│
└─────────────────────┘ └─────────────────────┘
What went wrong: The new seed produces a different building. The office moved. The cargo layout changed. The maintenance corridor is gone. There's a break room that didn't exist before. The result looks like someone demolished the warehouse and built a different facility. It doesn't look like an explosion happened — it looks like a different world.
Why XOR fails: XOR produces a uniformly different seed. A uniformly different seed produces a uniformly different Phase 2 output. There's no structural relationship between the original and the result. The template stamper doesn't know this is supposed to be a damaged version of the original — it's just stamping a new template from a new seed.
3.3 Approach B: Structured Damage via Overlay (My Recommendation)
Don't re-generate. Apply the explosion as a ChunkMutation overlay on the original chunk:
// The explosion system computes the damage mask
fn apply_gas_explosion(
chunk: &ChunkData,
center: (u16, u16),
radius: u8,
force: ExplosionForce,
) -> ChunkMutations {
let mut mutations = ChunkMutations::default();
for tile in tiles_in_radius(center, radius) {
let distance = distance(center, tile);
let original_tile = chunk.get_tile(tile);
if distance <= 3 {
// EPICENTER: total destruction
// Walls → rubble. Floor → blast crater. Objects → debris.
mutations.tile_overrides.push(TileOverride {
position: (tile.x, tile.y, tile.z),
new_tile: TileId::BlastCrater,
cause: MutationCause::Explosion {
radius,
source: gas_main_entity,
},
});
} else if distance <= 6 {
// INNER BLAST: structural damage
// Walls → breached walls. Objects → destroyed.
// Floor survives but is scorched.
if original_tile.is_wall() {
mutations.structural_changes.push(StructuralChange {
min: (tile.x, tile.y, tile.z),
max: (tile.x, tile.y, tile.z),
change_type: StructuralChangeType::WallDestroyed,
});
}
mutations.tile_overrides.push(TileOverride {
position: (tile.x, tile.y, tile.z),
new_tile: TileId::ScorchedFloor,
cause: MutationCause::Explosion {
radius,
source: gas_main_entity,
},
});
} else if distance <= radius as u16 {
// OUTER BLAST: cosmetic damage + scattered debris
// Walls survive. Objects knocked over. Glass shattered.
if original_tile.has_glass() {
mutations.tile_overrides.push(TileOverride {
position: (tile.x, tile.y, tile.z),
new_tile: TileId::ShatteredGlass,
cause: MutationCause::Explosion {
radius,
source: gas_main_entity,
},
});
}
// Scatter debris objects (30% chance per outer tile)
if seeded_chance(tile, event_seed, 0.3) {
mutations.placed_objects.push(PlacedObject {
position: (tile.x, tile.y, tile.z),
object_id: ObjectId::ExplosionDebris,
});
}
}
}
mutations
}
Result:
AFTER STRUCTURED OVERLAY:
┌─────────────────────┐
│ ┌──────┐ │
│ │Office│ Cargo │ ← Office survives (outside blast radius)
│ │(glass│ racks │ ← Cargo racks: outer ones damaged, inner intact
│ │broke)┘ ▒║║║║║▒ │
│ ░░░░░ ▒▒▒║║║║▒▒▒ │ ← Maintenance corridor: DESTROYED (epicenter)
│ ████ ▒▒▒▒░░░░▒▒▒ │ ← Inner blast: walls breached, floor scorched
│ ████ ▒▒░░░░░░░▒▒ │ ← Epicenter: blast crater, rubble
│ ████ ▒▒░░████░▒▒ │ ← ████ = rubble/crater (was maintenance corridor)
│ ░░░░ ▒▒▒▒▒▒▒▒▒▒ │ ← ░░ = scorched floor ▒▒ = debris scatter
│ ░░░░ ▒ ▒ │ ← Outer zone: dust and glass
│ ════loading dock════ │ ← Loading dock: partially damaged but recognizable
└─────────────────────┘
Legend: ████ = total destruction (blast crater)
░░░░ = scorched floor (inner blast)
▒▒▒▒ = debris scatter (outer blast)
║║║║ = surviving cargo racks
What went right:
- The warehouse is recognizable. The player's spatial memory works — "the office was in the northeast corner, and it's still there, but the glass is shattered."
- The damage radiates from the gas main — the epicenter is the west wall maintenance corridor, which makes physical sense. The player can SEE the cause.
- The loading dock partially survived — it was far enough from the blast center. The cargo racks nearest the west wall are destroyed; the ones on the east side are intact. The gradient is visible.
- New access routes were created: The maintenance corridor wall is breached. What was a sealed 2-tile pipe run is now a gaping hole. The player can walk through where walls used to be. LOS changed dramatically in the blast zone.
- Private geographic knowledge: The player who was here before the explosion knows the office has a terminal with manifests. The explosion didn't destroy the office — it destroyed the corridor between the office and the loading dock. The player's prior knowledge of the layout gives them an advantage in navigating the damage.
3.4 Mutation Scale Analysis
How many tile mutations does a gas explosion actually produce?
| Blast zone | Radius | Tiles affected | Mutations per tile | Total mutations |
|---|---|---|---|---|
| Epicenter | 0-3 | ~28 tiles | 1 (total destruction) | ~28 |
| Inner blast | 4-6 | ~65 tiles | 1-2 (wall + floor) | ~100 |
| Outer blast | 7-12 | ~325 tiles | 0.3 average (debris/glass) | ~100 |
| Total | ~418 tiles | ~228 mutations |
228 mutations. Each mutation is ~24 bytes (position + tile ID + cause). Total delta: ~5.5 KB. This is tiny. It's smaller than the chunk itself. There is no scenario where a single-point explosion overwhelms the overlay model.
Even a chain explosion that detonates 5 gas mains across a block produces ~1200 mutations (~29 KB). Still trivially small. The overlay model handles this without breaking a sweat.
3.5 When (If Ever) to Use Seed Modification
XOR re-seeding has exactly one valid use case: faction-level reconstruction over weeks/months of game time.
A district that was destroyed by a massive event and then rebuilt by the community over months — the rebuilt version should be different from the original because the rebuilders are making new decisions. This is not damage; this is new construction on a cleared site.
For this case, the structured approach is:
struct DistrictReconstruction {
original_skeleton: DistrictSkeletonRef,
damage_extent: DamageExtent, // which blocks were destroyed
reconstruction_era: Era, // the era of the new construction
reconstruction_heritage: HeritageRoot, // who rebuilt it
reconstruction_seed: u64, // seeded from event + time elapsed
}
Phase 1 re-runs on the damaged blocks only, with the reconstruction parameters informing the new skeleton. The result is a district where some blocks are original and some are new construction — visually distinct (different era, different condition, different heritage influence), spatially coherent (infrastructure connections preserved), and narratively legible (the player can see where the old meets the new).
This is a v0.5+ feature. For v0.1–v0.4, the overlay model handles all dynamic modification.
3.6 Verdict
XOR re-seeding is rejected for damage events. It produces uncaused-looking results that violate Ozzie's principle (destruction must feel caused, not random) and Miri's requirement (aftermath must intensify existing character, not replace it).
Structured overlay (ChunkMutations) is the correct approach for all events up to and including full-block destruction. The overlay preserves spatial memory, produces visually causal damage patterns, and creates new gameplay affordances (breached walls, new access routes) that feel earned.
Structured reconstruction (re-running Phase 1 on cleared blocks with reconstruction parameters) is the correct approach for long-term rebuilding after massive events. This is deferred to v0.5+.
4. DramaDensity NOT on DistrictSkeleton — Confirmed
All Round 3 participants agree. I'm confirming and documenting the architectural boundary formally.
4.1 Where DramaDensity Lives
┌─────────────────────────────────────────────────────┐
│ GENERATOR STATE (immutable after Phase 1) │
│ │
│ DistrictSkeleton │
│ ├── world_tier: WorldTier (static) │
│ ├── complexity: ComplexityTier (static) │
│ ├── ... (all spatial/social structure) │
│ └── NO DramaDensity field │
│ │
├─────────────────────────────────────────────────────┤
│ STORYTELLER STATE (runtime, dynamic, session-scoped) │
│ │
│ StorytellerDistrictState │
│ ├── drama_density: DramaDensity (dynamic) │
│ ├── active_modules: Vec<ModuleId> (dynamic) │
│ ├── activated_triangles: Vec<...> (dynamic) │
│ └── fragility_states: Vec<...> (dynamic) │
│ │
└─────────────────────────────────────────────────────┘
4.2 The Boundary Rule
The generator produces capacity (what the world CAN support). The storyteller produces utilization (what is happening NOW). The DistrictSkeleton is the contract for capacity. DramaDensity is utilization. They live in separate state stores.
The storyteller reads the DistrictSkeleton to determine:
- What ComplexityTier constrains the DramaDensity ceiling
- Which social sites and triangles exist (the activation targets)
- What spatial configuration enables or limits event placement
The storyteller writes to its own state:
- DramaDensity (can increase or decrease per session)
- Which modules are active
- Which triangles are fired
- Which fragilities are primed for activation
The DistrictSkeleton is never modified by the storyteller. This is a hard architectural boundary. If the storyteller could write to the skeleton, deterministic re-generation from seed would break.
4.3 Initial DramaDensity
At the start of a new game, the storyteller reads all DistrictSkeletons and assigns initial DramaDensity values based on:
- The game's seed (deterministic initial drama distribution)
- ComplexityTier ceilings (respecting the constraint matrix)
- Narrative intent (the storyteller's pacing algorithm decides which worlds start hot)
This initial assignment is stored in StorytellerState, not in the DistrictSkeleton. Same boundary. Even the "initial" drama level is runtime state, not generator state.
5. FINAL Canonical DistrictSkeleton — D-Record Version
This is the version that goes into the D-record. All naming resolved. All Round 3 additions included.
5.1 The Struct
/// ═══════════════════════════════════════════════════════════
/// CANONICAL DISTRICTSKELETON — D-RECORD VERSION
/// Generator output for one district.
/// Produced by Phase 1. Consumed by Phase 2.
/// Contract between world prep and local generation.
/// ═══════════════════════════════════════════════════════════
#[derive(Serialize, Deserialize, Clone, Debug)]
struct DistrictSkeleton {
// ── Identity ──────────────────────────────────────────
/// Unique identifier for this district
district_id: DistrictId,
/// Deterministic seed (derived from master seed via SeedChain)
seed: u64,
/// District classification (logistics hub, residential, mixed-use, etc.)
district_type: DistrictType,
/// World context (which system, which world, neighboring districts)
context: DistrictContext,
// ── Classification ────────────────────────────────────
/// Network importance of this world (galaxy-level significance)
world_tier: WorldTier,
/// Generator content budget for this district
complexity: ComplexityTier,
/// Physical setting type (station, urban, agricultural, etc.)
setting: SettingType,
/// Block layout mode (grid for planned, organic for grown)
layout_mode: DistrictLayoutMode,
// ── Spatial Structure ─────────────────────────────────
/// The 4×4 block grid (each block = 128×128 sim tiles = 2×2 chunks)
blocks: [[BlockSkeleton; 4]; 4],
/// Multi-block reservations (skyscrapers, parks, terminals, plazas)
reservations: Vec<MultiBlockReservation>,
/// Corridor spines connecting key access points
corridors: Vec<CorridorSpine>,
/// Z-level count for this district (practical cap: 64)
z_levels: u8,
// ── Social Structure ──────────────────────────────────
/// Social site placements with template tags and triangle configs
social_sites: Vec<SocialSitePlacement>,
/// District-level access points (entries/exits to neighboring districts)
access_points: Vec<AccessPoint>,
// ── Cultural / World Context ──────────────────────────
/// Society profile reference (Miri's cultural ingredients)
society_profile: SocietyProfileRef,
/// Zone palette definitions (base + modifiers per zone)
zone_palette: Vec<ZoneDefinition>,
// ── Boundary System ───────────────────────────────────
/// Edge descriptors for transition strips with neighboring districts
boundaries: DistrictBoundaries,
// ── Validation ────────────────────────────────────────
/// Guarantee audit result (conditional on complexity tier).
/// None for Empty districts. Tier-appropriate checks for others.
guarantee_audit: Option<GuaranteeAuditResult>,
}
5.2 Supporting Enums and Structs
// ═══ WorldTier ═══════════════════════════════════════════
// (Defined in §2.1 above — not repeated here)
// ═══ ComplexityTier ══════════════════════════════════════
/// Generator content budget. Determines which guarantees apply.
/// Derived from WorldTier + SettingType at Phase 1.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
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,
}
// ═══ SettingType ═════════════════════════════════════════
/// Physical setting. Merged from Gestalt's SettingGeometry + Tyre's TerrainType.
#[derive(Serialize, Deserialize, Clone, Debug)]
enum SettingType {
Station,
Urban,
Agricultural,
Maritime,
Wilderness { biome: Biome },
Water { water_type: WaterType },
Transitional,
Orbital,
Specialized { function: SpecializedFunction },
}
// ═══ DistrictLayoutMode ═════════════════════════════════
/// How blocks are placed within the district's 512×512 footprint.
#[derive(Serialize, Deserialize, Clone, Debug)]
enum DistrictLayoutMode {
/// Standard Cartesian grid. Perpendicular streets.
Grid,
/// Organic placement with per-block offsets and rotations.
Organic {
placements: [[BlockPlacement; 4]; 4],
},
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct BlockPlacement {
/// Offset from grid-aligned position (±16 sim tiles per axis max)
offset: (i16, i16),
/// Rotation in 15° increments (0-3, max 45°)
rotation_steps: u8,
/// Street width multiplier (0.75–2.0, default 1.0 = 4 visual tiles)
street_width_factor: f32,
}
// ═══ BlockSkeleton ══════════════════════════════════════
#[derive(Serialize, Deserialize, Clone, Debug)]
struct BlockSkeleton {
position: (u8, u8),
zoning: ZoningType,
reservation: Option<ReservationId>,
chunk_layout: ChunkLayout,
hosted_sites: Vec<SocialSiteId>,
era: Era,
era_modifications: Vec<EraModification>,
era_cause: Option<EraCause>,
density: f32,
landmark: Option<LandmarkSlot>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
enum EraCause {
Original,
CorporateMerger,
EmergencyExtension,
OrganicGrowth,
InstitutionalIncursion,
EconomicDisruption,
CulturalShift,
}
// ═══ MultiBlockReservation ══════════════════════════════
#[derive(Serialize, Deserialize, Clone, Debug)]
struct MultiBlockReservation {
blocks: Vec<(u8, u8)>,
template_tag: String,
function: ReservationFunction,
z_levels: u8,
base_z: u8,
floor_zones: Vec<FloorZone>,
z_band_count: u8,
z_band_zones: Vec<ZoneDefinition>,
vertical_corridors: Vec<VerticalCorridorSpec>,
hosted_sites: Vec<SocialSiteId>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct FloorZone {
z_level: u8,
zone_type: ZoningType,
zone_palette: ZonePalette,
access_tier: AccessTier,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct VerticalCorridorSpec {
block_coords: Vec<(u8, u8)>,
z_bands_connected: Vec<u8>,
access_tier: AccessTier,
corridor_type: VerticalCorridorType,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
enum ZLevelLoadState {
Loaded(ChunkData),
Skeleton(FloorZone),
Ungenerated,
}
// ═══ Social Structure ═══════════════════════════════════
#[derive(Serialize, Deserialize, Clone, Debug)]
struct SocialSitePlacement {
site_id: SocialSiteId,
blocks: Vec<(u8, u8)>,
template_tag: String,
access_tier: AccessTier,
triangles: Vec<TriangleAssignment>,
role_slots: Vec<RoleSlot>,
active_phases: Vec<DayPhase>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct TriangleAssignment {
template: TriangleTemplate,
purposes: Vec<TrianglePurpose>,
participants: Vec<RoleSlotId>,
staging_block: Option<(u8, u8)>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
enum TrianglePurpose {
Investigation,
Economic,
Social,
Political,
Tactical,
Mundane,
}
// ═══ Palette System ═════════════════════════════════════
#[derive(Serialize, Deserialize, Clone, Debug)]
struct ZonePalette {
base: BasePalette,
modifiers: Vec<PaletteModifier>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
enum PaletteModifier {
EconomicFunction(EconomicModifier),
Era(Era),
FactionPresence(FactionModifier),
Condition(ConditionModifier),
Heritage(HeritageRoot),
Season(Season),
}
// ═══ Destructible Boundaries ════════════════════════════
/// What exists on the far side of a wall tile.
/// Every wall tile in a generated chunk is tagged with one of these.
#[derive(Serialize, Deserialize, Clone, Debug)]
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.
/// Supports modified LOS and small object passing.
ServiceVoid,
/// Edge of chunk. Adjacent chunk's boundary tiles on the far side.
ChunkBoundary,
/// Faces outside (hull, exterior wall). Breach = catastrophic consequences.
Exterior,
}
// ═══ Dynamic Modification ═══════════════════════════════
/// Mutations applied to an already-generated chunk.
/// Stored alongside the chunk in the save file.
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
struct ChunkMutations {
tile_overrides: Vec<TileOverride>,
structural_changes: Vec<StructuralChange>,
placed_objects: Vec<PlacedObject>,
removed_objects: Vec<ObjectId>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct TileOverride {
position: (u16, u16, u8),
new_tile: TileId,
cause: MutationCause,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
enum MutationCause {
Explosion { radius: u8, source: EntityId },
Fire { spread_from: Option<(u16, u16)> },
Construction { builder: EntityId },
Decay { time_since_maintenance: u32 },
PlayerAction { action: ActionId },
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct StructuralChange {
min: (u16, u16, u8),
max: (u16, u16, u8),
change_type: StructuralChangeType,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
enum StructuralChangeType {
WallDestroyed,
FloorCollapsed,
CeilingBreached,
AreaSealed,
WallConstructed,
}
// ═══ Guarantee Audit ════════════════════════════════════
/// Conditional guarantee audit. Tier-aware: Minimal districts
/// get 3 checks; Full districts get up to 11.
#[derive(Serialize, Deserialize, Clone, Debug)]
struct GuaranteeAuditResult {
// Tier 1: Universal (all inhabited districts)
social_hub: AuditCheck,
informal_zone: AuditCheck,
encounter_corridor: AuditCheck,
// Tier 2: Full-complexity only (None for lower tiers)
traffic_chokepoint: Option<AuditCheck>,
institutional_space: Option<AuditCheck>,
insider_space: Option<AuditCheck>,
economic_node: Option<AuditCheck>,
// Tier 3: Conditional (None unless conditions met)
elevated_vantage: Option<AuditCheck>,
temporal_opacity_window: Option<AuditCheck>,
power_gradient_visibility: Option<AuditCheck>,
economic_asymmetry_signal: Option<AuditCheck>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct AuditCheck {
passed: bool,
satisfied_by: Option<SatisfiedBy>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
enum SatisfiedBy {
SocialSite(SocialSiteId),
Block(u8, u8),
Corridor(CorridorSpineId),
TerrainFeature(String),
}
5.3 Updated Memory Budget
With all Round 3 + Round 4 changes:
| Component | Size per district | Notes |
|---|---|---|
| Identity + classification | ~80 bytes | Fixed; WorldTier replaces SignificanceTier |
| Blocks (4×4 × BlockSkeleton) | ~2 KB | era_cause, density, landmark fields |
| Social sites + triangles | ~1-4 KB | Scales with ComplexityTier |
| Reservations + corridors | ~0.5-2 KB | Skyscraper reservations larger |
| Boundaries | ~4 KB | 4 edges × ~1 KB |
| Society profile ref | ~32 bytes | Reference only |
| Zone palette (with modifiers) | ~0.5-1 KB | Modifier stacks add ~50 bytes each |
| Guarantee audit | ~300 bytes | Now Option<> on Tier 2/3 checks |
| Layout mode (organic) | 0-1 KB | Only for organic districts |
| Total per district | ~8-14 KB | Unchanged from Round 3 |
MobileChunk memory:
| Component | Size per vessel | Notes |
|---|---|---|
| Identity + vessel class | ~96 bytes | Fixed |
| ChunkData (when loaded) | ~16 KB | Standard 64×64 chunk |
| Social sites + triangles | ~0.2-1 KB | Simpler than district |
| Crew + passengers | ~0.5-2 KB | Roster references |
| Mutations | ~0-1 KB | Accumulates over voyages |
| Total per vessel (loaded) | ~17-20 KB | |
| Total per vessel (unloaded) | ~1-4 KB | ChunkData not in memory |
System totals:
| Scope | Count | Memory |
|---|---|---|
| All district skeletons | 300 worlds × ~6 districts | ~21 MB |
| Active vessels (loaded) | ~5-10 in player vicinity | ~100-200 KB |
| All vessels (unloaded) | ~50-200 across all systems | ~200-800 KB |
| Total generator state | ~22 MB |
Trivial. Well within any platform's memory budget.
5.4 The MobileChunk as Companion Struct
The MobileChunk is NOT a DistrictSkeleton variant. It's a companion struct — a first-class world entity that uses the same ChunkData format but has its own generation and streaming rules.
DistrictSkeleton (static world) MobileChunk (mobile world)
├── 4×4 blocks ├── Single chunk (or 2×2 for Capital)
├── Full Phase 1 → Phase 2 ├── Template stamp only (no skeleton)
├── Spatial guarantees ├── No spatial guarantees
├── Complex NPC generation ├── Roster seeding (crew + passengers)
├── Multiple ComplexityTiers ├── VesselClass instead
├── Fixed world coordinates ├── Entity-attached coordinates
└── Immutable after gen └── Immutable after gen (mutations layer)
+ movement state (dynamic)
Both produce ChunkData. Both support ChunkMutations. Both feed the same rendering pipeline. The difference is in generation complexity and world-presence model.
6. Updated Cost Summary
Round 4 changes to the cost estimate:
| Change | Impact |
|---|---|
| XOR re-seeding removed | −0 dev-days (was never costed separately) |
| Structured reconstruction (v0.5+) | +3 dev-days (deferred, not in v0.1-v0.4 total) |
| MobileChunk Idle state added | +0.5 dev-days |
| WorldTier rename | 0 dev-days (nominal change) |
Revised total: ~54.5 dev-days (v0.1–v0.4) — essentially unchanged from Round 3.
| Milestone | Dev-Days | What Ships |
|---|---|---|
| v0.1 | ~7 | DistrictSkeleton stub, BlockPlan stub, Transit District validation fixture |
| v0.2 | ~10.5 | SeedChain, SocietyProfile, DistrictSkeleton impl, Phase 2 chunk fill, ChunkMutations struct |
| v0.3 | ~23 | Edge bleed, palette modifiers, organic layout, mobile chunks (docked+transit+idle), mutation rendering, wall backside, lazy z-levels, Phase 1 background thread |
| v0.4 | ~14 | Skyscrapers, explosion mutations, non-urban terrain, NPC reduced-fidelity floors |
7. Summary: What Round 4 Resolved
| OQ | Resolution | Status |
|---|---|---|
| OQ-R4-A: Vessel Architecture | MobileChunk (entity-carried) is FINAL. Vessel interiors are simplified (VesselClass, template-stamp, no spatial guarantees). Persistent docked vessels are the key differentiator. Cost delta vs instanced: +3 dev-days. | RESOLVED |
| OQ-R4-B: WorldTier Rename | SignificanceTier → WorldTier. Five tiers: Epicenter, Regional, Passage, Backwater, Waypoint. Constraint matrix defined. | RESOLVED |
| OQ-R4-F: XOR Re-Seeding | REJECTED. XOR produces uncaused-looking results. Structured overlay (ChunkMutations) for all damage events. A gas explosion produces ~228 mutations (~5.5 KB) — trivially within the overlay model's budget. Structured reconstruction for long-term rebuilding deferred to v0.5+. | RESOLVED |
| DramaDensity placement | Confirmed NOT on DistrictSkeleton. Lives in StorytellerDistrictState. Hard architectural boundary: generator produces capacity, storyteller produces utilization. | CONFIRMED |
| Canonical DistrictSkeleton | FINAL version with all naming resolved (WorldTier, DistrictLayoutMode, SettingType, all Round 3 additions). MobileChunk as companion struct. Memory budget: ~22 MB total. Ready for D-record. | FINAL |
Tyre — Round 4 complete. The DistrictSkeleton is canonical and ready for the D-record. MobileChunk is specified with Nigel's simplicity concern resolved. WorldTier is named. XOR is dead; overlays are the way. DramaDensity knows its place: runtime, not generator. The architecture is final. ~54.5 dev-days, v0.1–v0.4. Let's build it.