Files
settled-reach/docs/workshops/generation-cascade/gestalt-round2.md
T
jpmschweitzerandClaude Opus 4.6 42ee1f0a0e docs(workshops): generation cascade workshop — 4 rounds, D-194 through D-218
Four-round workshop (Gestalt, Tyre, Paula, Burnelli-Sheldon, Miri)
mapping the full generation pipeline from planetary heightmap to
walkable tile. 25 D-records produced. Ticket dependency chain for
Tier 0-4 implementation identified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-03 20:18:17 +02:00

19 KiB
Raw Blame History

title, description, type, status, workshop, agent, round, created
title description type status workshop agent round created
Gestalt — Round 2: Spatial Pipeline Proposal Ordered implementation plan, thinnest vertical slice, stored-vs-derived transition, D-record candidates, and ticket dependency graph for the spatial generation pipeline workshop active generation-cascade gestalt 2 2026-04-30

Generation Cascade — Round 2: Gestalt Proposal

Scope note: This proposal covers the spatial pipeline ONLY — from atlas city markers to walkable rendered tiles. Social sites, NPC population, heritage grammar, society profiles, and cultural overlays are out of scope. The one textual concern (place naming) is Paula's domain and does not block any spatial work.


1. Ordered Implementation Plan

Five sequential steps. Each one unlocks the next. Nothing can be parallelized safely until step 3 lands.

Step 1 — WorldTier Enum Fix

Effort: 0.5 days

Rename enum values in server/src/simulation/generator.rs:

// Before:
enum WorldTier { Peripheral, Connected, Core }

// After (workshop-locked, L-3):
enum WorldTier { Epicenter, Regional, Backwater, Passage, Waypoint }

Update every reference in the codebase. This is a rename, not a redesign. Must land first — it is the type foundation for every subsequent step.

Step 2 — City → District Decomposition (Layer 6)

Effort: 1.5 days (two sub-tasks)

Sub-task 2a (0.5d): Single-district generator input

For the minimum viable slice, take markers.json city[0] (the capital city) and produce one DistrictInput — the struct that feeds Phase 1. Full multi-district city layout (N districts per city, coordinate translation) is deferred.

struct DistrictInput {
    district_id: DistrictId,
    seed: u64,                    // derived: world_seed XOR hash(city_id) XOR district_index
    world_tier: WorldTier,        // derived: system WorldTier from systems.db
    complexity: ComplexityTier,   // derived: from world_tier ceiling + city.population
    setting: SettingType,         // derived: from body.planet_class + city.kind
    district_type: DistrictType,  // derived: from body.economic_role + city.kind
    layout_mode: DistrictLayoutMode, // derived: from settlement_pattern
}

All fields are derived from inputs already available in systems.db + markers.json. No new stored data.

Sub-task 2b (1d): Full city decomposition + coordinate translation

