Skip to main content

manabrew_engine/agent/
attach_ai.rs

1//! AI logic for equipment attachment decisions.
2//!
3//! Mirrors Java `forge/forge-ai/src/main/java/forge/ai/ability/AttachAi.java`.
4//! Determines whether the AI should offer an equip ability or skip it.
5
6use forge_foundation::ZoneType;
7
8use crate::card::Card;
9use crate::game::GameState;
10use crate::ids::{CardId, PlayerId};
11
12use super::creature_evaluator;
13
14/// Default threshold for re-equipping: the new creature must score at least
15/// this many points better than the currently equipped creature.
16///
17/// Mirrors Java `AiProps.MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD` (default 40).
18const MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD: i32 = 40;
19
20/// Returns `true` if the equip ability on `equipment_id` should be skipped
21/// (i.e., the AI would not play it because re-equipping is not worthwhile).
22///
23/// Mirrors Java `AttachAi.attachToCardAIPreferences()` lines 1348-1394:
24/// - Don't equip the same creature again
25/// - Don't move equipment if the current creature is still useful and no
26///   significantly better target exists
27/// - Don't move equipment if it was already moved this turn
28///
29/// Returns `false` (don't skip) when:
30/// - Equipment is not currently attached (needs equipping)
31/// - Equipment is attached to opponent's creature (need to re-equip)
32/// - There's a significantly better creature to equip to
33pub fn should_skip_equip(game: &GameState, equipment_id: CardId, player: PlayerId) -> bool {
34    let equipment = game.card(equipment_id);
35
36    // If equipment is not currently attached, always offer equip
37    let equipped_to = match equipment.attached_to {
38        Some(id) => id,
39        None => return false,
40    };
41
42    let equipped_creature = game.card(equipped_to);
43
44    // If equipped creature is not on the battlefield, don't skip (need to re-equip)
45    if equipped_creature.zone != ZoneType::Battlefield {
46        return false;
47    }
48
49    // If equipped to opponent's creature, don't skip (want to re-equip to own)
50    if equipped_creature.controller != player {
51        return false;
52    }
53
54    // If equipment was already moved this turn, skip to avoid ping-ponging.
55    // Mirrors Java `AiCardMemory.isRememberedCard(ATTACHED_THIS_TURN)`.
56    if equipment.attached_this_turn {
57        return true;
58    }
59
60    // Evaluate the currently equipped creature
61    let current_eval = creature_evaluator::evaluate_creature(equipped_creature);
62    let current_is_useless = creature_evaluator::is_useless_creature(equipped_creature);
63
64    // If current creature is useless, allow re-equip (don't skip)
65    if current_is_useless {
66        return false;
67    }
68
69    // Check if any valid target creature is significantly better.
70    // Mirrors Java: `evaluateCreature(new) - evaluateCreature(old) >= threshold`
71    let battlefield = game.cards_in_zone(ZoneType::Battlefield, player);
72    let has_better_target = battlefield.iter().any(|&cid| {
73        if cid == equipped_to {
74            return false; // Don't compare to self
75        }
76        let candidate = game.card(cid);
77        if !candidate.is_creature() {
78            return false;
79        }
80        if candidate.zone != ZoneType::Battlefield {
81            return false;
82        }
83        let candidate_eval = creature_evaluator::evaluate_creature(candidate);
84        candidate_eval - current_eval >= MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD
85    });
86
87    // If no significantly better target, skip equip
88    !has_better_target
89}
90
91/// Check if an activated ability is an equip/attach ability.
92///
93/// Identifies equip abilities by checking if the ability text contains
94/// "Attach" as the API type and the source card is an Equipment.
95pub fn is_equip_ability(card: &Card, ability_text: &str) -> bool {
96    if !card.type_line.has_subtype("Equipment") {
97        return false;
98    }
99    // Check for AB$ Attach (equip activated ability)
100    ability_text.contains("AB$ Attach")
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn threshold_is_positive() {
109        assert!(MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD > 0);
110    }
111}