Skip to main content

manabrew_engine/combat/
attack_restriction.rs

1use std::collections::HashSet;
2
3use forge_foundation::ZoneType;
4
5use crate::card::Card;
6use crate::ids::CardId;
7use crate::staticability::StaticMode;
8
9pub use super::attack_restriction_type::AttackRestrictionType;
10
11/// Parse attack restrictions from a creature's keywords.
12/// Mirrors Java's `AttackRestriction.setRestrictions()` — matches exact keyword strings.
13pub fn get_restrictions(card: &Card) -> HashSet<AttackRestrictionType> {
14    let mut restrictions = HashSet::new();
15
16    for kw in card
17        .keywords
18        .iter_strings()
19        .chain(card.granted_keywords.iter_strings())
20        .chain(card.pump_keywords.iter_strings())
21    {
22        // Java matches on exact keyword strings with "CARDNAME" prefix stripped.
23        // We use contains() on lowercased text for flexibility.
24        let kw_lower = kw.to_lowercase();
25
26        if kw_lower.contains("can only attack alone") {
27            restrictions.insert(AttackRestrictionType::OnlyAlone);
28        }
29        if kw_lower.contains("can't attack alone")
30            || kw_lower.contains("can't attack or block alone")
31        {
32            restrictions.insert(AttackRestrictionType::NotAlone);
33        }
34        if kw_lower.contains("can't attack unless a creature with greater power also attacks") {
35            restrictions.insert(AttackRestrictionType::NeedGreaterPower);
36        }
37        if kw_lower.contains("can't attack unless a black or green creature also attacks") {
38            restrictions.insert(AttackRestrictionType::NeedBlackOrGreen);
39        }
40        if kw_lower.contains("can't attack unless at least two other creatures attack") {
41            restrictions.insert(AttackRestrictionType::NeedTwoOthers);
42        }
43    }
44
45    // Check static abilities for CantAttack with restriction subtypes
46    for st_ab in &card.static_abilities {
47        if st_ab.check_mode(&StaticMode::CantAttack) {
48            if let Some(restriction) = st_ab.ir.restriction_text.as_deref() {
49                match restriction {
50                    "OnlyAlone" => {
51                        restrictions.insert(AttackRestrictionType::OnlyAlone);
52                    }
53                    "NotAlone" => {
54                        restrictions.insert(AttackRestrictionType::NotAlone);
55                    }
56                    "NeedGreaterPower" => {
57                        restrictions.insert(AttackRestrictionType::NeedGreaterPower);
58                    }
59                    "NeedBlackOrGreen" => {
60                        restrictions.insert(AttackRestrictionType::NeedBlackOrGreen);
61                    }
62                    "NeedTwoOthers" => {
63                        restrictions.insert(AttackRestrictionType::NeedTwoOthers);
64                    }
65                    "Never" => {
66                        restrictions.insert(AttackRestrictionType::Never);
67                    }
68                    _ => {}
69                }
70            }
71        }
72    }
73
74    // Java parity: contradictory restrictions mean creature can never attack.
75    // OnlyAlone + any of (NeedGreaterPower, NeedBlackOrGreen, NotAlone, NeedTwoOthers) = impossible.
76    if restrictions.contains(&AttackRestrictionType::OnlyAlone)
77        && (restrictions.contains(&AttackRestrictionType::NeedGreaterPower)
78            || restrictions.contains(&AttackRestrictionType::NeedBlackOrGreen)
79            || restrictions.contains(&AttackRestrictionType::NotAlone)
80            || restrictions.contains(&AttackRestrictionType::NeedTwoOthers))
81    {
82        restrictions.insert(AttackRestrictionType::Never);
83    }
84
85    restrictions
86}
87
88/// Check if a creature can attack given its restrictions and the number of
89/// other attackers. Mirrors Java's `AttackRestriction.canAttack()`.
90pub fn can_attack(card: &Card, num_attackers: usize) -> bool {
91    let restrictions = get_restrictions(card);
92
93    if restrictions.contains(&AttackRestrictionType::Never) {
94        return false;
95    }
96    if restrictions.contains(&AttackRestrictionType::OnlyAlone) && num_attackers > 1 {
97        return false;
98    }
99    if restrictions.contains(&AttackRestrictionType::NotAlone) && num_attackers <= 1 {
100        return false;
101    }
102    if restrictions.contains(&AttackRestrictionType::NeedTwoOthers) && num_attackers <= 2 {
103        return false;
104    }
105    true
106}
107
108/// Validate chosen attackers against attack restrictions.
109/// Returns a set of attacker IDs that are illegal and should be removed.
110///
111/// Mirrors Java's `AttackRestriction.getViolation()` — checks restrictions
112/// against the set of all chosen attackers (not just battlefield state).
113pub fn validate_attack_restrictions(attackers: &[CardId], cards: &[Card]) -> HashSet<CardId> {
114    let mut illegal = HashSet::new();
115    let num_attackers = attackers.len();
116
117    for &attacker_id in attackers {
118        let card = &cards[attacker_id.index()];
119        if card.zone != ZoneType::Battlefield {
120            illegal.insert(attacker_id);
121            continue;
122        }
123        let restrictions = get_restrictions(card);
124
125        // Never: can never attack
126        if restrictions.contains(&AttackRestrictionType::Never) {
127            illegal.insert(attacker_id);
128            continue;
129        }
130
131        // OnlyAlone: can only attack if it's the sole attacker
132        if restrictions.contains(&AttackRestrictionType::OnlyAlone) && num_attackers > 1 {
133            illegal.insert(attacker_id);
134            continue;
135        }
136
137        // NotAlone: can't attack alone
138        if restrictions.contains(&AttackRestrictionType::NotAlone) && num_attackers <= 1 {
139            illegal.insert(attacker_id);
140            continue;
141        }
142
143        // NeedTwoOthers: needs at least two other attackers
144        if restrictions.contains(&AttackRestrictionType::NeedTwoOthers)
145            && (num_attackers as i32 - 1) < 2
146        {
147            illegal.insert(attacker_id);
148            continue;
149        }
150
151        // NeedGreaterPower: another *attacking* creature must have greater power.
152        // Java: checks attackers.keySet() with predicate hasGreaterPowerThan(attacker.getNetPower()).
153        if restrictions.contains(&AttackRestrictionType::NeedGreaterPower) {
154            let my_power = card.power();
155            let has_greater = attackers.iter().any(|&other_id| {
156                other_id != attacker_id && cards[other_id.index()].power() > my_power
157            });
158            if !has_greater {
159                illegal.insert(attacker_id);
160                continue;
161            }
162        }
163
164        // NeedBlackOrGreen: another *attacking* creature must be black or green.
165        if restrictions.contains(&AttackRestrictionType::NeedBlackOrGreen) {
166            let has_bg = attackers.iter().any(|&other_id| {
167                if other_id == attacker_id {
168                    return false;
169                }
170                let other = &cards[other_id.index()];
171                other.color.has_black() || other.color.has_green()
172            });
173            if !has_bg {
174                illegal.insert(attacker_id);
175                continue;
176            }
177        }
178    }
179
180    // Second pass: re-check after removing illegals (for NotAlone/NeedTwoOthers counts)
181    let remaining: Vec<CardId> = attackers
182        .iter()
183        .copied()
184        .filter(|id| !illegal.contains(id))
185        .collect();
186    let remaining_count = remaining.len();
187
188    for &attacker_id in &remaining {
189        let card = &cards[attacker_id.index()];
190        let restrictions = get_restrictions(card);
191
192        if restrictions.contains(&AttackRestrictionType::NotAlone) && remaining_count <= 1 {
193            illegal.insert(attacker_id);
194        }
195        if restrictions.contains(&AttackRestrictionType::NeedTwoOthers)
196            && (remaining_count as i32 - 1) < 2
197        {
198            illegal.insert(attacker_id);
199        }
200    }
201
202    illegal
203}