feat(@projects/@magic-civilization): update culture pick objective status

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-14 19:49:22 -07:00
parent bf9295a5d9
commit febfaf4cb9
3 changed files with 42 additions and 17 deletions

View file

@ -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

View file

@ -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.

View file

@ -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.