Skip to content

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)

  1. 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_seed or site_index — the roll happened at expedition launch.
  2. 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.
  3. Compute usable_slots = clamp(round(mult · (4 + 2·size)), 6, 32) and the three-channel cap:
  4. C_slots = max L: usable_slots ≥ CITADEL_MIN_CELLS[L]
  5. C_footprint = max L: template supports L's largest required contiguous footprint
  6. C_practical = max L: usable_slots ≥ CITADEL_MIN_CELLS[L] + H[L] (calibrated, harness-verified)
  7. C_hard = min(C_slots, C_footprint) · guaranteed_max_level = min(C_slots, C_footprint, C_practical)
  8. Overlay traits — plot.terrain, plot.deposit {resource, mag}, plot.hazard {kind, sev}, plot.feature {id}, the per-cell axes — without altering the template topology.
  9. Set every cell's plot.revealed per the reveal schedule (L1 tranche unfogged, the rest fogged).
  10. 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 on planet.structures. structures.py is 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.py is the sole writer of the grid. No other service writes planet.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 existing HOUSING ladder.

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.