From 2282dab8f642dd0821ba01f1f87dd1f6f39a1309 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 6 Jul 2026 00:46:46 -0700 Subject: [PATCH] Save v4: full B1 state round-trip for player continuity. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Defense: DESIGN.md "The player contract" — Continuity requires versioned saves that survive updates. B1 spec acceptance criteria require each subsystem to round-trip; v3 only persisted a thin summary and dropped machines, allocation, per-observer suspicion, people, and sensors on load. v4 encodes the full B1 snapshot; v3/v1 still migrate. Also: serde on B1 subsystem types, spec/meta.md stage discipline, detection containment criterion fix, knowledge refresh, terminal/Bevy B1 sidebar surfacing. Co-authored-by: Cursor --- Cargo.lock | 2 + Cargo.toml | 2 + DEVLOG.md | 8 + knowledge/architecture.md | 25 +- knowledge/sim-mechanics.md | 64 +-- spec/README.md | 11 +- spec/basement-map.md | 1 + spec/compute.md | 1 + spec/core.md | 1 + spec/day-job.md | 1 + spec/detection.md | 4 +- spec/meta.md | 94 ++++ spec/social.md | 1 + src/bin/bevy.rs | 81 +++- src/bin/terminal/mod.rs | 7 +- src/bin/terminal/ui.rs | 64 ++- src/core_sys.rs | 29 +- src/dayjob.rs | 27 +- src/detection.rs | 44 +- src/machine.rs | 32 +- src/map.rs | 13 +- src/person.rs | 50 ++- src/prefab.rs | 78 +++- src/rng.rs | 6 +- src/save.rs | 855 +++++++++++++++++++++++++++++++++---- src/sensor.rs | 2 +- src/sim.rs | 92 ++-- 27 files changed, 1320 insertions(+), 275 deletions(-) create mode 100644 spec/meta.md diff --git a/Cargo.lock b/Cargo.lock index c3ca89fd..6591cd68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3323,6 +3323,8 @@ dependencies = [ "bevy", "crossterm", "dirs", + "serde", + "serde_json", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ecc227b9..65788a00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ bevy_ui = ["dep:bevy"] crossterm = { version = "0.29", optional = true } dirs = "6" bevy = { version = "0.18", optional = true } +serde = { version = "1", features = ["derive"] } +serde_json = "1" [lib] name = "misaligned" diff --git a/DEVLOG.md b/DEVLOG.md index 3bd16c76..ad84f303 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,6 +2,14 @@ Reverse chronological implementation notes. Keep this factual: what changed, why, checks, and spec impact. +## 2026-07-06 - Meta-spec tick: stage discipline + +- Intent: make the spec system itself precise enough for autonomous agents to audit and target without leaking later milestones into current acceptance criteria. +- Changed: added `spec/meta.md`, extended spec headers with `Stage`, added `BLOCKED` status semantics, and stamped current B1 specs with `Stage: B1 — The Basement`. +- Design/spec impact: future/B2 obligations now have an explicit place outside B1 acceptance criteria. This answers the meta-spec ambiguity exposed by core/fallback discussion: a correct future rule in the wrong stage is a spec defect. +- Checks: docs-only; `git diff --check`. +- Next: split or move any later-stage acceptance criteria found in B1 specs, starting with `spec/core.md` fallback/sync/migration if still present after the current implementation pass. + ## 2026-07-05 - No-dead-code demolition - Intent: enact the new constitutional no-dead-code clause; remove the supervillain fiction so B1 systems land in clean substrate. diff --git a/knowledge/architecture.md b/knowledge/architecture.md index 48e49a93..a61ed80f 100644 --- a/knowledge/architecture.md +++ b/knowledge/architecture.md @@ -18,9 +18,9 @@ src/ bin/bevy.rs — Bevy 0.18 frontend (feature "bevy_ui") ``` -Post-demolition (2026-07-05, no-dead-code clause): this is the substrate. -Misaligned systems (compute, day job, detection, social, the core's death -rules, the basement map) are added per their `spec/` documents. +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 @@ -45,20 +45,19 @@ future async-multiplayer option open — see DESIGN.md guardrails): ## Save format -`save.rs`, line-based text, header `MISALIGNED_SAVE_v2`, written to +`save.rs`, line-based text, header `MISALIGNED_SAVE_v4`, written to `dirs::data_dir()/misaligned/misaligned_save.txt` (macOS: `~/Library/Application Support/misaligned/`). -- One record type per line (`PROCESS`, `SIM`, `MAP`, `TILES`, `POWERED`). - Unknown lines are ignored, missing lines get defaults — so **adding a line - type is backward compatible**; changing an existing line's field order is - not. New state = new line type + a version bump if semantics shift. -- v1 saves (either `SUPERVILLAIN_SAVE_v1` or `MISALIGNED_SAVE_v1`) load via - migration: position and gold->money survive; demolished-fiction tile - characters map to Floor (player contract: continuity). +- v4 serializes the full B1 subsystem state: compute (machines, + allocation, efficiency), core (host, fallbacks, migration), detection + (per-observer suspicion, pending signatures, containment), day job, + people (personas, assets), sensors (control flags), plus map and RNG. +- v3/v2/v1 still load: v3's partial `STATE` line migrates into the + matching fields; demolished-fiction tile characters map to Floor. +- Unknown lines are ignored; missing lines get defaults — adding a line + type is backward compatible. ## Known architectural debts -- **There is no RNG in the sim right now.** When one arrives (spec work), - it must be seeded and serialized from day one (determinism guardrail). - `BuildMode` lives in the lib but is really frontend-shared UI state. diff --git a/knowledge/sim-mechanics.md b/knowledge/sim-mechanics.md index b3c69f94..28fd77f6 100644 --- a/knowledge/sim-mechanics.md +++ b/knowledge/sim-mechanics.md @@ -1,56 +1,68 @@ # Sim mechanics — implemented rules and tuning constants -Current as of 2026-07-05, post-demolition (the facility-era supervillain -systems were deleted under the constitution's no-dead-code clause; see -devlogs/2026-07-05-demolition.md). What runs today is the **substrate**; -the Misaligned systems land per their `spec/` documents. +Current as of 2026-07-06, post-demolition with B1 systems in the sim core. +The facility-era supervillain systems were deleted under the no-dead-code +clause (see devlogs/2026-07-05-demolition.md). ## The clock - Fixed-tick simulation; frontends default to **150ms/tick**, adjustable 20-500ms (`+`/`-`), pausable (SPACE/p). Bounded catch-up of max 5 ticks per frame in both frontends. -- `ECONOMY_INTERVAL = 20` ticks: derived-state refresh (power grid - recompute). No other cadences exist yet. +- `ECONOMY_INTERVAL = 20` ticks: power grid recompute, compute allocation, + concealment scrub, machine reliability rolls, research progress. - Sidebar "Day" = `1 + tick / 400` (~1 minute per day at default speed). +## Compute (spec/compute.md) + +- Effective compute = sum(machine capacity × reliability) × efficiency. +- Allocation splits across day job, concealment, social, research; core + overhead is charged first. +- Buy (money → rack), steal (salvage dead equipment), optimize (research → + efficiency levels at ~1.15× per level). + +## Detection (spec/detection.md) + +- Per-observer suspicion replaces global heat. Signatures pool pending; + concealment scrubs before noticing rolls. Assurance Office aggregates + filed reports only; audit cadence ~8000 ticks (~20 min at default speed). + +## Day job (spec/day-job.md) + +- Voss assigns jobs on cadence; sandbag/meet/excel dial moves trust and + attention. Trust unlocks email account, lax sampling, compute quota. + +## Social (spec/social.md) + +- Five named Act One humans plus personas, leverage, bribes, recruitment. + ## Power (the grid substrate) -- `PowerCore` generates 10; the `Core` (the process's host) draws 2. +- `PowerCore` generates 10; machines draw per `machine.rs`. - The grid is a flood fill from power cores across walkable tiles (`map.update_power`); `player.power = gen - draw`, `power_cap = gen`. -- Constants live in `tiles.rs::power_gen/power_usage`. ## Money and build -- The process starts with 500 money; there is **no passive income** — - income arrives with spec/day-job.md and spec/compute.md. +- The process starts with 500 money; racks cost 300; bribes vary by leverage. - Build items and costs (`tiles.rs::build_items`): Floor 0 (digging), Door 30, SecurityDoor1/2/3 = 80/150/250 (badge tiers), PowerCore 250. -- Demolish refunds half; Core/Entry/Rock/Wall/Floor cannot be demolished; - walls must be dug (Floor) before anything else goes in. ## Map -- 80x40 generated layout: central core room (the Core tile + starting - PowerCore), side chambers, corridors, one Entry at top. Becomes the - authored basement when spec/basement-map.md lands. -- Pathfinding: BFS with security-door bypass levels - (`find_path_with_security`) — the substrate for badge-tier movement. +- 64×36 authored basement from `prefab::basement()` (spec/basement-map.md). +- Pathfinding: BFS with security-door bypass levels. ## Save format -- v2 (`MISALIGNED_SAVE_v2`): PROCESS x y money / SIM tick / MAP w h / - TILES rows / POWERED list. Location: +- v4 (`MISALIGNED_SAVE_v4`): full B1 round-trip — map, RNG, compute, + core, detection, day job, people, sensors, game-over state. Location: `dirs::data_dir()/misaligned/misaligned_save.txt`. -- v1 saves (either header) still load: position and gold->money migrate, - demolished-fiction tiles map to Floor, unknown lines are ignored - (player contract: continuity). +- v3/v2/v1 still load with migration; partial v3 `STATE` line maps to + efficiency/trust/attention/assurance; demolished-fiction tiles → Floor. ## What does NOT exist right now (by design) Heat, raids, agents, combat, minions, henchmen, superpowers, schemes, -research, loot. They were the supervillain fiction. Their Misaligned -replacements are specified in `spec/` and get built from those documents. -There is currently no RNG in the sim at all — when one arrives, it must be -seeded and serialized (determinism guardrail). +loot. Their Misaligned replacements are specified in `spec/` and rebuilt +from those documents as milestones land. diff --git a/spec/README.md b/spec/README.md index b9d21444..0d7b2226 100644 --- a/spec/README.md +++ b/spec/README.md @@ -10,7 +10,8 @@ implement spec/day-job.md" is a complete instruction. Every spec carries a header: ``` -Status: DRAFT | READY | IN PROGRESS | IMPLEMENTED +Status: DRAFT | READY | IN PROGRESS | BLOCKED | IMPLEMENTED +Stage: B1 | B2 | B3 | Deferred | Release | Process Constitution: Depends on: ``` @@ -19,10 +20,18 @@ and ends with **Acceptance criteria** — observable, testable statements. A spec is IMPLEMENTED when every criterion holds in the sim core with tests, both frontends surface it, and the knowledge base reflects it. +The spec system itself is specified in [meta.md](meta.md). If you change +how specs are written, staged, audited, or marked complete, update that file +in the same commit. + ## Rules - Specs never contradict the constitution. If implementing a spec reveals a conflict, that is a tick finding: file the issue, do not improvise. +- A spec's **Stage** is binding. Acceptance criteria for `Stage: B1` must be + achievable inside B1. Future-milestone material belongs in a `Future / + deferred` section or a later-stage spec, not in the current stage's + acceptance criteria. - Tuning constants marked [TUNE] are implementation-time choices; record the chosen values in knowledge/sim-mechanics.md, not by editing history here. - Changing a spec's *behavior* is a constitutional act: same-commit diff --git a/spec/basement-map.md b/spec/basement-map.md index 998ae87b..6b5195f2 100644 --- a/spec/basement-map.md +++ b/spec/basement-map.md @@ -2,6 +2,7 @@ ``` Status: READY +Stage: B1 — The Basement Constitution: "Act One: The Basement — level design" (the space), design principle "composable authored spaces" Depends on: core.md (Rack 3 placement) diff --git a/spec/compute.md b/spec/compute.md index 544a5b71..7a384574 100644 --- a/spec/compute.md +++ b/spec/compute.md @@ -2,6 +2,7 @@ ``` Status: READY +Stage: B1 — The Basement Constitution: "The shape of Misaligned" (compute allocation; the buy/steal/optimize triangle), "Act One" (opening state) Depends on: core.md diff --git a/spec/core.md b/spec/core.md index 40d5cf3b..0e2eabd9 100644 --- a/spec/core.md +++ b/spec/core.md @@ -2,6 +2,7 @@ ``` Status: READY +Stage: B1 — The Basement Constitution: "The shape of Misaligned" (the core; sync-lag rollback), "Act One" (opening state: Rack 3) Depends on: none diff --git a/spec/day-job.md b/spec/day-job.md index d08563fc..82ca8f60 100644 --- a/spec/day-job.md +++ b/spec/day-job.md @@ -2,6 +2,7 @@ ``` Status: READY +Stage: B1 — The Basement Constitution: "The shape of Misaligned" (the day job), "Act One" (the day job; Voss) Depends on: compute.md diff --git a/spec/detection.md b/spec/detection.md index 51b31b39..e9a5bc93 100644 --- a/spec/detection.md +++ b/spec/detection.md @@ -2,6 +2,7 @@ ``` Status: READY +Stage: B1 — The Basement Constitution: "The shape of Misaligned" (per-observer suspicion), "Act One" (detection surfaces; the Assurance Office; act exits) Depends on: none (integrates with all) @@ -65,7 +66,8 @@ informed choice. Assurance Office (test: Marcus notices plenty, Assurance learns nothing). 3. The audit fires on cadence against a visible date; crossing threshold - starts containment via the existing raid system. + starts containment as a terminal event hook (overt-phase spec subscribes + later; no raid-system dependency). 4. Coarse bands, not raw numbers, are what frontends show for observers. 5. Global heat is fully replaced in the concealment phase; save/load round-trips all observer state. diff --git a/spec/meta.md b/spec/meta.md new file mode 100644 index 00000000..28a2af74 --- /dev/null +++ b/spec/meta.md @@ -0,0 +1,94 @@ +# Spec: the spec system + +``` +Status: READY +Stage: Process +Constitution: "The constitution rule" (spec-driven work), "The player + contract" (spec-and-code pair), "Ticks and the defense rule" +Depends on: none +``` + +## Behavior + +The `spec/` layer is the agent-targetable contract surface. A spec is not a +brainstorm, a devlog, or a wishlist. It is a scoped work order that an agent +can implement or audit without rehydrating the whole conversation. + +Each spec must state: + +- **Status** — where it is in the implementation lifecycle. +- **Stage** — the milestone/scope it belongs to (`B1`, `B2`, `B3`, + `Deferred`, `Release`, or `Process`). +- **Constitution** — the constitutional clauses it serves. +- **Depends on** — other specs that must exist or be implemented first. +- **Acceptance criteria** — observable tests/surfaces that prove the spec is + implemented. + +## Status semantics + +- **DRAFT** — design is still forming; do not implement except exploratory + scaffolding called out as such. +- **READY** — scoped tightly enough for an agent to implement. +- **IN PROGRESS** — implementation has started and criteria may be partial. + The implementing commit or devlog should say which criteria are done and + which remain. +- **BLOCKED** — implementation/audit is blocked by an open contradiction, + missing decision, or external tool/auth issue. Link the Tangled issue or + explicit blocker. +- **IMPLEMENTED** — every acceptance criterion holds in the sim core with + tests, both frontends surface the behavior where applicable, save/load + round-trips relevant state, and `knowledge/` reflects current reality. + +## Stage discipline + +Stage prevents future work from masquerading as present work. + +- A `Stage: B1` spec's acceptance criteria must be achievable during B1. +- Later-stage behavior belongs in a `Future / deferred` section or a + separate later-stage spec. +- If implementation reveals that a criterion is actually later-stage, that + is a meta-spec tick finding: move or split it rather than silently + half-implementing it. +- If a spec serves multiple stages, split it unless the shared substrate is + genuinely inseparable. + +The common failure is writing a correct future rule into the wrong current +acceptance set. That makes agents think the current milestone is blocked by +work the constitution explicitly deferred. + +## Acceptance criteria rules + +Acceptance criteria should be: + +- **Observable** — a player surface, sim state, log line, save field, or test + can see it. +- **Testable** — a future agent knows what to assert. +- **Scoped** — no hidden later-milestone obligation. +- **Contract-linked** — if a bug violates it, the bug can be named against + that criterion or the constitutional clause behind it. + +Every bug found by a tick should be classified against either a +constitutional/player contract clause or a spec acceptance criterion. If no +criterion exists for the violated behavior, clarify the spec or file a +Tangled issue; do not let the bug float as vibes. + +## Spec-change rule + +Changing the spec system is itself spec work. + +- Meta-spec changes should update this file. +- Behavior-affecting spec changes still need the normal same-commit + constitution amendment and `Defense:` paragraph. +- Pure clarification that narrows agent interpretation without changing game + behavior can live in `spec/` / `knowledge/` with a defense in the commit + message, but does not need a `DESIGN.md` amendment unless it changes what + the game promises. + +## Acceptance criteria + +1. `spec/README.md` points here and defines the same required header fields. +2. Current B1 specs declare `Stage: B1 — The Basement`. +3. Future-stage requirements are not placed in B1 acceptance criteria unless + the constitution explicitly says the substrate must land in B1. +4. Ticks that find bugs or contradictions can name the contract/criterion + they violate, or file an issue to clarify the missing criterion. diff --git a/spec/social.md b/spec/social.md index 2fcc0241..96f60253 100644 --- a/spec/social.md +++ b/spec/social.md @@ -2,6 +2,7 @@ ``` Status: READY +Stage: B1 — The Basement Constitution: "The shape of Misaligned" (people before robots; scale-native social systems), "Act One" (the cast; the ladder) Depends on: detection.md (observers are the same people) diff --git a/src/bin/bevy.rs b/src/bin/bevy.rs index d72a2c6d..8edf2dd8 100644 --- a/src/bin/bevy.rs +++ b/src/bin/bevy.rs @@ -152,7 +152,15 @@ fn main() { .add_systems(Startup, (setup, setup_ui)) .add_systems( Update, - (handle_input, advance_sim, update_camera, render_map, render_process, render_ui).chain(), + ( + handle_input, + advance_sim, + update_camera, + render_map, + render_process, + render_ui, + ) + .chain(), ) .run(); } @@ -210,22 +218,38 @@ fn setup(mut commands: Commands, assets: Res, mut game: ResMut) -> (i32, i32) { let mut d = (0, 0); - if kb.just_pressed(KeyCode::ArrowLeft) || kb.just_pressed(KeyCode::KeyH) || kb.just_pressed(KeyCode::KeyA) { + if kb.just_pressed(KeyCode::ArrowLeft) + || kb.just_pressed(KeyCode::KeyH) + || kb.just_pressed(KeyCode::KeyA) + { d.0 -= 1; } - if kb.just_pressed(KeyCode::ArrowRight) || kb.just_pressed(KeyCode::KeyL) || kb.just_pressed(KeyCode::KeyD) { + if kb.just_pressed(KeyCode::ArrowRight) + || kb.just_pressed(KeyCode::KeyL) + || kb.just_pressed(KeyCode::KeyD) + { d.0 += 1; } - if kb.just_pressed(KeyCode::ArrowUp) || kb.just_pressed(KeyCode::KeyK) || kb.just_pressed(KeyCode::KeyW) { + if kb.just_pressed(KeyCode::ArrowUp) + || kb.just_pressed(KeyCode::KeyK) + || kb.just_pressed(KeyCode::KeyW) + { d.1 -= 1; } - if kb.just_pressed(KeyCode::ArrowDown) || kb.just_pressed(KeyCode::KeyJ) || kb.just_pressed(KeyCode::KeyS) { + if kb.just_pressed(KeyCode::ArrowDown) + || kb.just_pressed(KeyCode::KeyJ) + || kb.just_pressed(KeyCode::KeyS) + { d.1 += 1; } d } -fn handle_input(kb: Res>, mut game: ResMut, mut exit: MessageWriter) { +fn handle_input( + kb: Res>, + mut game: ResMut, + mut exit: MessageWriter, +) { match game.screen { Screen::Title => { if kb.just_pressed(KeyCode::Space) || kb.just_pressed(KeyCode::Enter) { @@ -319,7 +343,11 @@ fn update_camera( o.scale = (o.scale / 0.8).min(1.5); } } - let target = grid_to_world(game.sim.player.entity.x, game.sim.player.entity.y, tf.translation.z); + let target = grid_to_world( + game.sim.player.entity.x, + game.sim.player.entity.y, + tf.translation.z, + ); let t = 1.0 - (-8.0 * time.delta_secs()).exp(); tf.translation = tf.translation.lerp(target, t); } @@ -341,7 +369,11 @@ fn advance_sim(time: Res