Files
settled-reach/docs/workshops/planet-down-cascade/tyre-round1.md
T
jpmschweitzerandClaude Opus 4.6 b9fd75b840 docs(workshops): planet-down cascade workshop + misc stray files
Planet-down cascade workshop (3 rounds, 5 agents): layer-by-layer
generation from empty world through population overlay, city planning,
and street rendering. Includes consultant review by Troblum.

Also commits: pre-Sprint-35 DB backup, Claude Code team-mode tmux
test log (team-test.md).

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

35 KiB
Raw Blame History

title, description, type, status, workshop, agent, round, created
title description type status workshop agent round created
Tyre — Round 1: Technical Inventory and Framing Per-layer technical inventory for the planet-down cascade: what exists, what's novel, what's schema, prerequisite ordering, spatial hierarchy dimensions, and performance analysis workshop active planet-down-cascade tyre 1 2026-05-01

Planet-Down Cascade — Round 1: Technical Inventory and Framing

Tyre — Technical Architect


Priority 0: The Architectural Pivot

My Round 3 output from the prior workshop designed Layers 1-2 as Python tooling (generate_regional.py). The consultant review overrides that entirely. This section documents exactly what changes and what carries forward, so the workshop doesn't accidentally build against the wrong model.

What the pivot changes

Was (my Round 3 design):

  • Layer 1-2: Python tooling, runs in generate_regional.py, output committed to systems.db
  • 64×32 regional biome grid: stored in atlas_regional_biomes, fully populated at build time
  • Settlement positions, road graph, territorial status: all in systems.db, pre-computed offline

Is now (consultant Amendment 1):

  • Layer 1-2: Rust runtime, runs on background threads, output lives in session state
  • 64×32 regional biome grid: still built at build time (atlas UI needs it), but sub-biome enrichment and geographic feature extraction run at runtime
  • Settlement positions, road graph, territorial status: derived at runtime per body, cached in session memory

The Python version of Layer 1-2 becomes a reference/validation tool, not the production implementation. Both must produce identical output from the same inputs — this is a regression-testing requirement.

What carries forward without change

  • The heightmap and 512×256 biome grid are still produced by generate_atlas.py at build time. No change here.
  • The 64×32 regional biome grid (atlas_regional_biomes table) is still populated at build time. I'm proposing the sub-biome columns also be computed at build time since generate_atlas.py already has terrain in memory — this saves the Rust runtime from re-running the coarse grid analysis.
  • The CityGenerationContext struct and the Phase 3 → Phase 5 handoff design from Round 3 carries forward with amendments (see Layer 3 section).
  • The SeedChain (FNV-1a), city-local coordinate system, GeneratorChunkData upgrade, and DistrictSkeleton Stages 1-2 all carry forward unchanged.
  • The Road graph tables (atlas_road_nodes, atlas_road_edges) — the schema carries forward, but the data is now generated at Rust runtime, not committed to systems.db.

The "Session DB" question

The consultant uses the term "Session DB (reproducible from seed)" for Layer 1-2 output. This needs a concrete definition before implementation can begin.

My recommendation: This is not a database at all — it's a BodyWorldState Bevy Resource keyed by body_id, held in memory during the session.

pub struct BodyWorldState {
    pub body_id: String,
    pub river_network: RiverNetwork,        // regional-res river polylines + confluence points
    pub settlements: Vec<GeneratedSettlement>, // all placed settlements, latent + active
    pub road_graph: RoadGraph,              // nodes + edges with maintenance authority
    pub territorial_grid: Vec<TerritorialCell>, // 64×32 cells with land_use + status
}

pub struct GenerationCache {
    pub bodies: HashMap<String, BodyWorldState>,
}

The cache is populated by the background generation queue. The player's current body is always present. Remote bodies are populated ahead of player movement by the priority queue. Nothing is serialized — on session resume, generation reruns from systems.db inputs + world seed. This satisfies "reproducible from seed."


Layer 1 — Empty World

Technical inventory

What already exists:

planet_simulation.py produces:

  • river_grid: bool (256, 512) — which cells carry rivers
  • rivers: list of [(row,col), ...] — polylines in grid coords
  • elevation: float32 (256, 512) — the full heightmap
  • biome: int8 (256, 512) — biome classification per pixel
  • surface_water: bool (256, 512) — ocean/lake mask

