1use forge_foundation::ZoneType;
2
3use crate::agent::PlayerAgent;
4use crate::card::{Card, CounterType};
5use crate::event::RunParams;
6use crate::game::GameState;
7use crate::ids::{CardId, PlayerId};
8use crate::replacement::replacement_handler::{
9 apply_replacements, apply_replacements_with_agents, ReplacementEvent, ReplacementRuntime,
10};
11use crate::replacement::GameLossReason;
12use crate::replacement::ReplacementResult;
13use crate::staticability::layer::{apply_continuous_effects, apply_etb_tapped_with_agents};
14use crate::trigger::handler::TriggerHandler;
15use crate::trigger::TriggerType;
16
17impl GameState {
19 pub fn record_player_damage_assignment(
20 &mut self,
21 source: Option<CardId>,
22 target_player: Option<PlayerId>,
23 amount: i32,
24 is_combat: bool,
25 ) {
26 self.player_record_damage_assignment(source, target_player, amount, is_combat);
27 }
28
29 pub fn move_card(&mut self, card_id: CardId, dest_zone: ZoneType, dest_owner: PlayerId) {
34 self.move_card_internal(card_id, dest_zone, dest_owner, None, None, true, false);
35 }
36
37 pub fn move_card_with_agents(
38 &mut self,
39 card_id: CardId,
40 dest_zone: ZoneType,
41 dest_owner: PlayerId,
42 agents: &mut [Box<dyn PlayerAgent>],
43 ) {
44 self.move_card_internal(
45 card_id,
46 dest_zone,
47 dest_owner,
48 Some(agents),
49 None,
50 true,
51 false,
52 );
53 }
54
55 pub fn move_card_with_agents_and_replacement_runtime(
56 &mut self,
57 card_id: CardId,
58 dest_zone: ZoneType,
59 dest_owner: PlayerId,
60 agents: &mut [Box<dyn PlayerAgent>],
61 runtime: &mut ReplacementRuntime<'_>,
62 ) {
63 self.move_card_internal(
64 card_id,
65 dest_zone,
66 dest_owner,
67 Some(agents),
68 Some(runtime.trigger_handler),
69 true,
70 false,
71 );
72 }
73
74 fn move_card_without_replacement(
75 &mut self,
76 card_id: CardId,
77 dest_zone: ZoneType,
78 dest_owner: PlayerId,
79 ) {
80 self.move_card_internal(card_id, dest_zone, dest_owner, None, None, false, false);
81 }
82
83 pub fn discard_card(
89 &mut self,
90 card_id: CardId,
91 discard_player: PlayerId,
92 sa: Option<&crate::spellability::SpellAbility>,
93 agents: Option<&mut [Box<dyn PlayerAgent>]>,
94 trigger_handler: &mut TriggerHandler,
95 ) {
96 let owner = self.card(card_id).owner;
97 self.player_record_discard(discard_player, 1);
98 self.card_mut(card_id).set_discarded(true);
99
100 self.move_card_internal(
103 card_id,
104 ZoneType::Graveyard,
105 owner,
106 agents,
107 Some(trigger_handler),
108 true,
109 true, );
111
112 if let Some(sa) = sa {
114 if sa.ir.remember_discarded {
115 if let Some(source_id) = sa.source {
116 self.card_mut(source_id).add_remembered_card(card_id);
117 }
118 }
119 }
120
121 trigger_handler.register_active_trigger(self, card_id);
123
124 let dest_zone = self.card(card_id).zone;
126 crate::ability::effects::zone_triggers::emit_zone_trigger(
127 trigger_handler,
128 card_id,
129 ZoneType::Hand,
130 dest_zone,
131 );
132
133 trigger_handler.run_trigger(
135 TriggerType::Discarded,
136 RunParams {
137 card: Some(card_id),
138 player: Some(discard_player),
139 ..Default::default()
140 },
141 false,
142 );
143 trigger_handler.run_trigger(
144 TriggerType::DiscardedAll,
145 RunParams {
146 card: Some(card_id),
147 cards: Some(vec![card_id]),
148 player: Some(discard_player),
149 ..Default::default()
150 },
151 false,
152 );
153 }
154
155 fn move_card_internal(
156 &mut self,
157 card_id: CardId,
158 dest_zone: ZoneType,
159 dest_owner: PlayerId,
160 mut agents: Option<&mut [Box<dyn PlayerAgent>]>,
161 mut trigger_handler: Option<&mut TriggerHandler>,
162 apply_move_replacement: bool,
163 is_discard: bool,
164 ) {
165 let (src_zone, src_owner, was_permanent, was_land, is_token) = {
166 let card = &self.cards[card_id.index()];
167 (
168 card.zone,
169 card.controller,
170 card.type_line.is_permanent(),
171 card.is_land(),
172 card.is_token,
173 )
174 };
175 if let Ok(filter) = std::env::var("FORGE_CARD_TRACE") {
176 if !filter.is_empty()
177 && self.cards[card_id.index()]
178 .card_name
179 .eq_ignore_ascii_case(&filter)
180 {
181 eprintln!(
182 "[card-trace] move {} {:?} {:?} -> {:?} (owner={:?} sick={} cast_from={:?})",
183 self.cards[card_id.index()].card_name,
184 card_id,
185 src_zone,
186 dest_zone,
187 dest_owner,
188 self.cards[card_id.index()].summoning_sick,
189 self.cards[card_id.index()].cast_from,
190 );
191 }
192 }
193 let mut moved_event = ReplacementEvent::Moved {
194 card: card_id,
195 origin: src_zone,
196 destination: dest_zone,
197 is_discard,
198 };
199 let tapped_before_replacement = self.card(card_id).tapped;
200 if apply_move_replacement {
201 if let Some(agents) = agents.as_deref_mut() {
202 apply_replacements_with_agents(self, agents, &mut moved_event);
203 } else {
204 apply_replacements(self, &mut moved_event);
205 }
206 }
207 let dest_zone = match moved_event {
208 ReplacementEvent::Moved { destination, .. } => destination,
209 _ => dest_zone,
210 };
211 let replacement_marked_etb_tapped = dest_zone == ZoneType::Battlefield
212 && self.card(card_id).tapped
213 && !tapped_before_replacement;
214 let dest_owner = if dest_zone == ZoneType::Command {
215 self.card(card_id).owner
216 } else {
217 dest_owner
218 };
219 let host_left_battlefield =
220 src_zone == ZoneType::Battlefield && dest_zone != ZoneType::Battlefield;
221 if host_left_battlefield && was_permanent {
222 self.player_record_permanent_left_battlefield(src_owner);
223 }
224 if host_left_battlefield {
227 self.card_mut(card_id).cast_sa = None;
228 crate::ability::effects::control_gain_effect::leaves_play_hook(self, card_id);
231 }
232 if dest_zone == ZoneType::Graveyard && was_permanent && !is_token {
233 self.player_record_permanent_put_into_graveyard(self.card(card_id).owner);
234 }
235 let forget_effects: Vec<CardId> = self
236 .cards
237 .iter()
238 .filter(|c| {
239 c.zone == ZoneType::Command
240 && c.forget_on_moved_origin == Some(src_zone)
241 && c.remembered_cards.contains(&card_id)
242 })
243 .map(|c| c.id)
244 .collect();
245
246 if is_token && dest_zone != ZoneType::Battlefield {
249 if let Some(table) = self.pending_change_zone_table.as_mut() {
250 table.put(Some(src_zone), Some(ZoneType::None), card_id);
251 }
252 let mut exile_effects = Vec::new();
253 for eff_id in forget_effects.iter().copied() {
254 let eff = &mut self.cards[eff_id.index()];
255 eff.remembered_cards.retain(|&rid| rid != card_id);
256 if eff.exile_when_no_remembered && eff.remembered_cards.is_empty() {
257 exile_effects.push(eff_id);
258 }
259 }
260 self.cards[card_id.index()].zone = ZoneType::None;
261 if src_zone != ZoneType::None {
262 self.remove_card_from_zone(src_zone, src_owner, card_id);
263 }
264 for eff_id in exile_effects {
268 let controller = self.card(eff_id).controller;
269 self.remove_card_from_zone(ZoneType::Command, controller, eff_id);
270 self.cards[eff_id.index()].zone = ZoneType::None;
271 }
272 apply_continuous_effects(self);
273 debug_assert!(self.card_zone_location_matches_card(card_id));
274 return;
275 }
276
277 if src_zone != ZoneType::None {
279 self.remove_card_from_zone(src_zone, src_owner, card_id);
280 }
281
282 if src_zone == ZoneType::Exile && dest_zone != ZoneType::Exile {
283 self.cards[card_id.index()]
284 .keywords
285 .retain(|kw| !kw.starts_with(crate::card::KEYWORD_PLOTTED_PREFIX));
286 }
287
288 self.cards[card_id.index()].zone = dest_zone;
290 if src_zone != dest_zone {
291 self.cards[card_id.index()].turn_in_zone = self.turn.turn_number;
292 }
293
294 if let Some(table) = self.pending_change_zone_table.as_mut() {
295 table.put(Some(src_zone), Some(dest_zone), card_id);
296 }
297
298 if dest_zone != ZoneType::Stack {
301 self.assign_zone_timestamp(card_id);
302 }
303
304 self.save_zone_lki(dest_zone, dest_owner, card_id, src_zone);
306
307 match dest_zone {
309 ZoneType::Battlefield => {
310 self.cards[card_id.index()].controller = dest_owner;
314 self.cards[card_id.index()].enter_battlefield();
315 if replacement_marked_etb_tapped {
316 self.cards[card_id.index()].set_tapped(true);
317 }
318 self.add_card_to_zone(dest_zone, dest_owner, card_id);
321 if was_land {
322 self.player_record_landfall(dest_owner);
323 }
324 if !replacement_marked_etb_tapped {
331 apply_etb_tapped_with_agents(self, card_id, agents.as_deref_mut());
332 }
333 let etb_keywords = self.cards[card_id.index()].keywords.as_string_list();
336 for kw in etb_keywords {
337 let mut parts = kw.split(':');
338 let head = parts.next().unwrap_or_default();
339 if !head.eq_ignore_ascii_case("etbCounter") {
340 continue;
341 }
342 let counter_type = parts.next().unwrap_or_default();
343 let amount_str = parts.next().unwrap_or_default();
344 let amount = amount_str.parse::<i32>().unwrap_or_else(|_| {
345 let card = &self.cards[card_id.index()];
348 if let Some(svar_expr) = card.svars.get(amount_str) {
349 if svar_expr == "Count$xPaid" || svar_expr == "Count$XPaid" {
350 card.svars
351 .get("XPaid")
352 .and_then(|v| v.parse::<i32>().ok())
353 .unwrap_or(0)
354 } else if let Ok(n) = svar_expr.parse::<i32>() {
355 n
356 } else {
357 crate::svar::resolve_count_svar(
358 svar_expr,
359 self,
360 card_id,
361 card.controller,
362 )
363 }
364 } else {
365 0
366 }
367 });
368 if amount <= 0 {
369 continue;
370 }
371 let ct = crate::ability::effects::parse_counter_type(counter_type);
372 if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
374 &self.cards,
375 &self.cards[card_id.index()],
376 &ct,
377 ) {
378 let mut add_event = ReplacementEvent::AddCounter {
382 target: card_id,
383 counter_type: ct.clone(),
384 count: amount,
385 is_effect: true,
386 };
387 if let Some(agents) = agents.as_deref_mut() {
388 apply_replacements_with_agents(self, agents, &mut add_event);
389 } else {
390 apply_replacements(self, &mut add_event);
391 }
392 let final_amount = if let ReplacementEvent::AddCounter { count, .. } = add_event {
393 count
394 } else {
395 amount
396 };
397 if final_amount > 0 {
398 self.cards[card_id.index()].add_counter(&ct, final_amount);
399 }
400 }
401 }
402 if self.cards[card_id.index()].type_line.is_planeswalker() {
405 let loyalty = self.cards[card_id.index()]
406 .initial_loyalty
407 .as_deref()
408 .and_then(|value| value.parse::<i32>().ok())
409 .unwrap_or(0);
410 if loyalty > 0 {
411 let ct = crate::card::CounterType::Loyalty;
412 if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
413 &self.cards,
414 &self.cards[card_id.index()],
415 &ct,
416 ) {
417 let mut add_event = ReplacementEvent::AddCounter {
418 target: card_id,
419 counter_type: ct.clone(),
420 count: loyalty,
421 is_effect: true,
422 };
423 if let Some(agents) = agents.as_deref_mut() {
424 apply_replacements_with_agents(self, agents, &mut add_event);
425 } else {
426 apply_replacements(self, &mut add_event);
427 }
428 let final_amount = if let ReplacementEvent::AddCounter { count, .. } = add_event {
429 count
430 } else {
431 loyalty
432 };
433 if final_amount > 0 {
434 self.cards[card_id.index()].add_counter(&ct, final_amount);
435 }
436 }
437 }
438 }
439 let etb_p1p1 = self.cards[card_id.index()].etb_counters_p1p1;
441 if etb_p1p1 > 0 {
442 let ct = crate::card::CounterType::P1P1;
443 if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
444 &self.cards,
445 &self.cards[card_id.index()],
446 &ct,
447 ) {
448 let mut add_event = ReplacementEvent::AddCounter {
451 target: card_id,
452 counter_type: ct.clone(),
453 count: etb_p1p1,
454 is_effect: true,
455 };
456 if let Some(agents) = agents {
457 apply_replacements_with_agents(self, agents, &mut add_event);
458 } else {
459 apply_replacements(self, &mut add_event);
460 }
461 let final_count = if let ReplacementEvent::AddCounter { count, .. } = add_event {
462 count
463 } else {
464 etb_p1p1
465 };
466 if final_count > 0 {
467 self.cards[card_id.index()].add_counter(&ct, final_count);
468 }
469 }
470 self.cards[card_id.index()].etb_counters_p1p1 = 0;
471 }
472 let sunburst = self.cards[card_id.index()].sunburst_count();
474 if sunburst > 0 && self.cards[card_id.index()].has_keyword("Sunburst") {
475 let ct = if self.cards[card_id.index()].is_creature() {
476 crate::card::CounterType::P1P1
477 } else {
478 crate::card::CounterType::Charge
479 };
480 if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
481 &self.cards,
482 &self.cards[card_id.index()],
483 &ct,
484 ) {
485 self.cards[card_id.index()].add_counter(&ct, sunburst);
486 }
487 }
488 self.update_lki_snapshot(card_id);
492 apply_continuous_effects(self);
493 debug_assert!(self.card_zone_location_matches_card(card_id));
494 return;
495 }
496 ZoneType::Graveyard | ZoneType::Hand | ZoneType::Exile | ZoneType::Library => {
497 let attachments: Vec<CardId> = self.cards[card_id.index()].attachments.clone();
499 for aura_id in attachments {
500 self.cards[aura_id.index()].attached_to = None;
501 self.cards[aura_id.index()].is_bestowed = false;
503 }
504 self.cards[card_id.index()].attachments.clear();
505 self.detach(card_id);
507
508 if src_zone == ZoneType::Battlefield {
511 let card = &self.cards[card_id.index()];
512 let lki_p = card.power();
513 let lki_t = card.toughness();
514 let card = &mut self.cards[card_id.index()];
515 card.lki_power = Some(lki_p);
516 card.lki_toughness = Some(lki_t);
517 }
518
519 let keep_counters =
521 crate::staticability::static_ability_counters_remain::counters_remain(
522 &self.cards,
523 &self.cards[card_id.index()],
524 dest_zone,
525 );
526 let card = &mut self.cards[card_id.index()];
527 card.tapped = false;
528 card.damage = 0;
529 card.power_modifier = 0;
530 card.toughness_modifier = 0;
531 card.static_power_modifier = 0;
532 card.static_toughness_modifier = 0;
533 card.static_set_power = None;
534 card.static_set_toughness = None;
535 card.granted_keywords.clear();
536 if let Some(type_line) = card.static_type_line_base.take() {
537 card.set_type_line(type_line);
538 }
539 card.static_added_subtypes.clear();
540 card.restore_changed_characteristics_baseline();
541 card.cant_attack_static = false;
542 card.cant_block_static = false;
543 card.summoning_sick = true;
544 card.monstrous = false;
545 card.controller = card.owner;
546 card.face_down = false;
547 card.is_bestowed = false;
548 card.cast_from = None;
552 card.reset_crewed();
553 if !keep_counters {
554 card.counters.clear();
555 }
556 card.clear_pump_triggers();
564 card.clear_pump_keywords();
565 if let Some(state) = card.animate_state.take() {
570 if let Some(orig_kws) = state.original_keywords {
571 card.keywords = orig_kws;
572 card.update_keywords();
573 }
574 }
575 if let Some(state) = card.clone_state.take() {
576 card.restore_clone_snapshot(state);
577 } else {
578 card.remove_clone_states();
579 }
580 }
581 ZoneType::Command => {
582 let attachments: Vec<CardId> = self.cards[card_id.index()].attachments.clone();
584 for aura_id in attachments {
585 self.cards[aura_id.index()].attached_to = None;
586 }
587 self.cards[card_id.index()].attachments.clear();
588 self.detach(card_id);
589
590 let keep_counters =
592 crate::staticability::static_ability_counters_remain::counters_remain(
593 &self.cards,
594 &self.cards[card_id.index()],
595 dest_zone,
596 );
597 let card = &mut self.cards[card_id.index()];
598 card.tapped = false;
599 card.damage = 0;
600 card.power_modifier = 0;
601 card.toughness_modifier = 0;
602 card.static_power_modifier = 0;
603 card.static_toughness_modifier = 0;
604 card.static_set_power = None;
605 card.static_set_toughness = None;
606 card.granted_keywords.clear();
607 if let Some(type_line) = card.static_type_line_base.take() {
608 card.set_type_line(type_line);
609 }
610 card.static_added_subtypes.clear();
611 card.restore_changed_characteristics_baseline();
612 card.cant_attack_static = false;
613 card.cant_block_static = false;
614 card.summoning_sick = true;
615 card.monstrous = false;
616 card.controller = card.owner;
617 card.cast_from = None;
618 if !keep_counters {
619 card.counters.clear();
620 }
621 if let Some(state) = card.clone_state.take() {
622 card.restore_clone_snapshot(state);
623 } else {
624 card.remove_clone_states();
625 }
626 }
627 _ => {}
628 }
629
630 self.add_card_to_zone(dest_zone, dest_owner, card_id);
632
633 let commander_entered_gy_or_exile = self.card(card_id).is_commander
636 && matches!(dest_zone, ZoneType::Graveyard | ZoneType::Exile);
637 self.cards[card_id.index()].move_to_command_zone = commander_entered_gy_or_exile;
638
639 let mut exile_effects = Vec::new();
641 for eff_id in forget_effects {
642 let eff = &mut self.cards[eff_id.index()];
643 eff.remembered_cards.retain(|&rid| rid != card_id);
644 if eff.exile_when_no_remembered && eff.remembered_cards.is_empty() {
645 exile_effects.push(eff_id);
646 }
647 }
648 for eff_id in exile_effects {
651 let controller = self.card(eff_id).controller;
652 self.remove_card_from_zone(ZoneType::Command, controller, eff_id);
653 self.cards[eff_id.index()].zone = ZoneType::None;
654 }
655
656 if host_left_battlefield {
659 let linked_effects: Vec<CardId> = self
660 .cards
661 .iter()
662 .filter(|c| c.zone == ZoneType::Command && c.temp_effect_host == Some(card_id))
663 .map(|c| c.id)
664 .collect();
665 for eff_id in linked_effects {
666 let controller = self.card(eff_id).controller;
667 self.remove_card_from_zone(ZoneType::Command, controller, eff_id);
668 self.cards[eff_id.index()].zone = ZoneType::None;
669 }
670
671 let exiled_by_host: Vec<(CardId, PlayerId)> = self
674 .cards
675 .iter()
676 .filter(|c| c.zone == ZoneType::Exile && c.exiled_by == Some(card_id))
677 .map(|c| (c.id, c.owner))
678 .collect();
679 for (exiled_id, owner) in exiled_by_host {
680 self.cards[exiled_id.index()].exiled_by = None;
681 self.move_card(exiled_id, ZoneType::Battlefield, owner);
682 if let Some(handler) = trigger_handler.as_deref_mut() {
683 let returned_zone = self.card(exiled_id).zone;
684 handler.register_active_trigger(self, exiled_id);
685 crate::ability::effects::zone_triggers::emit_zone_trigger(
686 handler,
687 exiled_id,
688 ZoneType::Exile,
689 returned_zone,
690 );
691 }
692 }
693 }
694
695 apply_continuous_effects(self);
696 debug_assert!(self.card_zone_location_matches_card(card_id));
697 }
698
699 pub fn deal_damage_to_card(&mut self, target: CardId, amount: i32) {
704 self.deal_damage_to_card_from(target, amount, None, false);
705 }
706
707 pub fn deal_damage_to_card_from(
709 &mut self,
710 target: CardId,
711 amount: i32,
712 source: Option<CardId>,
713 is_combat: bool,
714 ) {
715 self.deal_damage_to_card_from_with_agents(target, amount, source, is_combat, None);
716 }
717
718 pub fn deal_damage_to_card_from_with_agents(
720 &mut self,
721 target: CardId,
722 amount: i32,
723 source: Option<CardId>,
724 is_combat: bool,
725 agents: Option<&mut [Box<dyn crate::agent::PlayerAgent>]>,
726 ) {
727 if amount <= 0 {
728 return;
729 }
730 if !self.card(target).can_be_dealt_damage() {
731 return;
732 }
733 let mut event = ReplacementEvent::DamageToCard {
734 target,
735 amount,
736 source,
737 is_combat,
738 };
739 if let Some(agents) = agents {
740 apply_replacements_with_agents(self, agents, &mut event);
741 } else {
742 apply_replacements(self, &mut event);
743 }
744 if let ReplacementEvent::DamageToCard {
745 amount: mut final_amount,
746 ..
747 } = event
748 {
749 let shields = self.cards[target.index()].damage_prevention;
754 if shields > 0 && final_amount > 0 {
755 let consumed = shields.min(final_amount);
756 self.cards[target.index()].damage_prevention -= consumed;
757 final_amount -= consumed;
758 }
759 if final_amount > 0 {
760 let dealt = self.cards[target.index()].add_damage_after_prevention(final_amount);
761 let mut dealt_event = ReplacementEvent::DealtDamage {
763 target,
764 amount: dealt,
765 source,
766 };
767 if dealt > 0 {
768 apply_replacements(self, &mut dealt_event);
769 }
770 }
771 }
772 }
773
774 pub fn deal_damage_to_player(&mut self, target: PlayerId, amount: i32) -> i32 {
779 self.deal_damage_to_player_from(target, amount, None, false)
780 }
781
782 pub fn deal_damage_to_player_from(
784 &mut self,
785 target: PlayerId,
786 amount: i32,
787 source: Option<CardId>,
788 is_combat: bool,
789 ) -> i32 {
790 self.deal_damage_to_player_from_with_agents(target, amount, source, is_combat, None)
791 }
792
793 pub fn deal_damage_to_player_from_with_agents(
798 &mut self,
799 target: PlayerId,
800 amount: i32,
801 source: Option<CardId>,
802 is_combat: bool,
803 agents: Option<&mut [Box<dyn crate::agent::PlayerAgent>]>,
804 ) -> i32 {
805 if amount <= 0 {
806 return 0;
807 }
808 if crate::staticability::static_ability_cant_gain_lose_pay_life::cant_lose_life(
809 self, target,
810 ) {
811 return 0;
812 }
813 if crate::player::has_keyword(self, target, "Protection from everything") {
814 return 0;
815 }
816 let mut event = ReplacementEvent::DamageToPlayer {
817 target,
818 amount,
819 source,
820 is_combat,
821 };
822 if let Some(agents) = agents {
823 apply_replacements_with_agents(self, agents, &mut event);
824 } else {
825 apply_replacements(self, &mut event);
826 }
827 if let ReplacementEvent::DamageToPlayer {
828 amount: final_amount,
829 ..
830 } = event
831 {
832 if final_amount > 0 {
833 return self.player_deal_damage(target, final_amount);
834 }
835 }
836 0
837 }
838
839 pub fn check_state_based_actions(&mut self) -> bool {
841 self.check_state_based_actions_with_triggers(None, None)
842 }
843
844 pub fn check_state_based_actions_with_triggers(
849 &mut self,
850 trigger_handler: Option<&mut TriggerHandler>,
851 legend_keep_fn: Option<&mut dyn FnMut(PlayerId, &[CardId]) -> CardId>,
852 ) -> bool {
853 self.check_state_based_actions_impl(trigger_handler, legend_keep_fn, None)
854 }
855
856 pub fn check_state_based_actions_with_trigger_agents(
857 &mut self,
858 trigger_handler: Option<&mut TriggerHandler>,
859 agents: &mut [Box<dyn PlayerAgent>],
860 ) -> bool {
861 self.check_state_based_actions_impl(trigger_handler, None, Some(agents))
862 }
863
864 fn on_player_lost(
865 &mut self,
866 player: PlayerId,
867 trigger_handler: &mut Option<&mut TriggerHandler>,
868 ) {
869 self.player_mut(player).left_game = true;
870 let is_multiplayer = self.player_order.len() > 2;
871 let all_cards: Vec<CardId> = (0..self.cards.len()).map(|i| CardId(i as u32)).collect();
872
873 if !is_multiplayer {
874 for &cid in &all_cards {
876 self.cards[cid.index()].force_turn_face_up();
877 }
878 return;
879 }
880
881 if self.monarch == Some(player) {
883 let heir = if self.turn.active_player == player {
884 self.next_player(player)
885 } else {
886 self.turn.active_player
887 };
888 self.player_set_monarch(heir, trigger_handler.as_deref_mut());
889 }
890 if self.initiative_holder == Some(player) {
891 let heir = if self.turn.active_player == player {
892 self.next_player(player)
893 } else {
894 self.turn.active_player
895 };
896 self.player_take_initiative(heir, trigger_handler.as_deref_mut());
897 }
898
899 let next = self.next_player(player);
900 for &cid in &all_cards {
901 let (zone, owner, controller) = {
902 let card = &self.cards[cid.index()];
903 (card.zone, card.owner, card.controller)
904 };
905 if zone == ZoneType::None {
906 continue;
907 }
908 if owner != player {
909 if self.cards[cid.index()].attached_to_player == Some(player) {
911 self.cards[cid.index()].attached_to_player = None;
912 }
913 continue;
914 }
915 if self.cards[cid.index()].effect_source.is_some() && zone == ZoneType::Command {
916 self.remove_card_from_zone(ZoneType::Command, controller, cid);
919 self.cards[cid.index()].controller = next;
920 self.add_card_to_zone(ZoneType::Command, next, cid);
921 continue;
922 }
923 for &other in &all_cards {
925 if other == cid {
926 continue;
927 }
928 let other_card = &mut self.cards[other.index()];
929 other_card.imprinted_cards.retain(|&r| r != cid);
930 other_card.remembered_cards.retain(|&r| r != cid);
931 other_card.attachments.retain(|&r| r != cid);
932 other_card.gain_control_targets.retain(|&r| r != cid);
933 if other_card.attached_to == Some(cid) {
934 other_card.attached_to = None;
935 }
936 }
937 if let Some(handler) = trigger_handler.as_deref_mut() {
938 crate::ability::effects::emit_zone_trigger(handler, cid, zone, ZoneType::None);
939 }
940 self.remove_card_from_zone(zone, controller, cid);
941 self.cards[cid.index()].zone = ZoneType::None;
942 }
943
944 apply_continuous_effects(self);
945
946 for &cid in &all_cards {
948 let (zone, owner, controller) = {
949 let card = &self.cards[cid.index()];
950 (card.zone, card.owner, card.controller)
951 };
952 if zone == ZoneType::Battlefield && controller == player && owner != player {
953 if let Some(handler) = trigger_handler.as_deref_mut() {
954 crate::ability::effects::emit_zone_trigger(
955 handler,
956 cid,
957 ZoneType::Battlefield,
958 ZoneType::Exile,
959 );
960 }
961 self.move_card_without_replacement(cid, ZoneType::Exile, owner);
962 }
963 }
964 }
965
966 fn move_battlefield_card_to_graveyard_for_sba(
967 &mut self,
968 cid: CardId,
969 trigger_handler: &mut Option<&mut TriggerHandler>,
970 agents: &mut Option<&mut [Box<dyn PlayerAgent>]>,
971 ) {
972 let owner = self.card(cid).owner;
973 let mut moved_event = ReplacementEvent::Moved {
974 card: cid,
975 origin: ZoneType::Battlefield,
976 destination: ZoneType::Graveyard,
977 is_discard: false,
978 };
979 if let Some(agents) = agents.as_deref_mut() {
980 apply_replacements_with_agents(self, agents, &mut moved_event);
981 } else {
982 apply_replacements(self, &mut moved_event);
983 }
984 let final_dest = if let ReplacementEvent::Moved { destination, .. } = moved_event {
985 destination
986 } else {
987 ZoneType::Graveyard
988 };
989 let old_zone = self.card(cid).zone;
990 if let Some(handler) = trigger_handler.as_deref_mut() {
993 let lki_p1p1 = *self
994 .card(cid)
995 .counters
996 .get(&CounterType::P1P1)
997 .unwrap_or(&0);
998 let lki_power = self.card(cid).power();
999 let lki_toughness = self.card(cid).toughness();
1000 let lki_counters = self.card(cid).counters.clone();
1001 self.card_mut(cid).lki_counters = Some(lki_counters);
1002 self.card_mut(cid)
1003 .set_lki_power_toughness(Some(lki_power), Some(lki_toughness));
1004 crate::ability::effects::emit_zone_trigger_with_lki_counters(
1005 handler,
1006 cid,
1007 old_zone,
1008 final_dest,
1009 lki_p1p1,
1010 lki_power,
1011 lki_toughness,
1012 );
1013 handler.flush_waiting_triggers(self);
1014 }
1015 self.move_card_without_replacement(cid, final_dest, owner);
1016 }
1017
1018 fn check_state_based_actions_impl(
1019 &mut self,
1020 mut trigger_handler: Option<&mut TriggerHandler>,
1021 mut legend_keep_fn: Option<&mut dyn FnMut(PlayerId, &[CardId]) -> CardId>,
1022 mut agents: Option<&mut [Box<dyn PlayerAgent>]>,
1023 ) -> bool {
1024 self.pre_sba_battlefield = self
1029 .cards
1030 .iter()
1031 .filter(|c| c.zone == ZoneType::Battlefield)
1032 .map(|c| c.id)
1033 .collect();
1034
1035 let mut any_changes = false;
1036 let mut newly_lost_players: Vec<PlayerId> = Vec::new();
1037
1038 for pid in self.player_order.clone() {
1040 if self.player(pid).tried_to_draw_from_empty_library && self.player(pid).is_alive() {
1041 self.player_mut(pid).tried_to_draw_from_empty_library = false;
1042 let mut event = ReplacementEvent::GameLoss {
1043 player: pid,
1044 reason: GameLossReason::Milled,
1045 };
1046 let result = apply_replacements(self, &mut event);
1047 if result != ReplacementResult::Replaced && !self.player(pid).has_lost {
1048 self.player_mark_lost(pid, GameLossReason::Milled);
1049 newly_lost_players.push(pid);
1050 any_changes = true;
1051 }
1052 }
1053 if self.player(pid).life <= 0 && self.player(pid).is_alive() {
1054 let mut event = ReplacementEvent::GameLoss {
1055 player: pid,
1056 reason: GameLossReason::LifeReachedZero,
1057 };
1058 let result = apply_replacements(self, &mut event);
1059 if result != ReplacementResult::Replaced && !self.player(pid).has_lost {
1060 self.player_mark_lost(pid, GameLossReason::LifeReachedZero);
1061 newly_lost_players.push(pid);
1062 any_changes = true;
1063 }
1064 }
1065 if self.player(pid).poison_counters >= 10 && self.player(pid).is_alive() {
1067 let mut event = ReplacementEvent::GameLoss {
1068 player: pid,
1069 reason: GameLossReason::Poisoned,
1070 };
1071 let result = apply_replacements(self, &mut event);
1072 if result != ReplacementResult::Replaced {
1073 if !self.player(pid).has_lost {
1074 self.player_mark_lost(pid, GameLossReason::Poisoned);
1075 newly_lost_players.push(pid);
1076 }
1077 any_changes = true;
1078 }
1079 }
1080 if self.player(pid).commander_damage_enabled {
1082 let commander_dmg_entries: Vec<(u32, i32)> = self
1083 .player(pid)
1084 .commander_damage_received
1085 .iter()
1086 .map(|(&k, &v)| (k, v))
1087 .collect();
1088 for (_card_raw_id, dmg) in commander_dmg_entries {
1089 if dmg >= 21 && self.player(pid).is_alive() && !self.player(pid).has_lost {
1090 self.player_mark_lost(pid, GameLossReason::CommanderDamage);
1091 newly_lost_players.push(pid);
1092 any_changes = true;
1093 }
1094 }
1095 }
1096
1097 if self.player(pid).speed == 0
1100 && self
1101 .cards_in_zone(ZoneType::Battlefield, pid)
1102 .iter()
1103 .any(|&cid| self.card(cid).has_keyword("Start your engines"))
1104 {
1105 self.increase_player_speed(pid, None);
1106 any_changes = true;
1107 }
1108 }
1109
1110 for pid in self.player_order.clone() {
1111 if !self.player(pid).is_alive()
1112 && !self.player(pid).left_game
1113 && !newly_lost_players.contains(&pid)
1114 {
1115 newly_lost_players.push(pid);
1116 any_changes = true;
1117 }
1118 }
1119
1120 if !newly_lost_players.is_empty() {
1121 for pid in &newly_lost_players {
1122 self.on_player_lost(*pid, &mut trigger_handler);
1123 self.stack.remove_instances_controlled_by(*pid);
1124 }
1125 if let Some(handler) = trigger_handler.as_deref_mut() {
1126 for pid in &newly_lost_players {
1127 handler.run_trigger(
1128 TriggerType::LosesGame,
1129 RunParams {
1130 player: Some(*pid),
1131 ..Default::default()
1132 },
1133 false,
1134 );
1135 handler.on_player_lost(*pid);
1136 }
1137 }
1138 }
1139
1140 let battlefield_cards: Vec<CardId> = self
1142 .player_order
1143 .clone()
1144 .iter()
1145 .flat_map(|&pid| self.cards_in_zone(ZoneType::Battlefield, pid).to_vec())
1146 .collect();
1147
1148 for cid in battlefield_cards {
1149 let (is_creature, zero_toughness, lethal, should_die) = {
1150 let card = &self.cards[cid.index()];
1151 let is_creature = card.is_creature();
1152 let zero_toughness = card.toughness() <= 0;
1153 let lethal = card.lethal_damage() || card.has_deathtouch_damage;
1154 let should_die = zero_toughness || lethal;
1155 (is_creature, zero_toughness, lethal, should_die)
1156 };
1157 if is_creature && should_die {
1158 self.cards[cid.index()].has_deathtouch_damage = false;
1161 if lethal
1165 && !zero_toughness
1166 && self.cards[cid.index()].has_keyword("Indestructible")
1167 {
1168 continue;
1169 }
1170 let has_umbra = self.cards[cid.index()].attachments.iter().any(|&aid| {
1173 aid.index() < self.cards.len()
1174 && self.cards[aid.index()].zone == ZoneType::Battlefield
1175 && (self.cards[aid.index()].has_keyword("Umbra armor")
1176 || self.cards[aid.index()].has_keyword("Totem armor"))
1177 });
1178 if has_umbra && !zero_toughness {
1179 let umbra_id =
1181 self.cards[cid.index()]
1182 .attachments
1183 .iter()
1184 .copied()
1185 .find(|&aid| {
1186 aid.index() < self.cards.len()
1187 && self.cards[aid.index()].zone == ZoneType::Battlefield
1188 && (self.cards[aid.index()].has_keyword("Umbra armor")
1189 || self.cards[aid.index()].has_keyword("Totem armor"))
1190 });
1191 if let Some(umbra_id) = umbra_id {
1192 self.cards[cid.index()].damage = 0;
1194 self.cards[cid.index()].has_deathtouch_damage = false;
1195 let umbra_owner = self.cards[umbra_id.index()].owner;
1197 let old_zone = self.cards[umbra_id.index()].zone;
1198 self.move_card(umbra_id, ZoneType::Graveyard, umbra_owner);
1199 if let Some(handler) = trigger_handler.as_deref_mut() {
1200 crate::ability::effects::emit_zone_trigger(
1201 handler,
1202 umbra_id,
1203 old_zone,
1204 ZoneType::Graveyard,
1205 );
1206 }
1207 any_changes = true;
1208 continue; }
1210 }
1211
1212 if zero_toughness {
1213 self.move_battlefield_card_to_graveyard_for_sba(
1214 cid,
1215 &mut trigger_handler,
1216 &mut agents,
1217 );
1218 any_changes = true;
1219 continue;
1220 }
1221
1222 let mut destroy_event = ReplacementEvent::Destroy { target: cid };
1225 let result = apply_replacements(self, &mut destroy_event);
1226 if result != ReplacementResult::Replaced {
1227 self.move_battlefield_card_to_graveyard_for_sba(
1228 cid,
1229 &mut trigger_handler,
1230 &mut agents,
1231 );
1232 any_changes = true;
1236 } else {
1237 }
1240 }
1241 }
1242
1243 let battlefield_cards: Vec<CardId> = self
1244 .player_order
1245 .clone()
1246 .iter()
1247 .flat_map(|&pid| self.cards_in_zone(ZoneType::Battlefield, pid).to_vec())
1248 .collect();
1249
1250 for cid in battlefield_cards {
1251 let should_put_in_graveyard = {
1252 let card = self.card(cid);
1253 card.type_line.is_planeswalker() && card.counter_count(&CounterType::Loyalty) <= 0
1254 };
1255 if !should_put_in_graveyard {
1256 continue;
1257 }
1258
1259 self.move_battlefield_card_to_graveyard_for_sba(cid, &mut trigger_handler, &mut agents);
1260 any_changes = true;
1261 }
1262
1263 for &pid in &self.player_order.clone() {
1265 let battlefield = self.cards_in_zone(ZoneType::Battlefield, pid).to_vec();
1266 for cid in battlefield {
1267 let p1 = self.card(cid).counter_count(&CounterType::P1P1);
1268 let m1 = self.card(cid).counter_count(&CounterType::M1M1);
1269 if p1 > 0 && m1 > 0 {
1270 let cancel = p1.min(m1);
1271 self.card_mut(cid)
1272 .remove_counter(&CounterType::P1P1, cancel);
1273 self.card_mut(cid)
1274 .remove_counter(&CounterType::M1M1, cancel);
1275 any_changes = true;
1276 }
1277 }
1278 }
1279
1280 for &pid in &self.player_order.clone() {
1282 let mut commander_candidates = self.cards_in_zone(ZoneType::Graveyard, pid).to_vec();
1283 commander_candidates.extend(self.cards_in_zone(ZoneType::Exile, pid).iter().copied());
1284 for cid in commander_candidates {
1285 if !self.card(cid).can_move_to_command_zone() {
1286 continue;
1287 }
1288 self.card_mut(cid).move_to_command_zone = false;
1289 let accepted = if let Some(agents) = agents.as_deref_mut() {
1290 let name = self.card(cid).card_name.clone();
1291 let message = format!(
1292 "{}: If a commander is in a graveyard or in exile and that card was put into that zone since the last time state-based actions were checked, its owner may put it into the command zone.",
1293 name
1294 );
1295 agents[pid.index()].confirm_action(
1296 pid,
1297 Some("ChangeZoneToAltDestination"),
1298 &message,
1299 &[],
1300 Some(cid),
1301 None,
1302 )
1303 } else {
1304 false
1305 };
1306 if accepted {
1307 self.move_card_without_replacement(cid, ZoneType::Command, pid);
1308 any_changes = true;
1309 }
1310 }
1311 }
1312
1313 for &pid in &self.player_order.clone() {
1317 let battlefield = self.cards_in_zone(ZoneType::Battlefield, pid).to_vec();
1318 let mut by_name: std::collections::BTreeMap<String, Vec<CardId>> =
1319 std::collections::BTreeMap::new();
1320 for cid in battlefield {
1321 let c = self.card(cid);
1322 if !c.type_line.is_legendary() {
1323 continue;
1324 }
1325 if crate::staticability::static_ability_ignore_legend_rule::ignore_legend_rule(
1326 &self.cards,
1327 c,
1328 ) {
1329 continue;
1330 }
1331 by_name.entry(c.card_name.clone()).or_default().push(cid);
1332 }
1333 for (_name, ids) in by_name {
1334 if ids.len() <= 1 {
1335 continue;
1336 }
1337 let keep = if let Some(ref mut chooser) = legend_keep_fn {
1340 chooser(pid, &ids)
1341 } else if let Some(agents) = agents.as_deref_mut() {
1342 agents[pid.index()].choose_legend_keep(pid, &ids)
1343 } else {
1344 ids[0]
1345 };
1346 for cid in ids {
1347 if cid == keep {
1348 continue;
1349 }
1350 let owner = self.card(cid).owner;
1351 let old_zone = self.card(cid).zone;
1352 if let Some(agents) = agents.as_deref_mut() {
1353 self.move_card_with_agents(cid, ZoneType::Graveyard, owner, agents);
1354 } else {
1355 self.move_card(cid, ZoneType::Graveyard, owner);
1356 }
1357 if let Some(handler) = trigger_handler.as_deref_mut() {
1358 crate::ability::effects::emit_zone_trigger(
1359 handler,
1360 cid,
1361 old_zone,
1362 ZoneType::Graveyard,
1363 );
1364 }
1365 any_changes = true;
1366 }
1367 }
1368 }
1369
1370 {
1374 let aura_ids: Vec<CardId> = self
1375 .cards
1376 .iter()
1377 .filter(|c| {
1378 c.zone == ZoneType::Battlefield
1379 && c.type_line.has_subtype("Aura")
1380 && !c.type_line.is_creature() })
1382 .filter(|c| {
1383 match (c.attached_to, c.attached_to_player) {
1384 (None, None) => true, (None, Some(player_id)) => {
1386 if player_id.index() >= self.players.len() {
1387 return true;
1388 }
1389 let player = &self.players[player_id.index()];
1390 let enchant_type = c
1391 .keywords
1392 .iter_strings()
1393 .find_map(|kw| {
1394 crate::keyword::extract_keyword_cost_str(kw, "Enchant")
1395 })
1396 .unwrap_or_default();
1397 player.has_lost || !enchant_type.eq_ignore_ascii_case("Player")
1398 }
1399 (Some(host_id), _) => {
1400 if host_id.index() >= self.cards.len() {
1401 return true; }
1403 let host = &self.cards[host_id.index()];
1404 let enchant_type = c
1409 .keywords
1410 .iter_strings()
1411 .find_map(|kw| {
1412 crate::keyword::extract_keyword_cost_str(kw, "Enchant")
1413 })
1414 .unwrap_or_default();
1415 !crate::parsing::enchant_type_matches_card(enchant_type, host, Some(c))
1416 || !can_attachment_remain_attached(&self.cards, c, host, true)
1417 }
1418 }
1419 })
1420 .map(|c| c.id)
1421 .collect();
1422
1423 for aura_id in aura_ids {
1424 let owner = self.card(aura_id).owner;
1425 let old_zone = self.card(aura_id).zone;
1426 self.move_card(aura_id, ZoneType::Graveyard, owner);
1427 if let Some(handler) = trigger_handler.as_deref_mut() {
1428 crate::ability::effects::emit_zone_trigger(
1429 handler,
1430 aura_id,
1431 old_zone,
1432 ZoneType::Graveyard,
1433 );
1434 }
1435 any_changes = true;
1436 }
1437 }
1438
1439 let alive = self.alive_players();
1441 if alive.len() <= 1 {
1442 self.game_over = true;
1443 if alive.len() == 1 {
1444 self.winner = Some(alive[0]);
1445 }
1446 }
1447
1448 any_changes
1449 }
1450
1451 pub fn untap_all(&mut self, player: PlayerId) {
1454 let cards: Vec<CardId> = self.cards_in_zone(ZoneType::Battlefield, player).to_vec();
1455 for cid in cards {
1456 self.untap_during_untap_step(cid, player);
1458 }
1459 }
1460
1461 pub fn draw_card(&mut self, player: PlayerId) -> Option<CardId> {
1469 self.player_draw_one(player)
1470 }
1471
1472 pub fn draw_card_with_agents(
1474 &mut self,
1475 player: PlayerId,
1476 agents: &mut [Box<dyn crate::agent::PlayerAgent>],
1477 ) -> Option<CardId> {
1478 self.player_draw_one_internal(player, false, Some(agents))
1479 }
1480
1481 pub fn draw_cards(&mut self, player: PlayerId, n: usize) -> Vec<CardId> {
1483 self.player_draw_cards(player, n)
1484 }
1485
1486 pub fn shuffle_library(&mut self, player: PlayerId, rng: &mut impl rand::Rng) {
1488 self.shuffle_zone_cards_with_rand(ZoneType::Library, player, rng);
1489 }
1490
1491 pub fn new_turn_for_player(&mut self, player: PlayerId) {
1493 self.player_new_turn(player);
1494 for pid in &self.player_order.clone() {
1499 if *pid != player {
1500 self.player_reset_drawn_this_turn(*pid);
1501 let p = self.player_mut(*pid);
1502 p.life_started_this_turn_with = p.life;
1503 p.life_gained_this_turn = 0;
1504 p.life_gained_by_team_this_turn = 0;
1505 p.life_gained_times_this_turn = 0;
1506 p.life_lost_last_turn = p.life_lost_this_turn;
1507 p.life_lost_this_turn = 0;
1508 }
1509 }
1510
1511 let all_card_ids: Vec<CardId> = (0..self.cards.len()).map(|i| CardId(i as u32)).collect();
1512 for cid in all_card_ids {
1513 if self.cards[cid.index()].zone == ZoneType::Battlefield {
1514 self.cards[cid.index()].started_turn_tapped = self.cards[cid.index()].tapped;
1515 }
1516 if self.cards[cid.index()].controller == player {
1517 self.cards[cid.index()].new_turn();
1518 } else {
1519 self.cards[cid.index()].clear_global_turn_state();
1520 }
1521 }
1522 }
1523
1524 pub fn tap(&mut self, card_id: CardId) -> bool {
1527 let card = &self.cards[card_id.index()];
1528 if card.tapped {
1529 return false;
1530 }
1531 let mut event = ReplacementEvent::Tap { card: card_id };
1533 let result = apply_replacements(self, &mut event);
1534 if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
1535 return false; }
1537 self.cards[card_id.index()].tapped = true;
1538 true
1539 }
1540
1541 pub fn untap(&mut self, card_id: CardId) -> bool {
1544 self.untap_internal(card_id, None)
1545 }
1546
1547 pub fn untap_during_untap_step(&mut self, card_id: CardId, player: PlayerId) -> bool {
1548 self.untap_internal(card_id, Some(player))
1549 }
1550
1551 fn untap_internal(&mut self, card_id: CardId, player: Option<PlayerId>) -> bool {
1552 let card = &self.cards[card_id.index()];
1553 if !card.tapped {
1554 return false;
1555 }
1556 let stun = CounterType::Named("STUN".to_string());
1557 if card.counter_count(&stun) > 0 && card.can_remove_counters(&stun) {
1558 self.cards[card_id.index()].remove_counter(&stun, 1);
1561 return false;
1562 }
1563 let mut event = ReplacementEvent::Untap {
1565 card: card_id,
1566 player,
1567 };
1568 let result = apply_replacements(self, &mut event);
1569 if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
1570 return false; }
1572 self.cards[card_id.index()].tapped = false;
1573 crate::ability::effects::control_gain_effect::untap_hook(self, card_id);
1575 true
1576 }
1577
1578 pub fn change_controller(&mut self, card_id: CardId, new_controller: PlayerId) {
1582 let card = &self.cards[card_id.index()];
1583 if card.controller == new_controller {
1584 return;
1585 }
1586 let old_controller = card.controller;
1587 let zone = card.zone;
1588
1589 if zone != ZoneType::None {
1591 self.remove_card_from_zone(zone, old_controller, card_id);
1592 self.add_card_to_zone(zone, new_controller, card_id);
1593 }
1594 self.cards[card_id.index()].controller = new_controller;
1595 }
1596
1597 pub fn attach_to(&mut self, aura_id: CardId, target_id: CardId) {
1601 self.detach(aura_id);
1603 self.cards[aura_id.index()].attached_to = Some(target_id);
1604 self.cards[aura_id.index()].attached_to_player = None;
1605 self.cards[aura_id.index()].attached_this_turn = true;
1606 self.cards[target_id.index()].attachments.push(aura_id);
1607 }
1608
1609 pub fn attach_to_player(&mut self, aura_id: CardId, player_id: PlayerId) {
1610 self.detach(aura_id);
1611 self.cards[aura_id.index()].attached_to = None;
1612 self.cards[aura_id.index()].attached_to_player = Some(player_id);
1613 self.cards[aura_id.index()].attached_this_turn = true;
1614 }
1615
1616 pub fn detach(&mut self, aura_id: CardId) {
1619 if let Some(host_id) = self.cards[aura_id.index()].attached_to.take() {
1620 self.cards[host_id.index()]
1621 .attachments
1622 .retain(|&a| a != aura_id);
1623 self.cards[aura_id.index()].is_bestowed = false;
1625 }
1626 self.cards[aura_id.index()].attached_to_player = None;
1627 }
1628
1629 pub fn put_on_bottom_of_library(&mut self, card_id: CardId, owner: PlayerId) {
1632 let card = &self.cards[card_id.index()];
1633 let src_zone = card.zone;
1634 let src_owner = card.controller;
1635
1636 if src_zone != ZoneType::None {
1637 self.remove_card_from_zone(src_zone, src_owner, card_id);
1638 }
1639
1640 self.cards[card_id.index()].zone = ZoneType::Library;
1641 self.assign_zone_timestamp(card_id);
1642 self.add_card_to_zone_bottom(ZoneType::Library, owner, card_id);
1643 }
1644
1645 pub fn remove_from_stack(&mut self, entry_id: u32) -> bool {
1648 self.stack.remove_by_id(entry_id).is_some()
1649 }
1650}
1651
1652fn can_attachment_remain_attached(
1653 cards: &[Card],
1654 attachment: &Card,
1655 target: &Card,
1656 check_sba: bool,
1657) -> bool {
1658 if target.zone != ZoneType::Battlefield {
1659 return true;
1660 }
1661 if crate::staticability::static_ability_cant_attach::cant_attach(
1662 cards, attachment, target, check_sba,
1663 ) {
1664 return false;
1665 }
1666 !crate::staticability::static_ability_colorless_damage_source::target_is_protected_from_source(
1667 cards, target, attachment,
1668 )
1669}
1670
1671#[cfg(test)]
1672mod tests {
1673 use super::*;
1674 use crate::card::Card;
1675 use crate::player::RegisteredPlayer;
1676 use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
1677
1678 fn make_creature(game: &mut GameState, name: &str, owner: PlayerId, p: i32, t: i32) -> CardId {
1679 let card = Card::new(
1680 CardId(0),
1681 name.to_string(),
1682 owner,
1683 CardTypeLine::parse("Creature Bear"),
1684 ManaCost::parse("1 G"),
1685 ColorSet::GREEN,
1686 Some(p),
1687 Some(t),
1688 vec![],
1689 vec![],
1690 );
1691 game.create_card(card)
1692 }
1693
1694 #[test]
1695 fn move_card_to_battlefield() {
1696 let mut game = GameState::new(&["Alice", "Bob"], 20);
1697 let cid = make_creature(&mut game, "Bear", PlayerId(0), 2, 2);
1698 game.move_card(cid, ZoneType::Hand, PlayerId(0));
1699 assert_eq!(game.zone(ZoneType::Hand, PlayerId(0)).len(), 1);
1700
1701 game.move_card(cid, ZoneType::Battlefield, PlayerId(0));
1702 assert_eq!(game.zone(ZoneType::Hand, PlayerId(0)).len(), 0);
1703 assert_eq!(game.zone(ZoneType::Battlefield, PlayerId(0)).len(), 1);
1704 assert_eq!(game.card(cid).zone, ZoneType::Battlefield);
1705 }
1706
1707 #[test]
1708 fn state_based_actions_lethal_damage() {
1709 let mut game = GameState::new(&["Alice", "Bob"], 20);
1710 let cid = make_creature(&mut game, "Bear", PlayerId(0), 2, 2);
1711 game.move_card(cid, ZoneType::Battlefield, PlayerId(0));
1712
1713 game.deal_damage_to_card(cid, 2);
1714 assert!(game.check_state_based_actions());
1715 assert_eq!(game.zone(ZoneType::Graveyard, PlayerId(0)).len(), 1);
1716 }
1717
1718 #[test]
1719 fn state_based_actions_zero_life() {
1720 let mut game = GameState::new(&["Alice", "Bob"], 20);
1721 game.deal_damage_to_player(PlayerId(0), 20);
1722 game.check_state_based_actions();
1723 assert!(game.player(PlayerId(0)).has_lost);
1724 assert!(game.game_over);
1725 assert_eq!(game.winner, Some(PlayerId(1)));
1726 }
1727
1728 #[test]
1729 fn draw_card() {
1730 let mut game = GameState::new(&["Alice", "Bob"], 20);
1731 let cid = make_creature(&mut game, "Bear", PlayerId(0), 2, 2);
1732 game.move_card(cid, ZoneType::Library, PlayerId(0));
1733
1734 let drawn = game.draw_card(PlayerId(0));
1735 assert_eq!(drawn, Some(cid));
1736 assert_eq!(game.card(cid).zone, ZoneType::Hand);
1737 }
1738
1739 #[test]
1740 fn tap_untap() {
1741 let mut game = GameState::new(&["Alice", "Bob"], 20);
1742 let cid = make_creature(&mut game, "Bear", PlayerId(0), 2, 2);
1743 game.move_card(cid, ZoneType::Battlefield, PlayerId(0));
1744
1745 assert!(game.tap(cid));
1746 assert!(game.card(cid).tapped);
1747 assert!(!game.tap(cid)); assert!(game.untap(cid));
1749 assert!(!game.card(cid).tapped);
1750 }
1751
1752 #[test]
1753 fn stun_counter_replaces_untap() {
1754 let mut game = GameState::new(&["Alice", "Bob"], 20);
1755 let cid = make_creature(&mut game, "Bear", PlayerId(0), 2, 2);
1756 game.move_card(cid, ZoneType::Battlefield, PlayerId(0));
1757 game.tap(cid);
1758 game.card_mut(cid)
1759 .add_counter(&CounterType::Named("STUN".to_string()), 1);
1760
1761 assert!(!game.untap(cid));
1762 assert!(game.card(cid).tapped);
1763 assert_eq!(
1764 game.card(cid)
1765 .counter_count(&CounterType::Named("STUN".to_string())),
1766 0
1767 );
1768 }
1769}