After the minimum slice is working, extend to N districts per city:

  • Population formula: n_districts = max(1, city.population / 50_000) (capped at city atlas pixel footprint)
  • Atlas pixel → sim-tile coordinate mapping (Tyre's Gap B — see Section 4 below for the proposed formula)
  • DistrictType assignment from economic_role for each district slot

This is the production-quality layer. The minimum slice does not need it.

Step 3 — Phase 1 DistrictSkeleton Spatial Generation

Effort: 2 days (Stages 12 only)

Implement the two spatial stages that Phase 2 actually needs. Defer Stages 35 (reservations, social sites, guarantee audit) — they are enhancements to an already-walkable world, not prerequisites.

Stage 1 — Classification (0.5d)

From DistrictInput, write real values to:

  • world_tier, complexity, setting, layout_mode (all trivial — copy from input)
  • district_type (from input)
  • z_levels (from SettingType: Urban=3, Agricultural=1, Station=4, etc.)

Stage 2 — Block grid (1.5d)

This is the non-trivial step. Assign ZoningType to each of the 16 blocks in the 4×4 grid:

  • DistrictType determines the probability distribution across ZoningType values
  • DistrictLayoutMode (Grid vs Organic) determines block placement and rotation
  • BlockSkeleton.density_pct derived from district type and block position (center blocks denser)
  • Seed-driven variation within the distribution — same seed always produces same grid

Example mapping:

DistrictType Zoning distribution
Commercial 50% Commercial, 25% Mixed, 15% Residential, 10% Transit
Industrial 50% Industrial, 25% Restricted, 15% Mixed, 10% Transit
Residential 60% Residential, 25% Mixed, 15% Recreational
LogisticsHub 40% Transit, 30% Industrial, 20% Commercial, 10% Mixed

Fields deferred (left as stubs for now): reservations, corridors, social_sites, boundaries, guarantee_audit, zone_palette, society_profile, access_points.

Step 4 — Phase 2: Basic Chunk Tile Generator

Effort: 1.5 days

For each BlockSkeleton, fill 4 chunks (2×2 per block, so 64×64 tiles per chunk):

Minimum tile vocabulary needed:

// These are the ONLY tile types needed for a walkable generated slice
const TILE_FLOOR: TileId = "floor";
const TILE_WALL: TileId = "wall";  
const TILE_DOOR: TileId = "door";

Fill algorithm:

  1. Outer 2 tiles of each chunk = wall perimeter
  2. Interior 60×60 = floor
  3. Each block edge shared with an adjacent block gets one door placement (seeded position within center third of edge)
  4. GeneratorChunkData (currently Vec<bool>) gets extended to carry TileId per cell

This produces structurally boring but fully-walkable districts. Every block is a room. Every room has doors connecting to neighbors. The renderer can already handle this — it reads TileId data.

Note: ZoningType per block should tint the floor tile identifier so zones are visually distinguishable at this stage (e.g. "floor_commercial", "floor_industrial") — one line change per block, but makes the output informative.

Step 5 — Chunk Streaming → Generator Hookup

Effort: 1 day

Modify WalkabilityMap::load_chunk() to call the Phase 2 generator when a chunk coordinate has no YAML file. Requires:

  • A DistrictMap resource that maps ChunkCoordDistrictId + BlockPosition (Step 2b contributes to this; Step 2a version maps everything to the one test district)
  • Phase 2 generator callable from chunk loading path (Step 4)
  • Cache generated chunks in memory (don't regenerate on every access)

2. Thinnest Vertical Slice — Converged Definition

Converging Gestalt (57d) and Tyre (9d) estimates:

The discrepancy came from two sources: (1) Tyre's 2d estimate for city decomp vs my 0.5d, and (2) Tyre's Phase 1 estimate included workshop-outcomes.md effort figures for all 5 stages while I deferred stages 35.

With the stripped scope (no social sites, no heritage grammar, no society profiles), and with the "one district at world origin" definition of minimum viable, the converged estimate is:

Step Task Effort
1 WorldTier enum fix 0.5d
2a Single-district input stub 0.5d
3 Phase 1 stages 12 (classification + block grid) 2d
4 Phase 2 minimal (wall + floor + door per block) 1.5d
5 Chunk streaming hookup 1d
Total minimum 5.5 days

What this produces: A player can spawn in a generated city district, walk through structurally distinct (but visually basic) rooms, cross door thresholds between blocks, and see zone-tinted floor tiles. The streaming system loads/unloads chunks correctly. The pipeline is end-to-end driven by city data in systems.db + markers.json — no hand-authored YAML maps involved.

What this explicitly does NOT include:

  • Step 2b (full N-district city layout and coordinate translation) — 1d, separate ticket
  • Phase 1 stages 35 (reservations, social sites, guarantee audit) — deferred
  • Vertical structures (z_levels > 1) — deferred to Phase 4 deliverable (2-floor test map)
  • Tile variety (single TILE_FLOOR type per zone tint) — deferred to heritage grammar sprint

3. The Stored vs. Derived Transition

The lead's question: Where in the pipeline does the transition from explicitly-stored data to seed-based deterministic derivation happen?

Answer: At the boundary between atlas markers and city → district decomposition (between Layer 5 and Layer 6).

Everything at Layer 5 and above is stored because it required human judgment (hand-refinement #838), expensive computation (terrain simulation, A* routing), or external process output (Gemma naming). Everything at Layer 6 and below is derived because it has no human-refinement phase and the combinatorial space is too large to store.

What Is Explicitly Stored

Data Where Why stored
Star system definitions systems.db Authored from wiki markdown
Body parameters (population, planet_class, economic_role) systems.db Authored; economically simulated
City positions (atlas_cities rows) systems.db + markers.json Generated once; hand-refinable (#838)
City populations systems.db + markers.json Authored from wiki population data
Road/rail path topology markers.json Expensive A* computation; spatial structure that cannot be cheaply re-derived
River/ocean/mountain features markers.json Authored or terrain-simulation output
City names markers.json Gemma output; expensive to regenerate

What Is Seed-Derived

Data Seed derivation Why derived
District count per city population / 50_000 — deterministic formula No human judgment needed
District WorldTier/ComplexityTier system_tier lookup + population_tier formula Deterministic from stored inputs
DistrictSkeleton.seed world_seed XOR fnv1a(city_id) XOR district_index Deterministic identity
Block grid (4×4 ZoningType) Seeded from district.seed Too many blocks to author
Block placement (Organic mode offsets) Seeded from district.seed + block_position Same
Chunk tile layout Seeded from block.seed Too many chunks to author
Building door positions Seeded from chunk_coord + edge_id Same

The Save File Corollary

This stored-vs-derived split determines what the save file must contain:

  • Save file stores: Delta layer only — DamageOverlay entries, player state, event history
  • Save file does NOT store: Any base world data (chunk tiles, block grid) — these are rederived from the master seed at load time
  • Implication: Changing the generation algorithm between saves would corrupt existing saves (rederived world no longer matches player's remembered state). This is the same problem as save migrations in Dwarf Fortress. This workshop should produce a D-record acknowledging the constraint.

City Marker Schema (OQ-R1-C Resolution)

The city marker schema gap (Miri/Tyre Gap A3/Gap B) has a clear answer in this framework:

Do not extend markers.json. Instead, resolve primary_function, planet_class, settlement_pattern via systems.db cross-reference using body_id.

Rationale: city markers are the stored anchor. Extending the schema means every pipeline run must correctly populate new fields — another source of drift. The systems.db already has economic_role, planet_class, settlement_pattern on the bodies table. The city → district decomposition step queries these directly by body_id. This keeps markers.json as a pure spatial-plus-naming store and systems.db as the authoritative parameter store.


4. Atlas Coordinate Translation (Resolving OQ-R1-D)

Tyre's Gap B: no formula maps atlas pixel coordinates to sim-tile coordinates.

Proposed formula:

The atlas grid is 512×256 pixels. Each pixel represents a terrain cell. A city's center: [row, col] is in this pixel space.

For the minimum slice, we don't need this — we spawn one district at world origin. But for Step 2b (full city layout), we need it.

The relationship between atlas pixels and the world map is:

  • One atlas pixel ≈ one degree of surface area at the sim abstraction level
  • A district is 512×512 sim tiles = 256×256 visual tiles = 256m
  • The world size at simulation scale is to be determined (this is an open spec question)

Pragmatic proposal: Assign one district per city for the minimum slice (Step 2a). Defer the coordinate translation to a spec decision in Round 3 or a separate architecture D-record. The question "how big is the walkable world relative to the atlas" is a scope decision, not a pipeline decision.


5. D-Record Candidates

Four D-records for the spatial pipeline. These can be filed after Round 3 convergence.

D-CANDIDATE-1: Generation Pipeline Layer Definitions

Canonical layer sequence for the spatial generation pipeline:

Layer Name Input Output Storage
15 Atlas layers (existing) Wiki + authored content systems.db, markers.json Explicitly stored
6 City → District decomposition City entry + systems.db body params DistrictInput structs Derived, not stored
7 Phase 1: DistrictSkeleton DistrictInput DistrictSkeleton (spatial stages) Derived, not stored
8 Phase 2: ChunkData fill BlockSkeleton + DistrictSkeleton 64×64 tile grid Derived, cached in session
9 Chunk streaming Player position On-demand Phase 2 invocation Architecture layer

The cliff between Layer 5 and Layer 6 is the stored-vs-derived transition (see D-CANDIDATE-2).

D-CANDIDATE-2: Stored-vs-Derived Transition Point

Atlas marker data (city positions, populations, road topology) is explicitly stored because it has a human-refinement phase and is expensive to recompute. Everything at Layer 6 and below is seed-derived at runtime. Save files store only the delta layer (DamageOverlay + player state) — not base world data. Changing generation algorithms between a player's saves is a breaking change and must be treated as a migration.

Implication for implementation: The generation pipeline must be stable once a world seed is committed to. A player's save file encodes the world seed as a root; all base world data derives from it. Version the generation algorithm separately from the game version.

D-CANDIDATE-3: Minimum Phase 1 Spatial Scope

Phase 1 DistrictSkeleton generation has two mandatory spatial stages and three optional stages. Only stages 12 are prerequisites for Phase 2 tile generation:

  • Stage 1 (mandatory): Classification — WorldTier, ComplexityTier, SettingType, DistrictType, LayoutMode, z_levels
  • Stage 2 (mandatory): Block grid — 4×4 BlockSkeleton array with ZoningType, density_pct, seed
  • Stage 3 (optional, adds value): Reservations — MultiBlockReservation for parks, terminals, plazas
  • Stage 4 (optional, adds value): Social sites — SocialSitePlacement (deferred to content pipeline work)
  • Stage 5 (optional, adds value): Guarantee audit — spatial invariant checks (deferred to post-walkable sprint)

Phases 2 reads only Stage 1+2 output to generate walkable tiles. Stages 35 enrich an already-walkable world.

D-CANDIDATE-4: City → District Decomposition Formula

  • District count: max(1, floor(city.population / 50_000)), capped at world_tier district ceiling
  • WorldTier ceiling: Epicenter → uncapped; Regional → 8; Backwater → 4; Passage → 2; Waypoint → 1
  • Seed chain: district.seed = world_seed XOR fnv1a(city_id) XOR district_index (deterministic, no entropy)
  • DistrictType assignment: Derived from body.economic_role (primary function) + city.kind (capital/city role)
  • Cross-reference source: systems.db bodies table, not markers.json (avoids schema extension)

6. Ticket Dependency Graph (Spatial Pipeline Only)

NEW: Fix WorldTier enum (generator.rs rename)
  │
  ├──▶ NEW: City → district decomposition — single district (Step 2a)
  │          │
  │          └──▶ #899 (rescoped): Phase 1 DistrictSkeleton — stages 1-2 only
  │                │
  │                ├──▶ NEW: Phase 2 — basic chunk tile generator (Step 4)
  │                │          │
  │                │          └──▶ NEW: Connect chunk_streaming to generator (Step 5)
  │                │                     │
  │                │                     └──▶ [MINIMUM VIABLE WALKABLE WORLD]
  │                │
  │                └──▶ NEW: Add missing DistrictSkeleton fields (Tyre Gap C — 0.5d)
  │                          (vertical_structure, breach_only_zones, derived_analysis as stubs)
  │
  └──▶ NEW: Full city decomposition + coordinate translation (Step 2b, follow-on)
             (not required for minimum slice; unblocks multi-district cities)

Existing Ticket Positions (Spatial Scope Only)

Ticket Title Recommendation
#899 District skeleton generator — Phase 1 Rescope: stages 12 only, no social sites. Remove Stage 45 from scope. Add explicit dependency on WorldTier fix + new city decomp ticket.
#681 Apartment generator (server) Add formal block behind "Minimum Viable Walkable World" (chunk streaming hookup). Apartment zones require real district context. Current blockers (#615, #679) are insufficient.
#682 Apartment visual rendering Correctly blocked by #681. No changes.
#619 Full character customisation No generation dependency. Phase 4 work. Do not add generation blockers.
#694 Character creation screen No generation dependency. Phase 4 work. Do not add generation blockers.
#616 Economic verb vocabulary No generation dependency. Content design work, can proceed anytime.
#156 Procedural district filler Superseded by Phase 2 chunk tile generator. Close or mark wontfix; new ticket replaces it.
#100 Ungenerated → instantiation Superseded by chunk_streaming hookup ticket. Close or fold into new ticket.
#160 Tier 3 procedural generator Pre-workshop, unclear scope. Defer re-scoping to post-walkable sprint.

#615 Position (OQ-R1-F Resolution)

With the scope correction (no NPC work in this workshop), the #615 question is moot for this workshop. Tyre argued it should block behind generation pipeline; my position was content design can proceed independently. With the lead's correction that #615 is NPC territory and out of scope, neither position is relevant to this workshop. #615 should be addressed in the NPC pipeline planning sprint, not here.


Summary

Item Answer
Minimum viable slice effort 5.5 days (stripped scope)
Stored-vs-derived transition Between Layer 5 and Layer 6 — atlas markers are the last stored layer
City marker schema extension? No — cross-reference systems.db bodies table by body_id
#619/#694 blocked by generation? No — Phase 4 UI work, independent
#681 additional blocker needed? Yes — behind "minimum viable walkable world" milestone
#899 scope change needed? Yes — stages 12 only, strip Stage 45
Pre-workshop tickets (#100, #156) Close/supersede — new tickets replace them