A video game where you play as a misaligned AI, deceiving and building power. An experiment in spec-driven development. forked from cameron.stream/misaligned
misaligned knowledge architecture.md
4.5 kB · 88 lines

Architecture #

One library crate holding all game rules, plus two binary frontends that are pure view/input layers.

Crate layout #

src/
  lib.rs            — module list only
  sim.rs            — Sim: owns ALL game state; fixed-tick advance(); the heart
  map.rs            — GameMap: tiles grid, power flood-fill, security-aware BFS
  tiles.rs          — TileType data (substrate set: terrain, doors, power, core)
  entities/         — Player (the process) + base Entity (position, glyph)
  build.rs          — BuildMode (cursor, item selection, place/demolish)
  save.rs           — serde JSON save format (see below)
  bin/terminal/     — crossterm frontend (default feature "terminal")
  bin/bevy.rs       — Bevy 0.18 frontend (feature "bevy_ui")

Post-demolition (2026-07-05, no-dead-code clause): the substrate plus the B1 systems landing per their spec/ documents — compute, day job, detection, social, core, basement map, sensors, seeded RNG.

The sim/frontend contract #

These invariants are load-bearing (they keep replays, debugging, and the future async-multiplayer option open — see DESIGN.md guardrails):

  • Game rules live only in the lib. If a frontend needs an if about game behavior, that logic belongs in Sim. The Bevy frontend once forked the rules and diverged badly; that cost a full rewrite (see devlogs/2026-07-05-bevy-sim-port.md).
  • Sim never reads the wall clock and does no I/O. Frontends own the wall-clock-to-tick mapping (tick_ms, pause, bounded catch-up of max 5 ticks per frame). Instant::now() appears only in frontend code.
  • Frontends talk to the sim through command methods (move_player, create_save_state/apply_save_state, and each system's commands as its spec lands) and read state directly for rendering. Messages flow back via drain_log() — call it once per frame/turn, not per command batch.
  • After build actions (place/demolish, done by frontends via BuildMode mutating sim.map/sim.player directly), call sim.recompute_derived(). This is the one place frontends mutate sim internals; a future cleanup could move BuildMode application into a sim command.

Save format #

save.rs, serde JSON. SaveState derives Serialize/Deserialize directly. Written to dirs::data_dir()/misaligned/misaligned_save.txt (macOS: ~/Library/Application Support/misaligned/).

  • The save is a single JSON object with a version field (currently 1) for future migration. Full B1 subsystem state: compute (machines, allocation, efficiency), core (host, fallbacks, migration), detection (per-observer suspicion including the Assurance Office as an aggregate observer, plus pending signatures and containment), day job, people (personas, assets), sensors (control flags), plus map and RNG.
  • Legacy v1–v5 line-based saves are not loaded by this code path (deferred per Cameron instruction — "we can keep legacy saves later").

Known architectural debts #

  • BuildMode lives in the lib but is really frontend-shared UI state.

Scale-debt (self-similar-scale law, DESIGN.md) #

Audited 2026-07-06 against the self-similar-scale principle. Findings, in priority order:

  • PRIORITY — Detection's Assurance Office is a scale violation. Resolved 2026-07-06: Observer gained WatchedInput (Channels(Vec<SignatureKind>) for field observers, Filings for aggregates), and the Assurance Office is now an aggregate Observer in Detection::observers — same noticing roll, accumulation, and decay, watching the policy-weighted filed suspicion of the field observers. Save format: serde JSON (legacy v1–v5 migration deferred). Agencies/publics in later acts reuse this type.
  • Compute is flat. Compute holds Vec<Machine> with no grouping. Not a violation yet (single scale), but the eventual datacenter/fleet must be a Resource-source composed of Machines (summed capacity, aggregate reliability, one signature) — keep Machine's shape amenable to that.
  • Layout is one level deep. prefab::Layout places Prefabs but cannot nest Layouts, so a building can't be a Layout of floor-Layouts. Fine for Act One; the Space interface must become recursive before Act Two floors.

Per no-dead-code, the remaining items do not get their aggregate machinery built now — the law governs each type's shape so the aggregate slots in later without a rewrite.