These are Python algorithms. The drainage model is a downhill flow accumulation from moisture-seeded source cells. The output is a boolean river grid + polylines.

generate_atlas.py currently stores:

  • City positions (authored, now being replaced)
  • Road pixel-paths (authored, now being replaced)
  • Gate terminal POIs
  • Does NOT store the 512×256 heightmap as a retrievable BLOB

Critical gap: the heightmap is not in systems.db. The Rust runtime needs the heightmap to run drainage. It must be stored at build time.

What needs to be built — Layer 1

Schema addition (build time, Python adds to systems.db):

-- New: per-body heightmap blob for runtime drainage
CREATE TABLE IF NOT EXISTS atlas_body_heightmaps (
    body_id TEXT PRIMARY KEY REFERENCES bodies(body_id),
    width INTEGER NOT NULL DEFAULT 512,
    height INTEGER NOT NULL DEFAULT 256,
    elevation_f32_le BLOB NOT NULL,   -- packed float32 LE, row-major
    sea_level REAL NOT NULL,          -- elevation threshold from simulation
    seed INTEGER NOT NULL             -- body seed used for simulation
);

-- Extend atlas_regional_biomes (already exists) with new columns:
--   sub_biome_variant TEXT    (3-4 values per biome class, computed at build time)
--   terrain_modification_cost REAL  (0.0 = easy to settle, 1.0 = impassable)
--   geographic_feature_tags TEXT    (JSON array of feature tags)

sub_biome_variant and terrain_modification_cost can be computed at build time since generate_atlas.py has the terrain dict in memory. This is cheap to add (extend Layer B downsampling code) and saves the Rust runtime from needing to redo this analysis.

Rust algorithm work (runtime, background thread):

D8 drainage routing:

Input: elevation BLOB loaded from atlas_body_heightmaps
Algorithm: Priority-flood D8 flow routing
  - Fill sinks using priority queue (Planchon-Darboux or similar)
  - Accumulate flow: each cell routes to lowest downhill neighbor
  - Cells with accumulation > threshold are river cells
Output: river accumulation grid (u32 per cell)

D8 on 131,072 cells in Rust: ~50ms. This is the most expensive Layer 1 operation and is comfortably within the performance target.

Geographic attractor extraction:

From river accumulation grid:
  - Confluence: cell where two or more upstream branches merge
    (accumulation jumps non-monotonically from upstream to current)
  - River mouth: river cell adjacent to ocean/lake cell
  - Mountain pass: high-roughness cell in atlas_regional_biomes adjacent
    to significantly lower roughness cells on opposite sides
  - Coastal harbor: coastal cell with low terrain_modification_cost
    (natural harbor = sheltered, shallow approach)
  - Arable plain: large contiguous low-roughness non-ocean region
    (flood-fill counting connected cells above water_fraction threshold)

Output: Vec<GeographicAttractor> with location (grid_row, grid_col), attractor_type, and quality score.

Sub-biome variant generation (seed-derived per cell):

This is lightweight: for each regional cell, use FNV-1a child_seed to pick from 3-4 variants for the cell's biome_class. The sub_biome_variant column can be populated at build time if we want it in systems.db for the atlas UI, or derived at runtime. Given the atlas UI doesn't currently display sub-biome variants, I recommend runtime derivation with a deferred build-time population if the UI needs it.

What is genuinely novel vs. reuse

Work Novel or Reuse Notes
D8 drainage routing Novel — new Rust code Python version exists as reference; must be equivalent
Geographic attractor extraction Novel — new Rust code No equivalent exists
Sub-biome variant derivation Lightweight novel Pure FNV-1a table lookup, ~10 lines
Heightmap BLOB storage Schema only Add column + populate in generate_atlas.py
terrain_modification_cost Algorithm + schema Derived from roughness + biome at downsampling time

Layer 1 questions requiring answers before implementation

