Files
settled-reach/docs/workshops/wiki-review/round3-tyre.md
T
jpmschweitzerandClaude Opus 4.6 0100b33635 docs(workshops): archive wiki review workshop (4 rounds + lead interview)
Long-term content strategy workshop: 300-world generator model,
cultural ingredients menu, three-system NPC architecture (9 patterns
x 6 motivations), Sacred/Profane/Middle Kingdom framework. 9 agents
across 4 rounds plus lead interview establishing the production path
from hand-authored Sova to generated 300 worlds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:18:56 +01:00

45 KiB
Raw Blame History

Round 3 — Tyre (Technical Architect)

Long-term content strategy from the systems architecture perspective. Thinking about the full game: multiple star systems, dozens of districts, hundreds of NPCs, mod ecosystem, years of content production.

No v0.1 scoping. No tickets. No sprint timelines. Just: what does the engine need to be?


1. Is Complicity the Right Theme for the Whole Game?

cracks knuckles

Let me be honest about what this question means technically: the engine doesn't care about theme. The engine cares about mechanical patterns. The question for me isn't "is complicity universal" — it's "does the mechanical vocabulary we've built for complicity generalize to other moral textures, or does each theme need its own engine features?"

What the engine actually sees

The engine doesn't know about "complicity." It knows about:

