Citadel Grid¶
The citadel grid is the plot lattice a colony builds on. It is generated once, at settle, from the
at-launch expedition result's landscape shape, and is the source of truth from which
citadel_service.derive_citadel_level infers the citadel level.
Gated on ADR-0091. Player-facing rules and
implementation status live in FEATURES/planets/citadels.md and
FEATURES/planets/planetary-survey.md.
Generation pipeline (at settle)¶
- Receive the at-launch expedition result: contains the rolled shape class, template ID, and
trait overlay drawn from the validated template library (weighted by the planet's profile per M12).
No pre-stored
site_seedorsite_index— the roll happened at expedition launch. - Select the template from the validated library corresponding to the result's (shape class, size-bucket). The template fixes the silhouette, the cell adjacency graph, and the per-level reveal tranche assignment.
- Compute
usable_slots = clamp(round(mult · (4 + 2·size)), 6, 32)and the three-channel cap: C_slots = max L: usable_slots ≥ CITADEL_MIN_CELLS[L]C_footprint = max L: template supports L's largest required contiguous footprintC_practical = max L: usable_slots ≥ CITADEL_MIN_CELLS[L] + H[L](calibrated, harness-verified)C_hard = min(C_slots, C_footprint)·guaranteed_max_level = min(C_slots, C_footprint, C_practical)- Overlay traits —
plot.terrain,plot.deposit {resource, mag},plot.hazard {kind, sev},plot.feature {id}, the per-cell axes — without altering the template topology. - Set every cell's
plot.revealedper the reveal schedule (L1 tranche unfogged, the rest fogged). - Persist the grid and
structures.site {shape_class, template_id, usable_slots, c_slots, c_footprint, c_practical, c_hard, compression_gap, guaranteed_max_level, reveal_schedule, founded_at, sprawl_tax, h_table_version},structures.energy {source, native_supply_by_tier, variance},structures.site_traits[], and per-cell data as additive JSONB onplanet.structures.structures.pyis the single writer.
Shape classes¶
Five natural shape classes plus one engineered class, each a multiplier on the size envelope
B = 4 + 2·size:
| Class | Mult on B |
Max footprint | Notes |
|---|---|---|---|
| COMPACT | 0.85× | 2×2+ | dense near-square; lowest ceiling, most efficient upkeep |
| TERRACED | 1.00× | 2×2+ | the neutral anchor — TERRACED 1.0× reproduces the current size→level cap exactly |
| LINEAR | 1.10× | 2×1 only (no 2×2) | strip; structurally tops out at L4 regardless of slot count |
| IRREGULAR | 1.15× | 2×2+ (gappy) | disjoint clusters; clearing cost |
| SPRAWLING | 1.30× | 2×2+ | highest ceiling; per-slot upkeep + baseline power scale with footprint (sprawl tax) |
| ENGINEERED | 1.00× | 2×2+ | ARTIFICIAL worlds + the always-valid fallback for onboarding/floor guarantee |
usable_slots = clamp(round(mult · B), 6, 32) (slot cap raised 30 → 32). Provisional multipliers;
calibrated by build-time harness.
Three-channel cap¶
The citadel-level ceiling is determined by three independent channels; the binding cap is their minimum:
C_site = min( C_slots , C_footprint , C_practical ) # honest achievable ceiling (displayed at settle)
C_hard = min( C_slots , C_footprint ) # permanent site-bound hard ceiling
C_slots (hard): max L: usable_slots ≥ CITADEL_MIN_CELLS[L], where
CITADEL_MIN_CELLS = {1:2, 2:3, 3:5, 4:7, 5:11} (verified against structures.py). This is the
key-building packing gate only; FLOOR_AREA and HOUSING are separate gates handled in C_practical.
C_footprint (hard): topology gate — the maximum level whose largest required contiguous
footprint the template can support. Footprint ladder (code-verified against shipped key buildings):
L1–L3 require a 1×1 footprint; L4 requires 2×1 (SPACEPORT); L5 requires 2×2 (ADMIN_SPIRE).
LINEAR (2×1 max, no 2×2) therefore tops out at L4 regardless of slot count. C_footprint is a
per-template validated property, asserted by the build-time harness — not inferred from the
class label.
C_practical (honest achievable floor): max L: usable_slots ≥ CITADEL_MIN_CELLS[L] + H[L],
where H[L] is the spare cells a standard build needs to clear FLOOR_AREA[L] / HOUSING[L] at
achievable building levels. Provisional H = {1:0, 2:1, 3:2, 4:3, 5:4}, calibrated by the
build-time harness. H[L] is a cell-count input to the inverse function — it is never an addend
to the level itself.
The headline at-settle display is C_practical (the guaranteed achievable floor, not an optimistic
packing estimate). C_hard is surfaced as a clearly-labeled "potential ceiling — requires
densification investment" secondary.
Population (HOUSING) remains a separate gate from the spatial ceiling: HOUSING is satisfied
by the existing population→phase ladder, not by slots. Site choice sets the spatial ceiling;
population gates independently.
Progressive reveal schedule¶
The full grid is seeded server-side at settle by structures.py (single writer) and persisted fully
in planet.structures. Cells are fogged (visibility, not absence) and unfog on a
level-gated schedule:
| Citadel level reached | Cumulative fraction unfogged |
|---|---|
| L1 (settle) | 35% |
| L2 | 55% |
| L3 | 70% |
| L4 | 85% |
| L5 | 100% |
Each level tranche unfogs atomically when the L(N-1)→LN upgrade completes, reusing the existing
upgrade_hours build timer. No new timer; no fog↔timer rollback edge case (a cancelled upgrade
leaves no half-unfogged state).
Soft-lock guard: revealed(L) is always ≥ the cells level L genuinely needs to build (its
key-building footprint plus the H[L] spare cells). The schedule is monotonic.
Frontier reveal: cells unfog radiating outward from the citadel core along the cell adjacency graph. Severed clusters stay dark until reached or bridged. Player-directed reveal is deferred.
The grid GET returns revealed cells fully and fogged cells as silhouette-only (position + "unrevealed"), so the Grid Manager renders the shape outline with locked cells.
Template library¶
Grids are drawn from a constrained shape-template library: each (shape class, size-bucket) maps
to a set of pre-validated silhouette templates. Templates are validated offline, once, to
guarantee that the key buildings + FLOOR_AREA / HOUSING economy fit at the shape's allowed max
level. The library is the pool from which the at-launch expedition roll draws — not a per-planet
pre-stored map.
Planet profile (type, size, characteristic hazard, terrain) weights which templates within the
matching class+size bucket are more likely to be drawn. Traits overlay plot.terrain,
plot.deposit, plot.hazard, and plot.feature onto cells without altering the validated
topology — the floor guarantee holds regardless of trait overlay.
ENGINEERED is the always-valid fallback. A bad roll is bland (draws from the lower-quality templates), never bricked (the floor guarantee holds for all templates in the library).
Library breadth per (class, size-bucket) must be large enough to avoid repeated maps within a play session — drives the offline validation harness size.
Invariants¶
- One writer:
structures.pyis the sole writer of the grid. No other service writesplanet.structures. - Deterministic reproduction: given the template ID + rolled trait overlay, the grid is reproducible exactly (cold-start recovery, admin tooling).
- Cold-start floor: ENGINEERED always validates; no expedition result ever produces an unplayable grid.
- Reveal is monotonic:
revealed(L)never decreases; a level completion only unfogs. - Slot model is spatial only: the spatial ceiling is governed by
C_site; population gates independently via the existingHOUSINGladder.
What is NOT stored¶
site_seed— MOOT. There is no pre-stored deterministic per-planet seed; the roll happens at expedition launch.GALAXY_SALT— MOOT.settled_site_index— MOOT. There are no pre-seeded site indices.effective_cells/power_overhead_cells— runtime-emergent; compute on demand from the live grid.- Pre-settle candidate sites — MOOT. The expedition result is ephemeral; only the settled result persists.