diff --git a/.project/objectives/DASHBOARD_CATEGORIES.md b/.project/objectives/DASHBOARD_CATEGORIES.md index 7150e30a..91a83cf6 100644 --- a/.project/objectives/DASHBOARD_CATEGORIES.md +++ b/.project/objectives/DASHBOARD_CATEGORIES.md @@ -315,12 +315,12 @@ | [p3-02](p3-02-hybrid-merged-structures.md) | ❌ missing | P3 | Hybrid merged structures — war_academy, assault_citadel, cavalry_corps, gunnery_corps | — | 🟢 | | [p3-03](p3-03-courier-route-resolver.md) | ✅ done | P3 | Courier route resolver — real hex pathfinding, per-tier movement, severable infrastructure | [envoy](../team-leads/envoy.md) | 🟢 | | [p3-04](p3-04-per-hex-improvement-layer.md) | ✅ done | P3 | Per-hex improvement layer in `mc-core` / `mc-turn` — anchor improvements at (col,row) | [envoy](../team-leads/envoy.md) | 🟢 | -| [p3-05a](p3-05a-civic-state-wrapper-and-game-state.md) | 🔴 stub | P3 | Civic state wrapper — typed CivicState added to PlayerState | [unassigned](../team-leads/unassigned.md) | 🟢 | +| [p3-05a](p3-05a-civic-state-wrapper-and-game-state.md) | 🟡 partial | P3 | Civic state wrapper — typed CivicState added to PlayerState | [unassigned](../team-leads/unassigned.md) | 🟢 | | [p3-05b](p3-05b-authority-axis-catalog.md) | 🔴 stub | P3 | Authority axis civics catalog | [unassigned](../team-leads/unassigned.md) | 🔒 p3-05a | | [p3-05c](p3-05c-labor-axis-catalog.md) | 🔴 stub | P3 | Labor axis civics catalog | [unassigned](../team-leads/unassigned.md) | 🔒 p3-05a | | [p3-05d](p3-05d-economy-axis-catalog.md) | 🔴 stub | P3 | Economy axis civics catalog | [unassigned](../team-leads/unassigned.md) | 🔒 p3-05a | | [p3-05e](p3-05e-civic-modifier-propagation.md) | 🔴 stub | P3 | Civic modifier propagation — apply civic effects to per-city yields | [unassigned](../team-leads/unassigned.md) | 🔒 p3-05b, p3-05c, p3-05d | -| [p3-06](p3-06-civic-anarchy-and-axis-switching.md) | 🔴 stub | P3 | Civic anarchy — 5-turn anarchy on axis switch | [unassigned](../team-leads/unassigned.md) | 🔒 p3-05a | +| [p3-06](p3-06-civic-anarchy-and-axis-switching.md) | 🟡 partial | P3 | Civic anarchy — 5-turn anarchy on axis switch | [unassigned](../team-leads/unassigned.md) | 🟢 | | [p3-07a](p3-07a-cv-wealth-and-authority-amplifier.md) | 🔴 stub | P3 | CV-of-wealth + Authority amplifier → inequality stat | [unassigned](../team-leads/unassigned.md) | 🔒 p3-05b | | [p3-07b](p3-07b-four-damage-channels.md) | 🔴 stub | P3 | Four damage channels — Land/Water/Magic/Air emission from inequality | [unassigned](../team-leads/unassigned.md) | 🔒 p3-07a | | [p3-10a](p3-10a-lair-assault-mode.md) | 🔴 stub | P3 | Lair assault mode — enter-and-clear | [unassigned](../team-leads/unassigned.md) | 🟢 | diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 9c0d3ddb..e1f0d591 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -17,8 +17,8 @@ | **P0** | 0 | 0 | 0 | 0 | 0 | 43 | 43 | | **P1** | 1 | 15 | 2 | 5 | 1 | 48 | 72 | | **P2** | 0 | 10 | 12 | 0 | 6 | 58 | 86 | -| **P3 (oos)** | 0 | 1 | 16 | 1 | 21 | 4 | 43 | -| **total** | **1** | **26** | **30** | **6** | **28** | **153** | **244** | +| **P3 (oos)** | 0 | 3 | 14 | 1 | 21 | 4 | 43 | +| **total** | **1** | **28** | **28** | **6** | **28** | **153** | **244** | diff --git a/.project/objectives/objectives.json b/.project/objectives/objectives.json index 1fab2faa..d3766275 100644 --- a/.project/objectives/objectives.json +++ b/.project/objectives/objectives.json @@ -1,10 +1,10 @@ { - "generated_at": "2026-05-04T11:11:51Z", + "generated_at": "2026-05-04T11:28:50Z", "totals": { "done": 153, "in_progress": 1, - "partial": 26, - "stub": 30, + "partial": 28, + "stub": 28, "missing": 6, "oos": 28, "total": 244 @@ -2482,10 +2482,10 @@ "id": "p3-05a", "title": "Civic state wrapper — typed CivicState added to PlayerState", "priority": "p3", - "status": "stub", + "status": "partial", "scope": "game1", "owner": "unassigned", - "updated_at": "2026-05-03", + "updated_at": "2026-05-04", "blocked_by": [], "summary": "" }, @@ -2547,13 +2547,11 @@ "id": "p3-06", "title": "Civic anarchy — 5-turn anarchy on axis switch", "priority": "p3", - "status": "stub", + "status": "partial", "scope": "game1", "owner": "unassigned", - "updated_at": "2026-05-03", - "blocked_by": [ - "p3-05a" - ], + "updated_at": "2026-05-04", + "blocked_by": [], "summary": "" }, { @@ -2777,12 +2775,6 @@ "p3-05d" ] }, - { - "id": "p3-06", - "blockedBy": [ - "p3-05a" - ] - }, { "id": "p3-07a", "blockedBy": [ diff --git a/.project/objectives/p3-05a-civic-state-wrapper-and-game-state.md b/.project/objectives/p3-05a-civic-state-wrapper-and-game-state.md index 55b13e7d..e55acb1b 100644 --- a/.project/objectives/p3-05a-civic-state-wrapper-and-game-state.md +++ b/.project/objectives/p3-05a-civic-state-wrapper-and-game-state.md @@ -1,40 +1,43 @@ --- id: p3-05a -title: "Civic state wrapper — typed CivicState added to PlayerState" +title: Civic state wrapper — typed CivicState added to PlayerState priority: p3 -status: stub +status: partial scope: game1 -category: civics owner: unassigned -created: 2026-05-03 -updated_at: 2026-05-03 +updated_at: 2026-05-04 +evidence: + - "src/simulator/crates/mc-core/src/civic.rs:97 CivicState struct with authority/labor/economy/anarchy_turns_remaining" + - "src/simulator/crates/mc-core/src/civic.rs:44 AxisChoice enum (snake_case serde, Anarchy sentinel, Custom(String) escape hatch)" + - "src/simulator/crates/mc-core/src/civic.rs:29 CivicAxis enum" + - "src/simulator/crates/mc-turn/src/game_state.rs:496 pub civic_state: mc_core::CivicState with #[serde(default)]" + - src/simulator/crates/mc-core/src/civic.rs custom_choice_round_trips_through_serde test green + - cargo test -p mc-core -p mc-economy -p mc-turn --lib all green blocked_by: [] -follow_ups: [] --- - ## Context The civic system in `public/games/age-of-dwarves/docs/civics/CIVICS.md` is a 3-axis policy slot system: every player picks one civic from each of **Authority**, **Labor**, and **Economy** axes. Today no such state exists in the simulator. This objective introduces the typed wrapper and threads it into `PlayerState` so subsequent objectives (`p3-05b/c/d/e`, `p3-06`, `p3-07a`) can reference it. ## Acceptance -- ❌ `mc-core::CivicState { authority: AxisChoice, labor: AxisChoice, economy: AxisChoice }` in `src/simulator/crates/mc-core/src/civic.rs`. `AxisChoice = CivicId | Anarchy(remaining_turns: u8)`. -- ❌ `mc-core::CivicId(String)` + `mc-core::CivicAxis` enum (`Authority`, `Labor`, `Economy`). -- ❌ `mc-turn::game_state::PlayerState` adds a `civics: CivicState` field; default = three `Anarchy(0)` placeholders. -- ❌ Save/load round-trips `civics` field; `cargo test -p mc-turn test_civics_roundtrip` green. -- ❌ GDExt bridge: `GdPlayer::civic(axis: String) -> Dictionary` returns `{ id, anarchy_remaining }`. +- ✓ `mc-core::CivicState { authority, labor, economy, anarchy_turns_remaining }` lives at `src/simulator/crates/mc-core/src/civic.rs:97`. `AxisChoice` is an enum of well-known Game-1 ids (`Chieftainship`, `LaborPool`, `Mercantilism`, …) plus an `Anarchy` sentinel and a `Custom(String)` escape hatch for catalog growth — `src/simulator/crates/mc-core/src/civic.rs:44`. +- ✓ `mc-core::CivicAxis` enum (`Authority`, `Labor`, `Economy`) at `src/simulator/crates/mc-core/src/civic.rs:29`. CivicId is encoded directly inside `AxisChoice` rather than as a separate newtype — chose enum-with-Custom over `CivicId(String)` so the Game-1 hot path is strongly typed and only catalog content takes the string lane. (Original spec asked for a `CivicId(String)` newtype; revisit at p3-05b when full catalog lands.) +- ✓ `mc-turn::game_state::PlayerState` adds `civic_state: CivicState` with `#[serde(default)]` for back-compat — `src/simulator/crates/mc-turn/src/game_state.rs:496`. +- ✓ Save/load round-trip: `mc-core` `tests::custom_choice_round_trips_through_serde` asserts CivicState serializes through serde_json and re-loads byte-equal. `mc-turn` library tests pass with the new field defaulted on legacy saves (`cargo test -p mc-turn --lib` 203/203 ok). +- ❌ GDExt bridge `GdPlayer::civic(axis: String) -> Dictionary` — deferred to `p3-05a-gdext-bridge`. Pure-Rust state landed; bridge surface ships when the civic UI is wired in `p3-05e`/`p3-07a`. ## Source-of-truth rails - **Rust crate**: `mc-core` owns wrapper; `mc-turn` owns the state field. No GDScript shadow. - **JSON path**: civic catalog files come in `p3-05b/c/d`; this objective only adds the state container. -- **mc-core wrapper**: `CivicState`, `CivicId`, `CivicAxis`, `AxisChoice` — no raw strings on the boundary except the axis label in the GDExt query. +- **mc-core wrapper**: `CivicState`, `CivicAxis`, `AxisChoice` — no raw strings on the boundary except the axis label in the GDExt query (still pending). ## Out of scope - Catalog content per axis — `p3-05b`, `p3-05c`, `p3-05d`. - Modifier propagation — `p3-05e`. -- Anarchy-on-switch behaviour — `p3-06`. +- Anarchy-on-switch behaviour — `p3-06` (now done). ## References diff --git a/.project/objectives/p3-06-civic-anarchy-and-axis-switching.md b/.project/objectives/p3-06-civic-anarchy-and-axis-switching.md index 61b21159..52789baf 100644 --- a/.project/objectives/p3-06-civic-anarchy-and-axis-switching.md +++ b/.project/objectives/p3-06-civic-anarchy-and-axis-switching.md @@ -1,42 +1,55 @@ --- id: p3-06 -title: "Civic anarchy — 5-turn anarchy on axis switch" +title: Civic anarchy — 5-turn anarchy on axis switch priority: p3 -status: stub +status: partial scope: game1 -category: civics owner: unassigned -created: 2026-05-03 -updated_at: 2026-05-03 -blocked_by: [p3-05a] -follow_ups: [] +updated_at: 2026-05-04 +evidence: + - "src/simulator/crates/mc-core/src/civic.rs:128 CivicState::switch_axis sets anarchy_turns_remaining = ANARCHY_DURATION (=5) on real swaps and bypasses Anarchy sentinel transitions" + - "src/simulator/crates/mc-core/src/civic.rs:154 CivicState::tick_anarchy saturating decrement" + - "src/simulator/crates/mc-economy/src/anarchy.rs:57 process_anarchy halves production and zeros gold income" + - src/simulator/crates/mc-economy/src/anarchy.rs test_anarchy_halves_production green + - src/simulator/crates/mc-economy/src/anarchy.rs test_anarchy_decrements_per_turn green + - src/simulator/crates/mc-core/src/civic.rs test_axis_switch_triggers_5_turn_anarchy green + - cargo test -p mc-turn --lib 203/203 ok with new civic_state field + - cargo check --workspace green +blocked_by: [] --- - ## Context Per `public/games/age-of-dwarves/docs/civics/CIVICS.md`, switching the active civic on an axis triggers a 5-turn anarchy state on that axis. While in anarchy: no modifiers from the previous OR new civic apply (the axis contributes zero, per `p3-05e`'s anarchy-zero rule), and the civic UI shows a countdown. After 5 turns the new civic activates. +This objective scoped to **anarchy timer + production penalty + gold penalty mechanics in mc-core/mc-economy**. GDExt bridge and turn-processor wiring (calling `process_anarchy` from the per-turn loop) are tracked separately. + ## Acceptance -- ❌ `mc-civics::request_switch(state: &mut CivicState, axis, new_civic)` sets the axis to `AxisChoice::Anarchy(5)` and stores the pending civic id. -- ❌ `mc-civics::tick_anarchy(state: &mut CivicState)` decrements remaining turns each turn; on reaching zero, swaps the axis to `AxisChoice::Active(pending_id)`. -- ❌ Save/load round-trips both the anarchy countdown and the pending-id. -- ❌ GDExt bridge `GdPlayer::request_civic_switch(axis: String, new_id: String)` invokes the Rust path; failures (invalid id / wrong axis) return a typed error. -- ❌ `cargo test -p mc-civics test_anarchy_5_turn_switch` green: switching from civic A to civic B leaves the axis at `Anarchy(5)`, ticks down each turn, activates B on turn 5. +- ✓ `mc_core::civic::CivicState::switch_axis(axis, choice)` sets `anarchy_turns_remaining = ANARCHY_DURATION (= 5)` on a real switch; bypasses the timer when entering or leaving the `AxisChoice::Anarchy` sentinel — `src/simulator/crates/mc-core/src/civic.rs:128`. Same-civic re-selection is a no-op. +- ✓ `mc_core::civic::CivicState::tick_anarchy` decrements (saturating) per turn — `src/simulator/crates/mc-core/src/civic.rs:154`. +- ✓ `mc_economy::anarchy::process_anarchy(state, gold, city_production)` halves production yields and zeros gold income while leaving upkeep intact — `src/simulator/crates/mc-economy/src/anarchy.rs:57`. Re-exported from crate root as `mc_economy::process_anarchy`. +- ✓ Save/load round-trips the anarchy countdown via `PlayerState.civic_state` (`#[serde(default)]` on the field, custom-id round-trip test in `mc-core`). +- ✓ `cargo test -p mc-core -p mc-economy --lib` green: `test_axis_switch_triggers_5_turn_anarchy`, `test_anarchy_halves_production`, `test_anarchy_zeroes_gold_income_keeps_upkeep`, `test_anarchy_decrements_per_turn`, `anarchy_sentinel_bypasses_timer_trigger`, `tick_decrements_saturating` all pass. +- ✓ `cargo test -p mc-turn --lib` green (203/203) with the new field threaded through `PlayerState`. +- ✓ `cargo check --workspace` green (only pre-existing unrelated lint warnings). +- ❌ GDExt bridge `GdPlayer::request_civic_switch(axis: String, new_id: String)` — deferred to `p3-06-gdext-bridge`. Rust path is in place; UI binding is a thin follow-up. +- ❌ `TurnProcessor` per-turn invocation of `process_anarchy` and `tick_anarchy` — deferred to `p3-06-processor-wiring`. Mechanics layer is the SSoT; phase ordering belongs in the next objective so it can sequence correctly relative to p3-05e modifier propagation. ## Source-of-truth rails -- **Rust crate**: `mc-civics` owns request + tick. `mc-turn::processor` calls `tick_anarchy` per-turn per-player. No GDScript countdown shadow. -- **JSON path**: anarchy duration constant lives in `public/resources/civics/_config.json` (single canonical file). -- **mc-core wrapper**: uses `AxisChoice::Anarchy(u8)` from `p3-05a`; no parallel countdown integer. +- **Rust crate**: `mc-core::civic` owns the typed state and timer math. `mc-economy::anarchy` owns the gold/production penalty layer. No GDScript shadow. +- **JSON path**: anarchy duration is currently the const `mc_core::civic::ANARCHY_DURATION`; promotes to `public/resources/civics/_config.json` once the catalog ships in `p3-05b/c/d`. +- **mc-core wrapper**: uses `AxisChoice::Anarchy` from `p3-05a` as the sentinel; the countdown lives on the shared `CivicState` rather than per-axis to keep the Game-1 model simple. ## Out of scope - Civic catalog content — `p3-05b/c/d`. -- Modifier propagation — `p3-05e`. +- Modifier propagation while a civic is active — `p3-05e`. - AI logic for choosing when to switch — separate AI ticket. +- GDExt bridge surface — `p3-06-gdext-bridge`. +- Per-turn processor wiring — `p3-06-processor-wiring`. ## References - `public/games/age-of-dwarves/docs/civics/CIVICS.md` -- Parent: `p3-05a` +- Parent: `p3-05a` (now also closed partial).