Skip to main content

manabrew_engine/combat/
attack_constraints.rs

1use std::collections::HashMap;
2
3use super::attack_requirement::{self, AttackRequirement};
4use super::attack_restriction::{self, AttackRestrictionType};
5use super::global_attack_restrictions::GlobalAttackRestrictions;
6use super::DefenderId;
7use crate::card::Card;
8use crate::ids::{CardId, PlayerId};
9
10/// Constraints on which creatures can and must attack.
11/// Mirrors Java's `AttackConstraints.java`.
12///
13/// Holds per-creature restrictions (can't attack conditions) and requirements
14/// (must-attack conditions), plus global attack limits.
15#[derive(Debug, Clone)]
16pub struct AttackConstraints {
17    pub possible_attackers: Vec<CardId>,
18    pub possible_defenders: Vec<DefenderId>,
19    pub global_restrictions: GlobalAttackRestrictions,
20    pub restrictions: HashMap<CardId, AttackRestriction>,
21    pub requirements: HashMap<CardId, AttackRequirement>,
22}
23
24/// Per-creature attack restriction state.
25/// Mirrors Java's `AttackRestriction.java`.
26#[derive(Debug, Clone)]
27pub struct AttackRestriction {
28    pub attacker: CardId,
29    pub types: std::collections::HashSet<AttackRestrictionType>,
30    pub cant_attack: bool,
31    pub cant_attack_defenders: Vec<DefenderId>,
32}
33
34impl AttackRestriction {
35    pub fn new(
36        attacker: CardId,
37        cards: &[Card],
38        possible_defenders: &[DefenderId],
39        game: &crate::game::GameState,
40    ) -> Self {
41        let card = &cards[attacker.index()];
42        let types = attack_restriction::get_restrictions(card);
43
44        // Check which defenders this creature can't attack
45        let mut cant_attack_defenders = Vec::new();
46        for &defender in possible_defenders {
47            if !super::combat_util::can_attack_defender(game, attacker, defender) {
48                cant_attack_defenders.push(defender);
49            }
50        }
51
52        // Java parity: contradictory restrictions or can't-attack-any-defender = can't attack
53        let cant_attack = types.contains(&AttackRestrictionType::Never)
54            || (types.contains(&AttackRestrictionType::OnlyAlone)
55                && (types.contains(&AttackRestrictionType::NeedGreaterPower)
56                    || types.contains(&AttackRestrictionType::NeedBlackOrGreen)
57                    || types.contains(&AttackRestrictionType::NotAlone)
58                    || types.contains(&AttackRestrictionType::NeedTwoOthers)))
59            || cant_attack_defenders.len() == possible_defenders.len();
60
61        AttackRestriction {
62            attacker,
63            types,
64            cant_attack,
65            cant_attack_defenders,
66        }
67    }
68
69    /// Can this creature attack the given defender?
70    pub fn can_attack(&self, defender: DefenderId) -> bool {
71        !self.cant_attack && !self.cant_attack_defenders.contains(&defender)
72    }
73
74    /// Can this creature attack the given defender with the given set of
75    /// attackers? Checks both per-defender and per-attacker-set violations.
76    pub fn can_attack_with(
77        &self,
78        defender: DefenderId,
79        attackers: &[(CardId, DefenderId)],
80        cards: &[Card],
81    ) -> bool {
82        if !self.can_attack(defender) {
83            return false;
84        }
85        self.get_violations(attackers, cards).is_empty()
86    }
87
88    /// Get restriction type violations for the given set of attackers.
89    pub fn get_violations(
90        &self,
91        attackers: &[(CardId, DefenderId)],
92        cards: &[Card],
93    ) -> std::collections::HashSet<AttackRestrictionType> {
94        let mut violations = std::collections::HashSet::new();
95        let n = attackers.len();
96
97        if self.types.contains(&AttackRestrictionType::OnlyAlone) && n > 1 {
98            violations.insert(AttackRestrictionType::OnlyAlone);
99        }
100        if self.types.contains(&AttackRestrictionType::NotAlone) && n <= 1 {
101            violations.insert(AttackRestrictionType::NotAlone);
102        }
103        if self.types.contains(&AttackRestrictionType::NeedTwoOthers) && n <= 2 {
104            violations.insert(AttackRestrictionType::NeedTwoOthers);
105        }
106        if self
107            .types
108            .contains(&AttackRestrictionType::NeedGreaterPower)
109        {
110            let my_power = cards[self.attacker.index()].power();
111            let has_greater = attackers
112                .iter()
113                .any(|&(cid, _)| cid != self.attacker && cards[cid.index()].power() > my_power);
114            if !has_greater {
115                violations.insert(AttackRestrictionType::NeedGreaterPower);
116            }
117        }
118        if self
119            .types
120            .contains(&AttackRestrictionType::NeedBlackOrGreen)
121        {
122            let has_bg = attackers.iter().any(|&(cid, _)| {
123                cid != self.attacker && {
124                    let c = &cards[cid.index()];
125                    c.color.has_black() || c.color.has_green()
126                }
127            });
128            if !has_bg {
129                violations.insert(AttackRestrictionType::NeedBlackOrGreen);
130            }
131        }
132        violations
133    }
134
135    pub fn get_types(&self) -> &std::collections::HashSet<AttackRestrictionType> {
136        &self.types
137    }
138}
139
140/// Internal helper: an attack candidate with priority from requirements.
141#[derive(Debug, Clone)]
142struct Attack {
143    attacker: CardId,
144    defender: DefenderId,
145    requirements: i32,
146}
147
148impl AttackConstraints {
149    /// Build constraints for a combat phase.
150    /// `attacking_player` is the player declaring attacks.
151    pub fn new(
152        game: &crate::game::GameState,
153        attacking_player: PlayerId,
154        possible_defenders: &[DefenderId],
155    ) -> Self {
156        let possible_attackers: Vec<CardId> = game
157            .creatures_on_battlefield(attacking_player)
158            .into_iter()
159            .collect();
160
161        let global_restrictions = GlobalAttackRestrictions::get_global_restrictions(
162            &game.cards,
163            attacking_player,
164            possible_defenders,
165        );
166
167        let mut restrictions = HashMap::new();
168        let mut requirements = HashMap::new();
169
170        for &attacker in &possible_attackers {
171            restrictions.insert(
172                attacker,
173                AttackRestriction::new(attacker, &game.cards, possible_defenders, game),
174            );
175        }
176
177        let reqs = attack_requirement::compute_attack_requirements_with_defenders(
178            &game.cards,
179            &possible_attackers,
180            possible_defenders,
181        );
182        for req in reqs {
183            requirements.insert(req.attacker, req);
184        }
185
186        AttackConstraints {
187            possible_attackers,
188            possible_defenders: possible_defenders.to_vec(),
189            global_restrictions,
190            restrictions,
191            requirements,
192        }
193    }
194
195    pub fn get_restrictions(&self) -> &HashMap<CardId, AttackRestriction> {
196        &self.restrictions
197    }
198
199    pub fn get_global_restrictions(&self) -> &GlobalAttackRestrictions {
200        &self.global_restrictions
201    }
202
203    pub fn get_requirements(&self) -> &HashMap<CardId, AttackRequirement> {
204        &self.requirements
205    }
206
207    /// Get a set of legal attackers that minimizes requirement violations.
208    /// Returns `(attack_map, violation_count)`.
209    ///
210    /// Mirrors Java's `getLegalAttackers()`. This is a simplified port that
211    /// handles the most common cases (single-creature requirements, global
212    /// limits). The full recursive constraint solver from Java is replaced
213    /// with a greedy approach that works correctly for 2-player games.
214    pub fn get_legal_attackers(&self, cards: &[Card]) -> (Vec<(CardId, DefenderId)>, i32) {
215        let max = self
216            .global_restrictions
217            .get_max()
218            .unwrap_or(i32::MAX)
219            .min(self.possible_attackers.len() as i32);
220
221        if max == 0 {
222            return (Vec::new(), 0);
223        }
224
225        // Build sorted requirement list (highest priority first)
226        let mut reqs = self.get_sorted_filtered_requirements(cards);
227
228        // Remove creatures that can't possibly attack
229        reqs.retain(|a| {
230            let restriction = self.restrictions.get(&a.attacker);
231            if let Some(r) = restriction {
232                if r.cant_attack {
233                    return false;
234                }
235                let types = &r.types;
236                // Creatures with unfulfillable co-attacker requirements
237                if (types.contains(&AttackRestrictionType::NeedTwoOthers) && max <= 2)
238                    || (types.contains(&AttackRestrictionType::NotAlone) && max <= 1)
239                    || (types.contains(&AttackRestrictionType::NeedBlackOrGreen) && max <= 1)
240                    || (types.contains(&AttackRestrictionType::NeedGreaterPower) && max <= 1)
241                {
242                    return false;
243                }
244            }
245            true
246        });
247
248        // Try "only alone" creatures first (they must attack solo)
249        let mut best_result: Option<(Vec<(CardId, DefenderId)>, i32)> = None;
250
251        for req in &reqs {
252            if let Some(r) = self.restrictions.get(&req.attacker) {
253                if r.types.contains(&AttackRestrictionType::OnlyAlone) && req.requirements > 0 {
254                    let attack_map = vec![(req.attacker, req.defender)];
255                    let violations = self.count_violations(&attack_map, cards);
256                    if violations != -1 {
257                        match &best_result {
258                            None => best_result = Some((attack_map, violations)),
259                            Some((_, best_v)) if violations < *best_v => {
260                                best_result = Some((attack_map, violations));
261                            }
262                            _ => {}
263                        }
264                    }
265                }
266            }
267        }
268
269        // Remove only-alone attackers from normal pool
270        reqs.retain(|a| {
271            self.restrictions
272                .get(&a.attacker)
273                .is_none_or(|r| !r.types.contains(&AttackRestrictionType::OnlyAlone))
274        });
275
276        // Greedy: add creatures with requirements in priority order
277        let mut attack_map: Vec<(CardId, DefenderId)> = Vec::new();
278        let mut used: std::collections::HashSet<CardId> = std::collections::HashSet::new();
279        let mut remaining_max = max;
280
281        for req in &reqs {
282            if remaining_max <= 0 {
283                break;
284            }
285            if used.contains(&req.attacker) {
286                continue;
287            }
288            if req.requirements == 0 {
289                continue;
290            }
291
292            // Check per-defender limit
293            if let Some(&def_max) = self
294                .global_restrictions
295                .get_defender_max()
296                .get(&req.defender)
297            {
298                let count = attack_map
299                    .iter()
300                    .filter(|(_, d)| *d == req.defender)
301                    .count() as i32;
302                if count >= def_max {
303                    continue;
304                }
305            }
306
307            attack_map.push((req.attacker, req.defender));
308            used.insert(req.attacker);
309            remaining_max -= 1;
310        }
311
312        let greedy_violations = self.count_violations(&attack_map, cards);
313        if greedy_violations != -1 {
314            match &best_result {
315                None => best_result = Some((attack_map.clone(), greedy_violations)),
316                Some((_, best_v)) if greedy_violations < *best_v => {
317                    best_result = Some((attack_map.clone(), greedy_violations));
318                }
319                _ => {}
320            }
321        }
322
323        // Also try empty attack
324        let empty_violations = self.count_violations(&[], cards);
325        if empty_violations != -1 {
326            match &best_result {
327                None => best_result = Some((Vec::new(), empty_violations)),
328                Some((_, best_v)) if empty_violations < *best_v => {
329                    best_result = Some((Vec::new(), empty_violations));
330                }
331                _ => {}
332            }
333        }
334
335        best_result.unwrap_or((Vec::new(), 0))
336    }
337
338    /// Count the number of requirement violations for a proposed attack set.
339    /// Returns -1 if a restriction is violated (illegal attack).
340    ///
341    /// Mirrors Java's `countViolations()`.
342    pub fn count_violations(&self, attackers: &[(CardId, DefenderId)], cards: &[Card]) -> i32 {
343        if !self.global_restrictions.is_legal(attackers) {
344            return -1;
345        }
346
347        // Check per-creature restrictions
348        for &(attacker_id, defender) in attackers {
349            if let Some(restriction) = self.restrictions.get(&attacker_id) {
350                if !restriction.can_attack_with(defender, attackers, cards) {
351                    return -1;
352                }
353            }
354        }
355
356        // Count requirement violations
357        let mut violations = 0;
358        for &possible_attacker in &self.possible_attackers {
359            if let Some(requirement) = self.requirements.get(&possible_attacker) {
360                let defender = attackers
361                    .iter()
362                    .find(|(a, _)| *a == possible_attacker)
363                    .map(|(_, d)| *d);
364                violations += requirement.count_violations(defender);
365            }
366        }
367
368        violations
369    }
370
371    /// Build a sorted list of attack candidates from requirements.
372    /// Higher-priority (more requirements) come first.
373    fn get_sorted_filtered_requirements(&self, _cards: &[Card]) -> Vec<Attack> {
374        let mut result = Vec::new();
375
376        for (&attacker_id, req) in &self.requirements {
377            let restriction = self.restrictions.get(&attacker_id);
378            let sorted_reqs = req.get_sorted_requirements();
379
380            for (defender, count) in sorted_reqs {
381                let can_attack = restriction.is_none_or(|r| r.can_attack(defender));
382                if can_attack {
383                    result.push(Attack {
384                        attacker: attacker_id,
385                        defender,
386                        requirements: count,
387                    });
388                }
389            }
390        }
391
392        // Sort descending by requirements (highest priority first)
393        result.sort_by(|a, b| b.requirements.cmp(&a.requirements));
394        result
395    }
396}