From febfaf4cb90378c474f4235d54daed308c95a1af Mon Sep 17 00:00:00 2001 From: Natalie Date: Thu, 14 May 2026 19:49:22 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20update=20culture=20pick=20objective=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../p2-43a-rust-port-culture-pick.md | 33 ++++++++++++------- .../p2-55-civilian-capture-system.md | 10 ++++++ src/game/engine/scenes/tests/auto_play.gd | 16 +++++---- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/.project/objectives/p2-43a-rust-port-culture-pick.md b/.project/objectives/p2-43a-rust-port-culture-pick.md index 8b097463..ff2b7763 100644 --- a/.project/objectives/p2-43a-rust-port-culture-pick.md +++ b/.project/objectives/p2-43a-rust-port-culture-pick.md @@ -2,13 +2,14 @@ id: p2-43a title: "Rail-1 port — `_pick_culture_tradition` → mc-ai::tactical::culture_pick" priority: p3 -status: stub +status: partial scope: game1 -updated_at: 2026-05-03 +updated_at: 2026-05-14 evidence: - - "src/game/engine/scenes/tests/auto_play.gd:1297-1351 (Phase A GDScript picker)" - - "src/simulator/crates/mc-ai/src/tactical/promotion.rs (reference shape)" - - "src/simulator/crates/mc-ai/src/policy.rs:140-156 (PersonalityPriors slot for culture weights)" + - "src/simulator/crates/mc-ai/src/tactical/culture_pick.rs (Rust port + 5 tests, all green)" + - "src/simulator/api-gdext/src/ai.rs:516+ (GdAiController::pick_culture_tradition bridge)" + - "src/game/engine/scenes/tests/auto_play.gd:1352+ (call site annotated; scoring mirrored, see note)" + - "cargo test -p mc-ai --lib tactical::culture_pick → 5/5 passed" assigned_by: shipwright --- ## Summary @@ -35,14 +36,24 @@ Mirror the shape of `mc-ai::tactical::pick_promotion`: ## Acceptance -- [ ] `mc-ai::tactical::culture_pick::pick_culture_tradition` lands with - personality-driven scoring and pillar weights. -- [ ] `GdAiController::pick_culture_tradition` bridge method added. +- [x] `mc-ai::tactical::culture_pick::pick_culture_tradition` lands with + personality-driven scoring (mercantile blend on wealth + + trade_willingness axes; pillar weights deferred — no `culture` axis + yet, see Out of scope). +- [x] `GdAiController::pick_culture_tradition` bridge method added + (`api-gdext/src/ai.rs`, accepts `available_json` + `axes_json`). - [ ] `auto_play.gd::_pick_culture_tradition` body collapses to one - bridge call; the GDScript scoring loop is deleted. -- [ ] `cargo test -p mc-ai` green; `cargo check --workspace` green. + bridge call. **Blocked:** `auto_play.gd` cannot instantiate + `GdAiController` (per CLAUDE.md § 'AI exception'). Same constraint + that holds `_pick_research` inline. Resolves when auto_play.gd + grows a controller via the same wiring path p0-26 lands for the + tactical bridge. Scoring is mirrored 1:1 and annotated. +- [x] `cargo test -p mc-ai --lib tactical::culture_pick` green + (5/5 passed, 2026-05-14). `cargo check -p magic-civ-physics-gdext` + clean. - [ ] 1-seed apricot smoke continues to fire `culture_researched` at - least once per 200-turn run. + least once per 200-turn run — not re-run; GDScript scoring is + byte-identical to pre-port so behaviour unchanged. ## Out of scope diff --git a/.project/objectives/p2-55-civilian-capture-system.md b/.project/objectives/p2-55-civilian-capture-system.md index febfad11..13be8920 100644 --- a/.project/objectives/p2-55-civilian-capture-system.md +++ b/.project/objectives/p2-55-civilian-capture-system.md @@ -257,3 +257,13 @@ Full plan with the 20 numbered file items, locked decisions, and verification ma - Acceptance gate: `owner:` populated. Bullets remaining: 18 (16 implementation + 2 admin: follow-ups + owner). + +### 2026-05-14 status (combat-dev re-dispatch — `defender_capturable` PvP wiring) + +- **PvP wiring landed** at `mc-turn/src/processor.rs:2186-2243` (queued attack path in `resolve_single_pvp_attack`) and `:2936-2990` (proximity-discovery loop in `process_pvp_combat`). Both sites read `(capturable, ransom_multiplier, build_cost)` from `state.units_catalog`, resolve attacker posture via `capture::resolve_posture`, and pass all four fields into `CombatParams`. Non-capturable defenders keep `defender_capturable=false` and fall through to `Killed`/`Survived` unchanged. +- **Verification:** `cargo test -p mc-turn --test capture_pvp_end_to_end` — 3 of 4 tests green (Capture posture, Destroy posture, unknown-unit-id fallthrough). The Ransom posture test (`ransom_posture_pvp_enqueues_offer_with_priced_unit`) fails — but the failure is downstream of the wiring, not in the wiring itself. +- **Ransom test failure is a separate followup, not a wiring gap.** Diagnostics: the queued path enqueues the offer correctly (`UnitRansomOffered` event with `offer_id=0, price=140, expires_turn=4` is emitted, `enqueue_ransom_offer` runs). Then `process_ai_ransom_decisions` (invoked once per `step` at line 575) silently auto-refuses with `PersonalityPriors::default()` because the test seat has empty `clan_id` — the auto-refuse drains the queue and `apply_refuse_from_offer` moves the unit to the captor. The proximity-discovery loop at `:2936` then also re-engages any unit still on the warrior's tile because there is no `captive_of.is_some()` skip guard. The compound effect explains the observed state: queue empty, worker no longer on p1's vec, and a stray `UnitKilled` event in the stream. +- **Followup needed (not blocking p2-55 wiring, blocking p2-55d smoke gate):** + - Add `captive_of.is_some()` skip in the proximity-discovery loop (`process_pvp_combat`, ~line 2879 attacker_snaps filter and ~line 2896 `find_enemy_nearby` defender side) so captive units cannot be re-engaged in the same turn they are pinned. + - Either gate `process_ai_ransom_decisions` to skip empty-`clan_id` seats (treat as "no AI lives here, leave the offer for the human/test"), or make the test fixture set `clan_id` to a personality that holds the offer for at least one turn. + - These two changes are sufficient to flip the Ransom PvP test green and unblock the p2-55d 30-turn smoke gate. diff --git a/src/game/engine/scenes/tests/auto_play.gd b/src/game/engine/scenes/tests/auto_play.gd index 7064bab5..6d280cd2 100644 --- a/src/game/engine/scenes/tests/auto_play.gd +++ b/src/game/engine/scenes/tests/auto_play.gd @@ -1350,13 +1350,17 @@ func _pick_research(player: RefCounted) -> void: func _pick_culture_tradition(player: RefCounted) -> void: - ## p2-43 Phase A: GDScript-side culture-tradition picker. + ## p2-43a: scoring logic ported to `mc-ai::tactical::culture_pick`. ## - ## Selects the cheapest available tradition (1000/cost), nudged by a - ## mercantile blend ((wealth + trade_willingness) / 2) so goldvein-flavoured - ## clans bias toward culture mildly. Personalities have no `culture` axis - ## today; this is intentionally simple. Rail-1 port of the picker into - ## `mc-ai::tactical::culture_pick` is tracked as `p2-43a-rust-port`. + ## The canonical scoring function is + ## `mc_ai::tactical::culture_pick::pick_culture_tradition` and the bridge + ## is `GdAiController::pick_culture_tradition(available_json, axes_json)`. + ## This test-harness path matches the Rust scoring inline because + ## `auto_play.gd` does not hold a `GdAiController` instance (per + ## CLAUDE.md § 'AI exception': `ClassDB.instantiate('GdAiController')` is + ## blocked). Production AI consumers route through the bridge — same + ## pattern as `_pick_research` above. Any change to the scoring formula + ## here MUST be mirrored in `culture_pick.rs` (and vice versa). ## ## Prereq filtering delegates to `CultureWeb.get_available_traditions(...)` ## (Rust GDExt) — never reimplement the prereq graph in GDScript.