fix(@projects/@magic-civilization): 🐛 fix panic in turn processor

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-06-08 00:56:57 -07:00
parent 3765dc8b9f
commit 9e7566a628
3 changed files with 45 additions and 9 deletions

View file

@ -125,3 +125,37 @@ heuristics.
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.
---
## Step 1 RESULT (2026-06-08) — Rust turn + real AI = live-grade. Foundation de-risked.
The step-1 question ("can the Rust turn be the authoritative turn?") is answered
**yes, with evidence.**
**Bug fixed:** `mc-turn` panicked at `processor.rs:2697` on turn 8 whenever the
real AI (`run_ai_turn`) drove an actual capture — a stale capture index into a
vec emptied by an earlier same-phase kill/capture. Fixed with a bounds guard
matching the existing `killed`-dedup intent (committed `e21381037`). 234 mc-turn
tests pass; the one failing test (`five_players_overflow`) is pre-existing
(fails identically on HEAD, unrelated — chip filed).
**Viability proven:** with the panic fixed, the previously-blocked 250-turn
`long_game_transcript` (3 clans, slots 1+2 = real `run_ai_turn`) runs to turn 249
and produces a **live-grade game**: **2531 units killed**, 50 cities founded,
slot 1 winning by military dominance (47 cities / 439 units) while slot 0 is
conquered from 8 cities down to 1/0. Real combat, conquest, city loss, a winner
by force.
**The decisive contrast:** `dominion_bench` (inline `nearest_lair` AI) → **0**
PvP, 214-city runaway. `long_game_transcript` (`run_ai_turn`) → **2531 kills**,
47-city winner. **Same `mc-turn::step`, different AI.** The gap to live-grade was
never the rules or the turn processor — it is purely that the bench (and any
future authoritative path) must drive `run_ai_turn` via the controller registry,
not `mc-turn`'s inline stub.
**Next increment (cutover proper):** route the bench / authoritative path through
`mc_player_api`'s controller dispatch (`drive_ai_slot``run_ai_turn`),
**retiring `mc-turn`'s inline `nearest_lair` movement** (dead-stub removal,
no-debt). Then expansion-pacing reconciliation if 47 cities still reads high vs
the GDScript autoplay on a matched map.

View file

@ -33,9 +33,9 @@ use crate::game_state::{GameState, MapUnit, PlayerState, TechState};
/// [`AbstractRolloutState`].
///
/// Player slots are filled in `state.players` order, capped at
/// [`MAX_PLAYERS`] (= 5). Excess players are dropped — Game 1 ships 5-clan
/// max so this overflow path should never trigger. Missing slots stay
/// zero-initialised (the POD's `Zeroable` default).
/// [`MAX_PLAYERS`] (= 12, the `huge` map's `max_players`). Excess players are
/// dropped without panic. Missing slots stay zero-initialised (the POD's
/// `Zeroable` default).
///
/// Determinism: same `GameState` → byte-identical POD. The only RNG-touching
/// field is `rng_state`, derived via `derive_step(SeedDomain::AiRollout, …)`.

View file

@ -216,23 +216,25 @@ fn four_player_projection_fills_every_slot() {
}
}
// ── 4. Five-player overflow ─────────────────────────────────────────────────
// ── 4. Player overflow ──────────────────────────────────────────────────────
#[test]
fn five_players_overflow_truncates_to_max_players() {
fn overflow_truncates_to_max_players() {
// Push MAX_PLAYERS + 1 players to exercise the truncation path: the POD has
// exactly MAX_PLAYERS (= 12, the `huge` map's max_players) slots, so the
// excess player must be dropped without panic or overflow.
let mut state = GameState::default();
for i in 0..5u8 {
for i in 0..(MAX_PLAYERS as u8 + 1) {
let mut p = player(i);
p.gold = 7;
state.players.push(p);
}
let pod = to_abstract_rollout_state(&state);
// Only the first MAX_PLAYERS=4 slots are populated.
// All MAX_PLAYERS slots are populated from the first MAX_PLAYERS players.
for i in 0..MAX_PLAYERS {
assert_eq!(pod.players[i].gold, 7, "slot {i} not populated");
}
// The 5th player is silently dropped — no panic, no overflow.
// (Only 4 POD slots exist; nothing more to assert.)
// The (MAX_PLAYERS+1)th player is silently dropped — no panic, no overflow.
}
// ── 5. Determinism ──────────────────────────────────────────────────────────