1pub mod attack_constraints;
2pub mod attack_cost;
3pub mod attack_requirement;
4pub mod attack_restriction;
5pub mod attack_restriction_type;
6pub mod attacking_band;
7pub mod block_cost;
8pub mod combat_lki;
9pub mod combat_util;
10pub mod global_attack_restrictions;
11pub mod selector_domain;
12
13use std::collections::{HashMap, HashSet};
14
15use forge_foundation::ZoneType;
16use serde::{Deserialize, Serialize};
17
18use crate::agent::PlayerAgent;
19use crate::game::GameState;
20use crate::ids::{CardId, PlayerId};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub enum DefenderId {
25 Player(PlayerId),
26 Permanent(CardId),
27}
28
29impl DefenderId {
30 pub fn controlling_player(&self, game: &GameState) -> PlayerId {
33 match self {
34 DefenderId::Player(pid) => *pid,
35 DefenderId::Permanent(cid) => game.card(*cid).controller,
36 }
37 }
38
39 pub fn as_player(&self) -> Option<PlayerId> {
41 match self {
42 DefenderId::Player(pid) => Some(*pid),
43 DefenderId::Permanent(_) => None,
44 }
45 }
46}
47
48pub use combat_lki::CombatLki;
49
50#[derive(Debug, Clone)]
53pub struct CombatDamageEvent {
54 pub source: CardId,
55 pub target_player: Option<PlayerId>,
56 pub target_card: Option<CardId>,
57 pub amount: i32,
58 pub is_combat: bool,
59 pub lifelink_player: Option<PlayerId>,
60 pub lifelink_amount: i32,
61}
62
63#[derive(Debug, Clone, Default, Serialize, Deserialize)]
65pub struct CombatState {
66 pub attacking_player: Option<PlayerId>,
68 pub defending_player: Option<PlayerId>,
70 pub attackers: Vec<(CardId, DefenderId)>,
72 #[serde(default)]
74 pub attacker_zone_timestamps: HashMap<CardId, u64>,
75 pub blockers: Vec<(CardId, CardId)>,
77 #[serde(default)]
80 pub blocked_attackers: HashSet<CardId>,
81 #[serde(default)]
83 pub blocker_zone_timestamps: HashMap<CardId, u64>,
84 #[serde(default)]
88 pub damage_order: HashMap<CardId, Vec<CardId>>,
89 #[serde(skip)]
92 pub lki_cache: HashMap<CardId, CombatLki>,
93}
94
95impl CombatState {
96 pub fn new() -> Self {
97 Self::default()
98 }
99
100 pub fn clear(&mut self) {
101 self.attacking_player = None;
102 self.defending_player = None;
103 self.attackers.clear();
104 self.attacker_zone_timestamps.clear();
105 self.blockers.clear();
106 self.blocked_attackers.clear();
107 self.blocker_zone_timestamps.clear();
108 self.damage_order.clear();
109 self.lki_cache.clear();
110 }
111
112 pub fn clear_with_cards(&mut self, cards: &mut [crate::card::Card]) {
114 for &(attacker_id, _) in &self.attackers {
115 cards[attacker_id.index()].attacking_player = None;
116 }
117 let lki = std::mem::take(&mut self.lki_cache);
119 self.clear();
120 self.lki_cache = lki;
121 }
122
123 pub fn declare_attacker(
124 &mut self,
125 attacker: CardId,
126 defending: DefenderId,
127 zone_timestamp: u64,
128 ) {
129 self.attackers.push((attacker, defending));
130 self.attacker_zone_timestamps
131 .insert(attacker, zone_timestamp);
132 }
133
134 pub fn declare_blocker(&mut self, blocker: CardId, attacker: CardId, zone_timestamp: u64) {
135 self.blockers.push((blocker, attacker));
136 self.blocked_attackers.insert(attacker);
137 self.blocker_zone_timestamps.insert(blocker, zone_timestamp);
138 }
139
140 pub fn is_attacking(&self, card: CardId) -> bool {
141 self.attackers.iter().any(|(a, _)| *a == card)
142 }
143
144 pub fn is_blocked(&self, attacker: CardId) -> bool {
145 self.blockers.iter().any(|(_, a)| *a == attacker)
146 }
147
148 pub fn was_blocked_this_combat(&self, attacker: CardId) -> bool {
150 self.blocked_attackers.contains(&attacker) || self.is_blocked(attacker)
151 }
152
153 pub fn get_blockers_for(&self, attacker: CardId) -> Vec<CardId> {
154 self.blockers
155 .iter()
156 .filter(|(_, a)| *a == attacker)
157 .map(|(b, _)| *b)
158 .collect()
159 }
160
161 pub fn get_attackers_for(&self, blocker: CardId) -> Vec<CardId> {
162 self.blockers
163 .iter()
164 .filter(|(b, _)| *b == blocker)
165 .map(|(_, a)| *a)
166 .collect()
167 }
168
169 pub fn has_attackers(&self) -> bool {
170 !self.attackers.is_empty()
171 }
172
173 pub fn save_lki(&mut self, card_id: CardId) -> Option<CombatLki> {
175 if let Some((_, defender)) = self.attackers.iter().find(|(a, _)| *a == card_id) {
177 let lki = CombatLki {
178 is_attacker: true,
179 defender: Some(*defender),
180 blocked_attackers: vec![],
181 };
182 self.lki_cache.insert(card_id, lki.clone());
183 return Some(lki);
184 }
185 let blocked: Vec<CardId> = self
187 .blockers
188 .iter()
189 .filter(|(b, _)| *b == card_id)
190 .map(|(_, a)| *a)
191 .collect();
192 if !blocked.is_empty() {
193 let lki = CombatLki {
194 is_attacker: false,
195 defender: None,
196 blocked_attackers: blocked,
197 };
198 self.lki_cache.insert(card_id, lki.clone());
199 return Some(lki);
200 }
201 None
202 }
203
204 pub fn get_combat_lki(&self, card_id: CardId) -> Option<&CombatLki> {
206 self.lki_cache.get(&card_id)
207 }
208
209 pub fn was_attacking(&self, card_id: CardId) -> bool {
211 self.attackers.iter().any(|(a, _)| *a == card_id)
212 || self.lki_cache.get(&card_id).is_some_and(|l| l.is_attacker)
213 }
214
215 pub fn was_blocking(&self, card_id: CardId) -> bool {
217 self.blockers.iter().any(|(b, _)| *b == card_id)
218 || self.lki_cache.get(&card_id).is_some_and(|l| !l.is_attacker)
219 }
220
221 pub fn remove_absent_combatants(&mut self, cards: &[crate::card::Card]) -> bool {
227 let before_attackers = self.attackers.len();
228 let before_blockers = self.blockers.len();
229
230 self.attackers.retain(|&(id, _)| {
231 let card = &cards[id.index()];
232 let timestamp_ok = self
233 .attacker_zone_timestamps
234 .get(&id)
235 .map(|&ts| ts == card.zone_timestamp)
236 .unwrap_or(true);
237 card.zone == ZoneType::Battlefield && card.is_creature() && timestamp_ok
238 });
239 self.blockers.retain(|&(id, _)| {
240 let card = &cards[id.index()];
241 let timestamp_ok = self
242 .blocker_zone_timestamps
243 .get(&id)
244 .map(|&ts| ts == card.zone_timestamp)
245 .unwrap_or(true);
246 card.zone == ZoneType::Battlefield && card.is_creature() && timestamp_ok
247 });
248
249 let attacker_ids: HashSet<CardId> = self.attackers.iter().map(|(a, _)| *a).collect();
250 self.attacker_zone_timestamps
251 .retain(|attacker_id, _| attacker_ids.contains(attacker_id));
252
253 self.damage_order.retain(|k, _| attacker_ids.contains(k));
255
256 let blocker_ids: HashSet<CardId> = self.blockers.iter().map(|(b, _)| *b).collect();
258 self.blocker_zone_timestamps
259 .retain(|blocker_id, _| blocker_ids.contains(blocker_id));
260 for order in self.damage_order.values_mut() {
261 order.retain(|b| blocker_ids.contains(b));
262 }
263
264 self.attackers.len() != before_attackers || self.blockers.len() != before_blockers
265 }
266
267 pub fn has_first_strikers(&self, game: &GameState) -> bool {
269 for &(attacker_id, _) in &self.attackers {
270 if !game.card_is_in_zone(attacker_id, ZoneType::Battlefield) {
271 continue;
272 }
273 let card = game.card(attacker_id);
274 if card.has_first_strike() || card.has_double_strike() {
275 return true;
276 }
277 }
278 for &(blocker_id, _) in &self.blockers {
279 if !game.card_is_in_zone(blocker_id, ZoneType::Battlefield) {
280 continue;
281 }
282 let card = game.card(blocker_id);
283 if card.has_first_strike() || card.has_double_strike() {
284 return true;
285 }
286 }
287 false
288 }
289
290 pub fn resolve_damage_step(
295 &self,
296 game: &mut GameState,
297 agents: &mut [Box<dyn PlayerAgent>],
298 first_strike_only: bool,
299 as_unblocked_choices: &HashSet<CardId>,
300 ) -> Vec<CombatDamageEvent> {
301 if game.prevent_all_combat_damage {
303 return Vec::new();
304 }
305
306 let mut events = Vec::new();
307 let mut blocker_damage_allocations: HashMap<(CardId, CardId), i32> = HashMap::new();
308 let mut computed_blocker_allocations: HashSet<CardId> = HashSet::new();
309 let life_at_step_start: Vec<i32> = game.players.iter().map(|p| p.life).collect();
312
313 for (attacker_id, defender) in self.attackers.clone() {
314 if !game.card_is_in_zone(attacker_id, ZoneType::Battlefield) {
316 continue;
317 }
318
319 let attacker = game.card(attacker_id);
320 if crate::staticability::static_ability_assign_no_combat_damage::assign_no_combat_damage(
321 &game.cards,
322 attacker,
323 ) {
324 continue;
325 }
326 let attacker_has_fs = attacker.has_first_strike();
327 let attacker_has_ds = attacker.has_double_strike();
328 let attacker_has_trample = attacker.has_trample();
329 let attacker_has_deathtouch = attacker.has_deathtouch();
330 let attacker_has_lifelink = attacker.has_lifelink();
331 let defending_player = defender.controlling_player(game);
332 let attacker_has_infect_for_player = attacker.has_infect()
333 || crate::staticability::static_ability_infect_damage::is_infect_damage_with_life_override(
334 game,
335 &game.cards,
336 defending_player,
337 attacker.controller,
338 life_at_step_start.get(defending_player.index()).copied(),
339 );
340 let attacker_has_infect_for_creature = attacker.has_infect();
341 let attacker_has_wither = attacker.has_wither()
342 || crate::staticability::static_ability_wither_damage::is_wither_damage(
343 &game.cards,
344 attacker,
345 );
346 let attacker_toxic_count = attacker.get_toxic_count();
347 let attacker_controller = attacker.controller;
348 let can_divide_damage_as_choose = attacker.has_keyword(
349 "You may assign CARDNAME's combat damage divided as you choose among defending player and/or any number of creatures they control.",
350 );
351 let can_assign_unblocked_to_creature = attacker.has_keyword(
352 "If CARDNAME is unblocked, you may have it assign its combat damage to a creature defending player controls.",
353 );
354 let has_trample_planeswalker = attacker.has_keyword("Trample:Planeswalker");
355
356 let attacker_deals_damage = if first_strike_only {
358 attacker_has_fs || attacker_has_ds
359 } else {
360 !attacker_has_fs || attacker_has_ds
362 };
363
364 let attacker_power = if crate::staticability::static_ability_combat_damage_toughness::combat_damage_uses_toughness(
365 &game.cards,
366 game.card(attacker_id),
367 ) {
368 game.card(attacker_id).toughness()
369 } else {
370 game.card(attacker_id).power()
371 };
372
373 let attacker_card = game.card(attacker_id);
374 let assign_as_unblocked =
375 crate::staticability::static_ability_assign_combat_damage_as_unblocked::has_mandatory_assign_as_unblocked(
376 &game.cards,
377 attacker_card,
378 )
379 || crate::staticability::static_ability_assign_combat_damage_as_unblocked::assign_as_unblocked(
380 &game.cards,
381 attacker_card,
382 as_unblocked_choices.contains(&attacker_id),
383 );
384
385 let attacker_was_blocked = self.was_blocked_this_combat(attacker_id);
386 let blockers = if assign_as_unblocked {
387 Vec::new()
388 } else if let Some(ordered) = self.damage_order.get(&attacker_id) {
389 ordered.clone()
391 } else {
392 self.get_blockers_for(attacker_id)
393 };
394
395 if blockers.is_empty() && !attacker_was_blocked {
396 if !attacker_deals_damage || attacker_power <= 0 {
398 continue;
399 }
400
401 let defending_creatures = defending_player_creatures(game, defender);
402 if can_divide_damage_as_choose
403 && !defending_creatures.is_empty()
404 && agents[attacker_controller.index()].confirm_action(
405 attacker_controller,
406 Some("AlternativeDamageAssignment"),
407 &format!(
408 "Assign {} combat damage divided as you choose among defending player and/or creatures they control?",
409 game.card(attacker_id).card_name
410 ),
411 &[],
412 Some(attacker_id),
413 None,
414 )
415 {
416 let assignments = agents[attacker_controller.index()].assign_combat_damage(
417 game,
418 attacker_controller,
419 attacker_id,
420 &defending_creatures,
421 Some(DefenderId::Player(defending_player)),
422 attacker_power,
423 );
424 let (to_creatures, to_player) = validate_damage_assignment(
425 game,
426 attacker_id,
427 &defending_creatures,
428 Some(DefenderId::Player(defending_player)),
429 attacker_power,
430 &assignments,
431 );
432
433 for &(target_id, dmg) in &to_creatures {
434 deal_combat_damage_to_card(
435 game,
436 attacker_id,
437 target_id,
438 dmg,
439 attacker_has_deathtouch,
440 attacker_has_lifelink,
441 attacker_controller,
442 attacker_has_wither || attacker_has_infect_for_creature,
443 Some(agents),
444 );
445 events.push(CombatDamageEvent {
446 source: attacker_id,
447 target_player: None,
448 target_card: Some(target_id),
449 amount: dmg,
450 is_combat: true,
451 lifelink_player: if attacker_has_lifelink {
452 Some(attacker_controller)
453 } else {
454 None
455 },
456 lifelink_amount: if attacker_has_lifelink { dmg } else { 0 },
457 });
458 }
459 if to_player > 0 {
460 deal_combat_damage_to_player(
461 game,
462 attacker_id,
463 defending_player,
464 to_player,
465 attacker_has_lifelink,
466 attacker_controller,
467 attacker_has_infect_for_player,
468 attacker_toxic_count,
469 Some(agents),
470 );
471 events.push(CombatDamageEvent {
472 source: attacker_id,
473 target_player: Some(defending_player),
474 target_card: None,
475 amount: to_player,
476 is_combat: true,
477 lifelink_player: if attacker_has_lifelink {
478 Some(attacker_controller)
479 } else {
480 None
481 },
482 lifelink_amount: if attacker_has_lifelink { to_player } else { 0 },
483 });
484 if game.player_is_commander(game.card(attacker_id).owner, attacker_id) {
485 game.player_add_commander_damage(
486 defending_player,
487 attacker_id,
488 to_player,
489 );
490 }
491 }
492 continue;
493 }
494
495 if can_assign_unblocked_to_creature
496 && !attacker_was_blocked
497 && !defending_creatures.is_empty()
498 && agents[attacker_controller.index()].confirm_action(
499 attacker_controller,
500 Some("AlternativeDamageAssignment"),
501 &format!(
502 "Assign {} combat damage to a creature defending player controls?",
503 game.card(attacker_id).card_name
504 ),
505 &[],
506 Some(attacker_id),
507 None,
508 )
509 {
510 if let Some(chosen) = agents[attacker_controller.index()].choose_target_card(
511 attacker_controller,
512 &defending_creatures,
513 None,
514 ) {
515 deal_combat_damage_to_card(
516 game,
517 attacker_id,
518 chosen,
519 attacker_power,
520 attacker_has_deathtouch,
521 attacker_has_lifelink,
522 attacker_controller,
523 attacker_has_wither || attacker_has_infect_for_creature,
524 Some(agents),
525 );
526 events.push(CombatDamageEvent {
527 source: attacker_id,
528 target_player: None,
529 target_card: Some(chosen),
530 amount: attacker_power,
531 is_combat: true,
532 lifelink_player: if attacker_has_lifelink {
533 Some(attacker_controller)
534 } else {
535 None
536 },
537 lifelink_amount: if attacker_has_lifelink {
538 attacker_power
539 } else {
540 0
541 },
542 });
543 continue;
544 }
545 }
546 match defender {
547 DefenderId::Player(defending_player) => {
548 deal_combat_damage_to_player(
549 game,
550 attacker_id,
551 defending_player,
552 attacker_power,
553 attacker_has_lifelink,
554 attacker_controller,
555 attacker_has_infect_for_player,
556 attacker_toxic_count,
557 Some(agents),
558 );
559 events.push(CombatDamageEvent {
560 source: attacker_id,
561 target_player: Some(defending_player),
562 target_card: None,
563 amount: attacker_power,
564 is_combat: true,
565 lifelink_player: if attacker_has_lifelink {
566 Some(attacker_controller)
567 } else {
568 None
569 },
570 lifelink_amount: if attacker_has_lifelink {
571 attacker_power
572 } else {
573 0
574 },
575 });
576 if game.player_is_commander(game.card(attacker_id).owner, attacker_id) {
578 game.player_add_commander_damage(
579 defending_player,
580 attacker_id,
581 attacker_power,
582 );
583 }
584 }
585 DefenderId::Permanent(target_id) => {
586 deal_combat_damage_to_card(
588 game,
589 attacker_id,
590 target_id,
591 attacker_power,
592 attacker_has_deathtouch,
593 attacker_has_lifelink,
594 attacker_controller,
595 attacker_has_wither || attacker_has_infect_for_creature,
596 Some(agents),
597 );
598 events.push(CombatDamageEvent {
599 source: attacker_id,
600 target_player: None,
601 target_card: Some(target_id),
602 amount: attacker_power,
603 is_combat: true,
604 lifelink_player: if attacker_has_lifelink {
605 Some(attacker_controller)
606 } else {
607 None
608 },
609 lifelink_amount: if attacker_has_lifelink {
610 attacker_power
611 } else {
612 0
613 },
614 });
615 }
616 }
617 } else {
618 let remaining_damage = if attacker_deals_damage && attacker_power > 0 {
623 attacker_power
624 } else {
625 0
626 };
627 let mut alive_blockers: Vec<CardId> = blockers
631 .iter()
632 .copied()
633 .filter(|&bid| game.card_is_in_zone(bid, ZoneType::Battlefield))
634 .collect();
635 let mut effective_defender = defender;
636 if has_trample_planeswalker {
637 if let DefenderId::Permanent(target_id) = defender {
638 if !alive_blockers.contains(&target_id) {
639 alive_blockers.push(target_id);
640 }
641 effective_defender = DefenderId::Player(defending_player);
642 }
643 }
644
645 let defending_creatures = defending_player_creatures(game, effective_defender);
646 let use_divide_as_choose = can_divide_damage_as_choose
647 && !defending_creatures.is_empty()
648 && agents[attacker_controller.index()].confirm_action(
649 attacker_controller,
650 Some("AlternativeDamageAssignment"),
651 &format!(
652 "Assign {} combat damage divided as you choose among defending player and/or creatures they control?",
653 game.card(attacker_id).card_name
654 ),
655 &[],
656 Some(attacker_id),
657 None,
658 );
659 if use_divide_as_choose {
660 for cid in defending_creatures {
661 if !alive_blockers.contains(&cid) {
662 alive_blockers.push(cid);
663 }
664 }
665 }
666
667 let can_assign_to_defender = attacker_has_trample || use_divide_as_choose;
668 if alive_blockers.is_empty() && !can_assign_to_defender {
669 continue;
670 }
671 let must_prompt_assignment =
678 remaining_damage > 0 && (can_assign_to_defender || !alive_blockers.is_empty());
679
680 let assignments = if must_prompt_assignment {
681 let controller = game.card(attacker_id).controller;
682 let defender_for_prompt = if can_assign_to_defender {
683 Some(effective_defender)
684 } else {
685 None
686 };
687 agents[controller.index()].assign_combat_damage(
688 game,
689 controller,
690 attacker_id,
691 &alive_blockers,
692 defender_for_prompt,
693 remaining_damage,
694 )
695 } else if let Some(&only_blocker) = alive_blockers.first() {
696 vec![(Some(only_blocker), remaining_damage)]
697 } else if can_assign_to_defender {
698 vec![(None, remaining_damage)]
699 } else {
700 Vec::new()
701 };
702
703 let (damage_assignments, defender_damage) = validate_damage_assignment(
704 game,
705 attacker_id,
706 &alive_blockers,
707 can_assign_to_defender.then_some(effective_defender),
708 remaining_damage,
709 &assignments,
710 );
711
712 struct BlockerDamageInfo {
717 blocker_id: CardId,
718 power: i32,
719 has_deathtouch: bool,
720 has_lifelink: bool,
721 has_wither_or_infect: bool,
722 controller: PlayerId,
723 }
724 let mut blocker_damage_infos: Vec<BlockerDamageInfo> = Vec::new();
725 for &blocker_id in &blockers {
726 if !game.card_is_in_zone(blocker_id, ZoneType::Battlefield) {
727 continue;
728 }
729 let blocker_card = game.card(blocker_id);
730 if crate::staticability::static_ability_assign_no_combat_damage::assign_no_combat_damage(
731 &game.cards,
732 blocker_card,
733 ) {
734 continue;
735 }
736 let blocker_has_fs = blocker_card.has_first_strike();
737 let blocker_has_ds = blocker_card.has_double_strike();
738 let blocker_deals = if first_strike_only {
739 blocker_has_fs || blocker_has_ds
740 } else {
741 !blocker_has_fs || blocker_has_ds
742 };
743 if !blocker_deals {
744 continue;
745 }
746 if crate::staticability::static_ability_colorless_damage_source::target_is_protected_from_source(
747 &game.cards,
748 game.card(attacker_id),
749 game.card(blocker_id),
750 ) {
751 continue;
752 }
753 let blocker_power = if crate::staticability::static_ability_combat_damage_toughness::combat_damage_uses_toughness(
754 &game.cards,
755 game.card(blocker_id),
756 ) {
757 game.card(blocker_id).toughness()
758 } else {
759 game.card(blocker_id).power()
760 };
761 if blocker_power > 0 {
762 if !computed_blocker_allocations.contains(&blocker_id) {
763 let per_attacker = compute_blocker_damage_allocations(
764 self,
765 game,
766 agents,
767 first_strike_only,
768 blocker_id,
769 blocker_power,
770 );
771 for (target_attacker, dmg) in per_attacker {
772 blocker_damage_allocations
773 .insert((blocker_id, target_attacker), dmg);
774 }
775 computed_blocker_allocations.insert(blocker_id);
776 }
777 let assigned_to_this_attacker = blocker_damage_allocations
778 .get(&(blocker_id, attacker_id))
779 .copied()
780 .unwrap_or(0);
781 if assigned_to_this_attacker <= 0 {
782 continue;
783 }
784 let blocker_has_infect = blocker_card.has_infect();
785 let blocker_has_wither = blocker_card.has_wither()
786 || crate::staticability::static_ability_wither_damage::is_wither_damage(
787 &game.cards,
788 blocker_card,
789 );
790 blocker_damage_infos.push(BlockerDamageInfo {
791 blocker_id,
792 power: assigned_to_this_attacker,
793 has_deathtouch: blocker_card.has_deathtouch(),
794 has_lifelink: blocker_card.has_lifelink(),
795 has_wither_or_infect: blocker_has_wither || blocker_has_infect,
796 controller: blocker_card.controller,
797 });
798 }
799 }
800
801 for &(blocker_id, damage_to_blocker) in &damage_assignments {
804 deal_combat_damage_to_card(
805 game,
806 attacker_id,
807 blocker_id,
808 damage_to_blocker,
809 attacker_has_deathtouch,
810 attacker_has_lifelink,
811 attacker_controller,
812 attacker_has_wither || attacker_has_infect_for_creature,
813 Some(agents),
814 );
815 events.push(CombatDamageEvent {
816 source: attacker_id,
817 target_player: None,
818 target_card: Some(blocker_id),
819 amount: damage_to_blocker,
820 is_combat: true,
821 lifelink_player: if attacker_has_lifelink {
822 Some(attacker_controller)
823 } else {
824 None
825 },
826 lifelink_amount: if attacker_has_lifelink {
827 damage_to_blocker
828 } else {
829 0
830 },
831 });
832 }
833
834 if defender_damage > 0 {
835 match effective_defender {
836 DefenderId::Player(defending_player) => {
837 deal_combat_damage_to_player(
838 game,
839 attacker_id,
840 defending_player,
841 defender_damage,
842 attacker_has_lifelink,
843 attacker_controller,
844 attacker_has_infect_for_player,
845 attacker_toxic_count,
846 None, );
848 events.push(CombatDamageEvent {
849 source: attacker_id,
850 target_player: Some(defending_player),
851 target_card: None,
852 amount: defender_damage,
853 is_combat: true,
854 lifelink_player: if attacker_has_lifelink {
855 Some(attacker_controller)
856 } else {
857 None
858 },
859 lifelink_amount: if attacker_has_lifelink {
860 defender_damage
861 } else {
862 0
863 },
864 });
865 if game.card(attacker_id).is_commander {
866 game.player_add_commander_damage(
867 defending_player,
868 attacker_id,
869 defender_damage,
870 );
871 }
872 }
873 DefenderId::Permanent(target_id) => {
874 deal_combat_damage_to_card(
875 game,
876 attacker_id,
877 target_id,
878 defender_damage,
879 attacker_has_deathtouch,
880 attacker_has_lifelink,
881 attacker_controller,
882 attacker_has_wither || attacker_has_infect_for_creature,
883 Some(agents),
884 );
885 events.push(CombatDamageEvent {
886 source: attacker_id,
887 target_player: None,
888 target_card: Some(target_id),
889 amount: defender_damage,
890 is_combat: true,
891 lifelink_player: if attacker_has_lifelink {
892 Some(attacker_controller)
893 } else {
894 None
895 },
896 lifelink_amount: if attacker_has_lifelink {
897 defender_damage
898 } else {
899 0
900 },
901 });
902 }
903 }
904 }
905
906 for info in &blocker_damage_infos {
907 if !game.card_is_in_zone(info.blocker_id, ZoneType::Battlefield) {
909 continue;
910 }
911 deal_combat_damage_to_card(
912 game,
913 info.blocker_id,
914 attacker_id,
915 info.power,
916 info.has_deathtouch,
917 info.has_lifelink,
918 info.controller,
919 info.has_wither_or_infect,
920 Some(agents),
921 );
922 events.push(CombatDamageEvent {
923 source: info.blocker_id,
924 target_player: None,
925 target_card: Some(attacker_id),
926 amount: info.power,
927 is_combat: true,
928 lifelink_player: if info.has_lifelink {
929 Some(info.controller)
930 } else {
931 None
932 },
933 lifelink_amount: if info.has_lifelink { info.power } else { 0 },
934 });
935 }
936
937 }
940 }
941
942 events
943 }
944
945 pub fn init_constraints(&self, game: &GameState) -> attack_constraints::AttackConstraints {
950 let attacking_player = self
951 .attacking_player
952 .expect("init_constraints called without attacking player");
953 let possible_defenders = combat_util::get_possible_defenders(game, attacking_player);
954 attack_constraints::AttackConstraints::new(game, attacking_player, &possible_defenders)
955 }
956
957 pub fn end_combat(&mut self, game: &mut GameState) {
961 for card in game.cards.iter_mut() {
963 if card.zone == ZoneType::Battlefield {
964 card.damage_history.end_combat();
965 }
966 }
967
968 for &(attacker_id, _) in &self.attackers {
970 game.card_mut(attacker_id).clear_attacking_player();
971 }
972
973 self.clear();
974 }
975
976 pub fn clear_attackers(&mut self, game: &mut GameState) {
979 let attacker_ids: Vec<CardId> = self.attackers.iter().map(|(a, _)| *a).collect();
980 for attacker_id in attacker_ids {
981 self.remove_from_combat(attacker_id, game);
982 }
983 }
984
985 pub fn add_attacker(&mut self, attacker: CardId, defender: DefenderId) {
988 self.attackers.retain(|(a, _)| *a != attacker);
990 self.attackers.push((attacker, defender));
991 }
992
993 pub fn add_blocker(&mut self, attacker: CardId, blocker: CardId) {
996 self.blockers.push((blocker, attacker));
997 self.blocked_attackers.insert(attacker);
998 if let Some(order) = self.damage_order.get_mut(&attacker) {
1000 if !order.contains(&blocker) {
1001 order.push(blocker);
1002 }
1003 }
1004 }
1005
1006 pub fn remove_block_assignment(&mut self, attacker: CardId, blocker: CardId) {
1009 self.blockers
1010 .retain(|&(b, a)| !(b == blocker && a == attacker));
1011 if !self.blockers.iter().any(|(b, _)| *b == blocker) {
1012 self.blocker_zone_timestamps.remove(&blocker);
1013 }
1014 }
1015
1016 pub fn undo_blocking_assignment(&mut self, blocker: CardId) {
1019 self.blockers.retain(|&(b, _)| b != blocker);
1020 self.blocker_zone_timestamps.remove(&blocker);
1021 }
1022
1023 pub fn order_blockers_for_damage_assignment(
1030 &mut self,
1031 _game: &GameState,
1032 _agents: &mut [Box<dyn PlayerAgent>],
1033 ) {
1034 let attacker_ids: Vec<CardId> = self.attackers.iter().map(|(a, _)| *a).collect();
1035 for attacker_id in attacker_ids {
1036 let blockers = self.get_blockers_for(attacker_id);
1037 if blockers.is_empty() {
1038 continue;
1039 }
1040 self.damage_order.insert(attacker_id, blockers);
1043 }
1044 }
1045
1046 pub fn add_blocker_to_damage_assignment_order(&mut self, attacker: CardId, blocker: CardId) {
1049 let order = self.damage_order.entry(attacker).or_default();
1050 if !order.contains(&blocker) {
1051 order.push(blocker);
1052 }
1053 }
1054
1055 pub fn order_attackers_for_damage_assignment(
1058 &mut self,
1059 _game: &GameState,
1060 _agents: &mut [Box<dyn PlayerAgent>],
1061 ) {
1062 }
1067
1068 pub fn unregister_attacker(&mut self, card: CardId) {
1071 self.damage_order.remove(&card);
1073
1074 for order in self.damage_order.values_mut() {
1076 order.retain(|&c| c != card);
1077 }
1078
1079 self.attackers.retain(|(a, _)| *a != card);
1081 self.attacker_zone_timestamps.remove(&card);
1082 self.blockers.retain(|(_, a)| *a != card);
1083 let blocker_ids: HashSet<CardId> = self.blockers.iter().map(|(b, _)| *b).collect();
1084 self.blocker_zone_timestamps
1085 .retain(|blocker_id, _| blocker_ids.contains(blocker_id));
1086 }
1087
1088 pub fn unregister_defender(&mut self, card: CardId) {
1091 for order in self.damage_order.values_mut() {
1093 order.retain(|&c| c != card);
1094 }
1095
1096 self.blockers.retain(|(b, _)| *b != card);
1098 self.blocker_zone_timestamps.remove(&card);
1099 }
1100
1101 pub fn remove_from_combat(&mut self, card: CardId, game: &mut GameState) {
1104 if self.attackers.iter().any(|(a, _)| *a == card) {
1106 self.unregister_attacker(card);
1107 game.card_mut(card).clear_attacking_player();
1108 return;
1109 }
1110
1111 if self.blockers.iter().any(|(b, _)| *b == card) {
1113 self.unregister_defender(card);
1114 }
1115 }
1116
1117 pub fn fire_triggers_for_unblocked_attackers(&mut self) -> Vec<(CardId, DefenderId)> {
1123 let mut unblocked = Vec::new();
1124
1125 for &(attacker_id, defender) in &self.attackers {
1126 let is_blocked = self.blockers.iter().any(|(_, a)| *a == attacker_id);
1127 if !is_blocked {
1128 unblocked.push((attacker_id, defender));
1129 }
1130 }
1131
1132 unblocked
1133 }
1134
1135 pub fn assign_combat_damage(
1138 &self,
1139 game: &mut GameState,
1140 agents: &mut [Box<dyn PlayerAgent>],
1141 first_strike_damage: bool,
1142 as_unblocked_choices: &HashSet<CardId>,
1143 ) -> Vec<CombatDamageEvent> {
1144 self.resolve_damage_step(game, agents, first_strike_damage, as_unblocked_choices)
1145 }
1146
1147 pub fn deal_assigned_damage(&self, game: &mut GameState) {
1151 game.copy_last_state();
1155 }
1156
1157 pub fn get_attackers(&self) -> Vec<CardId> {
1159 self.attackers.iter().map(|(a, _)| *a).collect()
1160 }
1161
1162 pub fn get_all_blockers(&self) -> Vec<CardId> {
1164 let mut result = Vec::new();
1165 for &(b, _) in &self.blockers {
1166 if !result.contains(&b) {
1167 result.push(b);
1168 }
1169 }
1170 result
1171 }
1172
1173 pub fn get_defender_by_attacker(&self, attacker: CardId) -> Option<DefenderId> {
1175 self.attackers
1176 .iter()
1177 .find(|(a, _)| *a == attacker)
1178 .map(|(_, d)| *d)
1179 }
1180
1181 pub fn get_defender_player_by_attacker(
1184 &self,
1185 attacker: CardId,
1186 game: &GameState,
1187 ) -> Option<PlayerId> {
1188 self.get_defender_by_attacker(attacker)
1189 .map(|d| d.controlling_player(game))
1190 }
1191
1192 pub fn is_blocking(&self, blocker: CardId) -> bool {
1194 self.blockers.iter().any(|(b, _)| *b == blocker)
1195 }
1196
1197 pub fn is_blocking_attacker(&self, blocker: CardId, attacker: CardId) -> bool {
1199 self.blockers
1200 .iter()
1201 .any(|&(b, a)| b == blocker && a == attacker)
1202 }
1203
1204 pub fn is_unblocked(&self, attacker: CardId) -> bool {
1206 self.is_attacking(attacker) && !self.is_blocked(attacker)
1207 }
1208
1209 pub fn get_unblocked_attackers(&self) -> Vec<CardId> {
1211 self.attackers
1212 .iter()
1213 .filter(|(a, _)| !self.is_blocked(*a))
1214 .map(|(a, _)| *a)
1215 .collect()
1216 }
1217}
1218
1219fn validate_damage_assignment(
1220 game: &GameState,
1221 attacker_id: CardId,
1222 blockers_in_order: &[CardId],
1223 defender: Option<DefenderId>,
1224 total_damage: i32,
1225 assignments: &[(Option<CardId>, i32)],
1226) -> (Vec<(CardId, i32)>, i32) {
1227 if total_damage <= 0 {
1228 return (Vec::new(), 0);
1229 }
1230
1231 let mut per_blocker: HashMap<CardId, i32> = HashMap::new();
1232 let mut defender_damage = 0;
1233 let mut assigned_total = 0;
1234
1235 let mut invalid = false;
1236
1237 for &(assignee, amount) in assignments {
1238 if amount < 0 {
1239 invalid = true;
1240 break;
1241 }
1242 if amount == 0 {
1243 continue;
1244 }
1245 assigned_total += amount;
1246 match assignee {
1247 Some(blocker_id) => {
1248 if !blockers_in_order.contains(&blocker_id) {
1249 invalid = true;
1250 break;
1251 }
1252 *per_blocker.entry(blocker_id).or_insert(0) += amount;
1253 }
1254 None => {
1255 if defender.is_none() {
1256 invalid = true;
1257 break;
1258 }
1259 defender_damage += amount;
1260 }
1261 }
1262 }
1263
1264 if assigned_total != total_damage {
1265 invalid = true;
1266 }
1267
1268 let has_deathtouch = game.card(attacker_id).has_deathtouch();
1269 let mut can_move_to_next = true;
1270 for &blocker_id in blockers_in_order {
1271 if !game.card_is_in_zone(blocker_id, ZoneType::Battlefield) {
1272 continue;
1273 }
1274 if crate::staticability::static_ability_colorless_damage_source::target_is_protected_from_source(
1275 &game.cards,
1276 game.card(blocker_id),
1277 game.card(attacker_id),
1278 ) {
1279 continue;
1280 }
1281
1282 let assigned = per_blocker.get(&blocker_id).copied().unwrap_or(0);
1283 let lethal = if has_deathtouch {
1284 1
1285 } else if game.card(blocker_id).type_line.is_planeswalker() {
1286 game.card(blocker_id)
1287 .counter_count(&crate::card::CounterType::Loyalty)
1288 .max(0)
1289 } else {
1290 damage_needed_to_kill_for_assignment(game, blocker_id, attacker_id, assigned.max(1))
1291 };
1292
1293 if !can_move_to_next && assigned > 0 {
1294 invalid = true;
1295 break;
1296 }
1297 if assigned < lethal {
1298 can_move_to_next = false;
1299 }
1300 }
1301
1302 if defender_damage > 0 && !can_move_to_next {
1303 invalid = true;
1304 }
1305
1306 if invalid {
1307 return fallback_damage_assignment(
1308 game,
1309 attacker_id,
1310 blockers_in_order,
1311 defender,
1312 total_damage,
1313 );
1314 }
1315
1316 let mut ordered_blocker_assignments: Vec<(CardId, i32)> = Vec::new();
1317 for &blocker_id in blockers_in_order {
1318 if let Some(amount) = per_blocker.get(&blocker_id).copied() {
1319 if amount > 0 {
1320 ordered_blocker_assignments.push((blocker_id, amount));
1321 }
1322 }
1323 }
1324
1325 (ordered_blocker_assignments, defender_damage)
1326}
1327
1328fn fallback_damage_assignment(
1329 game: &GameState,
1330 attacker_id: CardId,
1331 blockers_in_order: &[CardId],
1332 defender: Option<DefenderId>,
1333 total_damage: i32,
1334) -> (Vec<(CardId, i32)>, i32) {
1335 if total_damage <= 0 {
1336 return (Vec::new(), 0);
1337 }
1338
1339 let mut assignments: Vec<(CardId, i32)> = Vec::new();
1340 let mut damage_left = total_damage;
1341 let has_deathtouch = game.card(attacker_id).has_deathtouch();
1342
1343 for &blocker_id in blockers_in_order {
1344 if damage_left <= 0 {
1345 break;
1346 }
1347 if !game.card_is_in_zone(blocker_id, ZoneType::Battlefield) {
1348 continue;
1349 }
1350 if crate::staticability::static_ability_colorless_damage_source::target_is_protected_from_source(
1351 &game.cards,
1352 game.card(blocker_id),
1353 game.card(attacker_id),
1354 ) {
1355 continue;
1356 }
1357
1358 let lethal = if has_deathtouch {
1359 1
1360 } else if game.card(blocker_id).type_line.is_planeswalker() {
1361 game.card(blocker_id)
1362 .counter_count(&crate::card::CounterType::Loyalty)
1363 .max(0)
1364 } else {
1365 damage_needed_to_kill_for_assignment(game, blocker_id, attacker_id, damage_left)
1366 };
1367 let assign = lethal.min(damage_left);
1368 if assign > 0 {
1369 assignments.push((blocker_id, assign));
1370 damage_left -= assign;
1371 }
1372 }
1373
1374 if damage_left > 0 {
1375 if defender.is_some() {
1376 return (assignments, damage_left);
1377 }
1378 if let Some((_, amount)) = assignments.last_mut() {
1379 *amount += damage_left;
1380 } else if let Some(&first) = blockers_in_order.first() {
1381 assignments.push((first, damage_left));
1382 }
1383 return (assignments, 0);
1384 }
1385
1386 (assignments, 0)
1387}
1388
1389fn damage_needed_to_kill_for_assignment(
1390 game: &GameState,
1391 target: CardId,
1392 source: CardId,
1393 max_damage: i32,
1394) -> i32 {
1395 if max_damage <= 0 {
1396 return 0;
1397 }
1398
1399 let target_card = game.card(target);
1400 let source_card = game.card(source);
1401 let mut kill_damage = (target_card.toughness() - target_card.damage).max(0);
1402
1403 if target_card.has_keyword("Indestructible")
1404 && !source_card.has_wither()
1405 && !source_card.has_infect()
1406 {
1407 return max_damage + 1;
1408 }
1409 if source_card.has_deathtouch() && target_card.is_creature() {
1410 kill_damage = 1;
1411 }
1412
1413 for damage in 1..=max_damage {
1414 let mut sim = game.clone();
1415 let mut event = crate::replacement::replacement_handler::ReplacementEvent::DamageToCard {
1416 target,
1417 amount: damage,
1418 source: Some(source),
1419 is_combat: true,
1420 };
1421 let _ = crate::replacement::replacement_handler::apply_replacements(&mut sim, &mut event);
1422 let final_damage = match event {
1423 crate::replacement::replacement_handler::ReplacementEvent::DamageToCard {
1424 amount,
1425 ..
1426 } => amount.max(0),
1427 _ => 0,
1428 };
1429 if final_damage >= kill_damage {
1430 return damage;
1431 }
1432 }
1433
1434 max_damage + 1
1435}
1436
1437fn defending_player_creatures(game: &GameState, defender: DefenderId) -> Vec<CardId> {
1438 let defending_player = defender.controlling_player(game);
1439 game.cards_in_zone(ZoneType::Battlefield, defending_player)
1440 .iter()
1441 .copied()
1442 .filter(|&cid| game.card(cid).is_creature())
1443 .collect()
1444}
1445
1446fn compute_blocker_damage_allocations(
1447 combat: &CombatState,
1448 game: &GameState,
1449 agents: &mut [Box<dyn PlayerAgent>],
1450 first_strike_only: bool,
1451 blocker_id: CardId,
1452 blocker_power: i32,
1453) -> Vec<(CardId, i32)> {
1454 if blocker_power <= 0 {
1455 return Vec::new();
1456 }
1457
1458 let blocker = game.card(blocker_id);
1459 let has_fs = blocker.has_first_strike();
1460 let has_ds = blocker.has_double_strike();
1461 let deals_this_step = if first_strike_only {
1462 has_fs || has_ds
1463 } else {
1464 !has_fs || has_ds
1465 };
1466 if !deals_this_step {
1467 return Vec::new();
1468 }
1469
1470 let attackers_for_blocker: Vec<CardId> = combat
1471 .get_attackers_for(blocker_id)
1472 .into_iter()
1473 .filter(|&aid| game.card_is_in_zone(aid, ZoneType::Battlefield))
1474 .collect();
1475 if attackers_for_blocker.is_empty() {
1476 return Vec::new();
1477 }
1478
1479 let controller = blocker.controller;
1486 let assignments = agents[controller.index()].assign_combat_damage(
1487 game,
1488 controller,
1489 blocker_id,
1490 &attackers_for_blocker,
1491 None,
1492 blocker_power,
1493 );
1494 let (per_attacker, _to_defender) = validate_damage_assignment(
1495 game,
1496 blocker_id,
1497 &attackers_for_blocker,
1498 None,
1499 blocker_power,
1500 &assignments,
1501 );
1502 per_attacker
1503}
1504
1505pub fn get_available_attackers(game: &GameState, player: PlayerId) -> Vec<CardId> {
1510 combat_util::get_available_attackers(game, player)
1511}
1512
1513pub fn get_possible_defenders(game: &GameState, attacking_player: PlayerId) -> Vec<DefenderId> {
1515 combat_util::get_possible_defenders(game, attacking_player)
1516}
1517
1518pub fn get_available_blockers(game: &GameState, player: PlayerId) -> Vec<CardId> {
1520 combat_util::get_available_blockers(game, player)
1521}
1522
1523pub fn can_creature_block(game: &GameState, blocker_id: CardId, attacker_id: CardId) -> bool {
1525 combat_util::can_creature_block(game, blocker_id, attacker_id)
1526}
1527
1528pub fn filter_legal_blockers(
1530 game: &GameState,
1531 attackers: &[CardId],
1532 blockers: &[CardId],
1533) -> Vec<CardId> {
1534 combat_util::filter_legal_blockers(game, attackers, blockers)
1535}
1536
1537fn deal_combat_damage_to_player(
1539 game: &mut GameState,
1540 source: CardId,
1541 target: PlayerId,
1542 amount: i32,
1543 lifelink: bool,
1544 source_controller: PlayerId,
1545 source_has_infect: bool,
1546 source_toxic_count: Option<i32>,
1547 agents: Option<&mut [Box<dyn PlayerAgent>]>,
1548) {
1549 if amount > 0 {
1550 if source_has_infect {
1551 if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_player(
1553 &game.cards,
1554 target,
1555 &crate::card::CounterType::Poison,
1556 ) {
1557 game.player_add_poison(target, amount);
1558 }
1559 } else {
1560 let dealt = game.deal_damage_to_player_from_with_agents(
1561 target,
1562 amount,
1563 Some(source),
1564 true,
1565 agents,
1566 );
1567 game.record_player_damage_assignment(Some(source), Some(target), dealt, true);
1568 }
1569 if let Some(toxic) = source_toxic_count {
1571 if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_player(
1572 &game.cards,
1573 target,
1574 &crate::card::CounterType::Poison,
1575 ) {
1576 game.player_add_poison(target, toxic);
1577 }
1578 }
1579 if lifelink
1580 && !crate::staticability::static_ability_cant_gain_lose_pay_life::cant_gain_life(
1581 game,
1582 source_controller,
1583 )
1584 {
1585 let mut gl_event =
1587 crate::replacement::replacement_handler::ReplacementEvent::GainLife {
1588 player: source_controller,
1589 amount,
1590 };
1591 let gl_result =
1592 crate::replacement::replacement_handler::apply_replacements(game, &mut gl_event);
1593 if gl_result != crate::replacement::ReplacementResult::Skipped
1594 && gl_result != crate::replacement::ReplacementResult::Replaced
1595 {
1596 let final_amount =
1597 if let crate::replacement::replacement_handler::ReplacementEvent::GainLife {
1598 amount: a,
1599 ..
1600 } = gl_event
1601 {
1602 a
1603 } else {
1604 amount
1605 };
1606 if final_amount > 0 {
1607 game.player_gain_life(source_controller, final_amount);
1608 game.player_add_team_life_gained(source_controller, final_amount);
1609 }
1610 }
1611 }
1612 game.card_mut(source).damage_history.register_damage(
1613 amount,
1614 true,
1615 Some(source),
1616 crate::card::card_damage_history::TrackedEntity::Player(target),
1617 );
1618 }
1619}
1620
1621fn deal_combat_damage_to_card(
1623 game: &mut GameState,
1624 source: CardId,
1625 target: CardId,
1626 amount: i32,
1627 deathtouch: bool,
1628 lifelink: bool,
1629 source_controller: PlayerId,
1630 source_has_wither_or_infect: bool,
1631 agents: Option<&mut [Box<dyn PlayerAgent>]>,
1632) {
1633 if amount > 0 {
1634 if crate::staticability::static_ability_colorless_damage_source::target_is_protected_from_source(
1635 &game.cards,
1636 game.card(target),
1637 game.card(source),
1638 ) {
1639 return;
1640 }
1641 if !game.card(target).damage_sources_this_turn.contains(&source) {
1643 game.card_mut(target).add_damage_source_this_turn(source);
1644 }
1645 if source_has_wither_or_infect {
1646 if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
1648 &game.cards,
1649 game.card(target),
1650 &crate::card::CounterType::M1M1,
1651 ) {
1652 game.card_mut(target)
1653 .add_counter(&crate::card::CounterType::M1M1, amount);
1654 }
1655 } else {
1656 game.deal_damage_to_card_from_with_agents(target, amount, Some(source), true, agents);
1657 }
1658 if deathtouch {
1659 game.card_mut(target).mark_deathtouch_damage();
1660 }
1661 if lifelink
1662 && !crate::staticability::static_ability_cant_gain_lose_pay_life::cant_gain_life(
1663 game,
1664 source_controller,
1665 )
1666 {
1667 let mut gl_event =
1669 crate::replacement::replacement_handler::ReplacementEvent::GainLife {
1670 player: source_controller,
1671 amount,
1672 };
1673 let gl_result =
1674 crate::replacement::replacement_handler::apply_replacements(game, &mut gl_event);
1675 if gl_result != crate::replacement::ReplacementResult::Skipped
1676 && gl_result != crate::replacement::ReplacementResult::Replaced
1677 {
1678 let final_amount =
1679 if let crate::replacement::replacement_handler::ReplacementEvent::GainLife {
1680 amount: a,
1681 ..
1682 } = gl_event
1683 {
1684 a
1685 } else {
1686 amount
1687 };
1688 if final_amount > 0 {
1689 game.player_gain_life(source_controller, final_amount);
1690 game.player_add_team_life_gained(source_controller, final_amount);
1691 }
1692 }
1693 }
1694 game.card_mut(source).damage_history.register_damage(
1695 amount,
1696 true,
1697 Some(source),
1698 crate::card::card_damage_history::TrackedEntity::Card(target),
1699 );
1700 }
1701}
1702
1703#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1708pub enum LureType {
1709 None,
1711 MustBeBlockedIfAble,
1713 AllMustBlock,
1715}
1716
1717pub fn get_lure_type(card: &crate::card::Card) -> LureType {
1719 combat_util::get_lure_type(card)
1720}
1721
1722pub fn compute_must_block_targets(
1724 game: &GameState,
1725 combat: &CombatState,
1726 blocker_id: CardId,
1727) -> Vec<CardId> {
1728 combat_util::compute_must_block_targets(game, combat, blocker_id)
1729}
1730
1731pub fn validate_blocks(game: &GameState, combat: &CombatState) -> Vec<(CardId, CardId)> {
1733 combat_util::validate_blocks(game, combat)
1734}