Two-round workshop producing D-041 (Knowledge Graph Data Model): - Round 1: independent analyses from Dudley, Gestalt, SI, Tyre, Paula - Round 2: synthesis resolving debates + Gestalt mechanics validation Key decisions: - 4-level confidence hierarchy (Suspects < KnowsOf < KnowsDetails < Direct) - BTreeMap for deterministic iteration (D-010 principle 4) - Per-entity Component model, not centralized Resource - StableEntityId + EntityRegistry for save/load stability (partial Q-019) - Sprint 2 stub: structs + direct observation + basic decay (~6.5 dev-days) Resolved Q-016 (knowledge hierarchy), raised Q-024/Q-025/Q-026. Created tickets #361-#368 under epic #351, reconciled #49 children. Updated sprint 2 briefings, agent briefings, and decision files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
19 KiB
19 KiB
Architecture Decisions
Technical foundation decisions that constrain implementation: engine, client-server, ECS, simulation, testability, performance budgets.
D-008: Action pillar design principles
- Date: 2026-02-08
- Decision: The action/combat system follows these principles:
- Rimworld/XCOM hybrid - simple mechanics, stats-driven. Simple gets complicated fast on its own.
- Z-levels - floors, verticality. Not 3D rendering, but 3D space. Fights across floors of a building.
- Perception-bounded - you see/hear what your character can. Rest is abstracted/occluded. Same principle as information asymmetry applied to physical space.
- LOD/occlusion - simulation reduces outside player view. Both a design principle and performance optimization.
- Large, varied maps - locations should feel big. Achieved through procedural generation (templates + procedural flesh), trickery, or both. Procedural layouts also feed replayability.
- Multiple maps - different locations are separate maps. Step through a wormhole, load a new place.
- Wildly asymmetric encounters - balanced fights are the exception. Power mismatches in both directions are the norm and the source of tension.
- Hubris wall as design principle - let the player feel powerful, then recontextualize their power level. Not cheap difficulty spikes - genuine "you were playing a smaller game than you thought" moments. The books are not always balanced; the fun comes when a character hits their hubris wall.
- Death = information loss, not game over - memory cell backup system means death costs you everything since your last backup. Lost knowledge, lost relationships, lost deals. The storyteller knows what you've lost and can exploit it.
- Scales with ascension - baseline human fights with weapons, Higher fights with biononics, ANA-connected fights with something else entirely. The pillar transforms as the character evolves.
- Design references: Rimworld (real-time, simple systems, personal stakes), XCOM (tactical asymmetry, pod activation moments), Dwarf Fortress (z-levels, simulation depth)
- Raised by: Team Leader (Jeroen), with input from full team.
D-009: Multiplayer - design for it, build single-player first
- Date: 2026-02-08
- Decision: Single-player is the build target. Multiplayer is designed into the architecture from day one so it can be added without rewriting the game.
- Rationale: Two players experiencing the same conspiracy from different keyholes (Senate insider + Guardian operative) is a killer feature. But building multiplayer too early kills projects. The compromise: architectural decisions now that make multiplayer a "add networking" problem later, not a "rewrite everything" problem. Team Leader flagged that bolting multiplayer on after the fact is one of the hardest things to do - so the architecture must be honest about this from the start.
- Cost: ~15-20% slower single-player development due to required abstractions. Accepted as cheap insurance.
- Raised by: Full team discussion. Tyre led technical framing, Team Leader insisted on architectural honesty.
D-010: Multiplayer-ready architectural baseline
- Date: 2026-02-08
- Decision: Four non-negotiable architectural principles that must be present from the first line of code:
- Client-server separation - even in single-player. The game simulation runs as a "server," the player view is a "client." Single-player = local client + local server. This is the single decision that makes or breaks retrofitting multiplayer.
- Information boundaries as a first-class system - every piece of game state is tagged with who knows it. Not fog-of-war bolted on - the engine fundamentally thinks in terms of "what does this observer have access to." Required for single-player asymmetric information anyway. Multiplayer just means multiple observers.
- No baking player identity into the game loop - the simulation doesn't know there's "the player." It knows there are characters, some of which are player-controlled. Adding a second player-controlled character should be a configuration change, not a rewrite.
- Deterministic simulation with input events - game state advances based on timestamped actions, not "whatever the local machine calculated." Enables synchronization later without rewriting the simulation.
- Side benefits (Nigel's observation): Every one of these makes single-player better too. Information boundaries make NPC AI smarter about what they know. Client-server makes save/load cleaner. Deterministic simulation makes debugging easier. No sacrifice.
- Engine implication: Client-server friendliness is now a hard requirement on the engine shortlist (see Q-001).
- Raised by: Tyre (Technical Architect), endorsed by Team Leader as "sound architectural baseline."
D-012: Chunk-based map architecture for future borderless generation
- Date: 2026-02-09
- Decision: Maps use chunk-based generation and loading from day one. Chunks load/unload around the player. A bounded map is "only generate chunks within this boundary." Removing the boundary later to enable Minecraft-style borderless generation is a configuration change, not a rewrite.
- v0.1: Bounded ~150x150 per world, 2-3 z-levels, chunk-based internally.
- Future: Borderless generation. The world generates as you explore. The map can never be "solved" by walking to every corner. New areas develop, existing areas change.
- Rationale: Same principle as D-010 (multiplayer architecture) - design for the future, build the simpler version now.
- Raised by: Tyre (Technical Architect), endorsed by Team Leader.
D-020: Engine and architecture selection — Godot client + Rust simulation via subprocess/IPC
- Date: 2026-02-09
- Decision: The game uses a split architecture: Godot 4 (GDScript) as the rendering client, Rust with bevy_ecs standalone as the simulation server. The two communicate via subprocess/IPC (local socket for single-player, TCP for multiplayer). NOT via GDExtension.
- Architecture:
- The Rust simulation is a standalone binary with zero Godot dependencies. It runs the ECS world, perception queries, AI, storyteller, combat — all game logic.
- The Godot client is a pure renderer: receives
ObserverSnapshotdata, draws tiles/sprites/fog, plays audio, shows UI, captures input. No game logic in GDScript. - Single-player: Godot launches the Rust binary as a child process. Local Unix socket or localhost TCP.
- Multiplayer: Godot connects to a remote Rust server. Same protocol. The simulation binary doesn't know the difference.
- This IS the D-010 client-server architecture — literally, not simulated.
- Serialization:
- MessagePack for all client-facing communication (Rust↔Godot). Dynamic structure supports variable HUD composition driven by perception modes (D-017). Cross-language, debuggable.
- bincode reserved for future Rust↔Rust server-to-server sync (same binary, hot path, zero overhead).
- protobuf rejected — solves deployment/versioning problems we don't have, poor GDScript support.
- Why subprocess over GDExtension:
- Eliminates entire risk categories: gdext pre-1.0 API churn, Godot version ABI breakage, FFI thread safety (
Gd<T>is!Send), cross-boundary memory management. - Decouples learning: build and test Rust simulation standalone, build Godot renderer standalone, connect when both work.
- Maps directly to D-010 client-server with no simulation — it IS client-server from day one.
- Either side can be upgraded, replaced, or scaled independently.
- Cost: ~1-5ms serialization latency per tick. Acceptable for a detective/strategy game, not a twitch shooter.
- Eliminates entire risk categories: gdext pre-1.0 API churn, Godot version ABI breakage, FFI thread safety (
- Key patterns:
ObserverSnapshot: the only data structure crossing the boundary. Contains visible entities, fog state, sound events, monologue triggers, HUD widget data. Variable shape per character build.PlayerInput: semantic actions (MoveNorth, Interact, UsePerceptionMode), not raw key events. Timestamped for deterministic processing.SimBridgetrait: abstracts transport.LocalBridge(subprocess, channels) andNetworkBridge(TCP, MessagePack) implement the same interface.
- Kill switch: If no working prototype (character + fog + one NPC) exists by week 8 of development, pivot to pure Godot. If bridge/sync code exceeds game logic for 3 consecutive sprints, the architecture tax is too high.
- Development sequence:
- Build Rust simulation as standalone binary (testable via terminal/logs)
- Build Godot renderer as standalone project (hardcoded test data)
- Connect via MessagePack protocol
- Evaluation reports:
docs/architecture/eval-godot-rust-bridge.md(Tyre),docs/architecture/risk-godot-rust-bridge.md(Troblum) - Raised by: Team Leader (Jeroen) proposed Godot client + Rust backend. Tyre designed architecture. Troblum's risk assessment shifted integration from GDExtension to subprocess/IPC. Full team endorsed.
- Dissent: None. Troblum's CRITICAL risk flags on GDExtension were accepted; subprocess approach addresses them.
D-026: Simulation tiers with timestamp-based eviction
- Date: 2026-02-10
- Decision: Four simulation tiers: Active (30-80 NPCs, full sim at 10-20 ticks/sec), Background (500-2,000 NPCs, state machine ticks 1/game-minute with 4 machines: schedule, mood, relationships, job), State-saved (10,000+, frozen serialized structs ~1-2KB each), Ungenerated (doesn't exist yet). Eviction uses interaction-timestamp LRU against available sim-space. NPCs with active scope tags (neighborhood, active-quest, colleague, known-contact) stay fully simulated. State-saved NPCs reactivate on player return (~2-5ms). Density follows the player — content generated ahead of arrival, home system fully instantiated at game start.
- Rationale: Timestamp-based eviction replaces categorical persistence rules with one priority queue. State-save makes disposal reversible. bevy_ecs dynamic component add/remove makes tier transitions seamless. Tyre confirmed all proposals fit within performance budgets.
- Cross-reference: Content density implications in D-029.
- Raised by: Team Leader (timestamp model), Tyre (technical validation), Gestalt (scope tags)
- Dissent: None
D-030: Testability architecture — 8 decisions for ticket #214
- Date: 2026-02-11
- Decision: The v0.1 testability architecture is confirmed with 8 sub-decisions from the Gap Analysis Workshop (Round 18):
- Rust test organization = Hybrid.
#[cfg(test)]for unit tests inside modules +tests/directory for integration tests. Both viacargo nextest run. - Godot test framework = gdUnit4 (changed from GUT). Native JSON output, stable headless via
GdUnitCmdTool,GdUnitSceneRunnerfor scene lifecycle tests, organizational maintenance. - IPC testing = Three-layer architecture. Layer 1: fixture-based serialization roundtrip (fast, every edit). Layer 2: mock subprocess protocol state machine (medium, every PR). Layer 3: real subprocess integration (slow, daily/pre-merge).
- Production code constraints + CauseChain. No
#[cfg(test)]in production. Public API is the test surface. ECS World setup replaces mock injection. CauseChain is a production component (monologue provenance, journal, debugging) that tests also leverage. - Test runner tooling.
cargo-nextest(Rust) + gdUnit4 (Godot) + bash wrapper scripts intest/directory, whitelistable for agent use. - Test output format = JSON summary. Consistent schema across all runners (suite, total, passed, failed, failures array). JUnit XML as secondary CI format.
- #201 (Deterministic replay) promoted to CRITICAL. Simulation must consume time, randomness, and input exclusively through injectable resources (
SimulationTime,SimRng,InputQueue). Required by D-010 principle 4. - Test priority aligned with hard blockers. Phase 1 (sprint 1-2): test infra + collision/pathfinding/time. Phase 2 (sprint 3-4): monologue pipeline integration test + information boundary negative tests. Phase 3 (sprint 5+): CauseChain verification + divergent snapshots.
- Rust test organization = Hybrid.
- Rationale: Two rounds of analysis by Tyre (Technical Architect) and Hoshe (QA Engineer) with cross-validation from all design agents. Key change: gdUnit4 over GUT driven by agent-driven development requirements (JSON output, headless stability, bus factor). CauseChain endorsed unanimously after all design agents independently identified the need for information provenance tracking.
- Raised by: Tyre (architecture), Hoshe (testability analysis). Full workshop endorsed.
- Dissent: GUT vs gdUnit4 resolved in Hoshe's favor — Tyre explicitly changed position. No remaining dissent.
D-031: Time system — game clock and day phases
- Date: 2026-02-11
- Decision: The v0.1 time system uses the following model:
- Tick-to-time mapping: 10 simulation ticks = 1 game-minute (at 10 tps, 1 real second = 1 game-minute). A 30-minute real-time play session covers ~12-18 game-hours — enough for a full NPC daily cycle.
- Day phases: Four phases drive routine transitions: Morning, Afternoon, Evening, Night. NPCs transition between routine activities at phase boundaries (e.g., go to work in Morning, to the bar in Evening).
- Time display: Diegetic — shown on the player's neural insert HUD. The character checks their insert to see the time, consistent with D-013.
- Pause: Available in single-player. Simulation freezes, UI stays responsive. Compatible with future multiplayer (D-009) where pause would be disabled or vote-based.
- Time-skip: Deferred for v0.1. The "wait/stake out" mechanic (if implemented) would advance time while the player observes from a fixed position.
- Not in scope for v0.1: Deep time (years/decades), day/night lighting, seasonal cycles, time zones between locations.
- Resolves: Q-009
- Raised by: Tyre (technical proposal), Gestalt (day-phase design). Confirmed in Round 18 Gap Analysis Workshop with full team consensus.
- Dissent: None.
D-041: Knowledge Graph Data Model
- Date: 2026-02-11
- Decision: The knowledge graph is a per-entity bevy_ecs Component with BTreeMap storage for deterministic iteration. Each entity that has knowledge (player character, Active-tier NPCs, Background-tier NPCs) gets a
KnowledgeGraphcomponent containing: (1) entity knowledge map:BTreeMap<StableId, EntityKnowledge>, (2) fact knowledge map:BTreeMap<FactId, FactKnowledge>. Knowledge confidence uses a 4-level hierarchy:Suspects < KnowsOf < KnowsDetails < Direct. Knowledge state tracks temporal/logical status:Active(believed true),Contradicted(conflicting information exists),Stale(aged beyond threshold). Knowledge source provides provenance per entry:DirectObservation,Heard,ToldBy,Inferred,Background. Stable entity IDs (StableId(u64)) replace bevy_ecs Entity handles in knowledge references, mapped viaEntityRegistryresource for bidirectionalStableId <-> Entitylookup. Knowledge updates flow through event-driven architecture: perception systems emitKnowledgeEventtoKnowledgeEventQueueresource, knowledge update system drains queue and writes toKnowledgeGraphcomponents. Basic decay runs once per game-minute (every 10 ticks per D-031), downgrading confidence levels based onlast_observed_tickage against configurableDecayThresholds. - Sprint 2 scope: Full data structures + direct observation flow + basic decay + observer snapshot integration (#112). Deferred to Sprint 3+: NPC-to-NPC gossip,
ToldBy/Inferredsource generation,Contradictedstate detection,Stalestate logic, knowledge-driven dialogue filtering, monologue triggering, misinformation. - Canonical reference: Full Rust struct definitions at
docs/workshops/knowledge-graph-information-boundaries/round2-synthesis.mdPart 3 (lines 320-752). All implementation must conform to those types. - Key design choices:
- BTreeMap over HashMap: D-010 principle 4 (deterministic simulation) requires stable iteration order. BTreeMap provides O(log N) lookups (~6-8 comparisons at N=50-200), deterministic serialization, and predictable replay behavior. HashMap iteration is non-deterministic and incompatible with deterministic replay (D-030 sub-decision #7).
- Per-entity Component, not centralized Resource: Enables
Changed<KnowledgeGraph>dirty tracking, per-entity serialization for D-026 tier transitions, no shared mutable state, natural ECS query patterns. - 4-level confidence hierarchy: Resolves Q-016.
Suspects= "something's off", gates initial investigation.KnowsOf= "X is involved in Y", gates topic-specific dialogue and peer-tier access (D-028).KnowsDetails= actionable detail, gates confrontation and secret-tier dialogue.Direct= currently in LOS, provides live position data and maximum rendering fidelity. Maps to D-028 access tiers:surfaceavailable at any level,realat KnowsOf+,secretat KnowsDetails+. - KnowledgeState for contradiction detection: THE FRIEND arc (D-034, D-039 wow moment #3) requires detecting when a
ToldByentry conflicts with aDirectObservationentry. Both entries receiveContradictedstate, triggering monologue event and relationship state shift (PersonOfInterest). Sprint 2 only usesActivestate; contradiction detection ships Sprint 3. - StableId for knowledge references: Partially resolves Q-019 for server-side and knowledge graph purposes. Knowledge graphs reference
StableId(u64)that persists across save/load cycles, not bevy_ecsEntity(generational index).EntityRegistrymaintains bidirectional mapping. Assigned once at entity creation, never changes. Client-side mapping (Godot StableId -> scene node) remains open. - Event-driven updates: Phase 2 (perception) emits events. Phase 3 (knowledge) consumes events and writes graphs. Phase 4 (snapshot) reads graphs. Prevents mutable borrow conflicts in bevy_ecs.
- Performance budget: ~14 KB per NPC knowledge graph (50 entities + 20 facts). Active tier (80 NPCs) = ~1.1 MB. Background tier (2,000 NPCs, 10 entries each) = ~5 MB. Total live memory: ~6 MB. Knowledge lookups are O(log N) at N=50 (~100ns per query). Not on critical path (shadowcasting/spatial queries consume 10-20ms per tick, knowledge operations <3ms).
- Resolves: Q-016 (knowledge hierarchy). Partially resolves Q-019 (entity ID stability, server-side).
- Blocks: #352 (Observer Snapshot Pipeline Workshop)
- Raised by: Tyre (architecture synthesis), Dudley (implementation analysis), Gestalt (mechanics validation), Paula (narrative requirements). Workshop participants: Tyre, Dudley, Gestalt, Paula, SI. Source: Knowledge Graph & Information Boundaries Workshop (Epic #351), 2026-02-11.
- Dissent: None. Gestalt initially proposed dual-axis model (confidence + understanding) but validated that single-axis confidence with future content-driven understanding progression is architecturally sufficient. Dudley proposed HashMap; synthesis chose BTreeMap per D-010 principle 4 with Dudley's explicit acknowledgment ("iteration order is a determinism time bomb").
9 decisions. Last updated: 2026-02-11