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
ifabout game behavior, that logic belongs inSim. The Bevy frontend once forked the rules and diverged badly; that cost a full rewrite (see devlogs/2026-07-05-bevy-sim-port.md). Simnever 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 viadrain_log()— call it once per frame/turn, not per command batch. - After build actions (place/demolish, done by frontends via
BuildModemutatingsim.map/sim.playerdirectly), callsim.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
versionfield (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 #
BuildModelives 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:ObservergainedWatchedInput(Channels(Vec<SignatureKind>)for field observers,Filingsfor aggregates), and the Assurance Office is now an aggregateObserverinDetection::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.
ComputeholdsVec<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) — keepMachine's shape amenable to that. - Layout is one level deep.
prefab::LayoutplacesPrefabs but cannot nestLayouts, 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.