L1-TECH-1 (prerequisite, blocks all): Heightmap storage format. Float32 LE blob is my recommendation. Size: 512×256×4 = 512KB per body. ~400 bodies = ~200MB uncompressed. SQLite stores BLOBs fine; DEFLATE compression in the Python write path would halve this. Should the loader decompress, or is raw float fine? Decision needed before Layer 1 Rust code can be written.

L1-TECH-2: River resolution for walkable world. The drainage algorithm produces a per-cell accumulation grid at 512×256 resolution. River courses at tile precision (Phase 5) can be derived from this at runtime via FNV-1a SeedChain — same pattern as chunk generation. My position: store the drainage accumulation grid at regional resolution (64×32) in the session state; derive tile-precision river courses in Phase 5 from the drainage network + chunk_seed. This is coherent with the stored-vs-derived boundary.

L1-TECH-3: Economic role as biome prior (L1-Q4). Burnelli-Sheldon proposes using bodies.economic_role to nudge sub-biome distribution toward plausibility. Technical cost: trivial (add a weight multiplier in the sub-biome variant sampling). My position: include it. The self-contradiction risk (agricultural world generating predominantly badlands) is real and cheap to prevent. One lookup table, one weight adjustment at sampling time.

L1-TECH-4 (deferred): Lake placement. The brief flags "scatter lakes with biome/river basin dependency." This is a Layer 1 enrichment, not the minimum viable slice. planet_simulation.py already produces surface_water which covers ocean/coastal lakes; inland lake placement is a future enhancement. Defer.


Layer 2 — Population Overlay

Technical inventory

What already exists:

  • generate_atlas.py has city placement (replaced by fully generative approach)
  • generate_atlas.py has A* road routing between cities (algorithm reusable concept, must be Rust)
  • atlas_road_nodes, atlas_road_edges schema from my Round 3 (carry forward, now populated at runtime)
  • atlas_regional_biomes.land_use column design from my Round 3

What's new from Amendment 3 (fully generative placement):

  • No authored city positions in markers.json anymore (markers.json stripped to topographic features)
  • Name reservation system: cities that are corporate headquarters (from corporations table) must be placed at plausible attractors
  • Attractor-matching algorithm: assign N named cities to M geographic attractors

Attractor-matching algorithm

This is the most algorithmically interesting new piece. It's a bipartite assignment problem.

Inputs:
  - named_cities: Vec<{name, economic_role, population, is_hq_for_corp}> from systems.db
  - attractors: Vec<GeographicAttractor> from Layer 1 output
  - body_seed: u64

Algorithm (greedy, deterministic):
  1. Sort named_cities by population descending (largest cities assigned first)
  2. Score each (city, attractor) pair:
       base_score = attractor.quality_score
       role_bonus:
         {transit, commercial} → +0.4 if attractor_type == CoastalHarbor
         {extraction, mining} → +0.3 if attractor_type == ResourceConcentration
         {agricultural} → +0.3 if attractor_type == ArablePlain
         {administrative_center} → +0.2 if attractor_type == Defensible
         any → +0.2 if attractor_type == RiverConfluence (universal settlement attractor)
       hq_constraint:
         if is_hq_for_corp AND attractor_type incompatible with corp's primary_operation:
           score → 0.0 (hard reject)
  3. Greedy assignment: for each city in sorted order, assign to highest-scored
     available attractor (remove from pool after assignment)
  4. If cities > attractors: place overflow cities near existing settlements
     (proximity placement within [15%-40%] of body scale from nearest assigned city)
  5. Derive FoundingOrientation from assigned attractor_type:
     CoastalHarbor → PortFacing
     Defensible → DefenseFacing
     ResourceConcentration → ExtractionFacing
     RiverConfluence → RiverFacing (default for most settlements)
     ArablePlain → AgrarianFacing

Output: Vec<PlacedCity> with (city_id, name, position_grid_row, position_grid_col, founding_orientation)

This runs in O(N×M) where N = cities (~20-30 per body) and M = attractors (~50-100). Negligible compute.

Name reservation data structure needed: The matching algorithm requires knowing which cities are name-locked (corporate HQ cross-references). This currently requires a JOIN between bodiesatlas_citiescorporations. With fully generative placement, atlas_cities no longer has positions at build time — it's populated by the runtime generator. The systems.db schema needs a atlas_city_names table (name + economic_role + population + is_hq_for_corp) that the runtime generator reads.

