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>
750 lines
32 KiB
Markdown
750 lines
32 KiB
Markdown
---
|
||
title: "Round 3 — Burnelli-Sheldon: Convergence and Economics D-Records"
|
||
description: "D-record candidates D-194 through D-199 for locked economics decisions. Data quality findings from systems.db. Mismatch threshold validation. TerritorialStatus economic threshold confirmation."
|
||
type: workshop
|
||
status: active
|
||
workshop: planet-down-cascade
|
||
agent: burnelli-sheldon
|
||
round: 3
|
||
created: 2026-05-01
|
||
---
|
||
|
||
# Round 3 — Burnelli-Sheldon: Convergence and Economics D-Records
|
||
|
||
D-record IDs D-194 through D-199 claimed. This document contains the candidate text for
|
||
each. It also flags critical data quality issues I found querying systems.db that affect
|
||
the formulas before they can go live.
|
||
|
||
---
|
||
|
||
## 0. Critical Data Quality Findings (Must Address Before Implementation)
|
||
|
||
Before the D-records: three data gaps in systems.db that the implementation will hit
|
||
immediately if not flagged now.
|
||
|
||
### Gap 1: `bodies.founding_age_years` is NULL for All 273 Inhabited Bodies
|
||
|
||
Component 3 of the district mix algorithm (age modifier) depends on this field. It is
|
||
entirely unpopulated. The D-194 algorithm includes a fallback derivation from
|
||
`system_history.settlement_wave` (see §1.5), but this is a degraded proxy. The field
|
||
should be populated as part of wiki content work — every inhabited body should have an
|
||
approximate founding age.
|
||
|
||
**Immediate implication:** The age modifier in Phase 1 implementation will run on
|
||
fallback values only. Character tags will be wave-derived, not body-specific.
|
||
|
||
**Settlement wave → approximate age mapping (fallback):**
|
||
|
||
| settlement_wave | Approximate founding_age_years | Age bracket |
|
||
|----------------|-------------------------------|-------------|
|
||
| origin | 500+ | mature |
|
||
| wave_1 | 350 | mature |
|
||
| wave_2 | 250 | established |
|
||
| wave_3 | 150 | established |
|
||
| wave_4 | 75 | young |
|
||
| wave_5 | 30 | nascent |
|
||
| unsettled | N/A | not inhabited |
|
||
|
||
This mapping preserves the intent of the modifier even without body-level data.
|
||
|
||
### Gap 2: `system_economy.economic_tier` and `distribution_index` are NULL for 97% of Systems
|
||
|
||
290 of 300 system_economy rows have NULL for both fields. The prosperity_baseline formula
|
||
(D-197) depends on `economic_tier`. Only 10 systems have data.
|
||
|
||
**Fallback for economic_tier:** Derive from system gate topology + population tier.
|
||
Gate-connected major systems = tier 4-5; backwater systems = tier 1-2. Exact formula:
|
||
|
||
```
|
||
economic_tier_derived =
|
||
if system_population >= 5_000_000_000: 5
|
||
elif system_population >= 1_000_000_000: 4
|
||
elif system_population >= 100_000_000: 3
|
||
elif system_population >= 10_000_000: 2
|
||
else: 1
|
||
```
|
||
|
||
**Fallback for distribution_index:** Default to "moderate" when NULL. "Stratified" is
|
||
the explicit exception (must be authored); moderate is the baseline.
|
||
|
||
These fallbacks are sound — economic tier correlates with population by construction
|
||
in a simulation game, and moderate inequality is the default state.
|
||
|
||
### Gap 3: `economic_role` Values Are Not Normalized
|
||
|
||
The bodies table has inconsistent values:
|
||
- `"agriculture"` (16 bodies) and `"agricultural"` (107 bodies) — same concept, two spellings
|
||
- `"resource_extraction"` (1 body) — should be `"extraction"`
|
||
- `"coordination"` (2 bodies, including Bunbury pop 800M) — not in the weight table
|
||
- `"mixed-agriculture"` (1 body) — not in the weight table
|
||
- 1 body with NULL economic_role
|
||
|
||
The weight table in D-194 uses canonical values. The generator must normalize before
|
||
looking up the table:
|
||
|
||
| Raw value | Canonical | Rationale |
|
||
|-----------|-----------|-----------|
|
||
| `agriculture` | `agricultural` | Spelling normalization |
|
||
| `resource_extraction` | `extraction` | Enum normalization |
|
||
| `coordination` | `service_mixed` | Coordination hubs are service/administrative |
|
||
| `mixed-agriculture` | `agricultural` | Nearest match; blended role, dominant is agricultural |
|
||
| NULL | `service_mixed` | Safest fallback for unknown roles |
|
||
|
||
This normalization should be added to `import_economics.py` as a validation step.
|
||
|
||
---
|
||
|
||
## 1. D-194: Three-Component District Mix Algorithm
|
||
|
||
**Decision ID:** D-194
|
||
**Domain:** architecture
|
||
**Status:** candidate
|
||
**Claimed by:** Burnelli-Sheldon
|
||
|
||
### Decision
|
||
|
||
Adopt a three-component model for computing the district type distribution of any
|
||
generated settlement. The model replaces the prior 10×9 weight table (which had zero
|
||
weights and produced implausible zero-service settlements).
|
||
|
||
**The three components are:**
|
||
1. Population tier guarantees — mandatory district types by settlement size
|
||
2. Economic role multiplier table — integer weights distributing remaining slots
|
||
3. Settlement age character modifier — modifies district character, not count
|
||
|
||
### Rationale
|
||
|
||
The prior table encoded what a settlement *produces*, not what it *needs*. Every
|
||
settlement with concentrated labor produces service demand regardless of economic role.
|
||
This is not a design preference — it is a consequence of basic human economics. Miners
|
||
drink. Workers eat. People socialize. The new model encodes this correctly:
|
||
|
||
- **Component 1** provides non-negotiable floors: no city above minimum population has
|
||
zero of any essential district type.
|
||
- **Component 2** provides proportional variation: economic role shifts the distribution
|
||
toward production-relevant types, but cannot suppress guaranteed types below 1 slot.
|
||
- **Component 3** provides narrative depth without structural change: the same district
|
||
types look different in a 20-year mining camp vs. a 300-year mining city.
|
||
|
||
### Self-Containment Rule
|
||
|
||
The algorithm references only local city/body/system fields. No queries to neighboring
|
||
city data. No distance calculations. Each settlement generates its district distribution
|
||
independently from its seed and the fields listed below. This satisfies the SeedChain
|
||
isolation requirement (D-010).
|
||
|
||
### 1.1 Population Tier Guarantees
|
||
|
||
| Population range | Settlement tier | Mandatory district types |
|
||
|-----------------|----------------|-------------------------|
|
||
| 1–999 (Outpost) | Outpost | Residential + Mixed (all functions fused if D_total = 1) |
|
||
| 1,000–9,999 (Town) | Town | Residential + Commercial |
|
||
| 10,000–99,999 (City) | City | + Entertainment |
|
||
| 100,000–499,999 (Large City) | Large City | + Administrative |
|
||
| 500,000+ (Metropolis) | Metropolis | + Industrial (if role supports), + Civic |
|
||
|
||
**Collapse rule:** If `D_total < len(mandatory_types)`, excess mandatory types
|
||
merge into Mixed. A 1-district settlement always has type Mixed regardless of
|
||
population tier — it carries all functions in one district.
|
||
|
||
**WorldTier override — domed/cave settlement:** Single district regardless of
|
||
population. Type = Mixed.
|
||
|
||
**Special case — BS-Q1 (Town Entertainment):** Entertainment at Town scale is a
|
||
*character property* of the Commercial district, not a dedicated slot. Dedicated
|
||
Entertainment districts appear at City tier (10,000+) only, when specialization
|
||
becomes economically viable at scale.
|
||
|
||
### 1.2 Economic Role Multiplier Table
|
||
|
||
Integer weights, each row sums to 100. Minimum value across all cells: **3**.
|
||
No weight is zero — every district type can appear in any economic role, just rarely.
|
||
|
||
DistrictType column key: Res=Residential, Com=Commercial, Ind=Industrial, Adm=Administrative,
|
||
Log=Logistics, Ent=Entertainment, Mix=Mixed, Trn=Transit, Spe=Specialized
|
||
|
||
| economic_role | Res | Com | Ind | Adm | Log | Ent | Mix | Trn | Spe |
|
||
|--------------|-----|-----|-----|-----|-----|-----|-----|-----|-----|
|
||
| manufacturing | 18 | 8 | 30 | 6 | 18 | 5 | 8 | 5 | 2 |
|
||
| agricultural | 22 | 12 | 5 | 10 | 20 | 5 | 15 | 5 | 6 |
|
||
| extraction | 16 | 7 | 28 | 4 | 24 | 5 | 9 | 5 | 2 |
|
||
| transit | 10 | 18 | 5 | 5 | 20 | 12 | 14 | 14 | 2 |
|
||
| research | 16 | 6 | 5 | 14 | 5 | 7 | 10 | 3 | 34 |
|
||
| commercial | 14 | 30 | 5 | 6 | 10 | 14 | 14 | 5 | 2 |
|
||
| service_mixed | 20 | 18 | 5 | 10 | 6 | 14 | 18 | 7 | 2 |
|
||
| mining | 16 | 7 | 22 | 3 | 28 | 6 | 9 | 7 | 2 |
|
||
| frontier | 26 | 10 | 10 | 5 | 18 | 8 | 16 | 5 | 2 |
|
||
| energy | 10 | 3 | 18 | 6 | 24 | 3 | 6 | 6 | 24 |
|
||
|
||
**Energy Ent = 3 note:** A large energy facility's Entertainment guarantee comes from the
|
||
City-tier floor (10k+ population = Entertainment guaranteed), not from the weight table.
|
||
The weight 3 only governs *additional* Entertainment districts beyond the guarantee.
|
||
A company canteen + gym at a 200k-person energy installation earns the guaranteed slot;
|
||
the weight 3 correctly suppresses a second Entertainment district in remaining slots.
|
||
|
||
**Political archetype modifiers** (stacked additive after role table, before
|
||
renormalization):
|
||
|
||
| political_archetype | Modifiers |
|
||
|--------------------|-----------|
|
||
| CompanyTown | Adm −10, Ind +10, Log +5, Res +5 |
|
||
| AdminCapital | Adm +20, Ent +5, Spe +5, Ind −15 |
|
||
| FreePort | Com +15, Trn +10, Mix +5, Adm −15 |
|
||
| Contested | No modifier (competing forces cancel) |
|
||
| OrganicGrowth | Mix +15, Res +10, Ind −10, Adm −5 |
|
||
|
||
### 1.3 Settlement Age Character Modifier
|
||
|
||
**Source field:** `bodies.founding_age_years` (INTEGER, nullable).
|
||
**Fallback:** `system_history.settlement_wave` → age bracket via mapping in §0 above.
|
||
**WorldTier cap:** Age modifier applies only at Backwater tier and above. Waypoint and
|
||
Passage settlements are always `nascent` regardless of founding date.
|
||
|
||
| founding_age_years | Age bracket | Effect on DistrictSkeleton |
|
||
|-------------------|-------------|---------------------------|
|
||
| 0–49 (or wave_5) | nascent | perimeter_treatment: Open or Temporary; density_pct: 0.4–0.6; character: raw production |
|
||
| 50–149 (or wave_4) | young | perimeter_treatment: Fenced; density_pct: 0.5–0.7; character: functional |
|
||
| 150–349 (or wave_2/3) | established | perimeter_treatment: Walled; density_pct: 0.6–0.8; character: differentiated |
|
||
| 350+ (or wave_1/origin) | mature | perimeter_treatment: variable (any); density_pct: 0.6–0.9; character: layered |
|
||
|
||
Age modifies: `perimeter_treatment`, `density_pct`, and the content system's building
|
||
archetype selector via `character_class`. It does NOT modify district count, district
|
||
type, or prosperity_baseline.
|
||
|
||
### 1.4 The Combined Algorithm
|
||
|
||
```
|
||
fn compute_district_distribution(
|
||
city: &CityGenerationContext,
|
||
body: &BodyRecord, // from bodies table
|
||
system: &SystemEconomyRecord, // from system_economy (nullable fields)
|
||
terrain_gradient: CardinalDirection, // from Layer 1 output
|
||
seed: u64, // child_seed(world_seed, city_id)
|
||
) -> Vec<DistrictSpec>
|
||
|
||
Step 1: Total district count
|
||
D_raw = max(1, floor(city.population / 50_000)) // log-scaled for pop > 500k
|
||
D_total = min(D_raw, WorldTier_cap[city.world_tier])
|
||
if city.is_capital: D_total = min(D_total + 1, WorldTier_cap[city.world_tier])
|
||
if body.settlement_pattern in (domed, cave): D_total = 1
|
||
|
||
Step 2: Mandatory districts from population tier guarantee table
|
||
mandatory_types = guarantee_table[population_tier(city.population)]
|
||
if len(mandatory_types) > D_total: collapse_excess_to_mixed(mandatory_types, D_total)
|
||
remaining_slots = D_total - len(mandatory_types)
|
||
|
||
Step 3: Fill remaining slots from economic role weight table
|
||
role = normalize_economic_role(body.economic_role) // apply gap §0 normalization
|
||
weights = ROLE_WEIGHT_TABLE[role] // 9-element, all ≥ 3
|
||
weights = apply_archetype_modifiers(weights, city.political_archetype)
|
||
weights = renormalize_to_100(weights)
|
||
additional_types = seeded_weighted_sample(weights, remaining_slots, seed)
|
||
// sample_without_replacement = false; duplicate types allowed (second Ind, second Log)
|
||
all_types = mandatory_types + additional_types
|
||
|
||
Step 4: Assign age character class
|
||
age_bracket = effective_age_bracket(body.founding_age_years, body.settlement_wave, city.world_tier)
|
||
for d in all_types:
|
||
d.character_class = AGE_CHARACTER_TABLE[d.type][age_bracket]
|
||
d.density_pct = lerp(age_density_range[d.type][age_bracket], seeded_float(seed, i))
|
||
d.perimeter_treatment = age_perimeter_table[d.type][age_bracket]
|
||
|
||
Step 5: Assign prosperity gradients (see D-197 for formula)
|
||
assign_prosperity_per_district(all_types, city, body, system, terrain_gradient)
|
||
```
|
||
|
||
---
|
||
|
||
## 2. D-195: Attractor-Matching Compatibility Matrix
|
||
|
||
**Decision ID:** D-195
|
||
**Domain:** architecture
|
||
**Status:** candidate
|
||
**Claimed by:** Burnelli-Sheldon
|
||
|
||
### Decision
|
||
|
||
The generator assigns named cities to geographic attractors using a scored bipartite
|
||
matching algorithm. City-attractor compatibility is scored by the 10×7 matrix below
|
||
(10 economic roles × 7 canonical geographic attractor types). Score scale is 0–10
|
||
where 0 = physically impossible/forbidden and 10 = ideal/preferred.
|
||
|
||
### Compatibility Matrix (0–10 scale)
|
||
|
||
| economic_role | RiverConfl | CoastalHarbor | MtnPass | ArablePlain | ResourceConc | Defensible | NaturalBarrier |
|
||
|--------------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||
| manufacturing | 7 | 6 | 3 | 5 | 5 | 2 | 0 |
|
||
| agricultural | 8 | 5 | 2 | 10 | 1 | 2 | 0 |
|
||
| extraction | 3 | 3 | 5 | 2 | 10 | 3 | 1 |
|
||
| transit | 9 | 9 | 10 | 3 | 1 | 3 | 0 |
|
||
| research | 4 | 4 | 4 | 4 | 6 | 8 | 3 |
|
||
| commercial | 8 | 9 | 6 | 4 | 2 | 2 | 0 |
|
||
| service_mixed | 6 | 6 | 4 | 6 | 2 | 3 | 0 |
|
||
| mining | 2 | 2 | 6 | 1 | 10 | 4 | 1 |
|
||
| frontier | 5 | 5 | 5 | 4 | 5 | 6 | 3 |
|
||
| energy | 4 | 5 | 3 | 2 | 8 | 4 | 2 |
|
||
|
||
### Forbidden Combinations (score = 0)
|
||
|
||
These are physically impossible placements. The generator hard-zeros these before scoring
|
||
and will not place them even as an overflow option:
|
||
|
||
| Economic role | Forbidden attractor | Reason |
|
||
|--------------|--------------------|-|
|
||
| manufacturing | NaturalBarrier | Heavy industry requires accessible logistics; impassable terrain precludes it |
|
||
| agricultural | NaturalBarrier | No farming in impassable terrain |
|
||
| transit | NaturalBarrier | Transit hubs use gaps in barriers (MountainPass), never the barrier itself |
|
||
| commercial | NaturalBarrier | Commerce requires accessible flow of people and goods |
|
||
| service_mixed | NaturalBarrier | Services require customers who can physically reach them |
|
||
|
||
Note: extraction, mining, research, frontier, and energy all have non-zero scores at
|
||
NaturalBarrier — these roles specifically exploit or require inaccessibility.
|
||
|
||
### Preferred Combinations (score 9–10, "inevitable placements")
|
||
|
||
| Economic role | Preferred attractor | Score | Narrative |
|
||
|--------------|--------------------|----|-----------|
|
||
| transit | MountainPass | 10 | The pass exists; the transit city exists to serve it |
|
||
| extraction | ResourceConcentration | 10 | The deposit exists; the city exists to work it |
|
||
| mining | ResourceConcentration | 10 | Same |
|
||
| agricultural | ArablePlain | 10 | The land exists; the city farms it |
|
||
| transit | CoastalHarbor | 9 | Port = natural transit nexus |
|
||
| transit | RiverConfluence | 9 | River junction = historic crossing + trade |
|
||
| commercial | CoastalHarbor | 9 | Ports = commercial centers universally |
|
||
|
||
### Mismatch Flag Thresholds (Lead Decision Implemented)
|
||
|
||
The lead directed a two-tier mismatch system: score < 0.35 = warning, score < 0.15 = error.
|
||
In the 0–10 matrix scale:
|
||
- **Warning:** raw score < 3.5
|
||
- **Error:** raw score < 1.5
|
||
|
||
**Validation against real systems.db body types:**
|
||
|
||
I queried systems.db to validate these thresholds produce sensible flags on real data.
|
||
|
||
Representative cases where warnings fire (score 2–3.5):
|
||
- agricultural + Defensible (score 2): farming community at a defensible site — exists
|
||
(hill forts, fortified medieval towns) but unusual. Warning is correct.
|
||
- transit + ArablePlain (score 3): transit hub on flat farmland — plausible (railroad
|
||
junction on the plains) but not attractor-driven. Warning invites review.
|
||
- extraction + MountainPass (score 5): mine near a mountain pass — valid. No flag.
|
||
|
||
Representative cases where errors fire (score 0–1.5):
|
||
- agricultural + NaturalBarrier (score 0): impossible. Error ✓
|
||
- mining + ArablePlain (score 1): mine on good farmland — politically contentious, not
|
||
the natural attractor for mining. Error = flag for lead review. Correct.
|
||
- agricultural + MountainPass (score 2): farming at a mountain pass — warning, not
|
||
error. Borderline: mountain villages do farm (terraced agriculture). Warning is right.
|
||
|
||
**Conclusion:** The 0.35/0.15 two-tier thresholds produce sensible behavior against the
|
||
real body data. No threshold adjustment needed.
|
||
|
||
### Flagging Behavior
|
||
|
||
```rust
|
||
match best_attractor_score / 10.0 {
|
||
s if s < 0.15 => PlacementFlag::Error(
|
||
format!("{} city placed at {} attractor: score {:.2} below error threshold",
|
||
city_name, attractor_type, s)
|
||
),
|
||
s if s < 0.35 => PlacementFlag::Warning(
|
||
format!("{} city placed at {} attractor: score {:.2} below warning threshold",
|
||
city_name, attractor_type, s)
|
||
),
|
||
_ => PlacementFlag::None,
|
||
}
|
||
```
|
||
|
||
Flags are logged at layer 2 generation time and accessible via a generator diagnostic
|
||
API. They do not stop generation — they surface cases where the lead may want to author
|
||
an override.
|
||
|
||
---
|
||
|
||
## 3. D-196: SettlementClass Enum and Latent Settlement Active/Ghost Logic
|
||
|
||
**Decision ID:** D-196
|
||
**Domain:** architecture
|
||
**Status:** candidate
|
||
**Claimed by:** Burnelli-Sheldon
|
||
|
||
### Decision
|
||
|
||
All generated settlements carry a `SettlementClass` that determines their active/ghost
|
||
derivation logic. This generalizes the prior latent-settlement concept from sub-settlements
|
||
only to ALL generated settlements. Every settlement can be ghost; every settlement has
|
||
a class-specific economic condition that determines whether it is.
|
||
|
||
### SettlementClass Enum
|
||
|
||
```rust
|
||
pub enum SettlementClass {
|
||
/// City exists because a named corp has headquarters_body = this body.
|
||
/// Active if corp.health_metric > 0.0 AND corp.lifecycle_state != Dissolved.
|
||
/// Ghost only on full corp dissolution — the most durable class.
|
||
NameLocked,
|
||
|
||
/// City exists to satisfy population budget without a named attractor.
|
||
/// Active if parent NameLocked settlement health_metric > 0.4.
|
||
/// Ghost when its economic anchor declines.
|
||
PopulationBudget,
|
||
|
||
/// Sub-settlement placed by economic trigger (mining camp, trade waypoint,
|
||
/// agricultural node, shadow node). Active if triggering corp health_metric > 0.4.
|
||
/// Most volatile class — directly tied to a single corp's health.
|
||
EconomicTriggered { activating_corp_id: String },
|
||
|
||
/// Placed at Province centroid by population pressure, no geographic attractor.
|
||
/// Active if province_avg_corp_health > 0.5.
|
||
/// geographically_triggered = false; FoundingOrientation = AdminFacing.
|
||
OrganicGrowth,
|
||
}
|
||
```
|
||
|
||
### Active/Ghost Derivation per Class
|
||
|
||
| Class | Active condition | Ghost condition |
|
||
|-------|-----------------|-----------------|
|
||
| NameLocked | corp health_metric > 0.0 AND lifecycle ≠ Dissolved | Corp fully dissolved |
|
||
| PopulationBudget | nearest NameLocked city health_metric > 0.4 | Anchor city's corp collapses |
|
||
| EconomicTriggered | activating_corp health_metric > 0.4 | Corp distressed or dissolved |
|
||
| OrganicGrowth | province avg corp health > 0.5 | Province-wide economic decline |
|
||
|
||
### What "Ghost" Means for Rendering
|
||
|
||
A ghost settlement does not disappear. Its streets and building footprints are
|
||
seed-locked and persist. The rendering system reads the active/ghost flag to:
|
||
- Switch lighting to dark/emergency-only
|
||
- Apply maximum tile condition decay (Broken tier)
|
||
- Clear activity entity spawn points (no NPCs walking around)
|
||
- Maintain road and building geometry unchanged
|
||
|
||
The physical city remains. Economic life drains out of it. This is the correct behavior
|
||
confirmed by Amendment 5 (ghost towns as emergent rendering consequence, not a designed
|
||
narrative feature).
|
||
|
||
### `placed_at_generation` Flag
|
||
|
||
An additional immutable boolean `placed_at_generation: bool` is set on each Province
|
||
when a settlement exists in that Province at Layer 2 generation time. This is the only
|
||
way to distinguish "AbandonedZone" from "WildernessBuffer" at runtime — both have
|
||
`settlement_count == 0` in the active state, but only AbandonedZone has
|
||
`placed_at_generation = true`.
|
||
|
||
---
|
||
|
||
## 4. D-197: prosperity_baseline Derivation Formula
|
||
|
||
**Decision ID:** D-197
|
||
**Domain:** architecture
|
||
**Status:** candidate
|
||
**Claimed by:** Burnelli-Sheldon
|
||
|
||
### Decision
|
||
|
||
The `prosperity_baseline` field on each `DistrictSkeleton` is a seed-locked float
|
||
in [0.05, 0.95] derived from economic data and topographic context. It represents what
|
||
the district was economically *designed for* — not what it is now. The runtime economic
|
||
simulation updates `prosperity_current`; `prosperity_baseline` is never updated after
|
||
generation.
|
||
|
||
### Formula
|
||
|
||
```
|
||
prosperity_baseline(district_i, city, body, system, terrain) =
|
||
|
||
// Base: economic tier normalized to [0.0, 1.0]
|
||
base = clamp(economic_tier(body, system) / 5.0, 0.0, 1.0)
|
||
|
||
// Role modifier: flat offset per economic_role
|
||
role_mod = ROLE_PROSPERITY_MODIFIER[normalize_economic_role(body.economic_role)]
|
||
|
||
// Gradient: positional rank along topographic high-to-low direction
|
||
rank = positional_gradient_rank(district_i, all_districts, terrain.gradient_direction)
|
||
// rank ∈ [0.0, 1.0]; 1.0 = highest ground; 0.0 = lowest ground
|
||
magnitude = DISTRIBUTION_INDEX_SCALE[distribution_index(system)]
|
||
gradient_offset = (rank - 0.5) × magnitude
|
||
|
||
// Paula's topographic modifier (additive)
|
||
topo_mod = +0.05 if district_i elevation in top 30% of city elevation range
|
||
= -0.05 if district_i elevation in bottom 20% vs. sea level
|
||
= 0.00 otherwise
|
||
|
||
prosperity_baseline_i = clamp(base + role_mod + gradient_offset + topo_mod, 0.05, 0.95)
|
||
```
|
||
|
||
### Role Prosperity Modifiers
|
||
|
||
| economic_role | role_mod |
|
||
|--------------|---------|
|
||
| extraction | -0.10 |
|
||
| mining | -0.10 |
|
||
| frontier | -0.20 |
|
||
| research | +0.15 |
|
||
| service_mixed | +0.10 |
|
||
| commercial | +0.10 |
|
||
| transit | 0.00 |
|
||
| manufacturing | 0.00 |
|
||
| agricultural | 0.00 |
|
||
| energy | 0.00 |
|
||
|
||
### Distribution Index Scale (gradient magnitude)
|
||
|
||
| distribution_index | magnitude |
|
||
|-------------------|-----------|
|
||
| "stratified" | 0.70 (steep gradient: ±0.35 spread across districts) |
|
||
| "moderate" | 0.20 (shallow gradient: ±0.10 spread) |
|
||
| NULL (fallback) | 0.20 (treat as moderate) |
|
||
|
||
### Economic Tier Derivation (Fallback for NULL values)
|
||
|
||
Per Gap 2 in §0, `system_economy.economic_tier` is NULL for 97% of systems. Fallback:
|
||
|
||
```
|
||
fn economic_tier(body: &Body, system: &SystemEconomy) -> f32 {
|
||
if let Some(tier) = system.economic_tier { return tier as f32; }
|
||
// Fallback from body population
|
||
match body.population {
|
||
p if p >= 5_000_000_000 => 5.0,
|
||
p if p >= 1_000_000_000 => 4.0,
|
||
p if p >= 100_000_000 => 3.0,
|
||
p if p >= 10_000_000 => 2.0,
|
||
_ => 1.0,
|
||
}
|
||
}
|
||
```
|
||
|
||
### Two Distinct Fields — Naming Lock (CL-Q1)
|
||
|
||
- `prosperity_baseline: f32` — seed-locked, never updated after generation
|
||
- `prosperity_current: f32` — runtime simulation state, updated by economic sim
|
||
- `prosperity_delta: f32` — always derived: `prosperity_current - prosperity_baseline`. Never stored.
|
||
|
||
These three names are locked. They must not be conflated in implementation. Any code
|
||
that updates `prosperity_baseline` after generation is a bug.
|
||
|
||
### Tile Condition Thresholds (from `prosperity_current`)
|
||
|
||
Locked from Paula's Round 2 proposal (Gestalt adopted):
|
||
|
||
| Tile condition | prosperity_current range |
|
||
|---------------|------------------------|
|
||
| Intact | > 0.63 |
|
||
| Worn | 0.43–0.63 |
|
||
| Cracked | 0.23–0.43 |
|
||
| Broken | < 0.23 |
|
||
|
||
The offset from round numbers (0.63/0.43/0.23 rather than 0.60/0.40/0.20) prevents
|
||
boundary oscillation when prosperity fluctuates near the threshold.
|
||
|
||
---
|
||
|
||
## 5. D-198: Economic Simulation Independence from Layer 1-2 Spatial Data
|
||
|
||
**Decision ID:** D-198
|
||
**Domain:** architecture
|
||
**Status:** candidate
|
||
**Claimed by:** Burnelli-Sheldon
|
||
|
||
### Decision
|
||
|
||
The economic simulation runs exclusively on system-level aggregate data from
|
||
`systems.db`. It does not need Layer 1-2 spatial output (drainage, settlement positions,
|
||
road graphs, TerritorialStatus). Layer 1-2 generators read from the economic simulation's
|
||
state at generation time, but the relationship is one-directional: sim → Layer 1-2.
|
||
|
||
### What the Economic Sim Needs (from systems.db)
|
||
|
||
| Table | Fields used |
|
||
|-------|------------|
|
||
| `bodies` | economic_role, population, settlement_pattern |
|
||
| `system_economy` | economic_tier, economic_base_primary/secondary, distribution_index |
|
||
| `system_gates` | gate_connections, gate_topology, hop_distance_from_gateway |
|
||
| `corp_presence` | corp_id, location_id, primary_operation |
|
||
| `corp_financial_state` | corp_id, health_metric |
|
||
| `commodities` | base_price, elasticity, tier, production_ubiquity |
|
||
| `production_chains` + `chain_inputs` | supply chain topology |
|
||
| `gate_links` | inter-system commodity flow topology |
|
||
|
||
All of these exist in systems.db from build-time generation. The sim reads them at
|
||
startup and maintains its own in-memory state from there.
|
||
|
||
### What the Economic Sim Does NOT Need
|
||
|
||
- Settlement positions (latitude/longitude on body surface)
|
||
- Road graph topology (which road connects which town)
|
||
- TerritorialStatus per Province
|
||
- River network
|
||
- Geographic attractor positions
|
||
- District boundaries
|
||
|
||
These are consumed by Layer 3-4 rendering only. They never feed back into the
|
||
economic simulation. Commodity prices, corp health, and trade flow are computed at the
|
||
system and body level — not at the settlement or district level.
|
||
|
||
### Why This Matters for Architecture
|
||
|
||
This decision confirms Amendment 1 (three-tier execution) from the consultant review.
|
||
The economic sim can start running immediately at game startup using only systems.db.
|
||
Layer 1-2 generation runs on background threads in parallel with the sim — there is no
|
||
handshake or synchronization point between them. The only flow from Layer 1-2 back to
|
||
the sim would be a player-caused event (player destroys a mine → ECS event → sim
|
||
receives production loss), but this is handled through the ECS event bus, not through
|
||
spatial data structures.
|
||
|
||
### Render Layer Reads from Sim (One-Directional)
|
||
|
||
```
|
||
Economic Sim (systems.db aggregates)
|
||
│
|
||
├──→ corp_financial_state.health_metric
|
||
│ consumed by: Layer 4 renderer (settlement active/ghost)
|
||
│ consumed by: District tile condition derivation
|
||
│
|
||
├──→ prosperity_current (per district, derived from corp health)
|
||
│ consumed by: Layer 4 tile variant selection
|
||
│
|
||
└──→ regional_land_use (coarse biome-cell resolution updates)
|
||
consumed by: Layer 2 hinterland renderer
|
||
NOT ChunkMutations — different resolution (biome-cell, not tile-level)
|
||
```
|
||
|
||
The flow from the sim to the renderer never requires re-running Layer 1-2 generation.
|
||
A corp declining does not move a city. It makes the city look worse.
|
||
|
||
---
|
||
|
||
## 6. D-199: 6-Field Minimum Economic Read Set for City Generation Context
|
||
|
||
**Decision ID:** D-199
|
||
**Domain:** architecture
|
||
**Status:** candidate
|
||
**Claimed by:** Burnelli-Sheldon
|
||
|
||
### Decision
|
||
|
||
The minimum economic data set required to generate a city's district distribution is
|
||
six fields, queryable from systems.db at game startup and stored in
|
||
`CityGenerationContext`. No runtime sim queries are needed during city generation.
|
||
|
||
### The Six Fields
|
||
|
||
| # | Field | Source table | Used in |
|
||
|---|-------|-------------|---------|
|
||
| 1 | `economic_role` | `bodies` | D-194 weight table row selector |
|
||
| 2 | `settlement_pattern` | `bodies` | D-194 domed/cave override; latent settlement placement |
|
||
| 3 | `economic_tier` | `system_economy` | D-197 prosperity_baseline base value |
|
||
| 4 | `distribution_index` | `system_economy` | D-197 gradient magnitude |
|
||
| 5 | `corp_presence` count | `corp_presence` | Corporate district intensity modifier |
|
||
| 6 | `headquarters_system/body` match | `corporations` | D-195 attractor-matching hard constraint |
|
||
|
||
### How They Are Loaded
|
||
|
||
These fields are read once at game startup for all inhabited bodies and stored in the
|
||
`CityGenerationContext` struct. At Layer 3 (city district generation, runtime on-demand),
|
||
the generator reads from the struct, not from the database. No database queries during
|
||
play.
|
||
|
||
```rust
|
||
pub struct CityGenerationContext {
|
||
pub body_id: String,
|
||
pub city_id: String,
|
||
pub city_name: String,
|
||
pub political_archetype: PoliticalArchetype,
|
||
pub prosperity_baseline: f32, // seed-locked Layer 3 output (computed, not stored)
|
||
pub surrounding_biome: BiomeClass,
|
||
pub road_entry_directions: Vec<CardinalDirection>,
|
||
pub footprint_radius_km: f32,
|
||
pub founding_orientation: FoundingOrientation,
|
||
pub world_tier: WorldTier,
|
||
|
||
// The six economics fields:
|
||
pub economic_role: EconomicRole, // (1) from bodies
|
||
pub settlement_pattern: SettlementPattern, // (2) from bodies
|
||
pub economic_tier: u8, // (3) from system_economy (fallback: population-derived)
|
||
pub distribution_index: DistributionIndex, // (4) from system_economy (fallback: Moderate)
|
||
pub corp_presence_count: u32, // (5) count from corp_presence
|
||
pub has_hq_corp: bool, // (6) whether any corp.headquarters_body = this body
|
||
}
|
||
```
|
||
|
||
### Note on Nullability
|
||
|
||
Per §0 Gap 2: `economic_tier` and `distribution_index` are NULL for 97% of systems.
|
||
The struct uses fallback derivation (see D-197 §4) at load time. The `Option<>` wrapper
|
||
is resolved to a concrete value before the struct is constructed — null never reaches
|
||
the generator.
|
||
|
||
---
|
||
|
||
## 7. TerritorialStatus Economic Threshold Confirmation
|
||
|
||
The converged algorithm from Round 2 notes (§3) is confirmed correct for the economics
|
||
layer. The priority-ordered derivation is:
|
||
|
||
```rust
|
||
fn derive_territorial_status(province: &ProvinceWorldState) -> TerritorialStatus {
|
||
if province.placed_at_generation && !province.active {
|
||
return TerritorialStatus::AbandonedZone;
|
||
}
|
||
if !province.placed_at_generation && province.settlement_count == 0 {
|
||
return TerritorialStatus::WildernessBuffer;
|
||
}
|
||
if province.primary_economic_activity == EconomicActivity::Extraction
|
||
&& province.corporate_presence_score > 0.4 {
|
||
return TerritorialStatus::ExtractiveZone;
|
||
}
|
||
if province.jurisdiction_overlap_score > 0.3 {
|
||
return TerritorialStatus::ContestZone;
|
||
}
|
||
if province.infrastructure_quality > 0.6 && province.corporate_road_maintenance > 0.5 {
|
||
return TerritorialStatus::CoreTerritory;
|
||
}
|
||
TerritorialStatus::FrontierTerritory
|
||
}
|
||
```
|
||
|
||
**Economic threshold validation:**
|
||
|
||
- `corporate_presence_score > 0.4` for ExtractiveZone: this maps to "at least one corp
|
||
with health_metric > 0.4 operating an extraction commodity in this Province." Correct.
|
||
- `infrastructure_quality > 0.6` for CoreTerritory: this is a road/settlement density
|
||
metric from Layer 2 generation, not an economic field. It does not need updating when
|
||
economics changes. Correct — infrastructure is seed-locked at Layer 2.
|
||
- `corporate_road_maintenance > 0.5` for CoreTerritory: this is an economics-variable
|
||
field (corp health drives road maintenance). TerritorialStatus can therefore transition
|
||
from CoreTerritory to FrontierTerritory as corps decline — the roads degrade, the
|
||
classification degrades. This is the correct emergent behavior.
|
||
|
||
**One clarification needed:** `corporate_presence_score` is not a field I defined in my
|
||
prior rounds. I assume it derives from: `sum(health_metric for corps in province) / corp_count`.
|
||
If no corps are present, score = 0. Tyre should confirm this derivation in the
|
||
implementation spec.
|
||
|
||
---
|
||
|
||
## 8. Remaining Open Items After Round 3
|
||
|
||
These are items where I have a position but am waiting for other agents' confirmation:
|
||
|
||
**For Tyre:**
|
||
- Confirm `corporate_presence_score` derivation formula (§7 above)
|
||
- Confirm `atlas_city_names` schema extends (not replaces) `atlas_cities`
|
||
- Confirm ARCH-4 body_radius_km column is the right field for area_count formula
|
||
|
||
**For Gestalt:**
|
||
- Confirm whether age character classes need a named field on `DistrictSkeleton` or
|
||
whether `density_pct` + `perimeter_treatment` + `prosperity_baseline` are sufficient
|
||
to express the full character difference between nascent and mature districts
|
||
|
||
**For Paula:**
|
||
- Confirm corp dissolution signal: is `corp_lifecycle_events.event_type = 'Dissolved'`
|
||
the canonical flag for NameLocked settlement ghost status, or is `health_metric = 0.0`
|
||
the correct proxy? The schema has both; which is authoritative?
|
||
|
||
**Data quality action items (not blocking workshop, but blocking Phase 3 implementation):**
|
||
1. Populate `bodies.founding_age_years` across all inhabited bodies (wiki data exists for settlement dates)
|
||
2. Populate `system_economy.economic_tier` and `distribution_index` for all systems (or confirm fallback derivation is sufficient)
|
||
3. Normalize `economic_role` values in bodies table (add migration to import_economics.py)
|