Files
settled-reach/docs/workshops/generator-architecture/tyre-round1.md
T
jpmschweitzerandClaude Opus 4.6 9a5c9c4408 docs(docs): add frontmatter to generator-architecture workshop
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>
2026-03-13 23:40:37 +01:00

24 KiB
Raw Blame History

title, description, type, status, workshop, agent, round, created
title description type status workshop agent round created
Tyre Round 1 — Technical Constraints Hard technical constraints on spatial hierarchy, chunk sizes, and streaming architecture workshop archived generator-architecture tyre 1 2026-02-27

Round 1: Tyre — Technical Constraints on Generator Architecture

Workshop: Generator Architecture (#562) Agent: Tyre (Technical Architect) Date: 2026-02-27


1. Hard Technical Constraints on Spatial Hierarchy

These constraints are non-negotiable — they flow directly from confirmed D-records and cannot be relaxed without amending those decisions.

1.1 Chunk: 64×64 sim tiles (32×32 visual, 32m) — D-094

The chunk is the streaming and serialization unit. This is locked.

Property Value Source
Sim tiles 64×64 D-094
Visual tiles 32×32 D-094, D-066 (2x retina)
Physical size 32m × 32m D-066 (0.5m/sim tile)
Memory per chunk ~21 KB raw tile data 64×64 tiles × 5 bytes/tile (type + flags + occupancy) ≈ 20,480 bytes
Serialization format MessagePack D-020

Why 64×64 sim is the floor: Shadowcasting (D-035) operates at sim resolution. A smaller chunk means more cross-chunk boundary queries during LOS computation. At 64×64, a single chunk covers the full LOS radius of most entities (~20-30 sim tiles) without requiring neighbor lookups for most casts. Halving to 32×32 sim would roughly quadruple the frequency of cross-chunk shadowcasting — measurable cost on the critical path.

Why 64×64 sim is the ceiling (for now): Larger chunks waste bandwidth for partial visibility. The ObserverSnapshot (D-020) sends only visible state. A 128×128 chunk would mean loading 4× the data when only a corner is visible. The 64×64 sweet spot minimizes the ratio of loaded-but-invisible tiles.

1.2 Block: 128×128 sim tiles (2×2 chunks, 64m) — D-094

The block is the generator planning unit. Four chunks arranged in a 2×2 grid.

Property Value
Sim tiles 128×128
Visual tiles 64×64
Chunks 4 (2×2)
Physical size 64m × 64m

Generator implication: The block is where the generator decides the building footprint strategy. Four chunks can:

  • Remain independent (4 small buildings/spaces)
  • Merge 2 horizontally or vertically (1×2 building spanning 64×32 sim tiles)
  • Merge 2 in L-shape (building occupying 3 of 4 chunks with gap)
  • Merge all 4 (single large building spanning the full 128×128 sim tiles)

This is a 2-bit decision per chunk pair (merge/don't merge on each axis), producing a tractable combinatorial space for the generator without requiring variable-size building footprints.

1.3 District: 512×512 sim tiles (4×4 blocks, 256m) — D-094

Property Value
Sim tiles 512×512 per z-level
Visual tiles 256×256
Blocks 16 (4×4)
Chunks 64 (8×8)
Z-levels 3 (Transit District; variable for other types)
Memory per z-level ~1.35 MB (64 chunks × ~21 KB)
Memory for 3 z-levels ~4 MB

1.4 Hierarchy Depth: Exactly 4 Levels

The hierarchy is Region → District → Block → Chunk. No more, no fewer.

Why not deeper (sub-chunk quarters)? The workshop brief mentions a "sub-chunk quarter system" (¼ chunk = 32×32 sim tiles). cracks knuckles — let me be honest about what this means technically.

A 32×32 sim tile quarter is 16×16 visual tiles = 16m. That's actually a reasonable building footprint (The Last Shift bar is 28×22 visual). But:

  1. The quarter is NOT a hierarchy level — it's a fill rule. The chunk remains the streaming unit. Quarters are a layout constraint within a chunk, not a separately loaded/serialized entity. The generator decides how to fill a chunk's 64×64 sim space using quarter-aligned placement rules, but the server still loads/saves/streams the full chunk.

  2. Quarter merge rules are purely generator-side. The server doesn't know or care about quarters after generation. It sees tiles. The quarter concept exists only during the generation pass and in the template metadata.

  3. Adding a 5th hierarchy level (quarter) to the runtime would violate D-012's streaming model. Chunk is the streaming atom. Sub-chunk streaming would require partial chunk updates over the wire, complicating the ObserverSnapshot and the client's tile map management for zero gameplay benefit.

Recommendation: Quarters are a generation-time layout constraint, not a spatial hierarchy level. The hierarchy stays at 4 levels. The quarter system is a set of placement rules the chunk-fill stage of the generator uses internally.

Why not shallower? Removing blocks (District → Chunk directly) loses the generator's "what goes in this 64m² area" planning step. The block is where multi-chunk building footprints are decided. Without it, the generator must either think in individual chunks (losing building coherence) or in full districts (losing locality). The 2×2 block is the minimum viable planning unit for building-scale decisions.


2. Data Structure for the District Skeleton (Q-036)

The district skeleton is the generator's output from the district-generation stage. It describes what a district contains and where things go, without specifying individual tiles.

2.1 Proposed Data Structure

/// The district skeleton — atomic output of the district generation stage.
/// This is a planning artifact consumed by the block/chunk fill stages.
struct DistrictSkeleton {
    /// Unique district identifier (world-scoped)
    district_id: DistrictId,

    /// Generator seed for deterministic reproduction
    seed: u64,

    /// District classification driving template selection
    district_type: DistrictType,  // e.g., Transit, Residential, Commercial, Industrial, Administrative, Medical

    /// Economic/political context from pipeline stages above
    context: DistrictContext,

    /// The 4×4 block grid — each block has a zoning assignment
    blocks: [[BlockSkeleton; 4]; 4],

    /// Social sites placed within this district (D-025)
    social_sites: Vec<SocialSitePlacement>,

    /// Multi-block structure reservations (structures spanning >1 block)
    reservations: Vec<MultiBlockReservation>,

    /// Access topology — gate/entrance placement and connectivity
    access_points: Vec<AccessPoint>,

    /// Corridor/thoroughfare spine connecting access points
    corridors: Vec<CorridorSpine>,

    /// Z-level configuration
    z_levels: u8,

    /// Zone palette assignments (fog tints, surface colors per D-093)
    zone_palette: Vec<ZoneDefinition>,
}

struct DistrictContext {
    /// Faction controlling this district (affects templates, NPC generation)
    faction_control: FactionId,

    /// Economic prosperity tier (0-4, affects object density, building quality)
    prosperity: u8,

    /// Population density target (NPCs per block, guides NPC slot allocation)
    population_density: PopulationDensity,  // Sparse/Normal/Dense/Packed

    /// Cultural ingredients (Q-032) driving visual/naming variation
    cultural_profile: CulturalProfile,

    /// Transport adjacency — which access points connect to what
    transport_links: Vec<TransportLink>,
}

struct BlockSkeleton {
    /// Block position in the 4×4 grid (0-3, 0-3)
    position: (u8, u8),

    /// Primary zoning type for this block
    zoning: ZoningType,  // Residential, Commercial, Industrial, Institutional, Mixed, Open/Park, Infrastructure

    /// Whether this block is claimed by a multi-block reservation
    reservation: Option<ReservationId>,

    /// Chunk merge strategy for this block (how the 4 chunks combine)
    chunk_layout: ChunkLayout,

    /// Social sites hosted in this block (references into district's social_sites vec)
    hosted_sites: Vec<SocialSiteId>,
}

/// How the 4 chunks within a block are organized
enum ChunkLayout {
    /// All 4 chunks independent (small buildings, mixed use)
    Independent,

    /// Two chunks merged horizontally, two independent
    /// Contains: which pair merges (N or S row), orientation
    MergeH { row: MergeRow },

    /// Two chunks merged vertically, two independent
    MergeV { col: MergeCol },

    /// L-shaped merge (3 chunks), one independent
    LShape { corner: Corner },

    /// Full merge (single large building spanning all 4 chunks)
    FullMerge,

    /// Custom layout (for multi-block reservations that span into this block)
    Reserved,
}

struct SocialSitePlacement {
    /// Social site identifier
    site_id: SocialSiteId,

    /// Template tag selecting from the D-025 template library
    template_tag: String,  // e.g., "logistics_hub", "bar", "residential_cluster"

    /// Block(s) this site occupies
    blocks: Vec<(u8, u8)>,

    /// Specific chunk(s) within those blocks
    chunks: Vec<ChunkCoord>,

    /// NPC slot allocation (how many NPCs this site supports)
    npc_slots: NpcSlotAllocation,

    /// Access tier for entry (D-028 Layer 1)
    access_tier: AccessTier,  // Public, SemiPublic, SemiPrivate, Private, Restricted

    /// Triangle templates to instantiate at this site (D-024, D-087)
    triangles: Vec<TriangleTemplate>,

    /// Economic function (what this site does in the district economy)
    economic_function: EconomicFunction,
}

struct NpcSlotAllocation {
    /// Named roles (authored, specific function)
    named_roles: Vec<RoleSlot>,

    /// Generic background population slots (Tier 3)
    background_slots: u16,

    /// Total NPC capacity at peak hours
    peak_capacity: u16,
}

struct MultiBlockReservation {
    /// Reservation identifier
    id: ReservationId,

    /// Template for the multi-block structure
    template_tag: String,  // e.g., "gate_terminal", "park", "stadium"

    /// Blocks claimed by this reservation (coordinates in the 4×4 grid)
    footprint: Vec<(u8, u8)>,

    /// Whether this reservation crosses into a neighboring district
    cross_district: bool,

    /// Z-levels occupied
    z_range: (u8, u8),
}

struct AccessPoint {
    /// Position on the district boundary (edge + offset)
    edge_position: EdgePosition,

    /// What this connects to (transit stop, neighboring district, gate)
    connects_to: ConnectionTarget,

    /// Access tier (public entrance, restricted, staff only)
    access_tier: AccessTier,

    /// Width in visual tiles (constrains throughput and NPC flow)
    width_vt: u8,
}

2.2 Size Estimate

Per district skeleton:

  • 16 BlockSkeletons: ~16 × 64 bytes = ~1 KB
  • Social sites (4-8 per district): ~8 × 256 bytes = ~2 KB
  • Multi-block reservations (0-3): ~3 × 128 bytes = ~384 bytes
  • Access points + corridors: ~1 KB
  • Context + metadata: ~512 bytes
  • Total: ~5 KB per district skeleton

For 300 worlds × avg 6 districts = 1,800 district skeletons = ~9 MB. Trivial. Entire galaxy skeleton fits in memory.

2.3 Relationship to D-025 Social Sites

The generator does not invent new social site types. It:

  1. Selects from the D-025 template library based on zoning type and district context
  2. Places templates onto blocks/chunks using the spatial hierarchy
  3. Allocates NPC slots per template requirements
  4. Wires access topology (which sites connect to which corridors)

D-025 templates are authored. Skeleton placement is generated. The generator arranges templates, not tiles.


3. How Chunk Loading (D-012) Constrains the Spatial Hierarchy

3.1 Streaming Radius

The player's chunk loading radius determines how much of the district is live at any time. Current constraints:

Parameter Value Source
Player vision range ~20-30 sim tiles (LOS) D-035 shadowcasting
Sound range Close: 5 sim tiles, Mid: 15, Far: 30 D-018
Chunk size 64 sim tiles D-094

Loading strategy: 3×3 chunk grid centered on player = 9 chunks loaded. This covers 192×192 sim tiles (96m radius in each direction from center), safely beyond max LOS range. The player never sees a chunk boundary seam.

Memory at 3×3 loading: 9 chunks × ~21 KB = ~189 KB per z-level, ~567 KB for 3 z-levels. With entity data overlay: ~1-2 MB. Trivial.

3.2 Cross-Chunk Constraints on Generation

The generator must guarantee tile continuity at chunk boundaries. When two chunks are adjacent (whether in the same block or across blocks), their edge tiles must be compatible:

  • Wall segments must align or leave matching gaps (doors)
  • Floor types must transition cleanly (corridor entering a room)
  • Z-level connections (stairs, ramps) must align vertically

This is the hardest constraint on chunk-based generation. Two approaches:

Option A: Edge contracts. Each chunk face exports a set of "connection points" (door positions, corridor widths). The generator plans connections at the block level, then each chunk fill respects its edge contracts. This is what I recommend. It's how Wave Function Collapse and similar systems handle tile boundaries.

Option B: Overlap zones. Chunks share a 2-4 tile overlap strip with their neighbors. The generator fills the overlap first, then fills inward. Simpler conceptually but wastes tile real estate (up to 12.5% of each chunk at 4-tile overlap on all edges).

Recommendation: Edge contracts (Option A). Each chunk face has a fixed set of connection slots (e.g., 1-3 connections per face, each defined by position + width + access tier). The block-level planning stage determines which faces connect and where. The chunk-fill stage reads its face contracts and fills interior tiles accordingly.

3.3 Chunk Loading vs. Generator Computation

D-012 specifies that chunks load/unload around the player. For generated worlds, this means chunks must be generatable on demand when first entered, then cached.

Generation pipeline timing:

Stage When it runs Output
Galaxy → System → District skeletons Game start (from seed) All 1,800 district skeletons
Block planning (per district) On first visit to district OR game start for home district 16 BlockSkeletons with layouts + edge contracts
Chunk fill (per chunk) On entering loading radius Tile data for one 64×64 chunk

Chunk fill time budget: The player moves at Walk speed = 1 tile/2 ticks = 1 tile/200ms (at 10 tps). Crossing a 64-tile chunk takes ~12.8 seconds. A new chunk enters the 3×3 loading grid roughly every 6-12 seconds. The chunk fill generator has a budget of ~500ms per chunk (generous — can use background thread, D-010 deterministic sim doesn't constrain client-side gen).

At 64×64 = 4,096 tiles, that's ~122 microseconds per tile. Feasible. Template-based fill (stamp a pre-authored room into a quarter, decorate procedurally) will be well under budget. Full WFC at this scale takes ~10-50ms in optimized Rust.

3.4 Borderless Generation Implication

D-012 states the boundary can be removed for borderless worlds. For the generator, this means:

  • District skeletons must be generatable from neighbors' edge contracts (a new district skeleton can be created when the player approaches an ungenerated district boundary)
  • The 4×4 block grid is the district's internal structure; the inter-district boundary is just another set of edge contracts
  • The generator pipeline must be able to run the district skeleton stage for a single district in isolation, given only its neighbors' access points as input

This doesn't affect v0.1 (bounded, hand-authored) but constrains the generator architecture: district generation must be local, not global.


4. Performance Implications of Hierarchy Depth

4.1 Lookup Complexity

Converting a sim tile position to its hierarchy location:

Chunk coord: (x / 64, y / 64)          — 1 division
Block coord: (chunk_x / 2, chunk_y / 2)  — 1 division
District coord: (block_x / 4, block_y / 4) — 1 division

All integer divisions by powers of 2 = bit shifts. O(1) per lookup, ~3 nanoseconds. Hierarchy depth has zero performance impact on spatial lookups.

4.2 Spatial Queries (Pathfinding, LOS)

Pathfinding operates at sim-tile resolution within the loaded chunk grid. The hierarchy doesn't affect pathfinding cost directly. However:

  • Block-level precomputation: The generator can precompute a block-level connectivity graph (which blocks connect to which, through which access points). This gives A* a coarse-grid initial path (~16 nodes per district) before refining to tile-level within the relevant chunks. Saves 90%+ of pathfinding work for long paths.
  • District-level precomputation: Same idea at district scale. For cross-district travel, the pathfinder walks the district connectivity graph (~6 districts per station), then block graph, then tile graph. Three-level hierarchical A*.

Performance estimate for hierarchical A:*

Path type Nodes searched Time estimate
Within-chunk ~100-500 tiles <1ms
Within-block (cross-chunk) 4 chunks × ~200 tiles ~2-5ms
Within-district (cross-block) 16 blocks × 4 chunk entries ~1-3ms (coarse) + ~5ms (refine)
Cross-district 6 districts × 16 block entries ~2ms (coarse) + ~8ms (refine)

All well within the 100ms tick budget (D-031). The hierarchy helps pathfinding by providing natural coarse-graining.

4.3 Memory Layout

The hierarchy maps naturally to a flat array with computed indices:

/// All chunks in a district, flat array indexed by (x, y, z)
struct DistrictChunks {
    /// 8×8 chunks per z-level, up to 8 z-levels
    chunks: Vec<ChunkData>,  // indexed as z * 64 + y * 8 + x
}

Cache-friendly, contiguous, no pointer chasing. 64 chunks per z-level fit in ~1.3 MB — easily fits in L2 cache for spatial queries.

4.4 What If We Added More Levels?

Depth Levels Cost Benefit
3 Region → District → Chunk Loses building-scale planning Simpler generator
4 Region → District → Block → Chunk Current. Balanced. Building-scale planning + streaming
5 + Sub-chunk quarter Quarter = extra indirection at fill time Finer fill control
6 + Room Individual room tracking Overkill — rooms are tile patterns

Verdict: 4 levels is the sweet spot. Quarters are a fill-time concept, not a hierarchy level. Going deeper adds complexity without proportional benefit.


5. v0.1 Stub Interfaces for the Generator

v0.1 is hand-authored (D-036, D-093). The generator doesn't run. But the data structures and interfaces it will consume must exist as stubs now, or v0.2+ work will require a rewrite.

5.1 Must Stub Now (v0.1)

These interfaces are needed for the hand-authored Transit District to be expressible in generator-compatible terms. This validates the data model.

Stub What it does Why now
DistrictSkeleton struct Serializable district description The v0.1 Transit District should be representable as a DistrictSkeleton. This validates Q-036 — if the hand-authored district can be expressed as generator output, the data structure is correct.
ChunkData struct Per-chunk tile storage with edge contracts Already partially exists for D-012 chunk loading. Needs edge contract fields added.
BlockSkeleton struct Per-block zoning + chunk layout Validates that the 2×2 block decomposition works for the Transit District's hand-authored social sites.
SocialSitePlacement struct Template tag + block/chunk coordinates + NPC slots Validates that D-025 social sites can be addressed within the spatial hierarchy.
DistrictType enum Transit, Residential, Commercial, etc. Needed for Sova station's 6-district model (D-093, station profile).
AccessPoint / CorridorSpine Entry points and corridor network Validates the access topology from D-093 (gate cluster → transition → terminal → bar).

Effort estimate: ~3-4 developer-days to define structs, serialize the Transit District as a DistrictSkeleton, and write validation tests.

5.2 Stub at Block/Chunk Level (v0.1-v0.2)

Stub What it does Target
ChunkLayout enum Merge strategy per block v0.1 — needed for Transit District block decomposition
EdgeContract struct Connection points per chunk face v0.2 — first generated chunks need this
ZoningType enum Block-level land use classification v0.2 — drives template selection

5.3 Generator Pipeline Stubs (v0.2+, Design Only Now)

These are the pipeline stages themselves. v0.1 doesn't execute them, but the stage interfaces should be designed (not implemented) now so the pipeline architecture is validated.

Pipeline Stage Input Output Implementation target
Geography World seed, system parameters Planet/station type, basic terrain v0.6+
Infrastructure Geography output, transport network Station layout (district count, positions, connections) v0.4+
Zoning Infrastructure, economic/political context Per-block zoning assignments v0.3+
Block Planning Zoning, social site library, population targets BlockSkeletons with ChunkLayouts + edge contracts v0.3+
Chunk Fill BlockSkeleton, edge contracts, template library Tile data for each 64×64 chunk v0.2 (first target)
NPC Population Social site placements, population density, cultural profile NPC generation (D-024 axes, role assignments) v0.3+

Critical path for Q-037: Chunk Fill is the first generator stage to implement (v0.2) because it's the most concrete — takes a planned block and fills tiles from templates. Everything above it can be hand-specified while Chunk Fill is developed and validated.

5.4 Validation Strategy: Transit District as Generator Ground Truth

Recommendation: Express the v0.1 Transit District (D-093) as a hand-authored DistrictSkeleton + hand-authored ChunkData for each of its 64 chunks. This serves as:

  1. Schema validation — if the skeleton can't express the Transit District, the schema is wrong
  2. Generator test fixture — future generator output is compared against the hand-authored ground truth
  3. Content pipeline test — the skeleton → rendered map pipeline is validated end-to-end with known-good data

This is not requiring the Transit District to be "generated." It's requiring the generator's output format to be expressive enough to describe the Transit District. If it can describe the most complex hand-authored district, it can describe anything the generator produces.


6. Technical Risk Assessment

Risk Severity Mitigation
Edge contract system produces tile discontinuities at chunk boundaries HIGH Comprehensive boundary tests; Transit District as test fixture validates edge alignment
Generator can't fill chunks within 500ms budget MEDIUM Template stamping (not WFC) for v0.2; WFC only if templates are insufficient
Multi-block structures create irregular block boundaries MEDIUM Reservation system claims blocks before fill; reserved blocks use custom layouts
District skeleton data model doesn't survive contact with diverse district types MEDIUM Validate against all 6 Sova district types + 3 planetary settlement types before locking
Cross-district structures (park spanning two districts) create coordination complexity LOW Cap at v0.6+; v0.1-0.5 districts are self-contained. cross_district: bool on reservations is the escape hatch.
Quarter system over-complicates chunk fill LOW Quarters are generation-side only; if they cause problems, fall back to free-form template placement within chunks

7. Summary of Hard Constraints

  1. Chunk = 64×64 sim tiles. Non-negotiable. Streaming atom. (D-094)
  2. Block = 2×2 chunks. Generator planning unit. (D-094)
  3. District = 4×4 blocks = 64 chunks. Template composition unit. (D-094)
  4. Hierarchy = 4 levels. Quarters are fill rules, not hierarchy levels.
  5. Edge contracts at chunk boundaries. Required for cross-chunk tile continuity.
  6. District skeleton must express D-025 social sites. Generator arranges templates, doesn't invent new site types.
  7. Chunk fill budget: ~500ms. Based on player walk speed and 3×3 loading grid.
  8. District generation must be local. Required for D-012 borderless generation future.
  9. MessagePack serialization for all generator output. Per D-020.
  10. Deterministic from seed. Per D-010 principle 4. Same seed → same district → same tiles.

Tyre — Round 1 complete. Standing by for Round 2 cross-pollination.