1use forge_foundation::ZoneType;
3
4use super::attack_constraints::AttackConstraints;
5use super::{CombatState, DefenderId, LureType};
6use crate::card::{valid_filter, Card};
7use crate::game::GameState;
8use crate::ids::{CardId, PlayerId};
9use crate::staticability::static_ability::StaticMode;
10use crate::staticability::static_ability_cant_attack_block;
11
12pub fn get_available_attackers(game: &GameState, player: PlayerId) -> Vec<CardId> {
13 let defending = game.opponent_of(player);
14 game.creatures_on_battlefield(player)
15 .into_iter()
16 .filter(|&cid| {
17 let card = game.card(cid);
18 if card.can_attack() {
19 return true;
20 }
21 card.is_creature()
22 && !card.tapped
23 && !card.cant_attack_static
24 && !card.detained
25 && (card.has_haste() || !card.summoning_sick)
26 && card.zone == ZoneType::Battlefield
27 && card.has_defender()
28 && crate::staticability::static_ability_can_attack_defender::can_attack_defender(
29 &game.cards,
30 card,
31 defending,
32 )
33 })
34 .collect()
35}
36
37pub fn can_attack_player(game: &GameState, player: PlayerId) -> bool {
38 !get_available_attackers(game, player).is_empty()
39}
40
41pub fn can_attack_defender(game: &GameState, attacker_id: CardId, defender: DefenderId) -> bool {
42 let card = game.card(attacker_id);
43
44 if !card.is_creature() || card.tapped || card.phased_out {
46 return false;
47 }
48 if card.summoning_sick && !card.has_haste() {
50 return false;
51 }
52
53 if card.cant_attack_static {
55 return false;
56 }
57 if card.detained {
58 return false;
59 }
60
61 if let DefenderId::Player(pid) = defender {
63 if !crate::staticability::static_ability_can_attack_defender::can_attack_defender(
64 &game.cards,
65 card,
66 pid,
67 ) {
68 return false;
69 }
70 }
71
72 true
73}
74
75pub fn validate_attackers(
76 game: &GameState,
77 constraints: &AttackConstraints,
78 current_attackers: &[(CardId, DefenderId)],
79) -> bool {
80 let my_violations = constraints.count_violations(current_attackers, &game.cards);
81 if my_violations == -1 {
82 return false;
83 }
84 let (_, best_violations) = constraints.get_legal_attackers(&game.cards);
85 my_violations <= best_violations
86}
87
88pub fn get_possible_defenders(game: &GameState, attacking_player: PlayerId) -> Vec<DefenderId> {
89 let mut defenders = Vec::new();
90 for pid in game.alive_players() {
91 if pid == attacking_player {
92 continue;
93 }
94 defenders.push(DefenderId::Player(pid));
95 for &cid in game.cards_in_zone(ZoneType::Battlefield, pid) {
97 let card = game.card(cid);
98 if card.type_line.is_planeswalker() {
99 defenders.push(DefenderId::Permanent(cid));
100 }
101 }
102 }
103 defenders
104}
105
106pub fn get_available_blockers(game: &GameState, player: PlayerId) -> Vec<CardId> {
107 game.creatures_on_battlefield(player)
108 .into_iter()
109 .filter(|&cid| can_block(game, cid))
110 .collect()
111}
112
113pub fn can_creature_block(game: &GameState, blocker_id: CardId, attacker_id: CardId) -> bool {
114 let attacker = game.card(attacker_id);
115 let blocker = game.card(blocker_id);
116
117 if !blocker.can_block() {
118 return false;
119 }
120
121 if attacker.has_flying() && !blocker.has_flying() && !blocker.has_reach() {
123 return false;
124 }
125 if attacker.has_fear() && !blocker.type_line.is_artifact() && !blocker.color.has_black() {
127 return false;
128 }
129 if attacker.has_intimidate()
131 && !blocker.type_line.is_artifact()
132 && !blocker.color.shares_color_with(attacker.color)
133 {
134 return false;
135 }
136 if attacker.has_shadow() != blocker.has_shadow() {
138 return false;
139 }
140 if attacker.has_horsemanship() && !blocker.has_horsemanship() {
142 return false;
143 }
144 if attacker.has_skulk() && blocker.power() > attacker.power() {
146 return false;
147 }
148 if attacker.is_protected_from(blocker) {
150 return false;
151 }
152 if cant_block_by(game, attacker_id, blocker_id) {
154 return false;
155 }
156 true
157}
158
159fn cant_block_by(game: &GameState, attacker_id: CardId, blocker_id: CardId) -> bool {
161 let attacker = game.card(attacker_id);
162 let blocker = game.card(blocker_id);
163
164 for source in game
165 .cards
166 .iter()
167 .filter(|c| c.zone == ZoneType::Battlefield || c.zone == ZoneType::Command)
168 {
169 for sa in &source.static_abilities {
170 if !sa.check_mode(&StaticMode::CantBlockBy) {
171 continue;
172 }
173
174 if let Some(valid_attacker) = sa.ir.valid_attacker.as_ref() {
175 if !valid_filter::matches_valid_card_selector_in_game(
176 valid_attacker,
177 attacker,
178 source,
179 game,
180 ) {
181 continue;
182 }
183 }
184
185 if let Some(valid_blocker) = sa.ir.valid_blocker.as_ref() {
186 if !valid_filter::matches_valid_card_selector_in_game(
187 valid_blocker,
188 blocker,
189 source,
190 game,
191 ) {
192 continue;
193 }
194 }
195
196 return true;
197 }
198 }
199 false
200}
201
202pub fn filter_legal_blockers(
204 game: &GameState,
205 attackers: &[CardId],
206 blockers: &[CardId],
207) -> Vec<CardId> {
208 blockers
209 .iter()
210 .filter(|&&blocker_id| {
211 attackers
212 .iter()
213 .any(|&attacker_id| can_creature_block(game, blocker_id, attacker_id))
214 })
215 .copied()
216 .collect()
217}
218
219pub fn validate_blocks(game: &GameState, combat: &CombatState) -> Vec<(CardId, CardId)> {
220 let mut invalid = Vec::new();
221
222 for &(attacker_id, _) in &combat.attackers {
223 let blockers_for = combat.get_blockers_for(attacker_id);
224 let num_blockers = blockers_for.len();
225
226 if num_blockers == 0 {
227 continue;
228 }
229
230 for &blocker_id in &blockers_for {
232 let blocker = game.card(blocker_id);
233 let cant_block_alone = blocker
234 .keywords
235 .iter_strings()
236 .chain(blocker.granted_keywords.iter_strings())
237 .chain(blocker.pump_keywords.iter_strings())
238 .any(|kw| kw.to_lowercase().contains("can't block alone"));
239
240 if cant_block_alone && num_blockers == 1 {
241 invalid.push((blocker_id, attacker_id));
242 }
243 }
244 }
245
246 invalid
247}
248
249pub fn must_block_an_attacker(game: &GameState, combat: &CombatState, blocker_id: CardId) -> bool {
251 let blocker = game.card(blocker_id);
252 if blocker.must_block {
253 return true;
254 }
255 !compute_must_block_targets(game, combat, blocker_id).is_empty()
256}
257
258pub fn get_lure_type(card: &Card) -> LureType {
260 for kw in card
261 .keywords
262 .iter_strings()
263 .chain(card.granted_keywords.iter_strings())
264 .chain(card.pump_keywords.iter_strings())
265 {
266 let lower = kw.to_lowercase();
267 if lower.contains("all creatures able to block") && lower.contains("do so") {
268 return LureType::AllMustBlock;
269 }
270 if lower.contains("must be blocked if able") {
271 return LureType::MustBeBlockedIfAble;
272 }
273 }
274 LureType::None
275}
276
277pub fn compute_must_block_targets(
279 game: &GameState,
280 combat: &CombatState,
281 blocker_id: CardId,
282) -> Vec<CardId> {
283 let mut targets = Vec::new();
284 let blocker = game.card(blocker_id);
285
286 for &(attacker_id, _) in &combat.attackers {
287 let attacker = game.card(attacker_id);
288 let lure = get_lure_type(attacker);
289 match lure {
290 LureType::AllMustBlock | LureType::MustBeBlockedIfAble => {
291 if can_creature_block(game, blocker_id, attacker_id) {
292 targets.push(attacker_id);
293 }
294 }
295 LureType::None => {}
296 }
297 }
298
299 for &attacker_id in &blocker.must_block_cards {
301 if combat.is_attacking(attacker_id)
302 && can_creature_block(game, blocker_id, attacker_id)
303 && !targets.contains(&attacker_id)
304 {
305 targets.push(attacker_id);
306 }
307 }
308
309 targets
310}
311
312pub fn can_block_more_creatures(
314 game: &GameState,
315 combat: &CombatState,
316 blocker_id: CardId,
317) -> bool {
318 let currently_blocking = combat
319 .blockers
320 .iter()
321 .filter(|(b, _)| *b == blocker_id)
322 .count();
323 if currently_blocking == 0 {
324 return true;
325 }
326 let card = game.card(blocker_id);
328 for kw in card
329 .keywords
330 .iter_strings()
331 .chain(card.granted_keywords.iter_strings())
332 .chain(card.pump_keywords.iter_strings())
333 {
334 let lower = kw.to_lowercase();
335 if lower.contains("can block any number of creatures") {
336 return true;
337 }
338 if lower.contains("can block an additional creature") && currently_blocking < 2 {
339 return true;
340 }
341 }
342 false
343}
344
345pub fn get_min_num_blockers_for_attacker(game: &GameState, attacker_id: CardId) -> usize {
347 let card = game.card(attacker_id);
348 if card.has_menace() {
349 2
350 } else {
351 1
352 }
353}
354
355pub fn can_attacker_be_blocked_with_amount(
357 game: &GameState,
358 attacker_id: CardId,
359 amount: usize,
360) -> bool {
361 amount >= get_min_num_blockers_for_attacker(game, attacker_id)
362}
363
364pub fn check_declared_attacker(game: &mut GameState, attacker_id: CardId, defender: DefenderId) {
366 let controlling = defender.controlling_player(game);
367 game.card_mut(attacker_id).set_attacking_player(controlling);
368}
369
370pub fn get_all_requirements(
372 game: &GameState,
373 attacking_player: PlayerId,
374 possible_defenders: &[DefenderId],
375) -> AttackConstraints {
376 AttackConstraints::new(game, attacking_player, possible_defenders)
377}
378
379pub fn can_attack(game: &GameState, attacker_id: CardId) -> bool {
381 let card = game.card(attacker_id);
382 let possible_defenders = get_possible_defenders(game, card.controller);
383 possible_defenders
384 .iter()
385 .any(|&defender| can_attack_defender(game, attacker_id, defender))
386}
387
388pub fn can_attack_next_turn(game: &GameState, attacker_id: CardId, defender: DefenderId) -> bool {
390 let card = game.card(attacker_id);
391
392 if !card.is_creature() || card.phased_out {
394 return false;
395 }
396 if card.cant_attack_static || card.detained {
397 return false;
398 }
399 if let DefenderId::Player(pid) = defender {
400 if !crate::staticability::static_ability_can_attack_defender::can_attack_defender(
401 &game.cards,
402 card,
403 pid,
404 ) {
405 return false;
406 }
407 }
408 true
409}
410
411pub fn could_attack_but_not_attacking(
413 game: &GameState,
414 combat: &CombatState,
415 attacker_id: CardId,
416) -> bool {
417 if combat.is_attacking(attacker_id) {
418 return false;
419 }
420 can_attack(game, attacker_id)
421}
422
423pub fn check_propaganda_effects(
425 game: &GameState,
426 attacker_id: CardId,
427 defender: DefenderId,
428) -> bool {
429 let attacker = game.card(attacker_id);
430 let cost = super::attack_cost::get_attack_cost(&game.cards, attacker, defender);
431 cost == 0
432}
433
434pub fn pay_required_block_costs(game: &GameState, blocker_id: CardId, attacker_id: CardId) -> bool {
436 let blocker = game.card(blocker_id);
437 let attacker = game.card(attacker_id);
438 let cost = super::block_cost::get_block_cost(&game.cards, blocker, attacker);
439 cost == 0
440}
441
442pub fn can_block(game: &GameState, blocker_id: CardId) -> bool {
444 let blocker = game.card(blocker_id);
445
446 if !blocker.is_creature() || blocker.phased_out {
447 return false;
448 }
449
450 if blocker.tapped
451 && !static_ability_cant_attack_block::can_block_tapped(game, &game.cards, blocker)
452 {
453 return false;
454 }
455
456 if blocker.has_keyword("CARDNAME can't block.")
457 || blocker.has_keyword("CARDNAME can't attack or block.")
458 {
459 return false;
460 }
461
462 if static_ability_cant_attack_block::cant_block(game, &game.cards, blocker) {
463 return false;
464 }
465
466 blocker.zone == ZoneType::Battlefield
467}
468
469pub fn can_be_blocked(
471 game: &GameState,
472 attacker_id: CardId,
473 potential_blockers: &[CardId],
474) -> bool {
475 potential_blockers
476 .iter()
477 .any(|&blocker_id| can_creature_block(game, blocker_id, attacker_id))
478}
479
480pub fn can_block_at_least_one(game: &GameState, blocker_id: CardId, attackers: &[CardId]) -> bool {
482 attackers
483 .iter()
484 .any(|&attacker_id| can_creature_block(game, blocker_id, attacker_id))
485}
486
487pub fn find_free_blockers(
489 game: &GameState,
490 combat: &CombatState,
491 defending_player: PlayerId,
492) -> Vec<CardId> {
493 let available = get_available_blockers(game, defending_player);
494 available
495 .into_iter()
496 .filter(|&blocker_id| !combat.is_blocking(blocker_id))
497 .collect()
498}