New schema for Amendment 3:

-- Replace atlas_cities authored positions with name reservation table
CREATE TABLE IF NOT EXISTS atlas_city_names (
    name_id TEXT PRIMARY KEY,   -- "{body_id}/{city_index}"
    body_id TEXT NOT NULL REFERENCES bodies(body_id),
    city_name TEXT NOT NULL,
    economic_role TEXT,         -- from bodies table or override
    population INTEGER NOT NULL,
    political_archetype TEXT,   -- derived or authored override
    political_archetype_override TEXT, -- NULL = always derived
    prosperity_override REAL,   -- NULL = always derived
    hq_for_corp TEXT,           -- corp_id if this city is a HQ, else NULL
    FOREIGN KEY (hq_for_corp) REFERENCES corporations(corp_id)
);

This table is populated at build time from wiki body definitions. The runtime generator reads it and produces actual positions.

Sub-settlement placement

From the brief's minimum viable Layer 2: mining camps + trade waypoints.

Mining camps (ExtractionFacing sub-settlements):

trigger: body.economic_role ∈ {extraction, mining}
         AND corp_presence has corps with relevant commodity
count: floor(qualifying_corps / 2), minimum 1
placement per camp:
  seed = child_seed(body_seed, camp_index)
  distance_from_tether_grid_cells = lerp(0.15, 0.40, rng_01(seed)) × body_scale_cells
  bearing = rng_0_2pi(child_seed(seed, 1))
  position = tether_city_pos + polar_to_grid(distance, bearing)
  snap to nearest Wilderness or Industrial regional cell

Trade waypoints:

trigger: road edge length > WAYPOINT_ROAD_THRESHOLD (e.g., 12 regional cells)
placement: geometric midpoint of road edge
count: 1 per qualifying edge

Road graph generation (Rust)

Layer C road graph from my Round 3 was authored (converting pixel paths). Now it's fully generated. The algorithm:

Input: PlacedCity list + geographic attractor list
Algorithm:
  1. Force-connect all cities (minimum spanning tree using Euclidean distance)
  2. Add secondary connections: cities within [30%-60%] of body scale get direct roads
     if their MST path exceeds 1.5× Euclidean distance (avoids long detours)
  3. Route each edge using A* on a terrain cost grid:
       COST_WATER = impassable
       COST_MOUNTAIN (roughness > 0.7) = 10.0
       COST_RIVER = 0.5  (rivers as preferred routes — follow valleys)
       COST_FLAT = 1.0
  4. Assign MaintenanceAuthority per edge based on political context:
       Capital→RegionalCity: Administrative
       Corp tether city: Corporate
       Remote→Remote: Trade
       Abandoned destination: Abandoned
  5. Place sub-settlement nodes (waypoints at midpoints of long edges)

Output: RoadGraph { nodes: Vec<RoadNode>, edges: Vec<RoadEdge> }

The terrain cost grid is the 64×32 regional grid — A* on 2048 cells is trivial even for full MST construction.

TerritorialStatus derivation

Post-processing pass over regional cells after settlement + road placement:

Per-cell algorithm (after road graph exists):
  if cell.water_fraction > 0.6: TerritorialStatus::Ocean
  if cell within city footprint radius: CoreTerritory
  if cell within 2 cells of road edge AND within 5 cells of nearest settlement:
    CoreTerritory if nearest settlement is City/Town
    FrontierTerritory if nearest settlement is Outpost
    ExtractiveZone if road MaintenanceAuthority == Corporate
  if cell has ResourceConcentration attractor AND Corporate corp present: ExtractiveZone
  if two settlements with overlapping road networks and different political archetypes
    both claim this cell: ContestZone
  if no settlement within 8 cells AND no road within 3 cells: WildernessBuffer
  if settlement was placed but corp_financial_state health_metric = 0 (exhausted):
    AbandonedZone