Mechanical Pattern Sova Implementation Generalizable?
Moral arc phases (state progression) Comfort → Doubt → Reckoning → Compromise Yes — any moral journey follows a phase progression. The phase names are content, not code.
Monologue triggers (moral_pang, loyalty_test) Smuggler-specific guilt triggers Partially — the trigger types are engine features, but new themes might need new trigger types. A "survival" theme might need resource_scarcity_observation or triage_decision.
Cultural gate modifier (Gore's proposal) Krenn privacy norms reduce disclosure Yes — every culture has social norms that modify disclosure. The modifier system is general. The RULES per culture are content.
Knowledge confidence progression Suspects → KnowsOf → KnowsDetails → Direct Yes — this is epistemology, not theme. Every information-asymmetric situation uses this.
Contradiction detection FRIEND's hidden behavior conflicts with presented behavior Yes — cognitive dissonance is universal. THE FRIEND pattern works for any relationship where someone conceals something from someone they care about.
THE MIRROR pattern Hael's transparency amplifies everyone's dishonesty Partially — the "honest character in a dishonest world" works for complicity/deception themes. A survival-themed district might need THE MIRROR to be "the person who refuses to compromise even when everyone else has." Same mechanical pattern (character without a secret who makes others uncomfortable), different moral content.

My architectural answer

Build a theme-agnostic moral engine with a per-district thematic vocabulary layer.

The engine provides:

  • Phased moral arc system (configurable phases per district or per NPC)
  • Extensible monologue trigger registry (core triggers + district-registered custom triggers)
  • Cultural modifier framework (per-region norms that affect dialogue selection)
  • Contradiction detection (general — any KnowledgeState can enter Contradicted)
  • Pool-based character patterns (FRIEND, MIRROR, future patterns all use pools)

The content provides:

  • Phase names and transition conditions per district's moral texture
  • Custom trigger definitions that map to the district's theme
  • Cultural rules per region
  • Specific contradiction content per NPC

This means: complicity is the right theme for Sova, and probably the right default theme (it's what the game is most deeply about). But the engine shouldn't assume it. Districts in other systems might explore loyalty, survival, identity, or institutional rot. The engine treats all of these as "moral arc with phases + triggers + cultural context." The thematic vocabulary is data, not code.

The scaling implication

SI asked the right question in Round 3: does thematic variation change the pipeline? Yes, modestly. Each new thematic texture costs ~2-3 days to define (phase vocabulary, trigger set, monologue patterns). But those definitions are reusable across all districts that share the texture. A "loyalty" texture defined once applies to every loyalty-themed district.

Tier of difficulty: Easy. The architecture we've designed is already theme-agnostic. The moral arc system is a state machine with configurable states. Monologue triggers are an extensible registry. No engine changes needed to support multiple themes — just content vocabulary definition.


2. Cultural Groups at Scale

15-25 cultural groups across the Settled Reach. Here's what the engine needs to know about culture, and what it doesn't.

What culture IS in the engine

Culture is reference data that modifies entity behavior at creation and dialogue selection at runtime. It is NOT simulation state. No cultural simulation runs per-tick. Culture is:

  1. A modifier set applied when NPCs are instantiated (naming patterns, greeting conventions, default disclosure norms, default trust baseline for outsiders)
  2. A dialogue filter parameter that affects line selection (Gore's cultural_gate modifier)
  3. A content authoring guide that informs how writers fill routine schedules, personality traits, and monologue pools

The data model for culture at scale

# global/regions/krenn-system.yaml
culture:
  id: krenn
  heritage_blend: ["finnish", "estonian", "latvian"]
  drift_years: 180

  # Engine-readable modifiers
  modifiers:
    outsider_trust_baseline: -2        # Outsiders start with trust penalty
    disclosure_to_authority: 0.4       # 40% disclosure rate to authority figures
    disclosure_to_insider: 0.9         # 90% disclosure rate to cultural insiders
    greeting_initiative: low           # NPCs don't initiate conversation with strangers

  # Naming generation
  naming:
    given_name_pool: ["Kael", "Sera", "Voss", "Lera", "Torek", ...]
    family_name_pool: ["Davan", "Venn", "Sessik", "Lintar", ...]
    family_name_usage: 0.29            # 29% of NPCs use family names socially
    address_convention: first_name     # Social context uses first names

  # Vocabulary (for writers, not engine)
  vocabulary:
    greetings: ["hüva", "tere"]
    food: ["leib", "kalaa", "supp"]
    drinks: ["kuum", "grain spirit"]
    slang:
      - word: "vakt"
        meaning: "lookout shift"
        connotation: "criminalized by ring usage"

  # Social norms (engine-readable as dialogue modifiers)
  social_norms:
    - norm: privacy_default
      effect: "NPCs do not discuss others' personal business"
      modifier: topic_block_personal_third_party
    - norm: labor_solidarity
      effect: "Workers cover for each other"
      modifier: reduce_disclosure_about_coworker_absences
    - norm: earned_trust
      effect: "No credential shortcut — trust through time and shared labor"
      modifier: ignore_authority_trust_bonus

Cross-cultural dynamics: the hard problem

Single-culture districts are straightforward. But the full game will have:

  • Districts where two cultures mix (trade hubs, diplomatic stations)
  • NPCs who've migrated from one cultural region to another
  • Cultural drift within a single region over time

The engine needs a cultural distance model:

cultural_distance(observer, subject) → modifier_strength

If both are Krenn, full cultural norms apply. If the observer is Krenn and the subject is from Vael System, some norms apply at reduced strength (shared labor solidarity if they work together, but outsider trust baseline still applies). If both are from different cultures entirely, minimal cultural modifier — fall back to personality-driven behavior.

Implementation approach: Each NPC has a culture_id field. The dialogue selection system looks up the cultural distance between the speaking NPC and the listener. Cultural modifiers apply at distance_modifier * full_strength. For v0.1, every NPC in Sova is Krenn and every outsider (detective) gets full outsider penalty. For full game, the distance calculation is a simple lookup table:

Observer Culture Subject Culture Distance Modifier Strength
Same Same 0 1.0
Same region Same region 1 0.7
Same system Different region 2 0.4
Different system Different system 3 0.2

~25 cultures × 25 cultures = 625 entries. But most are derivable from the 4-level hierarchy above. Only exceptions need explicit overrides.

Tier of difficulty: Moderate. The cultural modifier system is a data-driven parameter set. The cross-cultural distance model adds a lookup step to dialogue selection. The real complexity is in content — defining 25 cultures with enough depth to be mechanically meaningful. But that's Miri's job, not engine work.

Memory and performance at scale

25 cultural definitions × ~5KB structured YAML each = ~125KB total. Negligible. Cultural data loads once at game start and lives in a Cultures resource. NPC culture_id is a u8 (256 possible cultures, we'll use ~25). Per-NPC memory cost: 1 byte. The dialogue selection system does one hash lookup per conversation to get the cultural modifier set. Sub-microsecond.


3. Content Patterns That Scale

The distinction that matters: engine patterns (code, universal, built once) vs content patterns (data templates, per-district, authored many times).

Engine patterns — build once, use forever

These are the core systems. Each is a piece of the simulation server that processes content data:

Engine Pattern What It Does Scale Factor
Pool Selection Weighted random draw from candidate lists at seed time O(pools × candidates). Cheap — runs once at game start. Handles FRIEND, role slots, contraband, entanglement, everything.
Schedule System Per-tick NPC location resolution from routine YAML O(active NPCs). Already budgeted in D-026. 80 active NPCs × schedule lookup = trivial.
Dialogue Selection Tagged line pool filtering (access tier, trust, situation, mood, topic, cultural gate) O(eligible lines). Per-conversation. Pool sizes: ~50-200 lines per location per context. Filter chain is fast — 6 filters on 200 lines in <1ms.
Monologue Trigger Event-driven check: does current game state match any monologue trigger prerequisites? O(triggers × prerequisites). Per-tick for observation triggers, per-event for others. Budget: ~50-100 active triggers per district.
Knowledge Update Event queue drain → BTreeMap insert/update O(events per tick). Already budgeted at <3ms. Scales linearly with active NPCs.
Contradiction Detection Cross-entry comparison within a single KnowledgeGraph O(knowledge entries²) worst case, but only fires on new Direct observations. Practical: <10 comparisons per trigger.
Moral Arc State Machine Phase transition checker — does current state + trigger condition → advance phase? O(active arc NPCs × phase conditions). Runs per-event, not per-tick. Budget: 5-10 active arcs per district.
Storyteller Pacing controller — selects which events to trigger based on player engagement, time, and arc progress One system, runs once per game-minute (every 10 ticks). Evaluates ~20-30 potential events, selects 0-2 per minute.
Content Loader YAML parsing + schema validation + overlay merging Runs at game start and district transitions. Not performance-critical. Can take 1-5 seconds per district load.

That's ~9 engine patterns. I'd estimate we'll add 2-3 more as the game evolves (e.g., economy simulation, faction reputation system, travel/gate system). Call it 12-15 total engine patterns for the full game.

The critical insight: every new district uses these same 9+ patterns. Adding a district doesn't add engine code. It adds content data that the existing patterns process. This is what D-023 (three-tier content model) was designed to achieve.

Content patterns — define template, instantiate per district

These are the reusable structures that content authors fill in:

Content Pattern Template Definition Per-District Cost Total for 20 Districts
NPC Profile (Tier 1 FRIEND) ~3 pages, 70-100 lines 15-22 hours 2-4 per district × 20 = 40-80 NPCs
NPC Profile (Tier 1 MIRROR) ~2 pages, 50-70 lines 10-15 hours 1 per district × 20 = 20 NPCs
NPC Profile (Tier 2) ~1 page, 40-60 lines + 4x generation 4-6 hours 8-12 per district × 20 = 160-240 NPCs
NPC Profile (Tier 3) Template-generated, ~0.5 pages 0.5-1 hour (review only) 5-10 per district × 20 = 100-200 NPCs
Social Site Template Role slots, triangle constraints, pool refs 2-3 hours 3-5 per district × 20 = 60-100 templates
Triangle Definition 3 NPCs, fork conditions, cascade effects 1-2 hours 4-6 per district × 20 = 80-120 triangles
Pool Definition Candidate lists with weights 30 min 3-8 per district × 20 = 60-160 pools
Contradiction Arc 5-phase definition with trigger conditions 2-3 hours (part of FRIEND) 2-4 per district × 20 = 40-80 arcs
District Metadata Setting, locations, faction presence, ambient config 2-4 hours 1 per district × 20 = 20

What's genuinely local vs what's universal

Some things I thought were engine patterns turn out to be content, and vice versa:

Looks like engine, is actually content:

  • Storyteller decision nodes (Gestalt's volume escalation). Each is a custom fork with custom effects. The engine provides the decision-node FRAMEWORK. The specific decisions are authored YAML.
  • News ticker headline selection. Engine provides the selection mechanism. Headlines are content data.
  • Environmental text. Engine renders it. Content fills it.

Looks like content, is actually engine:

  • THE FRIEND pattern (pool selection + contradiction arc activation + tell progression). This is an engine pattern with content filling the slots. Critically: the PATTERN is code. Each specific FRIEND is data.
  • THE MIRROR pattern. Same — engine pattern, content instances.
  • Cultural gate modifier. The modifier system is engine. The specific norms per culture are content.

The pattern that worries me

Storyteller decision nodes. Gestalt's volume escalation decision is beautifully designed — but it's a fully custom YAML structure with custom effect mappings. At scale, each district needs 5-10 of these storyteller-triggered decision points. 20 districts = 100-200 decision nodes.

If each decision node is a bespoke YAML structure, we have a maintenance nightmare. The schema validation can check structure, but the EFFECTS (attribute changes, triangle tension adjustments, investigation signal modifications) are essentially a mini scripting language embedded in YAML.

My proposal: a decision node DSL. A small, constrained language for expressing decision effects:

decision_node:
  id: volume_escalation
  trigger: storyteller_mid_game
  options:
    - id: side_with_nils
      effects:
        - set_attribute: { entity: voss, key: trust_level, value: "resentful" }
        - set_attribute: { entity: voss, key: exposure_risk, value: "escalating" }
        - add_flag: { entity: maret_korr, flag: noticed_schedule_gap }
        - adjust_tension: { triangle: worried_knowledge, delta: +2 }
        - emit_signal: { signal: schedule_anomaly_visible }

The DSL is a fixed set of operations: set_attribute, add_flag, remove_flag, adjust_tension, emit_signal, start_arc, advance_phase. Maybe 10-15 operations total. The validator can check that every effect uses a known operation with valid parameters. Content authors compose effects from this vocabulary.

Tier of difficulty: Moderate. Designing the DSL is a week of architecture. Implementing the effect executor is a sprint. But it prevents 200 decision nodes from becoming 200 custom code paths.


4. Content Directory as a Platform

This is the section I care most about. The content directory isn't just a file layout — it's the platform that the entire content pipeline runs on. Let me map what "platform" means technically.

Platform requirements for the full game

Requirement What It Means Technical Approach
Lazy loading Only load the player's current district + queued adjacent districts. Don't load all 20+ districts at once. Content loader uses a manifest graph. District YAML declares dependencies. Loader resolves the dependency tree and loads only what's needed.
Cross-district references NPC in district A mentions faction headquartered in district B. Canonical_id krenn.sova.transit.npc.kael-davan resolves across any loaded district. The canonical_id resolver is a global registry populated at manifest-load time (just IDs and paths, not full content). Full content loads on demand.
Hot-reloading Dev-only: change a monologue line, see it in-game without restart. notify crate file watcher on content/ directory. On change: re-validate changed file → re-parse → hot-swap into running ECS world. Line pools and schedule data are hot-swappable. NPC creation requires restart.
Validation pipeline make validate-content catches errors before they reach the engine. JSON Schema validation (structural) + cross-reference resolver (semantic) + tier-conditional rules + custom DSL checks. CI runs this on every content PR.
DLC packaging A DLC is a content pack: new districts + new global entries + extensions to existing districts. DLC is a content directory with the same structure as base. _meta/manifest.yaml declares: base version dependency, ADD/REPLACE/MERGE operations, new district list. Load order: base → DLC1 → DLC2 → mods.
Mod overlay Third-party content uses ADD/REPLACE/MERGE semantics. Already designed in Round 2. The loader processes layers in load-order sequence. Each layer can add files, replace entity definitions, or merge pool/list content.
Generation tooling Tier 3 NPC generator, line expansion tool, skeleton generator. These tools READ templates + cultural data, WRITE NPC YAML files to the content directory. They're offline tools, not runtime. Output goes through the same validation pipeline as hand-authored content.
Multiple star systems Content is organized spatially: system → station → district. But the filesystem doesn't need to mirror this — canonical_id handles it. Flat districts/ directory with canonical_id carrying the full spatial address. A global/galaxy/systems.yaml provides the spatial index.

The content loader architecture

This is the most important subsystem I haven't fully specified yet. Here's the full picture:

Content Loader Pipeline:

1. MANIFEST PHASE (game startup, ~100ms)
   Read all _meta/manifest.yaml files (base + DLC + mods)
   Build dependency graph
   Determine load order
   Build canonical_id → file_path index (IDs only, not content)

2. GLOBAL PHASE (game startup, ~500ms)
   Load global/ directory: factions, technology, contraband, knowledge, enums, regions
   These are always in memory — they're shared vocabulary

3. SEED PHASE (new game / load game, ~200ms)
   Read seed configuration
   Execute pool selections (FRIEND, roles, contraband, entanglement)
   Produce seed-state record

4. DISTRICT PHASE (on district entry, ~1-5 seconds)
   Load target district: NPCs, locations, templates, lines, facts
   Apply overlay stack: base content → DLC modifications → mod modifications
   Validate loaded content against schemas
   Create ECS entities from loaded data
   Cross-reference check: all canonical_id references resolve

5. ADJACENT PHASE (background, ~2-10 seconds)
   Pre-load adjacent districts (connected by gate, one travel hop away)
   Parse and validate but don't create ECS entities yet
   Ready for instant transition when player travels

6. HOT-RELOAD PHASE (dev-only, on file change)
   Re-validate changed file
   If line pool / schedule / dialogue: hot-swap in running world
   If NPC definition: flag for full reload (too complex to hot-swap entity creation)
   If schema: re-validate all content against new schema

The manifest graph at scale

With 20+ districts, 2-3 DLCs, and mods, the manifest graph looks like:

base/
  _meta/manifest.yaml: { id: "base", version: "1.0" }
  global/                 ← always loaded
  districts/
    sova-transit/         ← load on demand
    vael-docks/           ← load on demand
    prime-central/        ← load on demand
    ...

dlc-outer-reach/
  _meta/manifest.yaml: {
    id: "outer-reach",
    version: "1.0",
    requires: "base >= 1.0",
    adds: ["districts/haven-drift", "districts/ember-station"],
    extends: {
      global: { factions: [ADD new-faction.yaml] },
      districts/sova-transit: { pools: [MERGE new-candidates.yaml] }
    }
  }

mod-jax/
  _meta/manifest.yaml: {
    id: "jax-the-veteran",
    version: "1.0",
    requires: "base >= 1.0",
    extends: {
      districts/sova-transit: {
        npcs: [ADD jax-korrenson.yaml],
        pools: [MERGE jax-pool-entries.yaml],
        lines/bar/dialogue: [MERGE jax-dialogue.yaml]
      }
    }
  }

The manifest declares WHAT it does. The loader EXECUTES the operations. This is the same pattern used by package managers (Cargo, npm) — declare dependencies and operations, resolve at load time.

Cross-district travel: the lazy loading challenge

When the player travels from Sova Transit to Vael Docks:

  1. Player approaches span gate → engine signals "travel imminent to vael-docks"
  2. Adjacent phase has already pre-loaded vael-docks content (parsed, validated, not yet ECS entities)
  3. Player enters gate → loading screen (or in-game gate transit animation)
  4. Engine tears down sova-transit ECS entities (but SAVES their state for return)
  5. Engine creates vael-docks ECS entities from pre-loaded content + seed-state
  6. Engine begins pre-loading districts adjacent to vael-docks
  7. Player arrives. Seamless.

State preservation on departure: When leaving a district, all active NPC states (knowledge graphs, relationship states, moral arc phases, schedule deviations) are serialized and stored. On return, the state restores. This is D-026's state-saved tier — the same mechanism handles "NPC too far from player to simulate" and "player left this district entirely."

The performance budget: District load creates ~20-50 ECS entities (NPCs) with components (schedule, knowledge graph, cultural modifiers, dialogue access). Entity creation: ~0.1ms each. Total: ~2-5ms for entities. The expensive part is YAML parsing and validation — but that's done in the adjacent-phase background pre-load. The actual transition should be <100ms of entity creation + state restoration.

Filesystem layout at full scale

content/
  _meta/
    manifest.yaml
    load-order.yaml           # base → DLC → mods

  _schema/
    npc.schema.yaml           # Handles all tiers via conditional validation
    location.schema.yaml
    faction.schema.yaml
    template.schema.yaml
    fact.schema.yaml
    dialogue.schema.yaml
    monologue.schema.yaml
    district.schema.yaml
    pool.schema.yaml
    decision-node.schema.yaml # Validates the DSL operations
    culture.schema.yaml       # Validates cultural modifier definitions

  global/
    galaxy/
      systems.yaml            # Star system index: coordinates, gate connections
      routes.yaml             # Travel graph: which gates connect to what
    factions/                  # ~10-15 major factions
    technology/               # ~10-20 technology entries
    contraband/               # ~5-10 contraband categories
    knowledge/
      facts.yaml              # Abstract/universal FactIds
      entity-attributes.yaml  # Canonical attribute vocabulary (19+ keys)
      relationship-states.yaml
    enums/                    # Canonical enum registries
      situations.yaml
      topics.yaml
      moods.yaml
      triggers.yaml           # Core + district-registerable custom triggers
      access-tiers.yaml
      activities.yaml
      moral-arc-phases.yaml   # Configurable per thematic texture
    regions/                  # 15-25 cultural groups
      krenn-system.yaml
      vael-system.yaml
      ...
    thematic-textures/        # 4-6 moral textures (SI's palette)
      complicity.yaml         # Phase vocabulary, trigger set, monologue patterns
      loyalty.yaml
      survival.yaml
      identity.yaml

  districts/
    sova-transit/             # Krenn System, Station Sova
      district.yaml           # Metadata: system, station, culture_id, theme_texture
      facts.yaml              # District-specific FactIds (Gestalt's split)
      pools.yaml              # All pool definitions for seed-time selection
      npcs/                   # 15-25 NPCs per district
      locations/              # 3-8 locations per district
      templates/              # 3-5 social site templates
      triangles/              # 4-6 triangle definitions
      decisions/              # Storyteller decision nodes (DSL)
      lines/                  # Dialogue + monologue pools per location
        terminal/
        bar/
        corridor/
    vael-docks/               # Vael System
    prime-central/            # Krenn Prime (surface city)
    ...                       # 20+ districts total

At 20 districts, this is ~1000-2000 YAML files total. Each is small (1-10KB). Total content on disk: ~5-20MB. Negligible. The constraint is authoring time, never storage.

Content versioning for live service

If the game has DLC and mod support, content WILL change post-release. The canonical_id system handles this:

  • A canonical_id is permanent. Once krenn.sova.transit.npc.kael-davan exists, it exists forever.
  • Content behind a canonical_id can change (REPLACE operation), but the ID persists.
  • Save games reference canonical_ids, not file paths. If a DLC changes Kael's dialogue, existing saves still reference kael-davan and get the updated content.
  • If a mod is removed, the loader detects missing canonical_ids referenced in the save game and flags them. The game can degrade gracefully (NPC exists but has fallback content) or warn the player.

Tier of difficulty: Challenging but doable. The content loader is a significant subsystem — I'd estimate 2-3 sprints to build the full pipeline (manifest resolution, lazy loading, overlay merging, hot-reload). But it's well-understood engineering. Package managers solved this problem decades ago. We're applying the same principles to game content.


5. Randomization Philosophy

Beyond Sova's axes. What's the full-game randomization architecture?

The three scales of randomization

Scale 1: Per-District (what Sova already does)

Pool-based selection within a single district. FRIEND identity, role assignments, social site population, entanglement map, secondary contraband. Each district is an independent randomization domain.

This scales linearly. 20 districts × 5-10 pools each = 100-200 independent pool draws. The seed determines all of them deterministically. Fast, simple, proven.

Scale 2: Cross-District (new for multi-district game)

When the game spans multiple districts, some randomization must be COORDINATED:

Cross-District Axis Why It Can't Be Independent Constraint
Faction dominance If the Lattice Commission is strong in Sova, that affects their presence in adjacent stations Conservation of faction power across the system
NPC migration An NPC who left Sova for Vael must exist in Vael and NOT in Sova Entity uniqueness across districts
Investigation threads Evidence found in Sova points to Vael. The target must actually exist in Vael. Cross-district reference integrity
Economic conditions System-wide boom/bust should be coherent, not random per district System-level economic state → per-district modifier

This requires a two-phase randomizer:

Phase 1: System-level draws
  - Faction power distribution across all systems
  - Economic condition per system
  - Major event seeds (which crises are active this playthrough)

Phase 2: District-level draws (constrained by Phase 1)
  - FRIEND pools, role slots, entanglement maps
  - Constrained by: faction presence (Phase 1), NPC availability, cross-district references

The pool system handles both phases — Phase 1 pools are in global/, Phase 2 pools are in districts/. The constraint is: Phase 2 draws must satisfy Phase 1 constraints. This is constraint-satisfaction, not simple random drawing.

Scale 3: Temporal Variation (the living world)

The hardest form of randomization: things that change during play.

  • A faction loses power after a player action in one district → effects visible in adjacent districts
  • An NPC's arc resolves → consequences propagate to cross-district contacts
  • The storyteller triggers a system-wide event (gate malfunction, Commission crackdown) → every active district responds

This isn't pool selection anymore. This is event propagation across the district graph. The simulation needs:

Event: { type: faction_power_shift, faction: lattice-commission, delta: -1, origin: sova-transit }
Propagation: adjacent districts receive the event at the next tick boundary
Effect: per-district faction modifier adjusts, NPC mood/behavior shifts

Tier of difficulty: Scale 1 is Easy (done). Scale 2 is Moderate (constraint-satisfaction at seed time). Scale 3 is Hard (runtime event propagation across a district graph). Build them in order. Scale 3 is a v1.0 feature, not v0.2.

The anti-metagaming architecture

Nigel's core concern: "Two friends play the game. They compare notes. They played different games." At full scale, the metagaming surface grows:

What Players Might Metagame Anti-Metagaming Approach
FRIEND identity Pool with 3-4 candidates per character per district
Evidence locations Multiple possible evidence placements per FactId progression
Compromised NPC identity Role-slot randomization (who fills the "compromised" slot)
Investigation path Multiple valid paths to the same discovery (not A→B→C, but A→{B,C,D}→E)
NPC schedules Minor time variance per seed (±15 minutes on routine transitions)
Cultural tells Per-seed social weight on vocabulary (Nigel's "vakt" variation)

The key insight: don't randomize everything. Randomize the things that matter for surprise, keep the things that matter for coherence.

Fixed per district (never randomized):

  • Cultural identity and norms
  • Primary contraband type
  • Physical layout (locations, spatial relationships)
  • Faction presence (which factions, not how strong)
  • Core social site structure (the bar exists, has an owner, has regulars)

Variable per seed (always randomized):

  • WHO fills which role slots
  • WHO is the FRIEND
  • WHERE evidence appears
  • WHAT secondary complications arise
  • WHEN the storyteller triggers escalation events

This gives consistent spatial memory ("I know where the bar is") with inconsistent social memory ("I don't know who's compromised"). Players can learn the world but not solve it.


6. PC-as-NPC at Full Scale

v0.1 has 2 PC archetypes. The full game might have 4-6. Here's what this means architecturally.

The PC archetype as engine pattern

A PC archetype is:

  1. A starting knowledge graph (what you know at game start)
  2. A cultural identity (which community you belong to)
  3. A lattice tier (which perception modes are available — D-017)
  4. An access tier map (who trusts you, who doesn't)
  5. A monologue voice (separate line pools per D-032)
  6. A moral arc template (which thematic journey you're on)
  7. An NPC-mode behavior AI (what you do when you're not player-controlled)

Items 1-6 are content data. Item 7 is the engine problem.

NPC-mode behavior AI

When the player plays archetype A, archetype B exists as an NPC. But archetype B isn't just walking a routine — they're an ACTIVE CHARACTER doing their job:

Archetype NPC-Mode Behavior Complexity
Detective Follows investigation routine: arrives, interviews, observes, takes notes. Progresses through the investigation arc at storyteller-controlled pace. High — the detective NPC needs to "investigate" convincingly. Not just walk around, but approach NPCs, trigger investigation dialogue, react to evidence.
Smuggler Works shifts, runs operations, socializes at bar. Maintains cover. Ring operations triggered by storyteller. Moderate — follows routine with deviation triggers. The ring operations are storyteller-controlled, not autonomous.
Senate aide (hypothetical v0.3 archetype) Political navigation: attends meetings, lobbies, collects favors. High — political behavior AI is complex.
Guardian operative (hypothetical v0.4 archetype) Surveillance and counter-surveillance. Watches, follows, reports. Moderate — pattern-following behavior with perception system integration.

The behavior AI architecture

I'd propose a behavior tree system for NPC-mode PCs. Not a full planning AI — that's overkill and non-deterministic. A behavior tree is:

  1. Deterministic (D-010 principle 4)
  2. Configurable via content data (behavior tree definitions in YAML)
  3. Observable by players (the NPC does visible things, not abstract "investigating")
  4. Scalable (behavior trees are well-understood, performant, widely documented)

Each PC archetype gets a behavior tree YAML:

# global/archetypes/detective-npc-behavior.yaml
behavior_tree:
  root: sequence
  children:
    - selector:
        - condition: is_work_hours
          action: investigate
          subtree:
            - go_to: investigation_target_location
            - interact_with: investigation_target_npc
            - observe_for: 5_minutes
            - take_notes  # visible action, triggers monologue for watching PC
        - condition: is_social_hours
          action: socialize
          subtree:
            - go_to: bar
            - interact_with: known_contact  # usually Sera if she's FRIEND
            - order_drink
        - action: follow_routine  # fallback to base schedule

The storyteller controls WHEN the detective NPC escalates their investigation. The behavior tree controls HOW the detective behaves during each phase.

PC archetype scaling costs

Component Per Archetype (one-time) Per District (per archetype)
Voice card + moral arc definition 8-12 hours 0
Behavior tree definition 4-8 hours 2-3 hours adaptation
Monologue pool per district 0 10-15 hours
Starting knowledge per district 0 3-5 hours
Reverse knowledge entries from NPCs 0 2-4 hours
PC-as-NPC routine per district 0 1-2 hours
Total 12-20 hours 18-29 hours per district

For 4 archetypes across 20 districts:

  • One-time: 48-80 hours
  • Per-district: 72-116 hours × 20 = 1,440-2,320 hours
  • Grand total: ~1,500-2,400 hours of PC-related content

SI estimated ~800 hours for 2 archetypes across 20 districts. My number is higher because I'm including the behavior tree adaptation and the reverse knowledge entries, which SI's estimate didn't fully capture.

The critical dependency: behavior tree authoring tooling. Writing behavior trees in raw YAML is painful. We need a visual behavior tree editor — even a simple one (node graph in a web UI). This is a tooling investment that pays back across every archetype and every district.

Tier of difficulty: Hard. The behavior tree system itself is a 2-3 sprint investment. The content per archetype per district is the larger cost. I'd recommend maxing out at 4 archetypes for v1.0. Each additional archetype multiplies per-district authoring by ~20-30 hours.


7. Authoring Pipeline at Scale

300+ NPCs. 20+ districts. LLM-assisted generation. Here's the technical pipeline.

The authoring tiers, technically

Tier Authoring Method Engine Input Quality Gate
Tier 1 (FRIEND, MIRROR) 100% human-authored Handcrafted YAML, every line reviewed Human review + schema validation
Tier 2 (entangled + mundane triangles) Human-authored base (10 lines) + LLM expansion (40 lines) Authored YAML + generated supplement Schema validation + voice consistency check + human spot-review
Tier 3 (flat NPCs) Template-generated from cultural brief + district config Generated YAML from templates Schema validation + automated checks only
Environmental text Mixed — headlines authored, signs generated from templates Generated from district + culture data Schema validation + Mellanie review

The LLM generation pipeline

For Tier 2 line expansion (write 10, generate 40):

Input:
  - 10 authored dialogue lines (human-written, voice-perfect)
  - NPC profile YAML (personality, role, relationships)
  - Cultural brief (Krenn norms, vocabulary, speech patterns)
  - Tag taxonomy (D-035 tags for each generated line)
  - Voice card (character-specific writing guide)

Process:
  1. LLM generates 40 candidate lines, tagged, following voice card
  2. Schema validator checks: all tags present, valid enum values, IDs unique
  3. Voice consistency checker: embedding similarity against authored lines (reject outliers)
  4. Human reviewer: spot-check 10 of 40, approve/reject batch

Output:
  - 40 generated lines merged into the line pool
  - Generation metadata preserved (which lines are authored vs generated)
  - Rejected lines logged for model fine-tuning

The generation quality gate is critical. If generated lines don't match the authored voice, the pool degrades. The voice consistency checker is an embedding-based comparison: encode each generated line, measure cosine similarity against the authored set, reject anything below threshold.

At scale: Tier 2 NPCs across 20 districts = 160-240 NPCs × 40 generated lines each = 6,400-9,600 generated lines. With a 70% acceptance rate, that's ~4,500-6,700 usable lines from LLM generation. The human review cost: ~10 minutes per NPC batch × 200 NPCs = ~33 hours of human review. Manageable.

The Tier 3 generator

For flat NPCs (routine + greeting, social wallpaper):

Input:
  - District config (locations, factions present, social sites)
  - Cultural brief (naming pool, greeting conventions, personality distribution)
  - Tier 3 template (3 required fields: routine, greeting, faction)
  - Seed (for deterministic generation)

Process:
  1. Draw name from cultural naming pool
  2. Generate routine from template schedule (location + activity + time, cultural pattern-conformant)
  3. Generate 3-5 greeting lines from cultural greeting conventions
  4. Assign faction from district faction distribution
  5. Assign personality traits from cultural trait distribution
  6. Generate 0-1 "overheard conversation" snippets from district topics

Output:
  - Complete Tier 3 NPC YAML, schema-valid
  - Deterministic: same seed + same inputs = same NPC

The Tier 3 generator is a Rust CLI tool, not an LLM. It's template expansion with seeded randomization. Fast, deterministic, no API costs. Runs offline during the content build phase.

At scale: 100-200 Tier 3 NPCs generated in <1 second. Zero human authoring time. Review: spot-check 10% for cultural coherence.

The full pipeline at 300+ NPCs

Phase 1: Foundation (one-time, ~2 weeks)
  ├── Lock all templates (NPC, social site, triangle, pool, decision node)
  ├── Build schema validation (`make validate-content`)
  ├── Build Tier 3 generator
  ├── Build LLM line expansion pipeline
  ├── Build voice consistency checker
  └── Build content skeleton generator

Phase 2: Per-district production (repeating, ~2-3 weeks per district)
  ├── Day 1-2: District brief (Miri) + content skeleton (tooling)
  ├── Day 3-7: Tier 2 NPCs authored (Paula + Mellanie, parallel)
  ├── Day 5-10: Tier 1 NPCs authored (Paula, sequential)
  ├── Day 8-10: LLM line expansion for Tier 2 (tooling + Mellanie review)
  ├── Day 9-10: Tier 3 generation (tooling, review by Miri for cultural fit)
  ├── Day 10-12: Validation pass (Hoshe + schema + cross-reference)
  └── Day 12-14: Integration + smoke test (Dudley)

Phase 3: Cross-district integration (per system, ~1 week)
  ├── Cross-reference validation (all canonical_ids resolve)
  ├── Faction coherence check (power distribution sums correctly)
  ├── NPC migration consistency (nobody exists in two places)
  └── Story thread validation (investigation paths are complete)

20 districts × 2-3 weeks per district = 40-60 weeks of production. With 2 districts in parallel: 20-30 weeks. With 3 in parallel: 13-20 weeks. SI's estimate of 10-15 months aligns with my 2-parallel-district projection.

Tooling priority order

Build these in order. Each one unblocks the next scale step:

Priority Tool Unblocks Effort
P0 Schema validation (make validate-content) Everything — no content pipeline without validation 1 sprint
P1 Content skeleton generator Per-district production — authors start from valid stubs 0.5 sprint
P1 Tier 3 NPC generator Population fill — 50-100 NPCs per run 1 sprint
P2 LLM line expansion pipeline Tier 2 scaling — 4x content from 1x authoring 1-2 sprints
P2 Voice consistency checker Quality gate for generated content 1 sprint
P2 Cross-reference validator Multi-district integrity 1 sprint
P3 Decision node DSL validator Storyteller content validation 0.5 sprint
P3 Behavior tree editor PC archetype authoring efficiency 2 sprints
P3 Content dashboard Production tracking and visibility 1 sprint

Total tooling investment: ~9-11 sprints. This is the cost of scaling from 1 district to 20. It's front-loaded — most of it needs to exist before district 2 production begins.


Synthesis: The Architecture for 10 Years of Content

The Settled Reach is architecturally a content platform that happens to be a game. The simulation engine (9-15 patterns, well-defined, stable) processes content data (hundreds of YAML files, constantly growing, authored by a pipeline).

The engine's job is to be SMALL, CORRECT, and STABLE. Engine changes should be rare after the core patterns are built. Adding a new district should NEVER require engine changes. Adding a new thematic texture, a new cultural group, or a new PC archetype should require AT MOST a new behavior tree definition and content data — not new Rust code.

The content directory's job is to be a PLATFORM — loadable, validatable, overlayable, extensible. The manifest system, overlay mechanics, and canonical_id resolution give it the properties of a package manager. DLC and mods are first-class concepts, not afterthoughts.

The authoring pipeline's job is to make content FAST without making it CHEAP. Tier 1 content is irreducibly human. Tier 2 content is human-seeded and machine-expanded. Tier 3 content is fully generated. The tooling exists to enforce quality gates at each tier.

What I'm genuinely excited about

The single design decision that solves the most problems simultaneously: pool-based selection with overlay-compatible content packs.

This one pattern handles:

  • FRIEND selection (pool of authored candidates)
  • Role slot assignment (pool of eligible NPCs per template role)
  • Contraband variation (pool of secondary types)
  • Entanglement randomization (pool of compromise-eligible NPCs)
  • Social site population (pool of regulars per venue)
  • DLC expansion (DLC adds candidates to existing pools via MERGE)
  • Mod extension (mods add candidates to existing pools via MERGE)
  • Seed-based replayability (different draws from same pools)
  • Anti-metagaming (pool draws are unpredictable)

One pattern, nine use cases. That's elegant architecture. The pool system + overlay mechanics + canonical_id resolution form the architectural triad that makes everything else possible.

The honest difficulty assessment

Capability Full-Game Difficulty When to Build
Single-district content (what we have) Done Now
Content loader with overlay support Moderate Before district 2
Multi-district lazy loading Moderate Before district 3
Cross-district reference resolution Moderate Before district 3
Cultural distance model Easy Before second cultural group
Theme-agnostic moral arc system Easy Before second thematic texture
Decision node DSL Moderate Before 10+ decision nodes
PC behavior trees Hard Before 3rd PC archetype
Two-phase randomizer (system + district) Moderate Before second star system
Runtime event propagation across districts Hard v1.0
LLM generation pipeline Moderate Before district 2 Tier 2 production
Content dashboard Easy When production tracking matters

Nothing here is a research problem. Everything is well-understood engineering applied to game content. The hardest individual piece (behavior trees + runtime event propagation) is maybe 4-5 sprints of focused work. The total platform build is ~15-20 sprints spread over the full development timeline.

The game's ambition is large. The architecture is proportional. The technical risk is low — it's a lot of KNOWN work, not a little UNKNOWN work. That's the best kind of engineering problem to have.


Tyre out. The platform scales. The engine stays small. The content grows forever. Ship it.