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>
35 KiB
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.pyat build time. No change here. - The 64×32 regional biome grid (
atlas_regional_biomestable) is still populated at build time. I'm proposing the sub-biome columns also be computed at build time sincegenerate_atlas.pyalready has terrain in memory — this saves the Rust runtime from re-running the coarse grid analysis. - The
CityGenerationContextstruct 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 riversrivers: list of [(row,col), ...]— polylines in grid coordselevation: float32 (256, 512)— the full heightmapbiome: int8 (256, 512)— biome classification per pixelsurface_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.pyhas city placement (replaced by fully generative approach)generate_atlas.pyhas A* road routing between cities (algorithm reusable concept, must be Rust)atlas_road_nodes,atlas_road_edgesschema from my Round 3 (carry forward, now populated at runtime)atlas_regional_biomes.land_usecolumn 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
corporationstable) 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 bodies → atlas_cities → corporations. 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_presencewith 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:
- Heightmap storage format in systems.db
- Session state design (BodyWorldState as Bevy Resource, LRU policy)
atlas_city_namesschema (what the runtime reads for name assignment)- 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.