Quantitative thresholds for L2-Q5 — I'm proposing these as defaults tunable by workshop:

  • CoreTerritory: settlement_density > 1 city per 4 cells OR road within 2 cells of City/Town
  • FrontierTerritory: settlement present + road within 5 cells OR within 3 cells of City/Town but no road
  • ExtractiveZone: corp_presence with qualifying commodity + road marked Corporate
  • ContestZone: two CoreTerritory zones from different political archetypes overlap within 2 cells
  • WildernessBuffer: no settlement within 8 cells + no road within 3 cells
  • AbandonedZone: settlement placed + no active corp_presence + economic basis metric ≤ 0

Latent settlement storage (L2-Q6)

The brief asks where the active/ghost flag lives. My answer: in the Rust session state, not in systems.db. The latent settlement positions are generated at runtime (seed-derived). The active/ghost state is a function of current corp_financial_state.health_metric — a simulation variable. Having this in systems.db would require runtime writes to the asset store, which violates the asset pipeline rules.

In practice: the GenerationCache::BodyWorldState.settlements Vec contains all latent positions. Each settlement has an is_active: bool flag that the economic simulation updates via event. When corp financial health drops below threshold, simulation fires an event that flips the flag. No systems.db write required.

Layer 2 questions requiring answers

L2-TECH-1 (prerequisite, blocks schema): atlas_city_names table design. The fully generative model requires this table at build time. Need to confirm it replaces atlas_cities or extends it. My recommendation: replace — atlas_cities currently stores authored positions which are now runtime-generated. The build-time table is atlas_city_names (name + economic role + population + hq constraint). Runtime-generated positions go into BodyWorldState, never systems.db.

L2-TECH-2: Multiple cities competing for best attractor. The greedy algorithm handles this by population order. But it can produce suboptimal global assignments (largest city takes the best harbor even if a smaller city would be more plausible there). This is a design decision: optimal assignment (Hungarian algorithm, O(N³)) vs. greedy (O(NM)). For N ≤ 30, the Hungarian algorithm is trivially fast. Recommend Hungarian for correctness, greedy for first implementation.

L2-TECH-3: Road routing across the 512→64 resolution boundary. The road routing runs on the 64×32 regional grid. But road paths between close cities (< 2 regional cells apart) have low resolution. For the minimum slice, this is acceptable. For the Atlas UI, pixel-precision paths are desirable. Proposal: regional resolution roads for functional connectivity (Phase 2 minimum), then an optional higher-resolution routing pass for Atlas UI display.


Layer 3 — City-Level Planning (carry-forward analysis)

Layer 3 is the Phase 1/Phase 5 Rust-runtime generation that the prior workshop designed in depth. This section covers only what changes and what new questions the consultant's amendments raise.

What carries forward without change

  • DistrictSkeleton Stages 1-2 (D-C3)
  • City-local coordinate system (D-C5)
  • SeedChain / FNV-1a (D-C6)
  • GeneratorChunkData upgrade (D-C7)
  • Prosperity + perimeter_treatment on DistrictSkeleton (from Paula, confirmed)
  • D-C4 district count formula

CityGenerationContext update needed

The struct from my Round 3 needs amendments for the new model:

pub struct CityGenerationContext {
    pub body_id: String,                          // NEW — consultant open item 1
    pub city_id: String,
    pub city_name: String,                        // NEW — for internal monologue / UI
    pub political_archetype: PoliticalArchetype,
    pub prosperity_baseline: f32,                 // RENAMED from prosperity_index (CL-Q1)
    pub surrounding_biome: BiomeClass,
    pub road_entry_directions: Vec<CardinalDirection>,
    pub footprint_radius_km: f32,
    pub founding_orientation: FoundingOrientation, // NEW — Layer 2 output
    pub world_tier: WorldTier,                    // FIX — must use correct enum
    pub district_grid_width: u8,                  // from D-C4 formula
    pub district_count: u8,                       // from D-C4 formula
}

This struct is populated from BodyWorldState.settlements (Layer 2 runtime output), not from systems.db, since cities no longer have static positions in systems.db.

WorldTier enum — still a critical blocker

// CURRENT (wrong):
pub enum WorldTier { Peripheral, Connected, Core }

// REQUIRED:
pub enum WorldTier { Epicenter, Regional, Backwater, Passage, Waypoint }

