Skip to main content

manabrew_engine/combat/
attack_requirement.rs

1use std::collections::HashMap;
2
3use super::DefenderId;
4use crate::card::Card;
5use crate::ids::{CardId, PlayerId};
6use crate::staticability::static_ability_must_attack;
7
8/// Represents a requirement for a creature to attack.
9/// Mirrors Java's `AttackRequirement.java`.
10#[derive(Debug, Clone)]
11pub struct AttackRequirement {
12    /// The creature that must attack.
13    pub attacker: CardId,
14    /// True if the creature must attack any legal defender.
15    pub must_attack_any: bool,
16    /// If set, the creature must attack this specific defender (if able).
17    pub must_attack_defender: Option<PlayerId>,
18    /// The player that goaded this creature (it can't attack that player).
19    pub goaded_by: Option<PlayerId>,
20    /// Per-defender requirement counts: defender → number of reasons to attack it.
21    /// Mirrors Java's `defenderSpecific` map.
22    pub defender_specific: HashMap<DefenderId, i32>,
23}
24
25impl AttackRequirement {
26    /// Mirrors Java's `hasRequirement()`.
27    /// Returns true if this creature has any reason it must attack.
28    pub fn has_requirement(&self) -> bool {
29        self.defender_specific.values().any(|&v| v > 0) || self.must_attack_any
30    }
31
32    /// Mirrors Java's `countViolations()`.
33    /// Count how many attack requirements are violated if this creature is
34    /// attacking `defender` (or `None` if not attacking at all).
35    pub fn count_violations(&self, defender: Option<DefenderId>) -> i32 {
36        if !self.has_requirement() {
37            return 0;
38        }
39
40        let total: i32 = self.defender_specific.values().sum();
41        let is_attacking = defender.is_some();
42
43        let credit = if is_attacking {
44            defender
45                .and_then(|d| self.defender_specific.get(&d).copied())
46                .unwrap_or(0)
47        } else {
48            0
49        };
50
51        total - credit
52    }
53
54    /// Get sorted requirements: (defender, count) pairs sorted ascending.
55    /// Mirrors Java's `getSortedRequirements()`.
56    pub fn get_sorted_requirements(&self) -> Vec<(DefenderId, i32)> {
57        let mut entries: Vec<(DefenderId, i32)> = self
58            .defender_specific
59            .iter()
60            .map(|(&d, &c)| (d, c))
61            .collect();
62        entries.sort_by_key(|&(_, c)| c);
63        entries
64    }
65}
66
67/// Compute attack requirements for all available creatures.
68/// Returns a list of requirements — creatures that must attack if able.
69///
70/// Sources of must-attack:
71/// 1. Static abilities with `MustAttack` mode (existing `must_attack()` check)
72/// 2. Goad: creature is goaded and must attack a player other than the goader
73pub fn compute_attack_requirements(
74    cards: &[Card],
75    available: &[CardId],
76    defending: PlayerId,
77) -> Vec<AttackRequirement> {
78    compute_attack_requirements_with_defenders(cards, available, &[DefenderId::Player(defending)])
79}
80
81/// Compute attack requirements with a full list of possible defenders.
82pub fn compute_attack_requirements_with_defenders(
83    cards: &[Card],
84    available: &[CardId],
85    possible_defenders: &[DefenderId],
86) -> Vec<AttackRequirement> {
87    let mut requirements = Vec::new();
88
89    for &attacker_id in available {
90        let card = &cards[attacker_id.index()];
91
92        let must_from_static = static_ability_must_attack::must_attack(cards, card);
93        let goaded = card.goaded_by;
94
95        let must_attack_any = must_from_static || goaded.is_some();
96
97        // Build defender_specific map: each defender gets credit for
98        // generic "must attack anything" requirements.
99        let mut n_attack_anything: i32 = 0;
100        if goaded.is_some() {
101            n_attack_anything += 1;
102        }
103        if must_from_static {
104            n_attack_anything += 1;
105        }
106
107        let mut defender_specific = HashMap::new();
108        for &defender in possible_defenders {
109            defender_specific.insert(defender, n_attack_anything);
110        }
111
112        let goaded_by_player = goaded;
113        let defending = possible_defenders
114            .iter()
115            .find_map(|d| d.as_player())
116            .unwrap_or(PlayerId(0));
117        let must_attack_defender = if goaded.is_some() && goaded != Some(defending) {
118            Some(defending)
119        } else {
120            None
121        };
122
123        if must_attack_any || !defender_specific.is_empty() {
124            requirements.push(AttackRequirement {
125                attacker: attacker_id,
126                must_attack_any,
127                must_attack_defender,
128                goaded_by: goaded_by_player,
129                defender_specific,
130            });
131        }
132    }
133
134    requirements
135}
136
137/// Get all creature IDs that must attack (from requirements).
138pub fn must_attack_ids(requirements: &[AttackRequirement]) -> Vec<CardId> {
139    requirements
140        .iter()
141        .filter(|r| r.must_attack_any)
142        .map(|r| r.attacker)
143        .collect()
144}