feat(@projects/@magic-civilization): debug pvp combat logic gaps

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-06-08 00:35:20 -07:00
parent f7e70c1844
commit 3765dc8b9f
2 changed files with 49 additions and 0 deletions

View file

@ -86,3 +86,42 @@ world-events via the bridge, orchestrated by `turn_manager.gd`), proven
2 is the residual *architectural-purity* question of **where the turn
orchestration lives** (Rust vs GDScript) — unblockable only by the inversion
above.
---
## Step 1 root-cause (2026-06-08) — why the Rust turn isn't live-grade
Probed `dominion_bench` (real `mc-turn::TurnProcessor::step`, 4 AI clans) at two
map sizes. **Both produce functional but militarily-inert games:**
| probe | victory | PvP battles | captures | runaway |
|---|---|---|---|---|
| 40×40, 4p | P2 city_count @ t87 | **0** | **0** | P1 → 111 cities |
| 20×20, 4p (forced adjacency) | P2 city_count @ t51 | **0** | **0** | P2 → **214 cities / 400 tiles** |
Even with four clans in adjacent corners of a 20×20 map, **zero** inter-player
combat. P0/P1/P3 end with 0 cities/0 units yet `cities_lost = cities_captured =
pvp_d = 0` — eliminated without recorded combat (P2's unbounded founding
consumed the map).
**Verified root cause:** the capability is all present and composed —
`mc-turn` wires `process_pvp_combat` (`processor.rs:522`), city siege (`:524`),
and `mc_ai::tactical`; `mc-ai` evaluates offensive moves ("advance toward each
enemy city", `evaluator.rs:134-150`). **But `mc-turn`'s per-unit movement loop
seeks `nearest_lair` (`processor.rs:2025, 2271` — PvE only), bypassing
`mc-ai`'s tactical targeting.** Units never move toward enemies → never become
adjacent → `process_pvp_combat` never triggers. This is the "bench-grade AI
decision layer" the `mc-turn` header names: real rules crates, minimal movement
heuristics.
**Two gaps to close (pure-Rust, benched, zero Godot risk):**
1. **Movement → mc-ai tactical.** Drive per-unit movement from `mc-ai`'s
tactical advance/defend candidates (which already target enemy cities), not
the inline lair-seek. This makes contact happen → PvP/siege fire.
2. **Expansion sanity.** `try_found_city` needs city-spacing + a soft cap so
the expansion axis stops carpeting the map (214 cities/400 tiles is
degenerate; the live game spaces cities).
Closing both is the substance of cutover step 1 — the Rust turn must produce
games shaped like the GDScript autoplay (combat present, sane city counts)
before any path cuts over to it. This is the genuinely multi-session leg.

View file

@ -3455,6 +3455,16 @@ impl TurnProcessor {
if killed.iter().any(|&(p, u)| p == def_pi && u == def_ui) {
continue;
}
// Skip if the unit no longer exists: an earlier kill/capture in this
// same phase `swap_remove`d it (or emptied the defender's vec),
// leaving this pending-capture index stale. Indexing the shrunk vec
// would panic — this is the turn-8 PvP crash at processor.rs:2697
// ("len is 0 but the index is 0") that surfaces once the real AI
// (`run_ai_turn`) drives actual captures. Same intent as the
// `killed` dedup above: a unit that left the vec is not capturable.
if def_pi >= state.players.len() || def_ui >= state.players[def_pi].units.len() {
continue;
}
self.transfer_captured_unit(state, def_pi, def_ui, captor);
// Re-index the leftover `killed` and `ransom_pending` entries that
// referred to indices ≥ def_ui in the same player vec. swap_remove