This has been a known blocker since Round 1 of the prior workshop. It must be fixed before any Layer 3 code can run correctly. First ticket, 0.5 dev-days.

10×9 Weight Table — consultant amendment

Amendment 6 removes the weight table from Given Facts and challenges it to prove it produces plausible settlements. The critique: energy-producing cities have zero Commercial and zero Entertainment. This is wrong — people who live in mining towns still drink.

Technical proposal for replacement approach:

Population + settlement_age → baseline district mix (minimum viable service requirements):

baseline_residential_frac = 0.35  (people live everywhere)
baseline_commercial_frac = clamp(0.10 + log10(population/1000) × 0.05, 0.10, 0.25)
baseline_entertainment_frac:
  population < 10000: 0.0
  population 10k-100k: 0.05
  population > 100k: 0.10
baseline_administrative_frac:
  WorldTier Waypoint: 0.0
  Passage: 0.05
  Backwater+: 0.10

Then economic role modifies the remaining fraction (after baseline is allocated):

remaining = 1.0 - baseline_total
distribute remaining by economic_role weight table

This guarantees no settlement above minimum population has zero of any essential district type. The weight table still governs the CHARACTER and PROPORTION of above-baseline districts. Amendment 6 is satisfied.

The new weight table is now a "secondary allocation modifier" for the non-baseline fraction only, which means the 10×9 table values need to be renormalized. This is arithmetic work, not design work.

L3-Q1 — FoundingOrientation spatial effect

