Design document defining what the randomizer produces at game-start: FRIEND selections, pool draws, template assignments, triangle config, entanglement config, contraband selection, starting knowledge. Includes ChaCha20 RNG protocol, validation rules, and implementation roadmap. 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/krenn/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.