fix(@projects/@magic-civilization): 🧹 is_at_war defaults absent relation to PEACE (p3-16 cleanup)

movement.rs::is_at_war defaulted a missing relation slot to `true` (at war) —
the legacy p1-01 "missing → war" model that courier-diplomacy (p3-16) supersedes:
every pair starts at PEACE, war begins only on a war-dec envelope. An absent slot
now defaults to peace (`map_or(false, …)`), so the AI never treats a
not-yet-projected pair as a phantom war. project_tactical_relations fills the vec
from real courier state, so genuine wars are unaffected. mc-ai+mc-player-api 556/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-25 00:43:21 -04:00
parent 6a4d87a029
commit c8896d50c9

View file

@ -381,20 +381,20 @@ fn is_military(unit: &TacticalUnit) -> bool {
// ── Enemy / diplomacy enumeration ────────────────────────────────────────
pub(super) fn is_at_war(me: &TacticalPlayerState, opponent_index: u8) -> bool {
// Canonical model is courier-diplomacy: pairs start at PEACE and war
// begins when a player dispatches a war-dec envelope (COMMUNICATIONS.md
// §"War declaration semantics"; p1-01's "missing → war" is superseded,
// see p3-16). `project_tactical_relations` fills the relations vec, so a
// slot is normally present; the `map_or(true, …)` fallback only fires for
// a genuinely absent slot and is left open so a not-yet-projected pair
// never silently blocks retaliation.
// Canonical model is courier-diplomacy: every pair starts at PEACE and war
// begins only when a player dispatches a war-dec envelope (COMMUNICATIONS.md
// §"War declaration semantics"). p1-01's legacy "missing relation → war" is
// SUPERSEDED (p3-16): an absent slot now defaults to PEACE, so the AI never
// treats a not-yet-projected pair as a phantom war. `project_tactical_relations`
// fills the vec from the courier relation state, so real wars are present;
// `< 0` is the at-war marker.
if (opponent_index as usize) == (me.index as usize) {
return false;
}
me.relations
.get(opponent_index as usize)
.copied()
.map_or(true, |r| r < 0)
.map_or(false, |r| r < 0)
}
fn collect_enemy_units<'a>(