System S-057 assigned to real star GJ 35 (Van Maanen's Star, DG white dwarf at 13.9 ly). Renamed across all content, server code, docs, decisions, wiki lore, and config files. 224 files updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
31 KiB
Seed Configuration Schema — Design Document
Ticket: #394 Author: Tyre (Technical Architect) Status: Draft Date: 2026-02-13 Decisions referenced: D-010, D-024, D-025, D-027, D-029, D-034, D-035, D-036, D-037, D-041
1. Purpose
The seed configuration is the handoff contract between authored content (pools, templates, triangles in content/) and a running game instance (ECS entities in memory). It answers one question: given this content and this seed value, what specific world do we instantiate?
At game start, the seeder reads content definitions and a seed value, then produces a SeedConfig — a deterministic, serializable record of every randomized selection. This record is saved with the game state and replayed identically on reload.
What the seed config is NOT:
- Not a content authoring format (content authors write pools/templates/triangles — the seed config consumes them)
- Not a runtime state snapshot (that's the ECS world — the seed config is the recipe that built it)
- Not a save file (the save file contains the seed config alongside mutable game state)
2. Design Constraints
| Constraint | Source | Impact |
|---|---|---|
| Deterministic reproduction | D-010 principle 4, D-030 #7 | Same seed + same content version = identical SeedConfig. No HashMap iteration, no platform-dependent RNG. |
| BTreeMap for ordered collections | D-041 | All maps in the seed config use BTreeMap, not HashMap. |
| 30/50/20 entanglement ratio (variable) | D-029 | ~30% flat, ~50% mundane triangles, ~20% intrigue-entangled. Ratios vary per seed to prevent metagaming. |
| Single-candidate pools in v0.1 | Sprint briefing | Architecture supports N candidates; v0.1 pools contain exactly 1 candidate each. |
| Template instantiation via role slots | D-025 | Social sites define roles; the seed assigns NPCs to roles. Single ownership with reference links. |
| Two playable characters | D-027 | Smuggler + detective. Seed config records which character the player selected. |
| Saved with game state | Ticket #394 | Serialized into save files. Must be self-contained (no external content references that could drift). |
| Content version pinning | Implicit | Seed config records content version to detect content/save incompatibility. |
3. Schema Overview
SeedConfig
├── meta
│ ├── seed: u64
│ ├── content_version: String
│ ├── schema_version: u32
│ └── generated_at: String (ISO 8601)
├── character_selection: CharacterSelection
├── pool_draws: BTreeMap<PoolId, PoolDraw>
├── template_assignments: BTreeMap<TemplateId, TemplateAssignment>
├── triangle_config: TriangleConfig
├── entanglement: EntanglementConfig
├── contraband: ContrabandSelection
└── starting_knowledge: BTreeMap<CharacterId, Vec<KnowledgeEntry>>
4. Schema Detail
4.1 Meta
struct SeedMeta {
/// The seed value. u64 for sufficient randomness space.
/// v0.1: displayed nowhere; v0.2+: player can enter a seed for shared runs.
seed: u64,
/// Content version string from content/content.yaml.
/// If the save's content_version doesn't match the loaded content,
/// the loader warns or refuses to load (prevents desync).
content_version: String,
/// Schema version for forward compatibility. Increment on breaking changes.
schema_version: u32,
/// ISO 8601 timestamp of generation (informational, not used in logic).
generated_at: String,
}
Rationale: seed is the root of determinism — every randomized decision traces back to this value through a deterministic RNG (see section 6). content_version pins the content snapshot to prevent save/content drift.
4.2 Character Selection
struct CharacterSelection {
/// Which character the player chose. Determines starting knowledge,
/// access tiers, monologue pools, perception modes.
player_character: CharacterId,
/// All available characters for this campaign (for reference/validation).
available_characters: Vec<CharacterDefinition>,
}
/// CharacterId is a string enum matching content definitions.
/// v0.1: "smuggler" | "detective"
type CharacterId = String;
struct CharacterDefinition {
id: CharacterId,
display_name: String,
/// Starting social site (determines spawn location)
home_template: TemplateId,
/// Starting access tiers for NPC interactions
default_access: Vec<AccessTier>,
}
Note: Character selection is the one non-deterministic input — the player chooses. Everything else flows from seed + player_character.
4.3 Pool Draws
Pools are the core randomization mechanism. Each pool defines N candidates for a role; the seeder draws one.
/// Pool identifier matching content pool definitions.
/// Format: "{scope}:{pool_name}" — e.g., "transit:friend_smuggler"
type PoolId = String;
struct PoolDraw {
/// Which pool this draw came from
pool_id: PoolId,
/// The selected candidate's NPC canonical ID
selected: NpcId,
/// All candidates that were available (for debugging/replay verification)
candidates: Vec<NpcId>,
/// Index into candidates that was selected (for replay verification)
selected_index: usize,
}
v0.1 pools (single-candidate each):
| Pool ID | Selected | Purpose |
|---|---|---|
transit:friend_smuggler |
npc:kael-davan |
Smuggler's FRIEND (D-034) |
transit:friend_detective |
npc:sera-venn |
Detective's FRIEND (D-034) |
transit:bar_regulars |
(set of NPCs) | Bar regular population |
transit:compromised_inspector |
npc:torek-lintar |
The compromised Commission inspector |
transit:primary_contraband |
contraband:lattice-components |
What's being smuggled |
v0.2+ expansion: Pools grow to N candidates. friend_smuggler might offer 3 dock workers who could each be the FRIEND, with different contradiction arcs. The seeder draws one. Same schema, more candidates.
Pool categories:
enum PoolCategory {
/// Selects one NPC for a named narrative role
NpcRole,
/// Selects a set of NPCs for a group (bar regulars, shift workers)
NpcGroup,
/// Selects a contraband type
Contraband,
/// Selects an entanglement pattern (which NPCs are intrigue-connected)
EntanglementPattern,
}
4.4 Template Assignments
Templates (D-025 social sites) define role slots; the seeder fills them with NPCs.
/// Template identifier matching content template definitions.
/// Format: location slug — e.g., "logistics-hub", "bar", "smuggling-ring"
type TemplateId = String;
struct TemplateAssignment {
template_id: TemplateId,
/// Which location(s) this template is instantiated in
locations: Vec<LocationId>,
/// Role slot → NPC assignments
role_assignments: BTreeMap<RoleSlotId, RoleAssignment>,
}
/// Role slot identifier from template definition.
/// Format: "{template}:{role}" — e.g., "logistics-hub:shift-supervisor"
type RoleSlotId = String;
struct RoleAssignment {
/// The NPC assigned to this role slot
npc_id: NpcId,
/// Whether this NPC is the primary owner of this template (D-025 single ownership)
is_owner: bool,
/// If not owner, this is a reference link with relationship metadata
reference_metadata: Option<ReferenceLink>,
}
struct ReferenceLink {
/// The template that owns this NPC
owning_template: TemplateId,
/// Why this NPC appears in this template (relationship context)
relationship: String,
/// How many time phases this NPC spends at this template's location
presence_phases: Vec<DayPhase>,
}
Example — v0.1 Sova Transit District:
# Logistics Hub template
template: logistics-hub
locations: [the-terminal]
roles:
shift-supervisor:
npc: npc:voss
is_owner: true
dock-worker-1:
npc: npc:kael-davan
is_owner: true
dock-worker-2:
npc: npc:drin
is_owner: true
new-hire:
npc: npc:renn
is_owner: true
scheduler:
npc: npc:maret-korr
is_owner: true
courier:
npc: npc:harek
is_owner: true
# Bar template
template: bar
locations: [the-last-shift]
roles:
bar-owner:
npc: npc:lera-sessik
is_owner: true
bartender:
npc: npc:pell
is_owner: true
bar-regular-1:
npc: npc:sera-venn
is_owner: false
reference:
owning_template: null # Sera owns herself (Commission field tech, not bar staff)
relationship: "social anchor — evening regular"
presence_phases: [evening]
bar-regular-2:
npc: npc:resha
is_owner: true
# Smuggling ring template
template: smuggling-ring
locations: [maintenance-corridors]
roles:
ring-operative:
npc: npc:devra
is_owner: true
contact:
npc: npc:kael-davan
is_owner: false
reference:
owning_template: logistics-hub
relationship: "ring member — dual role"
presence_phases: [night]
4.5 Triangle Configuration
struct TriangleConfig {
/// All triangles instantiated in this seed
triangles: BTreeMap<TriangleId, TriangleInstance>,
/// Which triangles are initially active (storyteller can activate others later)
initially_active: Vec<TriangleId>,
}
/// Triangle identifier matching content triangle definitions.
type TriangleId = String;
struct TriangleInstance {
triangle_id: TriangleId,
/// The 3 NPCs assigned to this triangle's member slots.
/// Maps triangle role → NPC ID.
members: BTreeMap<String, NpcId>,
/// Initial fork state. In v0.1: all triangles start at their default state.
/// In v0.2+: seed can randomize starting fork positions for variety.
initial_fork_state: Option<String>,
/// Whether this triangle is "active" (storyteller managing) or "passive" (running on its own)
activation_mode: TriangleActivationMode,
}
enum TriangleActivationMode {
/// Storyteller actively manages fork progression based on player proximity
Active,
/// Triangle runs on background simulation, forks resolve without storyteller intervention
Passive,
}
v0.1 triangle instances:
| Triangle | Members | Mode | Notes |
|---|---|---|---|
hub-power |
Voss (authority), Kael (subordinate), Maret (caught-between) | Active | Workplace hierarchy tension |
worried-knowledge |
Sera (holder), Torek (subject), Naia (protected) | Active | Sera's unreported evidence |
bar-tensions |
Lera (owner), Resha (regular), Pell (bartender) | Passive | Mundane social friction |
worried-partner |
Kael (partner), Naia (worried), Devra (cause) | Active | Ring pressure on relationship |
informant-question |
Torek (inspector), Drin (worker), Olin (bystander) | Passive | Mundane workplace gossip |
4.6 Entanglement Configuration
struct EntanglementConfig {
/// The target ratio for this seed (varies around 30/50/20 per D-029)
target_ratio: EntanglementRatio,
/// The actual ratio achieved after assignment (may differ slightly due to rounding)
actual_ratio: EntanglementRatio,
/// Per-NPC entanglement tier assignment
npc_tiers: BTreeMap<NpcId, EntanglementTier>,
/// Module attachment ratio: known vs stranger NPCs for intrigue connections
module_attachment_ratio: ModuleAttachmentRatio,
}
struct EntanglementRatio {
/// Percentage of NPCs that are truly flat (routine + greeting only)
flat_pct: u8,
/// Percentage of NPCs in mundane triangles (no conspiracy connection)
mundane_pct: u8,
/// Percentage of NPCs entangled with intrigue content
entangled_pct: u8,
}
struct ModuleAttachmentRatio {
/// Percentage of intrigue-connected NPCs that are known to the player character
known_pct: u8,
/// Percentage that are strangers
stranger_pct: u8,
}
enum EntanglementTier {
/// Routine + greeting, no triangle membership, social wallpaper
Flat,
/// Member of mundane triangle(s), no conspiracy connection
Mundane,
/// Connected to intrigue content (ring member, compromised, witness, etc.)
Entangled,
}
v0.1 entanglement breakdown (17 NPCs):
| Tier | Count | Pct | NPCs |
|---|---|---|---|
| Flat | 5 | 29% | Renn, Harek, Sess, Sabel, Tav |
| Mundane | 9 | 53% | Voss, Lera, Pell, Resha, Maret, Drin, Olin, Naia, Torek* |
| Entangled | 3 | 18% | Kael (ring), Devra (ring), Sera (witness) |
Torek straddles mundane/entangled — he's compromised (entangled) but his triangle surface reads as mundane institutional friction. The seed config records him as entangled; the player discovers this through gameplay.
Revised breakdown with Torek entangled:
| Tier | Count | Pct | NPCs |
|---|---|---|---|
| Flat | 5 | 29% | Renn, Harek, Sess, Sabel, Tav |
| Mundane | 8 | 47% | Voss, Lera, Pell, Resha, Maret, Drin, Olin, Naia |
| Entangled | 4 | 24% | Kael, Devra, Sera, Torek |
This lands at 29/47/24 — within D-029's variable range around 30/50/20.
4.7 Contraband Selection
struct ContrabandSelection {
/// Primary contraband type for this seed
primary: ContrabandType,
/// Secondary contraband types available (for variety in future seeds)
secondary: Vec<ContrabandType>,
}
struct ContrabandType {
/// Identifier matching content/global/knowledge/contraband.yaml
id: String,
/// Display name for content systems (dialogue lines reference this)
display_name: String,
/// What the ring calls it internally (used in insider-access dialogue)
ring_codename: String,
/// Moral valence — affects monologue tone when player discovers it
moral_ambiguity: MoralAmbiguity,
}
enum MoralAmbiguity {
/// Clearly wrong (weapons, poisons)
Clear,
/// Morally complex (medical supplies, access tech)
Ambiguous,
/// Arguably justified (survival supplies, freedom tech)
Sympathetic,
}
v0.1: Single contraband type — unlicensed lattice components (D-037). moral_ambiguity: Ambiguous. The ring is smuggling access, not weapons.
4.8 Starting Knowledge
/// Per-character starting knowledge state.
/// Loaded into KnowledgeGraph components at entity creation time (D-041).
struct StartingKnowledge {
/// Facts this character knows at game start
facts: Vec<StartingFact>,
/// Entity knowledge at game start (NPCs the character already knows about)
entities: Vec<StartingEntityKnowledge>,
}
struct StartingFact {
/// Fact ID from content/global/knowledge/*.yaml
fact_id: String,
/// Starting confidence level
confidence: ConfidenceLevel,
/// Source of this knowledge
source: KnowledgeSource,
}
struct StartingEntityKnowledge {
/// NPC stable ID
entity_id: NpcId,
/// What attributes the character knows about this NPC at start
known_attributes: BTreeMap<String, AttributeKnowledge>,
/// Starting confidence
confidence: ConfidenceLevel,
/// Source
source: KnowledgeSource,
}
/// Maps to D-041's 4-level hierarchy
enum ConfidenceLevel {
Suspects,
KnowsOf,
KnowsDetails,
Direct,
}
enum KnowledgeSource {
/// Character background — they knew this before game start
Background,
/// Institutional knowledge — comes with the job
Institutional,
}
Smuggler starting knowledge:
- Knows colleagues at logistics hub (KnowsOf: Voss, Drin, Renn, Maret, Harek)
- Knows FRIEND deeply (KnowsDetails: Kael)
- Knows bar regulars casually (Suspects: Lera, Pell)
- Knows ring exists, knows Devra (KnowsDetails: Devra, ring operations)
- Does NOT know Sera, Torek, or Commission personnel (detective's world)
- Knows contraband type (KnowsDetails: lattice components)
Detective starting knowledge:
- Knows Commission chain of command (KnowsOf: Torek)
- Knows FRIEND (KnowsDetails: Sera)
- Knows bar casually (Suspects: Lera — goes there off-duty)
- Knows assignment briefing (Suspects: smuggling activity on Sova)
- Does NOT know ring members, specific smugglers, or insider logistics operations
- Does NOT know contraband type (investigation target)
5. File Format and Location
5.1 Authored content (input to seeder)
Lives in content/ under the campaign hierarchy. Relevant files:
content/
├── global/
│ └── knowledge/
│ └── contraband.yaml # Contraband type definitions
├── campaigns/main/systems/van-maanens-star/stations/sova/districts/transit/
│ ├── npcs/*.yaml # NPC profiles (candidates for pool draws)
│ ├── triangles/*.yaml # Triangle definitions (instantiated by seed)
│ ├── locations/*.yaml # Location definitions
│ ├── routines/schedules.yaml # NPC daily schedules
│ └── pools.yaml # Pool definitions (NEW — ticket #389)
5.2 Pool definition format (content/...pools.yaml)
# Pool definitions for Sova Transit District
# Each pool defines N candidates for a named role.
# The seeder draws from these pools using the seed value.
pools:
- pool_id: "transit:friend_smuggler"
category: npc_role
description: "Smuggler's FRIEND — closest colleague, emotional anchor"
constraints:
- must_be_in_template: "logistics-hub"
- must_have_pattern: "FRIEND"
- bonded_character: "smuggler"
candidates:
- npc_id: "npc:kael-davan"
weight: 1 # v0.1: only candidate
# v0.2+: additional candidates with different contradiction arcs
- pool_id: "transit:friend_detective"
category: npc_role
description: "Detective's FRIEND — social anchor, information holder"
constraints:
- bonded_character: "detective"
candidates:
- npc_id: "npc:sera-venn"
weight: 1
- pool_id: "transit:compromised_inspector"
category: npc_role
description: "The Commission inspector compromised by the ring"
constraints:
- must_have_access: "authority"
candidates:
- npc_id: "npc:torek-lintar"
weight: 1
- pool_id: "transit:primary_contraband"
category: contraband
description: "What the ring is smuggling"
candidates:
- id: "contraband:lattice-components"
weight: 1
5.3 Generated seed config (runtime output)
Format: RON (Rusty Object Notation) for Rust-native deserialization. Mirrors the Rust structs from section 4.
Location: Embedded in save files. Not a standalone file during gameplay — the seeder generates it in memory, the ECS consumes it, and the save system serializes it alongside game state.
Debug dump location: runtime/debug/seed-config-{seed}.ron — written only in debug builds or when --dump-seed flag is passed. Useful for content authors testing pool behavior.
5.4 Schema file
A JSON Schema for validating pool definition files goes into content/_schema/pools.schema.json. The seed config itself is validated at the Rust type level (serde deserialization), not via JSON Schema.
6. Generation Algorithm
6.1 Seeder pipeline
Input: seed: u64, player_character: CharacterId, content: LoadedContent
Output: SeedConfig
1. Initialize deterministic RNG from seed
└── Use `rand_chacha::ChaCha20Rng::seed_from_u64(seed)`
└── ChaCha20 is platform-independent, deterministic, cryptographically strong
2. Draw pool selections (order: alphabetical by pool_id for determinism)
├── For each pool in sorted order:
│ ├── Compute weighted random selection from candidates
│ ├── Record PoolDraw { selected, candidates, selected_index }
│ └── Advance RNG state (consumed regardless of pool size)
└── Validate: no NPC selected for conflicting roles
3. Assign templates
├── For each template in sorted order:
│ ├── Fill mandatory role slots from pool draws
│ ├── Fill remaining slots from available NPCs (weighted by fit)
│ ├── Record ownership (first template assigned = owner)
│ └── Create reference links for cross-template NPCs
└── Validate: every NPC has exactly one owning template
4. Configure triangles
├── Map pool-drawn NPCs into triangle member slots
├── Determine activation mode per triangle
│ └── Active if any member is entangled; passive otherwise
└── Set initial fork states (v0.1: all default)
5. Compute entanglement
├── Generate target ratio (vary around 30/50/20 using seed RNG)
│ └── flat_pct: 25-35 (uniform draw)
│ └── entangled_pct: 15-25 (uniform draw)
│ └── mundane_pct: 100 - flat - entangled
├── Classify NPCs: pool draws determine entangled set, triangle
│ membership determines mundane, remainder is flat
└── Record actual ratio achieved
6. Select contraband
└── Draw from contraband pool (v0.1: single candidate)
7. Generate starting knowledge
├── For each character:
│ ├── Background knowledge from character definition
│ ├── Institutional knowledge from character role
│ ├── Social knowledge from template assignments
│ │ └── Character knows NPCs in their home template (KnowsOf)
│ │ └── Character knows FRIEND deeply (KnowsDetails)
│ └── Investigation knowledge (detective only: assignment briefing)
└── Validate: no character knows things they shouldn't
8. Assemble SeedConfig and return
6.2 Determinism guarantees
The seeder MUST produce identical output given identical inputs. This requires:
- Platform-independent RNG: ChaCha20 (not platform
thread_rng). Same byte stream on Linux, macOS, Windows. - Sorted iteration: All collections iterated in sorted order (BTreeMap handles this; Vec collections must be pre-sorted or iteration order must be specified).
- No floating-point in selection logic: Weights are integers. Selection uses integer arithmetic only.
- Content version pinning: The
content_versionfield detects if content changed between save and load. - RNG consumption order: The RNG advances in a fixed order regardless of pool sizes or skip conditions. This prevents "butterfly effect" where adding a candidate to one pool shifts all subsequent draws.
6.3 RNG consumption protocol
To prevent butterfly effects when pools change size between content versions:
For each pool (sorted alphabetically):
1. Consume exactly `max_candidates` RNG values (configurable per pool, default 8)
2. Use the first consumed value to select from actual candidates
3. Remaining consumed values are discarded
This means adding a candidate to pool A doesn't shift the RNG
sequence for pool B.
v0.1 simplification: With single-candidate pools, all draws are deterministic regardless. The protocol matters for v0.2+ when pools have real variation.
7. Integration Points
7.1 Who writes seed configs
| Component | Responsibility |
|---|---|
Seeder system (Rust, server/src/simulation/seeder.rs) |
Generates SeedConfig from seed + content |
| Save system (Rust) | Serializes SeedConfig into save files |
| Content loader (Rust, ticket #408) | Reads pool definitions from content YAML |
Debug CLI (Rust, --dump-seed flag) |
Writes debug RON dump |
7.2 Who reads seed configs
| Component | What it reads | Why |
|---|---|---|
| Entity spawner | Pool draws, template assignments | Creates ECS entities with correct components |
| Knowledge initializer | Starting knowledge | Populates KnowledgeGraph components (D-041) |
| Storyteller | Triangle config, entanglement | Knows which triangles to manage, which NPCs are intrigue-relevant |
| Save/load | Full SeedConfig | Restores game state from save |
| Replay system | SeedMeta | Verifies deterministic reproduction |
| Line previewer (ticket #407) | Full SeedConfig | Simulates line selection for a given seed |
7.3 Content author workflow
- Author writes NPC profiles, templates, triangles, pools in
content/ - Author runs
make validate-contentto check schemas - Author runs line previewer (
tooling/line-previewer) with a test seed to verify line selection - Author can inspect seed config via
--dump-seedto verify NPC assignments match expectations
8. v0.1 vs v0.2+ Scope
| Aspect | v0.1 | v0.2+ |
|---|---|---|
| Pool candidates | 1 per pool (deterministic) | N per pool (randomized) |
| Entanglement ratio | Fixed at ~29/47/24 | Variable per seed (25-35 / 40-55 / 15-25) |
| Contraband types | 1 (lattice components) | 3+ with different moral valences |
| Triangle initial states | All default | Seed-randomized starting positions |
| Module attachment | Fixed known/stranger split | Variable per seed (D-029: 60-70/30-40) |
| Starting knowledge | Hardcoded per character | Generated from character definition + pool draws |
| Cross-district pools | N/A (one district) | NPCs can be drawn across district boundaries |
| Template variants | Fixed templates | Template variants (same social site, different layouts) |
9. Validation Rules
The seeder validates invariants after generation:
fn validate(config: &SeedConfig, content: &LoadedContent) -> Result<(), SeedError> {
// 1. Every NPC has exactly one owning template
assert_single_ownership(&config.template_assignments)?;
// 2. FRIEND NPCs are assigned to the correct character
assert_friend_bonds(&config.pool_draws)?;
// 3. Triangle members match NPC assignments
assert_triangle_consistency(&config.triangle_config, &config.template_assignments)?;
// 4. Entanglement ratio is within acceptable range
assert_entanglement_bounds(&config.entanglement)?;
// 5. No circular ownership in reference links
assert_no_circular_refs(&config.template_assignments)?;
// 6. Starting knowledge respects information boundaries
// (smuggler doesn't know detective-only facts, etc.)
assert_knowledge_boundaries(&config.starting_knowledge, content)?;
// 7. All NPC IDs reference valid content profiles
assert_npc_ids_valid(&config, content)?;
// 8. Content version matches
assert_content_version(&config.meta, content)?;
Ok(())
}
10. Serialization Format
The SeedConfig serializes to RON for save files and debug dumps:
SeedConfig(
meta: SeedMeta(
seed: 42,
content_version: "0.1.0",
schema_version: 1,
generated_at: "2026-02-13T10:00:00Z",
),
character_selection: CharacterSelection(
player_character: "smuggler",
available_characters: [
CharacterDefinition(
id: "smuggler",
display_name: "Dock Worker",
home_template: "logistics-hub",
default_access: [Public, Insider],
),
CharacterDefinition(
id: "detective",
display_name: "Commission Investigator",
home_template: "logistics-hub",
default_access: [Public, Authority],
),
],
),
pool_draws: {
"transit:compromised_inspector": PoolDraw(
pool_id: "transit:compromised_inspector",
selected: "npc:torek-lintar",
candidates: ["npc:torek-lintar"],
selected_index: 0,
),
"transit:friend_detective": PoolDraw(
pool_id: "transit:friend_detective",
selected: "npc:sera-venn",
candidates: ["npc:sera-venn"],
selected_index: 0,
),
"transit:friend_smuggler": PoolDraw(
pool_id: "transit:friend_smuggler",
selected: "npc:kael-davan",
candidates: ["npc:kael-davan"],
selected_index: 0,
),
"transit:primary_contraband": PoolDraw(
pool_id: "transit:primary_contraband",
selected: "contraband:lattice-components",
candidates: ["contraband:lattice-components"],
selected_index: 0,
),
},
// ... (template_assignments, triangle_config, etc.)
)
11. Crate Dependencies
# In server/Cargo.toml or wherever the seeder lives
[dependencies]
serde = { version = "1", features = ["derive"] }
ron = "0.8" # RON serialization
rand = "0.8" # RNG traits
rand_chacha = "0.3" # Platform-independent deterministic RNG
No new dependencies beyond what the server already uses. rand and serde are existing deps. ron and rand_chacha are standard Rust ecosystem crates with no transitive bloat.
12. Open Questions
| # | Question | Impact | Suggested Resolution |
|---|---|---|---|
| 1 | Should the seed config include routine schedule overrides, or should routines be purely content-driven? | If the seeder can modify routines (e.g., FRIEND's deviation schedule depends on which FRIEND was drawn), routines become partially generated. If not, content must pre-author all variants. | Content-driven for v0.1 (one candidate = one routine). v0.2+: seeder generates routine deviations based on drawn contradiction arcs. |
| 2 | Should max_candidates (RNG consumption budget per pool) be configurable per pool or global? |
Per-pool allows fine-grained control but complicates the protocol. Global is simpler but wastes RNG state for small pools. | Global default of 8, with per-pool override in pool definition YAML. 8 handles up to 8 candidates without waste, which covers v0.2 comfortably. |
| 3 | How does the seed config interact with the storyteller's module activation system (D-023 Tier 1)? | The storyteller needs to know which Tier 1 modules are available (not yet activated) vs activated vs completed. Does the seed config pre-select available modules, or does the storyteller draw from its own pool at runtime? | Seed config pre-selects available modules from a module pool. Storyteller activates them based on player proximity. This keeps all randomization in the seeder for determinism. |
| 4 | Should the debug dump include a human-readable narrative summary (e.g., "Kael Davan is the smuggler's FRIEND, smuggling lattice components...")? | Useful for content authors, trivial to generate, but adds code surface. | Yes. Add a summary: String field to SeedMeta generated at dump time. Not serialized into save files. |
13. Implementation Sequence
This is a design document. Implementation is Sprint 5+. Suggested build order:
- Rust types — Define
SeedConfigand all sub-structs with serde derives. ~1 day. - Pool definition schema —
pools.schema.jsonincontent/_schema/. ~0.5 day. - Pool loader — Extend content loader to parse pools.yaml. ~1 day.
- Seeder system —
server/src/simulation/seeder.rs. Core generation logic. ~2 days. - Validation — Invariant checks from section 9. ~1 day.
- Save integration — Serialize/deserialize SeedConfig in save system. ~0.5 day.
- Debug dump —
--dump-seedCLI flag. ~0.5 day. - Starting knowledge generation — Section 4.8 logic. ~1 day.
Total estimate: ~7-8 developer-days. Parallelizable with content authoring work.
Design document for ticket #394. Implementation deferred to Sprint 5+. Cross-references: D-010, D-024, D-025, D-027, D-029, D-034, D-035, D-036, D-037, D-041.