# Conflicts: # CHANGELOG.md # content/_meta/README.md # content/_meta/npc-authoring-style-guide.md # wiki/_templates/cultural-group.md # wiki/_templates/institution.md # wiki/_templates/star-system.md # wiki/characters/devra.md # wiki/characters/drin.md # wiki/characters/harek.md # wiki/characters/lera-sessik.md # wiki/characters/maret-korr.md # wiki/characters/naia-tamm.md # wiki/characters/nils-davan.md # wiki/characters/pell.md # wiki/characters/renn.md # wiki/characters/resha.md # wiki/characters/sabel.md # wiki/characters/sera-venn.md # wiki/characters/torek-lintar.md # wiki/characters/voss.md # wiki/star-systems/krenn/index.md
828 lines
31 KiB
Markdown
828 lines
31 KiB
Markdown
---
|
|
title: "Seed Configuration Schema — Design Document"
|
|
description: "Schema and generation algorithm for deterministic world instantiation from seed value — pool draws, template assignments, triangles, entanglement, contraband, and starting knowledge"
|
|
type: architecture
|
|
status: draft
|
|
ticket: "#394"
|
|
decision_refs: [D-010, D-024, D-025, D-027, D-029, D-034, D-035, D-036, D-037, D-041]
|
|
author: "Tyre"
|
|
created: 2026-02-13
|
|
updated: 2026-02-13
|
|
---
|
|
|
|
# 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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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.
|
|
|
|
```rust
|
|
/// 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:**
|
|
|
|
```rust
|
|
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.
|
|
|
|
```rust
|
|
/// 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:**
|
|
|
|
```yaml
|
|
# 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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
/// 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)
|
|
|
|
```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:
|
|
|
|
1. **Platform-independent RNG:** ChaCha20 (not platform `thread_rng`). Same byte stream on Linux, macOS, Windows.
|
|
2. **Sorted iteration:** All collections iterated in sorted order (BTreeMap handles this; Vec collections must be pre-sorted or iteration order must be specified).
|
|
3. **No floating-point in selection logic:** Weights are integers. Selection uses integer arithmetic only.
|
|
4. **Content version pinning:** The `content_version` field detects if content changed between save and load.
|
|
5. **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
|
|
|
|
1. Author writes NPC profiles, templates, triangles, pools in `content/`
|
|
2. Author runs `make validate-content` to check schemas
|
|
3. Author runs line previewer (`tooling/line-previewer`) with a test seed to verify line selection
|
|
4. Author can inspect seed config via `--dump-seed` to 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:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```ron
|
|
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
|
|
|
|
```toml
|
|
# 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:
|
|
|
|
1. **Rust types** — Define `SeedConfig` and all sub-structs with serde derives. ~1 day.
|
|
2. **Pool definition schema** — `pools.schema.json` in `content/_schema/`. ~0.5 day.
|
|
3. **Pool loader** — Extend content loader to parse pools.yaml. ~1 day.
|
|
4. **Seeder system** — `server/src/simulation/seeder.rs`. Core generation logic. ~2 days.
|
|
5. **Validation** — Invariant checks from section 9. ~1 day.
|
|
6. **Save integration** — Serialize/deserialize SeedConfig in save system. ~0.5 day.
|
|
7. **Debug dump** — `--dump-seed` CLI flag. ~0.5 day.
|
|
8. **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.*
|