My position: spatial orientation change, not just gradient direction. The cost is low (it's a grid rotation flag, not a new algorithm) and the legibility payoff is high. A PortFacing city where the industrial districts are inland and the transit districts are on the water edge is immediately readable.

Implementation: add orientation_rotation: CardinalDirection to district placement. The district at grid position (0,0) is always "closest to FoundingOrientation direction." The weight table biases which DistrictType goes there.

L3-Q2 — political_archetype as spatial arrangement

My position: explicit arrangement patterns, but simple. Three patterns, hardcoded:

  • CompanyTown: spine layout (facility at col=0 or col=max, residential fills other cols)
  • AdminCapital: center layout (administrative district forced to center grid position)
  • FreePort: distributed layout (no single forced center; Transit/Commercial districts take all 4 corners)
  • Others: emerge from weight table + FoundingOrientation

Adding three explicit spatial arrangement rules is 30-40 lines of Rust code. The legibility benefit is significant.


Layer 4 — Street-Level Rendering (carry-forward only)

Layer 4 is largely settled from prior workshop work. New items only:

L4-Q1 — Economics-variable rendering mechanism

My position: threshold-based cache invalidation (option c from the brief).

pub struct ChunkConditionState {
    pub prosperity_snapshot: f32,  // prosperity_current when last computed
    pub tile_conditions: Vec<TileCondition>, // 4096 entries
}

enum TileCondition { Intact, Worn, Cracked, Broken }

Cache invalidation rule: when |prosperity_current - prosperity_snapshot| > 0.05, recompute tile_conditions for the chunk. The 0.05 threshold means ~20 condition bands, smooth enough for visual continuity, cheap enough computationally (most chunks never recompute).

Compute-on-demand (option b) is too expensive — prosperity changes happen on every tick cycle for active economic systems. Bake-at-generation (option a) violates the determinism rule.

L4-Q6 — prosperity_delta

The two-field model (prosperity_baseline, prosperity_current) is the correct implementation. prosperity_delta = prosperity_current - prosperity_baseline is always derived, never stored. The rendering system receives both fields. Agreed.

L4-Q4 — Interior generation trigger

My position: option (a), pre-generate when player is within N tiles of any door. N = 32 tiles (one chunk width). This avoids a generation stall at the threshold and the compute cost is bounded — each building interior is small, and only doors within the loaded chunk radius are candidates.


Spatial Hierarchy — Dimension Locking

The consultant directs us to lock concrete dimensions at every tier and define the formula mapping body size to area count. This is my proposal; the workshop should either confirm or amend.

The dimension table

Tier Name Base Unit Physical Size (reference body) Notes
0 Chunk 64×64 sim tiles 32m × 32m Immutable — all prior architecture depends on this
1 Block 128×128 sim tiles 64m × 64m 2×2 chunks
2 District 512×512 sim tiles 256m × 256m 4×4 blocks, 8×8 chunks
3 Region city district grid bounding box varies per city size Not a fixed grid cell
4 Province 1 drainage basin on regional grid ~60-200km on reference body Defined by watershed analysis, not fixed grid
5 Area 1 contiguous terrain feature zone ~400-1500km on reference body Coastlines, continents, major highlands
6 Body full planet/moon/station surface varies Absorbed here
7 System star system Gate graph

Reference body calibration

Reference body: temperate terrestrial, radius ~5000km (slightly smaller than Earth). Atlas heightmap: 512×256 pixels covering the full surface. Pixel scale at equator: ~2π × 5000 / 512 ≈ 61km per pixel. Regional cell (8×8 pixels): ~500km × 250km.

This seems large, but the 64×32 grid covers a whole planet — these cells are continent-scale. Province and Area are derived from natural boundaries on this grid, so their sizes are naturally variable.

Province definition (concrete): A Province is a drainage basin identified by watershed analysis on the 64×32 regional grid. Typical size: 2-8 regional cells = ~1000-4000km² on the reference body. For gameplay, a Province is "the region you'd see on one regional travel map."

Area definition (concrete): An Area is a connected set of regional cells with the same dominant terrain class (ocean, highland, lowland, polar). Typical: 5-15 regional cells across. One Area per continent/ocean basin.

Body-size-to-area-count formula

area_count = round(sqrt(body_surface_area_km2 / AREA_BASE_SIZE_KM2))

Where AREA_BASE_SIZE_KM2 = 6,000,000 km² (approximately the size of Australia — 
a plausible single continent/ocean feature)

For reference body (radius 5000km, surface area ~314,000,000 km²):
  area_count = round(sqrt(314,000,000 / 6,000,000)) = round(7.24) = 7

For small moon (radius 500km, surface area ~3,140,000 km²):
  area_count = round(sqrt(3,140,000 / 6,000,000)) = round(0.72) = 1

For large super-terrestrial (radius 10,000km, surface area ~1,256,000,000 km²):
  area_count = round(sqrt(1,256,000,000 / 6,000,000)) = round(14.47) = 14

This formula is smooth, has reasonable results at the extremes, and produces integer counts. The formula should be validated against the actual bodies in the Reach when locked.

Important: The surface_area_km2 (or body_radius) field needs to be in the bodies table. Currently bodies has surface_gravity and other physical fields — check whether radius/surface area is already there. If not, this is a schema addition needed before the formula can run.

Region (tier 3) clarification

Region is NOT a fixed grid cell in the spatial hierarchy — it's the city + its immediate service area. The bounding box is city_footprint_radius_km × 2 plus some hinterland buffer. This is computed from CityGenerationContext.footprint_radius_km at Layer 3 time. Region boundaries are fuzzy and can overlap between nearby cities (contested regions, per TerritorialStatus).

This means Region is a semantic tier, not a storage tier. Tiers 0-2 (Chunk/Block/District) are generation units. Tiers 4-5 (Province/Area) are geographic classification units. Tier 3 (Region) is a lookup concept: "which settlements are in this province" + "which province does this settlement sit in."


Performance Analysis

Target: Layer 1-2 for a single body completes in low single-digit seconds.

Estimated Rust operation timings:

Operation Grid Size Estimated Time
Load heightmap BLOB from SQLite 512KB ~5ms
D8 flow routing (priority-flood) 512×256 ~50ms
Extract river centerlines 512×256 ~20ms
Geographic attractor extraction 64×32 cells ~5ms
Sub-biome variant sampling 64×32 cells ~2ms
Attractor matching (N≈25 cities) N×M ~1ms
A* road routing (MST) 64×32 grid, N nodes ~30ms
Sub-settlement placement N camps ~5ms
Territorial status propagation 64×32 cells ~15ms
Total ~133ms

133ms is well within single-digit seconds. Even with 5× safety margin for SQLite overhead and cache misses, this is ~650ms — still under 1 second. The performance target is not a concern for the minimum viable implementation.

Background thread model: Layer 1-2 for body N-1 can run while the player is on body N. The priority queue means by the time the player considers traveling to a system mentioned in news, that system's generation is likely complete.

Memory budget per body state: ~2MB per BodyWorldState (river network polylines + settlement list + road graph + territorial grid). 400-500 bodies × 2MB = 800MB-1GB peak if all bodies are cached. This is too much. The cache should use an LRU policy: keep the last N bodies generated, where N is tuned for available memory (target: 50 bodies = ~100MB). Bodies beyond LRU are dropped and regenerated on access.


Open Questions — Ordered by Blocking Priority

Must resolve before Layer 1-2 Rust code can be written

ID Question Who resolves Stakes
ARCH-1 Heightmap storage: float32 LE BLOB confirmed? Compression? Column on atlas_body_heightmaps? Lead + Tyre Blocks all Layer 1-2 Rust
ARCH-2 Session state representation: BodyWorldState as Bevy Resource confirmed? LRU policy? Tyre + Gestalt Blocks Layer 2 → 3 handoff
ARCH-3 atlas_city_names schema: replaces or extends atlas_cities? Tyre + lead Blocks attractor-matching
ARCH-4 Body physical size (radius/surface area) in bodies table? Add field? Tyre + miri Blocks area-count formula

Must resolve before Layer 2 sub-settlement code can be written

ID Question Who resolves
L2-1 Sub-settlement exact placement: greedy (simple) or Hungarian (optimal)? Tyre
L2-2 Road routing resolution: 64×32 only, or optional higher-res pass for Atlas UI? Tyre + Gestalt
L2-3 TerritorialStatus thresholds: my proposed numbers above, or adjusted by Burnelli-Sheldon/Paula? Burnelli-Sheldon + Paula

Resolved by consultant (no longer workshop questions)

  • L1-Q1 — Eliminated (rivers are fully generated)
  • L2-Q1 — Eliminated (city positions are fully derived)
  • CL-Q4 — Eliminated (same resolution as L1-Q1)

Inherited open questions that still need answers

Original ID Question My position
L1-Q2 River resolution for walkable world Regional (64×32) in session state; tile-precision at Phase 5 from seed
L1-Q3 Confluence points stored vs. derived Derived at runtime from D8 output; stored in BodyWorldState only
L1-Q4 Economic role as biome prior Include — cheap, prevents self-contradiction
L2-Q3 Road algorithm: authored vs. generated New: fully generated; authored roads gone
L2-Q5 TerritorialStatus thresholds My proposed thresholds above
L2-Q6 Latent settlement flag storage In BodyWorldState (Rust session state), not systems.db
L3-Q1 FoundingOrientation spatial effect Spatial grid orientation change (not just gradient)
L3-Q2 Political archetype as spatial arrangement Explicit patterns for 3 archetypes
L4-Q1 Economics-variable rendering mechanism Threshold-based cache invalidation
L4-Q6 prosperity_delta architecture Two fields (baseline + current), delta always derived
CL-Q1 Naming: prosperity_baseline vs. prosperity_current Two distinct fields, confirmed
CL-Q2 Regional land-use evolution boundary Runtime parameter in BodyWorldState, not ChunkMutations

Summary: What This Round Establishes

The architectural pivot from Python offline to Rust runtime background is the dominant finding. The existing generate_atlas.py + planet_simulation.py Python infrastructure remains for heightmap generation and build-time data, but all of the "understanding the world" work (drainage, settlement placement, road construction, territorial analysis) moves to Rust runtime.

Technically this is feasible. The algorithms are geometry and constraint-solving on a 64×32 grid. The performance target (low single-digit seconds per body) is met with 5× headroom at 133ms. The memory cost is bounded by an LRU cache policy.

The four things the workshop must lock before Layer 1-2 implementation can start:

  1. Heightmap storage format in systems.db
  2. Session state design (BodyWorldState as Bevy Resource, LRU policy)
  3. atlas_city_names schema (what the runtime reads for name assignment)
  4. Physical body dimensions in systems.db (for area-count formula)

Everything else in Layer 1-2 is algorithm design work that can proceed after those four are locked.


Tyre — Round 1. Written 2026-05-01.