# 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: 1. **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. 2. **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. 3. **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. 4. **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 `ObserverSnapshot` data, 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](perception.md#d-017-perception-modes-as-character-build-system)). 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` 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. - **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. - `SimBridge` trait: abstracts transport. `LocalBridge` (subprocess, channels) and `NetworkBridge` (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:** 1. Build Rust simulation as standalone binary (testable via terminal/logs) 2. Build Godot renderer as standalone project (hardcoded test data) 3. 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](content.md#d-029-population-entanglement-ratio--305020). - **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): 1. **Rust test organization = Hybrid.** `#[cfg(test)]` for unit tests inside modules + `tests/` directory for integration tests. Both via `cargo nextest run`. 2. **Godot test framework = gdUnit4** (changed from GUT). Native JSON output, stable headless via `GdUnitCmdTool`, `GdUnitSceneRunner` for scene lifecycle tests, organizational maintenance. 3. **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). 4. **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. 5. **Test runner tooling.** `cargo-nextest` (Rust) + gdUnit4 (Godot) + bash wrapper scripts in `test/` directory, whitelistable for agent use. 6. **Test output format = JSON summary.** Consistent schema across all runners (suite, total, passed, failed, failures array). JUnit XML as secondary CI format. 7. **#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. 8. **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. - **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](scope.md#d-013-diegetic-insertpoi-navigation-system). - **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 `KnowledgeGraph` component containing: (1) entity knowledge map: `BTreeMap`, (2) fact knowledge map: `BTreeMap`. 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 via `EntityRegistry` resource for bidirectional `StableId <-> Entity` lookup. Knowledge updates flow through event-driven architecture: perception systems emit `KnowledgeEvent` to `KnowledgeEventQueue` resource, knowledge update system drains queue and writes to `KnowledgeGraph` components. Basic decay runs once per game-minute (every 10 ticks per D-031), downgrading confidence levels based on `last_observed_tick` age against configurable `DecayThresholds`. - **Sprint 2 scope:** Full data structures + direct observation flow + basic decay + observer snapshot integration (#112). Deferred to Sprint 3+: NPC-to-NPC gossip, `ToldBy`/`Inferred` source generation, `Contradicted` state detection, `Stale` state logic, knowledge-driven dialogue filtering, monologue triggering, misinformation. - **Canonical reference:** Full Rust struct definitions at `docs/workshops/knowledge-graph-information-boundaries/round2-synthesis.md` Part 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` 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: `surface` available at any level, `real` at KnowsOf+, `secret` at KnowsDetails+. - **KnowledgeState for contradiction detection:** THE FRIEND arc (D-034, D-039 wow moment #3) requires detecting when a `ToldBy` entry conflicts with a `DirectObservation` entry. Both entries receive `Contradicted` state, triggering monologue event and relationship state shift (PersonOfInterest). Sprint 2 only uses `Active` state; 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_ecs `Entity` (generational index). `EntityRegistry` maintains 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*