From 52207e6ab9db50ed4a342239b89bbcb5196253b3 Mon Sep 17 00:00:00 2001 From: Natalie Date: Wed, 13 May 2026 16:48:32 -0700 Subject: [PATCH] =?UTF-8?q?fix(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=90=9B=20update=20ai=20ransom=20hook=20status=20and=20acc?= =?UTF-8?q?eptance=20criteria?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../p2-55d-ai-ransom-decision-hook.md | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/.project/objectives/p2-55d-ai-ransom-decision-hook.md b/.project/objectives/p2-55d-ai-ransom-decision-hook.md index 28fee056..1600a8cc 100644 --- a/.project/objectives/p2-55d-ai-ransom-decision-hook.md +++ b/.project/objectives/p2-55d-ai-ransom-decision-hook.md @@ -2,14 +2,16 @@ id: p2-55d title: "AI ransom accept/refuse hook in mc-turn start-of-turn" priority: p2 -status: stub +status: partial scope: game1 category: combat owner: created: 2026-05-03 -updated_at: 2026-05-03 +updated_at: 2026-05-13 blocked_by: [] -follow_ups: [] +follow_ups: + - "Bridge sets MC_AI_DATA_DIR env var on startup (api-gdext / claude_player_main) — without it the hook silently refuses every offer in production. Currently only mc-turn integration tests pin the var." + - "30-turn raider-vs-merchant smoke run — blocked on the upstream PvP `defender_capturable=false` wiring gap documented at the top of `mc-turn/tests/capture_chronicle_pipeline.rs`." parent: p2-55 --- @@ -21,12 +23,23 @@ This objective wires the call site so AI personalities exercise their `ransom_ac ## Acceptance criteria -- [ ] `mc-turn::processor` start-of-turn phase iterates `state.ransom_queue.pending_for(ai_id)` for each non-human player. -- [ ] For each offer: looks up that AI's `PersonalityPriors`, calls `decide_ransom_response(offer.price, ai_player.gold, &priors)`. -- [ ] On `RansomDecision::Accept` and `ai_player.gold >= offer.price`: deducts gold, restores `MapUnit.owner_id` to original owner, clears `captive_of`, calls `queue.accept(offer.id)`, emits `UnitRansomAccepted` event. -- [ ] On `RansomDecision::Refuse` (or insufficient gold): calls `queue.refuse(offer.id)`, transfers unit ownership to captor, clears `captive_of`, emits `UnitRansomExpired` event. -- [ ] Integration test in `mc-turn/tests/ransom_ai.rs`: spin up 2 AI players (raider + merchant), push an offer, advance one turn, assert the merchant accepted (gold sufficient) while the raider would refuse if they were the owner. -- [ ] 30-turn raider-vs-merchant smoke run produces a non-zero count of `UnitRansomAccepted` events in the chronicle. +- [x] `mc-turn::processor` start-of-turn phase iterates `state.ransom_queue.pending_for(ai_id)` for each non-human player. — `processor::process_ai_ransom_decisions` (called at the tail of `step`, after `recharge_action_points`). Game 1 has no human-player marker in mc-turn so the hook iterates every slot; a future `is_human` gate is documented inline at the call site. +- [x] For each offer: looks up that AI's `PersonalityPriors`, calls `decide_ransom_response(offer.price, ai_player.gold, &priors)`. — Priors resolved via `mc_ai::tactical::ransom_decision::priors_for_clan(clan_id, MC_AI_DATA_DIR)`; decisions delegated to `decide_for_offers` which wraps `decide_ransom_response`. Empty `clan_id` / unset env var falls back to `PersonalityPriors::default()` (silent refuse) without panic. +- [x] On `RansomDecision::Accept` and `ai_player.gold >= offer.price`: deducts gold, clears `captive_of`, calls `queue.accept(offer.id)`, emits `UnitRansomAccepted` event. — `apply_ransom_accept` in processor.rs. (Spec said "restores `MapUnit.owner_id`"; in this codebase units are owned via residency in `PlayerState::units`, and accept keeps them in the owner's vec — `captive_of` is the only field cleared, matching the existing Wave 1 contract.) +- [x] On `RansomDecision::Refuse` (or insufficient gold): calls `queue.refuse(offer.id)`, transfers unit ownership to captor, clears `captive_of`, emits `UnitRansomExpired` event. — `apply_ransom_refuse` → `apply_refuse_from_offer`. Emits both `UnitRansomExpiredEvent` and `UnitCapturedEvent` plus the `TurnEvent::UnitCaptured` chronicle entry to mirror `process_ransom_expiry`. +- [x] Integration test in `mc-turn/tests/ransom_ai.rs`. — Three tests (`goldvein_merchant_accepts_when_solvent`, `blackhammer_raider_refuses_when_solvent_but_priors_say_no`, `unset_data_dir_falls_back_to_silent_refuse`) all pass; env-var mutation serialised through a `Mutex` so they coexist in the integration-test binary's parallel scheduler. +- [ ] 30-turn raider-vs-merchant smoke run produces a non-zero count of `UnitRansomAccepted` events in the chronicle. — Not yet wired: there is no harness in this crate that runs a 30-turn AI-vs-AI game with `defender_capturable=true` in PvP combat (see the wiring-gap note at the top of `capture_chronicle_pipeline.rs`). The unit-level acceptance bullet above proves the hook fires; the longer smoke run waits on that bridge wiring (tracked as a follow-up on the parent p2-55 closure note). + +## Implementation notes (2026-05-13) + +- New file `mc-ai/src/tactical/ransom_decision.rs` (the only file-disjoint location available per the objective's edit charter) with 5 lib tests (all green). +- `mc-ai/src/tactical/mod.rs` registers the new submodule (one-line touch — unavoidable for module declaration). +- `mc-turn/src/processor.rs` appends one call line in `step()` and three private associated fns (`process_ai_ransom_decisions`, `apply_ransom_accept`, `apply_ransom_refuse`, `apply_refuse_from_offer`). No edits to `GameState` or to existing phase handlers. +- Priors loader uses env var `MC_AI_DATA_DIR` — the Godot bridge will need to set this on startup for the production path to fire (currently silently refuses without it). That env-var plumbing is the gap holding the last acceptance bullet open. + +## Status rationale + +5/6 acceptance bullets ticked with cited evidence; status stays `partial` per `objective-integrity.md` rather than `done`. The remaining bullet (30-turn smoke run) depends on the upstream PvP-capture wiring gap noted in `capture_chronicle_pipeline.rs`; it is not a defect in this objective. ## Out of scope