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 etb_counters = std::collections::BTreeMap::new();
194 if dest_zone == ZoneType::Battlefield {
195 for keyword in self.cards[card_id.index()].keywords.as_string_list() {
196 let mut parts = keyword.split(':');
197 if !parts
198 .next()
199 .is_some_and(|head| head.eq_ignore_ascii_case("etbCounter"))
200 {
201 continue;
202 }
203 let counter_type =
204 crate::ability::effects::parse_counter_type(parts.next().unwrap_or_default());
205 let amount_text = parts.next().unwrap_or_default();
206 let amount = amount_text.parse::<i32>().unwrap_or_else(|_| {
207 let card = &self.cards[card_id.index()];
208 card.svars
209 .get(amount_text)
210 .map(|expression| {
211 if matches!(expression.as_str(), "Count$xPaid" | "Count$XPaid") {
212 card.svars
213 .get("XPaid")
214 .and_then(|value| value.parse().ok())
215 .unwrap_or(0)
216 } else {
217 expression.parse().unwrap_or_else(|_| {
218 crate::svar::resolve_count_svar(
219 expression, self, card_id, dest_owner,
220 )
221 })
222 }
223 })
224 .unwrap_or(0)
225 });
226 *etb_counters.entry(counter_type).or_default() += amount.max(0);
227 }
228 let card = &self.cards[card_id.index()];
229 if card.type_line.has_subtype("Saga") && card.has_chapter() {
230 let amount = if card.has_keyword("Read ahead") {
231 agents
232 .as_deref_mut()
233 .and_then(|agents| {
234 agents[dest_owner.index()].choose_number(
235 dest_owner,
236 Some(card_id),
237 "How many lore counters?",
238 Some("Choose a chapter and start with that many lore counters."),
239 1,
240 card.get_final_chapter_nr(),
241 )
242 })
243 .unwrap_or(1)
244 .clamp(1, card.get_final_chapter_nr())
245 } else {
246 1
247 };
248 *etb_counters.entry(CounterType::Lore).or_default() += amount;
249 }
250 if card.type_line.is_planeswalker() {
251 let loyalty = card
252 .initial_loyalty
253 .as_deref()
254 .and_then(|value| value.parse::<i32>().ok())
255 .unwrap_or(0);
256 *etb_counters
257 .entry(crate::card::CounterType::Loyalty)
258 .or_default() += loyalty.max(0);
259 }
260 *etb_counters
261 .entry(crate::card::CounterType::P1P1)
262 .or_default() += card.etb_counters_p1p1.max(0);
263 let sunburst = card.sunburst_count();
264 if sunburst > 0 && card.has_keyword("Sunburst") {
265 let counter_type = if card.is_creature() {
266 crate::card::CounterType::P1P1
267 } else {
268 crate::card::CounterType::Charge
269 };
270 *etb_counters.entry(counter_type).or_default() += sunburst;
271 }
272 etb_counters.retain(|_, amount| *amount > 0);
273 }
274 let counter_cause = self.cards[card_id.index()].cast_sa.clone();
275 let counter_map = (!etb_counters.is_empty()).then(|| {
276 vec![crate::replacement::replacement_handler::CounterMapValue {
277 source: Some(dest_owner),
278 counters: etb_counters,
279 }]
280 });
281 let mut moved_event = ReplacementEvent::Moved {
282 card: card_id,
283 origin: src_zone,
284 destination: dest_zone,
285 is_discard,
286 counter_map,
287 counter_cause,
288 counter_is_effect: dest_zone == ZoneType::Battlefield,
289 after_replacement_static_abilities: Vec::new(),
290 };
291 let tapped_before_replacement = self.card(card_id).tapped;
292 if apply_move_replacement {
293 if let Some(agents) = agents.as_deref_mut() {
294 apply_replacements_with_agents(self, agents, &mut moved_event);
295 } else {
296 apply_replacements(self, &mut moved_event);
297 }
298 }
299 let (dest_zone, etb_counter_map, counter_cause, after_replacement_static_abilities) =
300 match moved_event {
301 ReplacementEvent::Moved {
302 destination,
303 counter_map,
304 counter_cause,
305 after_replacement_static_abilities,
306 ..
307 } => (
308 destination,
309 counter_map,
310 counter_cause,
311 after_replacement_static_abilities,
312 ),
313 _ => (dest_zone, None, None, Vec::new()),
314 };
315 let replacement_marked_etb_tapped = dest_zone == ZoneType::Battlefield
316 && self.card(card_id).tapped
317 && !tapped_before_replacement;
318 let dest_owner = if dest_zone == ZoneType::Command {
319 self.card(card_id).owner
320 } else {
321 dest_owner
322 };
323 let host_left_battlefield =
324 src_zone == ZoneType::Battlefield && dest_zone != ZoneType::Battlefield;
325 if host_left_battlefield && was_permanent {
326 self.player_record_permanent_left_battlefield(src_owner);
327 }
328 if host_left_battlefield {
331 self.card_mut(card_id).cast_sa = None;
332 crate::ability::effects::control_gain_effect::leaves_play_hook(self, card_id);
335 }
336 if dest_zone == ZoneType::Graveyard && was_permanent && !is_token {
337 self.player_record_permanent_put_into_graveyard(self.card(card_id).owner);
338 }
339 let forget_effects: Vec<CardId> = self
340 .cards
341 .iter()
342 .filter(|c| {
343 c.zone == ZoneType::Command
344 && c.forget_on_moved_origin == Some(src_zone)
345 && c.remembered_cards.contains(&card_id)
346 })
347 .map(|c| c.id)
348 .collect();
349
350 if is_token && dest_zone != ZoneType::Battlefield {
353 if let Some(table) = self.pending_change_zone_table.as_mut() {
354 table.put(Some(src_zone), Some(ZoneType::None), card_id);
355 }
356 let mut exile_effects = Vec::new();
357 for eff_id in forget_effects.iter().copied() {
358 let eff = &mut self.cards[eff_id.index()];
359 eff.remembered_cards.retain(|&rid| rid != card_id);
360 if eff.exile_when_no_remembered && eff.remembered_cards.is_empty() {
361 exile_effects.push(eff_id);
362 }
363 }
364 self.cards[card_id.index()].zone = ZoneType::None;
365 if src_zone != ZoneType::None {
366 self.remove_card_from_zone(src_zone, src_owner, card_id);
367 }
368 for eff_id in exile_effects {
372 let controller = self.card(eff_id).controller;
373 self.remove_card_from_zone(ZoneType::Command, controller, eff_id);
374 self.cards[eff_id.index()].zone = ZoneType::None;
375 }
376 apply_continuous_effects(self);
377 debug_assert!(self.card_zone_location_matches_card(card_id));
378 return;
379 }
380
381 if src_zone != ZoneType::None {
383 self.remove_card_from_zone(src_zone, src_owner, card_id);
384 }
385
386 if src_zone == ZoneType::Exile && dest_zone != ZoneType::Exile {
387 self.cards[card_id.index()]
388 .keywords
389 .retain(|kw| !kw.starts_with(crate::card::KEYWORD_PLOTTED_PREFIX));
390 }
391
392 self.cards[card_id.index()].zone = dest_zone;
394 if src_zone != dest_zone {
395 self.cards[card_id.index()].turn_in_zone = self.turn.turn_number;
396 }
397
398 if let Some(table) = self.pending_change_zone_table.as_mut() {
399 table.put(Some(src_zone), Some(dest_zone), card_id);
400 }
401
402 if dest_zone != ZoneType::Stack {
405 self.assign_zone_timestamp(card_id);
406 }
407
408 self.save_zone_lki(dest_zone, dest_owner, card_id, src_zone);
410
411 match dest_zone {
413 ZoneType::Battlefield => {
414 self.cards[card_id.index()].controller = dest_owner;
418 self.cards[card_id.index()].enter_battlefield();
419 if replacement_marked_etb_tapped {
420 self.cards[card_id.index()].set_tapped(true);
421 }
422 self.add_card_to_zone(dest_zone, dest_owner, card_id);
425 if was_land {
426 self.player_record_landfall(dest_owner);
427 }
428 if !replacement_marked_etb_tapped {
435 apply_etb_tapped_with_agents(self, card_id, agents);
436 }
437 if let Some(handler) = trigger_handler.as_deref_mut() {
438 handler.register_active_trigger(self, card_id);
439 }
440 if let Some(counter_map) = etb_counter_map {
441 let table =
442 crate::game_entity_counter_table::GameEntityCounterTable::from_counter_map(
443 crate::agent::GameEntity::Card(card_id),
444 counter_map,
445 );
446 table.apply_replaced_counter_effect(
447 self,
448 trigger_handler.as_deref_mut(),
449 counter_cause.as_deref(),
450 RunParams::default(),
451 );
452 for (source, static_abilities) in after_replacement_static_abilities {
453 crate::replacement::replace_add_counter::apply_after_replacement_static_abilities(
454 self,
455 source,
456 static_abilities,
457 );
458 }
459 }
460 self.cards[card_id.index()].etb_counters_p1p1 = 0;
461 self.update_lki_snapshot(card_id);
465 apply_continuous_effects(self);
466 debug_assert!(self.card_zone_location_matches_card(card_id));
467 return;
468 }
469 ZoneType::Graveyard | ZoneType::Hand | ZoneType::Exile | ZoneType::Library => {
470 let attachments: Vec<CardId> = self.cards[card_id.index()].attachments.clone();
472 for aura_id in attachments {
473 self.cards[aura_id.index()].attached_to = None;
474 self.cards[aura_id.index()].is_bestowed = false;
476 }
477 self.cards[card_id.index()].attachments.clear();
478 self.detach(card_id);
480
481 if src_zone == ZoneType::Battlefield {
484 let card = &self.cards[card_id.index()];
485 let lki_p = card.power();
486 let lki_t = card.toughness();
487 let card = &mut self.cards[card_id.index()];
488 card.lki_power = Some(lki_p);
489 card.lki_toughness = Some(lki_t);
490 }
491
492 let keep_counters =
494 crate::staticability::static_ability_counters_remain::counters_remain(
495 &self.cards,
496 &self.cards[card_id.index()],
497 dest_zone,
498 );
499 let card = &mut self.cards[card_id.index()];
500 card.tapped = false;
501 card.damage = 0;
502 card.power_modifier = 0;
503 card.toughness_modifier = 0;
504 card.static_power_modifier = 0;
505 card.static_toughness_modifier = 0;
506 card.static_set_power = None;
507 card.static_set_toughness = None;
508 card.granted_keywords.clear();
509 if let Some(type_line) = card.static_type_line_base.take() {
510 card.set_type_line(type_line);
511 }
512 card.static_added_subtypes.clear();
513 card.restore_changed_characteristics_baseline();
514 card.cant_attack_static = false;
515 card.cant_block_static = false;
516 card.summoning_sick = true;
517 card.monstrous = false;
518 card.controller = card.owner;
519 card.face_down = false;
520 card.is_bestowed = false;
521 card.cast_from = None;
525 card.reset_crewed();
526 if !keep_counters {
527 card.counters.clear();
528 }
529 card.clear_pump_triggers();
537 card.clear_pump_keywords();
538 if let Some(state) = card.animate_state.take() {
543 if let Some(orig_kws) = state.original_keywords {
544 card.keywords = orig_kws;
545 card.update_keywords();
546 }
547 }
548 if let Some(state) = card.clone_state.take() {
549 card.restore_clone_snapshot(state);
550 } else {
551 card.remove_clone_states();
552 }
553 }
554 ZoneType::Command => {
555 let attachments: Vec<CardId> = self.cards[card_id.index()].attachments.clone();
557 for aura_id in attachments {
558 self.cards[aura_id.index()].attached_to = None;
559 }
560 self.cards[card_id.index()].attachments.clear();
561 self.detach(card_id);
562
563 let keep_counters =
565 crate::staticability::static_ability_counters_remain::counters_remain(
566 &self.cards,
567 &self.cards[card_id.index()],
568 dest_zone,
569 );
570 let card = &mut self.cards[card_id.index()];
571 card.tapped = false;
572 card.damage = 0;
573 card.power_modifier = 0;
574 card.toughness_modifier = 0;
575 card.static_power_modifier = 0;
576 card.static_toughness_modifier = 0;
577 card.static_set_power = None;
578 card.static_set_toughness = None;
579 card.granted_keywords.clear();
580 if let Some(type_line) = card.static_type_line_base.take() {
581 card.set_type_line(type_line);
582 }
583 card.static_added_subtypes.clear();
584 card.restore_changed_characteristics_baseline();
585 card.cant_attack_static = false;
586 card.cant_block_static = false;
587 card.summoning_sick = true;
588 card.monstrous = false;
589 card.controller = card.owner;
590 card.cast_from = None;
591 if !keep_counters {
592 card.counters.clear();
593 }
594 if let Some(state) = card.clone_state.take() {
595 card.restore_clone_snapshot(state);
596 } else {
597 card.remove_clone_states();
598 }
599 }
600 _ => {}
601 }
602
603 self.add_card_to_zone(dest_zone, dest_owner, card_id);
605
606 let commander_entered_gy_or_exile = self.card(card_id).is_commander
609 && matches!(dest_zone, ZoneType::Graveyard | ZoneType::Exile);
610 self.cards[card_id.index()].move_to_command_zone = commander_entered_gy_or_exile;
611
612 let mut exile_effects = Vec::new();
614 for eff_id in forget_effects {
615 let eff = &mut self.cards[eff_id.index()];
616 eff.remembered_cards.retain(|&rid| rid != card_id);
617 if eff.exile_when_no_remembered && eff.remembered_cards.is_empty() {
618 exile_effects.push(eff_id);
619 }
620 }
621 for eff_id in exile_effects {
624 let controller = self.card(eff_id).controller;
625 self.remove_card_from_zone(ZoneType::Command, controller, eff_id);
626 self.cards[eff_id.index()].zone = ZoneType::None;
627 }
628
629 if host_left_battlefield {
632 let linked_effects: Vec<CardId> = self
633 .cards
634 .iter()
635 .filter(|c| c.zone == ZoneType::Command && c.temp_effect_host == Some(card_id))
636 .map(|c| c.id)
637 .collect();
638 for eff_id in linked_effects {
639 let controller = self.card(eff_id).controller;
640 self.remove_card_from_zone(ZoneType::Command, controller, eff_id);
641 self.cards[eff_id.index()].zone = ZoneType::None;
642 }
643
644 let exiled_by_host: Vec<(CardId, PlayerId)> = self
647 .cards
648 .iter()
649 .filter(|c| c.zone == ZoneType::Exile && c.exiled_by == Some(card_id))
650 .map(|c| (c.id, c.owner))
651 .collect();
652 for (exiled_id, owner) in exiled_by_host {
653 self.cards[exiled_id.index()].exiled_by = None;
654 self.move_card(exiled_id, ZoneType::Battlefield, owner);
655 if let Some(handler) = trigger_handler.as_deref_mut() {
656 let returned_zone = self.card(exiled_id).zone;
657 handler.register_active_trigger(self, exiled_id);
658 crate::ability::effects::zone_triggers::emit_zone_trigger(
659 handler,
660 exiled_id,
661 ZoneType::Exile,
662 returned_zone,
663 );
664 }
665 }
666 }
667
668 apply_continuous_effects(self);
669 debug_assert!(self.card_zone_location_matches_card(card_id));
670 }
671
672 pub fn deal_damage_to_card(&mut self, target: CardId, amount: i32) {
677 self.deal_damage_to_card_from(target, amount, None, false);
678 }
679
680 pub fn deal_damage_to_card_from(
682 &mut self,
683 target: CardId,
684 amount: i32,
685 source: Option<CardId>,
686 is_combat: bool,
687 ) {
688 self.deal_damage_to_card_from_with_agents(target, amount, source, is_combat, None);
689 }
690
691 pub fn deal_damage_to_card_from_with_agents(
693 &mut self,
694 target: CardId,
695 amount: i32,
696 source: Option<CardId>,
697 is_combat: bool,
698 agents: Option<&mut [Box<dyn crate::agent::PlayerAgent>]>,
699 ) {
700 if amount <= 0 {
701 return;
702 }
703 if !self.card(target).can_be_dealt_damage() {
704 return;
705 }
706 let mut event = ReplacementEvent::DamageToCard {
707 target,
708 amount,
709 source,
710 is_combat,
711 };
712 if let Some(agents) = agents {
713 apply_replacements_with_agents(self, agents, &mut event);
714 } else {
715 apply_replacements(self, &mut event);
716 }
717 if let ReplacementEvent::DamageToCard {
718 amount: mut final_amount,
719 ..
720 } = event
721 {
722 let shields = self.cards[target.index()].damage_prevention;
727 if shields > 0 && final_amount > 0 {
728 let consumed = shields.min(final_amount);
729 self.cards[target.index()].damage_prevention -= consumed;
730 final_amount -= consumed;
731 }
732 if final_amount > 0 {
733 let dealt = self.cards[target.index()].add_damage_after_prevention(final_amount);
734 let mut dealt_event = ReplacementEvent::DealtDamage {
736 target,
737 amount: dealt,
738 source,
739 };
740 if dealt > 0 {
741 apply_replacements(self, &mut dealt_event);
742 }
743 }
744 }
745 }
746
747 pub fn deal_damage_to_player(&mut self, target: PlayerId, amount: i32) -> i32 {
752 self.deal_damage_to_player_from(target, amount, None, false)
753 }
754
755 pub fn deal_damage_to_player_from(
757 &mut self,
758 target: PlayerId,
759 amount: i32,
760 source: Option<CardId>,
761 is_combat: bool,
762 ) -> i32 {
763 self.deal_damage_to_player_from_with_agents(target, amount, source, is_combat, None)
764 }
765
766 pub fn deal_damage_to_player_from_with_agents(
771 &mut self,
772 target: PlayerId,
773 amount: i32,
774 source: Option<CardId>,
775 is_combat: bool,
776 agents: Option<&mut [Box<dyn crate::agent::PlayerAgent>]>,
777 ) -> i32 {
778 if amount <= 0 {
779 return 0;
780 }
781 if crate::staticability::static_ability_cant_gain_lose_pay_life::cant_lose_life(
782 self, target,
783 ) {
784 return 0;
785 }
786 if crate::player::has_keyword(self, target, "Protection from everything") {
787 return 0;
788 }
789 let mut event = ReplacementEvent::DamageToPlayer {
790 target,
791 amount,
792 source,
793 is_combat,
794 };
795 if let Some(agents) = agents {
796 apply_replacements_with_agents(self, agents, &mut event);
797 } else {
798 apply_replacements(self, &mut event);
799 }
800 if let ReplacementEvent::DamageToPlayer {
801 amount: final_amount,
802 ..
803 } = event
804 {
805 if final_amount > 0 {
806 return self.player_deal_damage(target, final_amount);
807 }
808 }
809 0
810 }
811
812 pub fn check_state_based_actions(&mut self) -> bool {
814 self.check_state_based_actions_with_triggers(None, None)
815 }
816
817 pub fn check_state_based_actions_with_triggers(
822 &mut self,
823 trigger_handler: Option<&mut TriggerHandler>,
824 legend_keep_fn: Option<&mut dyn FnMut(PlayerId, &[CardId]) -> CardId>,
825 ) -> bool {
826 self.check_state_based_actions_impl(trigger_handler, legend_keep_fn, None)
827 }
828
829 pub fn check_state_based_actions_with_trigger_agents(
830 &mut self,
831 trigger_handler: Option<&mut TriggerHandler>,
832 agents: &mut [Box<dyn PlayerAgent>],
833 ) -> bool {
834 self.check_state_based_actions_impl(trigger_handler, None, Some(agents))
835 }
836
837 fn state_based_action_saga(
838 &self,
839 cid: CardId,
840 trigger_handler: Option<&TriggerHandler>,
841 sacrifice_list: &mut Vec<CardId>,
842 ) -> bool {
843 let card = self.card(cid);
844 if !card.type_line.has_subtype("Saga") || !card.has_chapter() {
845 return false;
846 }
847 if crate::staticability::static_ability_cant_sacrifice::cant_sacrifice(
848 &self.cards,
849 card,
850 None,
851 true,
852 ) {
853 return false;
854 }
855 if card.counter_count(&CounterType::Lore) < card.get_final_chapter_nr() {
856 return false;
857 }
858 if self.stack.has_source_chapter_on_stack(self, cid)
859 || trigger_handler.is_some_and(|handler| handler.has_source_chapter_pending(self, cid))
860 {
861 return false;
862 }
863 sacrifice_list.push(cid);
864 true
865 }
866
867 fn on_player_lost(
868 &mut self,
869 player: PlayerId,
870 trigger_handler: &mut Option<&mut TriggerHandler>,
871 ) {
872 self.player_mut(player).left_game = true;
873 let is_multiplayer = self.player_order.len() > 2;
874 let all_cards: Vec<CardId> = (0..self.cards.len()).map(|i| CardId(i as u32)).collect();
875
876 if !is_multiplayer {
877 for &cid in &all_cards {
879 self.cards[cid.index()].force_turn_face_up();
880 }
881 return;
882 }
883
884 if self.monarch == Some(player) {
886 let heir = if self.turn.active_player == player {
887 self.next_player(player)
888 } else {
889 self.turn.active_player
890 };
891 self.player_set_monarch(heir, trigger_handler.as_deref_mut());
892 }
893 if self.initiative_holder == Some(player) {
894 let heir = if self.turn.active_player == player {
895 self.next_player(player)
896 } else {
897 self.turn.active_player
898 };
899 self.player_take_initiative(heir, trigger_handler.as_deref_mut());
900 }
901
902 let next = self.next_player(player);
903 for &cid in &all_cards {
904 let (zone, owner, controller) = {
905 let card = &self.cards[cid.index()];
906 (card.zone, card.owner, card.controller)
907 };
908 if zone == ZoneType::None {
909 continue;
910 }
911 if owner != player {
912 if self.cards[cid.index()].attached_to_player == Some(player) {
914 self.cards[cid.index()].attached_to_player = None;
915 }
916 continue;
917 }
918 if self.cards[cid.index()].effect_source.is_some() && zone == ZoneType::Command {
919 self.remove_card_from_zone(ZoneType::Command, controller, cid);
922 self.cards[cid.index()].controller = next;
923 self.add_card_to_zone(ZoneType::Command, next, cid);
924 continue;
925 }
926 for &other in &all_cards {
928 if other == cid {
929 continue;
930 }
931 let other_card = &mut self.cards[other.index()];
932 other_card.imprinted_cards.retain(|&r| r != cid);
933 other_card.remembered_cards.retain(|&r| r != cid);
934 other_card.attachments.retain(|&r| r != cid);
935 other_card.gain_control_targets.retain(|&r| r != cid);
936 if other_card.attached_to == Some(cid) {
937 other_card.attached_to = None;
938 }
939 }
940 if let Some(handler) = trigger_handler.as_deref_mut() {
941 crate::ability::effects::emit_zone_trigger(handler, cid, zone, ZoneType::None);
942 }
943 self.remove_card_from_zone(zone, controller, cid);
944 self.cards[cid.index()].zone = ZoneType::None;
945 }
946
947 apply_continuous_effects(self);
948
949 for &cid in &all_cards {
951 let (zone, owner, controller) = {
952 let card = &self.cards[cid.index()];
953 (card.zone, card.owner, card.controller)
954 };
955 if zone == ZoneType::Battlefield && controller == player && owner != player {
956 if let Some(handler) = trigger_handler.as_deref_mut() {
957 crate::ability::effects::emit_zone_trigger(
958 handler,
959 cid,
960 ZoneType::Battlefield,
961 ZoneType::Exile,
962 );
963 }
964 self.move_card_without_replacement(cid, ZoneType::Exile, owner);
965 }
966 }
967 }
968
969 fn move_battlefield_card_to_graveyard_for_sba(
970 &mut self,
971 cid: CardId,
972 trigger_handler: &mut Option<&mut TriggerHandler>,
973 agents: &mut Option<&mut [Box<dyn PlayerAgent>]>,
974 ) {
975 let owner = self.card(cid).owner;
976 let mut moved_event = ReplacementEvent::Moved {
977 card: cid,
978 origin: ZoneType::Battlefield,
979 destination: ZoneType::Graveyard,
980 is_discard: false,
981 counter_map: None,
982 counter_cause: None,
983 counter_is_effect: false,
984 after_replacement_static_abilities: Vec::new(),
985 };
986 if let Some(agents) = agents.as_deref_mut() {
987 apply_replacements_with_agents(self, agents, &mut moved_event);
988 } else {
989 apply_replacements(self, &mut moved_event);
990 }
991 let final_dest = if let ReplacementEvent::Moved { destination, .. } = moved_event {
992 destination
993 } else {
994 ZoneType::Graveyard
995 };
996 let old_zone = self.card(cid).zone;
997 if let Some(handler) = trigger_handler.as_deref_mut() {
1000 let lki_p1p1 = *self
1001 .card(cid)
1002 .counters
1003 .get(&CounterType::P1P1)
1004 .unwrap_or(&0);
1005 let lki_power = self.card(cid).power();
1006 let lki_toughness = self.card(cid).toughness();
1007 let lki_counters = self.card(cid).counters.clone();
1008 self.card_mut(cid).lki_counters = Some(lki_counters);
1009 self.card_mut(cid)
1010 .set_lki_power_toughness(Some(lki_power), Some(lki_toughness));
1011 crate::ability::effects::emit_zone_trigger_with_lki_counters(
1012 handler,
1013 cid,
1014 old_zone,
1015 final_dest,
1016 lki_p1p1,
1017 lki_power,
1018 lki_toughness,
1019 );
1020 handler.flush_waiting_triggers(self);
1021 }
1022 self.move_card_without_replacement(cid, final_dest, owner);
1023 }
1024
1025 fn check_state_based_actions_impl(
1026 &mut self,
1027 mut trigger_handler: Option<&mut TriggerHandler>,
1028 mut legend_keep_fn: Option<&mut dyn FnMut(PlayerId, &[CardId]) -> CardId>,
1029 mut agents: Option<&mut [Box<dyn PlayerAgent>]>,
1030 ) -> bool {
1031 self.pre_sba_battlefield = self
1036 .cards
1037 .iter()
1038 .filter(|c| c.zone == ZoneType::Battlefield)
1039 .map(|c| c.id)
1040 .collect();
1041
1042 let mut any_changes = false;
1043 let mut newly_lost_players: Vec<PlayerId> = Vec::new();
1044 let mut sacrifice_list: Vec<CardId> = Vec::new();
1045
1046 for pid in self.player_order.clone() {
1048 if self.player(pid).tried_to_draw_from_empty_library && self.player(pid).is_alive() {
1049 self.player_mut(pid).tried_to_draw_from_empty_library = false;
1050 let mut event = ReplacementEvent::GameLoss {
1051 player: pid,
1052 reason: GameLossReason::Milled,
1053 };
1054 let result = apply_replacements(self, &mut event);
1055 if result != ReplacementResult::Replaced && !self.player(pid).has_lost {
1056 self.player_mark_lost(pid, GameLossReason::Milled);
1057 newly_lost_players.push(pid);
1058 any_changes = true;
1059 }
1060 }
1061 if self.player(pid).life <= 0 && self.player(pid).is_alive() {
1062 let mut event = ReplacementEvent::GameLoss {
1063 player: pid,
1064 reason: GameLossReason::LifeReachedZero,
1065 };
1066 let result = apply_replacements(self, &mut event);
1067 if result != ReplacementResult::Replaced && !self.player(pid).has_lost {
1068 self.player_mark_lost(pid, GameLossReason::LifeReachedZero);
1069 newly_lost_players.push(pid);
1070 any_changes = true;
1071 }
1072 }
1073 if self.player(pid).poison_counters >= 10 && self.player(pid).is_alive() {
1075 let mut event = ReplacementEvent::GameLoss {
1076 player: pid,
1077 reason: GameLossReason::Poisoned,
1078 };
1079 let result = apply_replacements(self, &mut event);
1080 if result != ReplacementResult::Replaced {
1081 if !self.player(pid).has_lost {
1082 self.player_mark_lost(pid, GameLossReason::Poisoned);
1083 newly_lost_players.push(pid);
1084 }
1085 any_changes = true;
1086 }
1087 }
1088 if self.player(pid).commander_damage_enabled {
1090 let commander_dmg_entries: Vec<(u32, i32)> = self
1091 .player(pid)
1092 .commander_damage_received
1093 .iter()
1094 .map(|(&k, &v)| (k, v))
1095 .collect();
1096 for (_card_raw_id, dmg) in commander_dmg_entries {
1097 if dmg >= 21 && self.player(pid).is_alive() && !self.player(pid).has_lost {
1098 self.player_mark_lost(pid, GameLossReason::CommanderDamage);
1099 newly_lost_players.push(pid);
1100 any_changes = true;
1101 }
1102 }
1103 }
1104
1105 if self.player(pid).speed == 0
1108 && self
1109 .cards_in_zone(ZoneType::Battlefield, pid)
1110 .iter()
1111 .any(|&cid| self.card(cid).has_keyword("Start your engines"))
1112 {
1113 self.increase_player_speed(pid, None);
1114 any_changes = true;
1115 }
1116 }
1117
1118 for pid in self.player_order.clone() {
1119 if !self.player(pid).is_alive()
1120 && !self.player(pid).left_game
1121 && !newly_lost_players.contains(&pid)
1122 {
1123 newly_lost_players.push(pid);
1124 any_changes = true;
1125 }
1126 }
1127
1128 if !newly_lost_players.is_empty() {
1129 for pid in &newly_lost_players {
1130 self.on_player_lost(*pid, &mut trigger_handler);
1131 self.stack.remove_instances_controlled_by(*pid);
1132 }
1133 if let Some(handler) = trigger_handler.as_deref_mut() {
1134 for pid in &newly_lost_players {
1135 handler.run_trigger(
1136 TriggerType::LosesGame,
1137 RunParams {
1138 player: Some(*pid),
1139 ..Default::default()
1140 },
1141 false,
1142 );
1143 handler.on_player_lost(*pid);
1144 }
1145 }
1146 }
1147
1148 let battlefield_cards: Vec<CardId> = self
1150 .player_order
1151 .clone()
1152 .iter()
1153 .flat_map(|&pid| self.cards_in_zone(ZoneType::Battlefield, pid).to_vec())
1154 .collect();
1155
1156 for cid in battlefield_cards {
1157 let (is_creature, zero_toughness, lethal, should_die) = {
1158 let card = &self.cards[cid.index()];
1159 let is_creature = card.is_creature();
1160 let zero_toughness = card.toughness() <= 0;
1161 let lethal = card.lethal_damage() || card.has_deathtouch_damage;
1162 let should_die = zero_toughness || lethal;
1163 (is_creature, zero_toughness, lethal, should_die)
1164 };
1165 if is_creature && should_die {
1166 self.cards[cid.index()].has_deathtouch_damage = false;
1169 if lethal
1173 && !zero_toughness
1174 && self.cards[cid.index()].has_keyword("Indestructible")
1175 {
1176 continue;
1177 }
1178 let has_umbra = self.cards[cid.index()].attachments.iter().any(|&aid| {
1181 aid.index() < self.cards.len()
1182 && self.cards[aid.index()].zone == ZoneType::Battlefield
1183 && (self.cards[aid.index()].has_keyword("Umbra armor")
1184 || self.cards[aid.index()].has_keyword("Totem armor"))
1185 });
1186 if has_umbra && !zero_toughness {
1187 let umbra_id =
1189 self.cards[cid.index()]
1190 .attachments
1191 .iter()
1192 .copied()
1193 .find(|&aid| {
1194 aid.index() < self.cards.len()
1195 && self.cards[aid.index()].zone == ZoneType::Battlefield
1196 && (self.cards[aid.index()].has_keyword("Umbra armor")
1197 || self.cards[aid.index()].has_keyword("Totem armor"))
1198 });
1199 if let Some(umbra_id) = umbra_id {
1200 self.cards[cid.index()].damage = 0;
1202 self.cards[cid.index()].has_deathtouch_damage = false;
1203 let umbra_owner = self.cards[umbra_id.index()].owner;
1205 let old_zone = self.cards[umbra_id.index()].zone;
1206 self.move_card(umbra_id, ZoneType::Graveyard, umbra_owner);
1207 if let Some(handler) = trigger_handler.as_deref_mut() {
1208 crate::ability::effects::emit_zone_trigger(
1209 handler,
1210 umbra_id,
1211 old_zone,
1212 ZoneType::Graveyard,
1213 );
1214 }
1215 any_changes = true;
1216 continue; }
1218 }
1219
1220 if zero_toughness {
1221 self.move_battlefield_card_to_graveyard_for_sba(
1222 cid,
1223 &mut trigger_handler,
1224 &mut agents,
1225 );
1226 any_changes = true;
1227 continue;
1228 }
1229
1230 let mut destroy_event = ReplacementEvent::Destroy { target: cid };
1233 let result = apply_replacements(self, &mut destroy_event);
1234 if result != ReplacementResult::Replaced {
1235 self.move_battlefield_card_to_graveyard_for_sba(
1236 cid,
1237 &mut trigger_handler,
1238 &mut agents,
1239 );
1240 any_changes = true;
1244 } else {
1245 }
1248 }
1249 }
1250
1251 let battlefield_cards: Vec<CardId> = self
1252 .player_order
1253 .clone()
1254 .iter()
1255 .flat_map(|&pid| self.cards_in_zone(ZoneType::Battlefield, pid).to_vec())
1256 .collect();
1257
1258 for cid in battlefield_cards {
1259 let should_put_in_graveyard = {
1260 let card = self.card(cid);
1261 card.type_line.is_planeswalker() && card.counter_count(&CounterType::Loyalty) <= 0
1262 };
1263 if !should_put_in_graveyard {
1264 continue;
1265 }
1266
1267 self.move_battlefield_card_to_graveyard_for_sba(cid, &mut trigger_handler, &mut agents);
1268 any_changes = true;
1269 }
1270
1271 let saga_cards: Vec<CardId> = self
1272 .player_order
1273 .clone()
1274 .iter()
1275 .flat_map(|&pid| self.cards_in_zone(ZoneType::Battlefield, pid).to_vec())
1276 .collect();
1277 for cid in saga_cards {
1278 any_changes |=
1279 self.state_based_action_saga(cid, trigger_handler.as_deref(), &mut sacrifice_list);
1280 }
1281
1282 if !sacrifice_list.is_empty() {
1283 if let (Some(handler), Some(agents)) =
1284 (trigger_handler.as_deref_mut(), agents.as_deref_mut())
1285 {
1286 if !crate::game_loop::perform_sacrifice(self, handler, agents, &sacrifice_list)
1287 .is_empty()
1288 {
1289 any_changes = true;
1290 }
1291 } else {
1292 for cid in sacrifice_list.drain(..) {
1293 let owner = self.card(cid).owner;
1294 self.move_card_without_replacement(cid, ZoneType::Graveyard, owner);
1295 }
1296 any_changes = true;
1297 }
1298 }
1299
1300 for &pid in &self.player_order.clone() {
1302 let battlefield = self.cards_in_zone(ZoneType::Battlefield, pid).to_vec();
1303 for cid in battlefield {
1304 let p1 = self.card(cid).counter_count(&CounterType::P1P1);
1305 let m1 = self.card(cid).counter_count(&CounterType::M1M1);
1306 if p1 > 0 && m1 > 0 {
1307 let cancel = p1.min(m1);
1308 self.card_mut(cid)
1309 .remove_counter(&CounterType::P1P1, cancel);
1310 self.card_mut(cid)
1311 .remove_counter(&CounterType::M1M1, cancel);
1312 any_changes = true;
1313 }
1314 }
1315 }
1316
1317 for &pid in &self.player_order.clone() {
1319 let mut commander_candidates = self.cards_in_zone(ZoneType::Graveyard, pid).to_vec();
1320 commander_candidates.extend(self.cards_in_zone(ZoneType::Exile, pid).iter().copied());
1321 for cid in commander_candidates {
1322 if !self.card(cid).can_move_to_command_zone() {
1323 continue;
1324 }
1325 self.card_mut(cid).move_to_command_zone = false;
1326 let accepted = if let Some(agents) = agents.as_deref_mut() {
1327 let name = self.card(cid).card_name.clone();
1328 let message = format!(
1329 "{name}: 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."
1330 );
1331 agents[pid.index()].confirm_action(
1332 pid,
1333 Some("ChangeZoneToAltDestination"),
1334 &message,
1335 &[],
1336 Some(cid),
1337 None,
1338 )
1339 } else {
1340 false
1341 };
1342 if accepted {
1343 self.move_card_without_replacement(cid, ZoneType::Command, pid);
1344 any_changes = true;
1345 }
1346 }
1347 }
1348
1349 for &pid in &self.player_order.clone() {
1353 let battlefield = self.cards_in_zone(ZoneType::Battlefield, pid).to_vec();
1354 let mut by_name: std::collections::BTreeMap<String, Vec<CardId>> =
1355 std::collections::BTreeMap::new();
1356 for cid in battlefield {
1357 let c = self.card(cid);
1358 if !c.type_line.is_legendary() {
1359 continue;
1360 }
1361 if crate::staticability::static_ability_ignore_legend_rule::ignore_legend_rule(
1362 &self.cards,
1363 c,
1364 ) {
1365 continue;
1366 }
1367 by_name.entry(c.card_name.clone()).or_default().push(cid);
1368 }
1369 for (_name, ids) in by_name {
1370 if ids.len() <= 1 {
1371 continue;
1372 }
1373 let keep = if let Some(ref mut chooser) = legend_keep_fn {
1376 chooser(pid, &ids)
1377 } else if let Some(agents) = agents.as_deref_mut() {
1378 agents[pid.index()].choose_legend_keep(pid, &ids)
1379 } else {
1380 ids[0]
1381 };
1382 for cid in ids {
1383 if cid == keep {
1384 continue;
1385 }
1386 let owner = self.card(cid).owner;
1387 let old_zone = self.card(cid).zone;
1388 if let Some(agents) = agents.as_deref_mut() {
1389 self.move_card_with_agents(cid, ZoneType::Graveyard, owner, agents);
1390 } else {
1391 self.move_card(cid, ZoneType::Graveyard, owner);
1392 }
1393 if let Some(handler) = trigger_handler.as_deref_mut() {
1394 crate::ability::effects::emit_zone_trigger(
1395 handler,
1396 cid,
1397 old_zone,
1398 ZoneType::Graveyard,
1399 );
1400 }
1401 any_changes = true;
1402 }
1403 }
1404 }
1405
1406 {
1410 let aura_ids: Vec<CardId> = self
1411 .cards
1412 .iter()
1413 .filter(|c| {
1414 c.zone == ZoneType::Battlefield
1415 && c.type_line.has_subtype("Aura")
1416 && !c.type_line.is_creature() })
1418 .filter(|c| {
1419 match (c.attached_to, c.attached_to_player) {
1420 (None, None) => true, (None, Some(player_id)) => {
1422 if player_id.index() >= self.players.len() {
1423 return true;
1424 }
1425 let player = &self.players[player_id.index()];
1426 let enchant_type = c
1427 .keywords
1428 .iter_strings()
1429 .find_map(|kw| {
1430 crate::keyword::extract_keyword_cost_str(kw, "Enchant")
1431 })
1432 .unwrap_or_default();
1433 player.has_lost || !enchant_type.eq_ignore_ascii_case("Player")
1434 }
1435 (Some(host_id), _) => {
1436 if host_id.index() >= self.cards.len() {
1437 return true; }
1439 let host = &self.cards[host_id.index()];
1440 let enchant_type = c
1445 .keywords
1446 .iter_strings()
1447 .find_map(|kw| {
1448 crate::keyword::extract_keyword_cost_str(kw, "Enchant")
1449 })
1450 .unwrap_or_default();
1451 !crate::parsing::enchant_type_matches_card(enchant_type, host, Some(c))
1452 || !can_attachment_remain_attached(&self.cards, c, host, true)
1453 }
1454 }
1455 })
1456 .map(|c| c.id)
1457 .collect();
1458
1459 for aura_id in aura_ids {
1460 let owner = self.card(aura_id).owner;
1461 let old_zone = self.card(aura_id).zone;
1462 self.move_card(aura_id, ZoneType::Graveyard, owner);
1463 if let Some(handler) = trigger_handler.as_deref_mut() {
1464 crate::ability::effects::emit_zone_trigger(
1465 handler,
1466 aura_id,
1467 old_zone,
1468 ZoneType::Graveyard,
1469 );
1470 }
1471 any_changes = true;
1472 }
1473 }
1474
1475 let alive = self.alive_players();
1477 if alive.len() <= 1 {
1478 self.game_over = true;
1479 if alive.len() == 1 {
1480 self.winner = Some(alive[0]);
1481 }
1482 }
1483
1484 any_changes
1485 }
1486
1487 pub fn untap_all(&mut self, player: PlayerId) {
1490 let cards: Vec<CardId> = self.cards_in_zone(ZoneType::Battlefield, player).to_vec();
1491 for cid in cards {
1492 self.untap_during_untap_step(cid, player);
1494 }
1495 }
1496
1497 pub fn draw_card(&mut self, player: PlayerId) -> Option<CardId> {
1505 self.player_draw_one(player)
1506 }
1507
1508 pub fn draw_card_with_agents(
1510 &mut self,
1511 player: PlayerId,
1512 agents: &mut [Box<dyn crate::agent::PlayerAgent>],
1513 ) -> Option<CardId> {
1514 self.player_draw_one_internal(player, false, Some(agents))
1515 }
1516
1517 pub fn draw_cards(&mut self, player: PlayerId, n: usize) -> Vec<CardId> {
1519 self.player_draw_cards(player, n)
1520 }
1521
1522 pub fn shuffle_library(&mut self, player: PlayerId, rng: &mut impl rand::Rng) {
1524 self.shuffle_zone_cards_with_rand(ZoneType::Library, player, rng);
1525 }
1526
1527 pub fn new_turn_for_player(&mut self, player: PlayerId) {
1529 self.player_new_turn(player);
1530 for pid in &self.player_order.clone() {
1535 if *pid != player {
1536 self.player_reset_drawn_this_turn(*pid);
1537 let p = self.player_mut(*pid);
1538 p.life_started_this_turn_with = p.life;
1539 p.life_gained_this_turn = 0;
1540 p.life_gained_by_team_this_turn = 0;
1541 p.life_gained_times_this_turn = 0;
1542 p.life_lost_last_turn = p.life_lost_this_turn;
1543 p.life_lost_this_turn = 0;
1544 }
1545 }
1546
1547 let all_card_ids: Vec<CardId> = (0..self.cards.len()).map(|i| CardId(i as u32)).collect();
1548 for cid in all_card_ids {
1549 if self.cards[cid.index()].zone == ZoneType::Battlefield {
1550 self.cards[cid.index()].started_turn_tapped = self.cards[cid.index()].tapped;
1551 }
1552 if self.cards[cid.index()].controller == player {
1553 self.cards[cid.index()].new_turn();
1554 } else {
1555 self.cards[cid.index()].clear_global_turn_state();
1556 }
1557 }
1558 }
1559
1560 pub fn tap(&mut self, card_id: CardId) -> bool {
1563 let card = &self.cards[card_id.index()];
1564 if card.tapped {
1565 return false;
1566 }
1567 let mut event = ReplacementEvent::Tap { card: card_id };
1569 let result = apply_replacements(self, &mut event);
1570 if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
1571 return false; }
1573 self.cards[card_id.index()].tapped = true;
1574 true
1575 }
1576
1577 pub fn untap(&mut self, card_id: CardId) -> bool {
1580 self.untap_internal(card_id, None)
1581 }
1582
1583 pub fn untap_during_untap_step(&mut self, card_id: CardId, player: PlayerId) -> bool {
1584 self.untap_internal(card_id, Some(player))
1585 }
1586
1587 fn untap_internal(&mut self, card_id: CardId, player: Option<PlayerId>) -> bool {
1588 let card = &self.cards[card_id.index()];
1589 if !card.tapped {
1590 return false;
1591 }
1592 let stun = CounterType::Named("STUN".to_string());
1593 if card.counter_count(&stun) > 0 && card.can_remove_counters(&stun) {
1594 self.cards[card_id.index()].remove_counter(&stun, 1);
1597 return false;
1598 }
1599 let mut event = ReplacementEvent::Untap {
1601 card: card_id,
1602 player,
1603 };
1604 let result = apply_replacements(self, &mut event);
1605 if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
1606 return false; }
1608 self.cards[card_id.index()].tapped = false;
1609 crate::ability::effects::control_gain_effect::untap_hook(self, card_id);
1611 true
1612 }
1613
1614 pub fn change_controller(&mut self, card_id: CardId, new_controller: PlayerId) {
1618 let card = &self.cards[card_id.index()];
1619 if card.controller == new_controller {
1620 return;
1621 }
1622 let old_controller = card.controller;
1623 let zone = card.zone;
1624
1625 if zone != ZoneType::None {
1627 self.remove_card_from_zone(zone, old_controller, card_id);
1628 self.add_card_to_zone(zone, new_controller, card_id);
1629 }
1630 self.cards[card_id.index()].controller = new_controller;
1631 }
1632
1633 pub fn attach_to(&mut self, aura_id: CardId, target_id: CardId) {
1637 self.detach(aura_id);
1639 self.cards[aura_id.index()].attached_to = Some(target_id);
1640 self.cards[aura_id.index()].attached_to_player = None;
1641 self.cards[aura_id.index()].attached_this_turn = true;
1642 self.cards[target_id.index()].attachments.push(aura_id);
1643 }
1644
1645 pub fn attach_to_player(&mut self, aura_id: CardId, player_id: PlayerId) {
1646 self.detach(aura_id);
1647 self.cards[aura_id.index()].attached_to = None;
1648 self.cards[aura_id.index()].attached_to_player = Some(player_id);
1649 self.cards[aura_id.index()].attached_this_turn = true;
1650 }
1651
1652 pub fn detach(&mut self, aura_id: CardId) {
1655 if let Some(host_id) = self.cards[aura_id.index()].attached_to.take() {
1656 self.cards[host_id.index()]
1657 .attachments
1658 .retain(|&a| a != aura_id);
1659 self.cards[aura_id.index()].is_bestowed = false;
1661 }
1662 self.cards[aura_id.index()].attached_to_player = None;
1663 }
1664
1665 pub fn put_on_bottom_of_library(&mut self, card_id: CardId, owner: PlayerId) {
1668 let card = &self.cards[card_id.index()];
1669 let src_zone = card.zone;
1670 let src_owner = card.controller;
1671
1672 if src_zone != ZoneType::None {
1673 self.remove_card_from_zone(src_zone, src_owner, card_id);
1674 }
1675
1676 self.cards[card_id.index()].zone = ZoneType::Library;
1677 self.assign_zone_timestamp(card_id);
1678 self.add_card_to_zone_bottom(ZoneType::Library, owner, card_id);
1679 }
1680
1681 pub fn remove_from_stack(&mut self, entry_id: u32) -> bool {
1684 self.stack.remove_by_id(entry_id).is_some()
1685 }
1686}
1687
1688fn can_attachment_remain_attached(
1689 cards: &[Card],
1690 attachment: &Card,
1691 target: &Card,
1692 check_sba: bool,
1693) -> bool {
1694 if target.zone != ZoneType::Battlefield {
1695 return true;
1696 }
1697 if crate::staticability::static_ability_cant_attach::cant_attach(
1698 cards, attachment, target, check_sba,
1699 ) {
1700 return false;
1701 }
1702 !crate::staticability::static_ability_colorless_damage_source::target_is_protected_from_source(
1703 cards, target, attachment,
1704 )
1705}
1706
1707#[cfg(test)]
1708mod tests {
1709 use super::*;
1710 use crate::card::Card;
1711 use crate::player::RegisteredPlayer;
1712 use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
1713
1714 fn make_creature(game: &mut GameState, name: &str, owner: PlayerId, p: i32, t: i32) -> CardId {
1715 let card = Card::new(
1716 CardId(0),
1717 name.to_string(),
1718 owner,
1719 CardTypeLine::parse("Creature Bear"),
1720 ManaCost::parse("1 G"),
1721 ColorSet::GREEN,
1722 Some(p),
1723 Some(t),
1724 vec![],
1725 vec![],
1726 );
1727 game.create_card(card)
1728 }
1729
1730 #[test]
1731 fn move_card_to_battlefield() {
1732 let mut game = GameState::new(&["Alice", "Bob"], 20);
1733 let cid = make_creature(&mut game, "Bear", PlayerId(0), 2, 2);
1734 game.move_card(cid, ZoneType::Hand, PlayerId(0));
1735 assert_eq!(game.zone(ZoneType::Hand, PlayerId(0)).len(), 1);
1736
1737 game.move_card(cid, ZoneType::Battlefield, PlayerId(0));
1738 assert_eq!(game.zone(ZoneType::Hand, PlayerId(0)).len(), 0);
1739 assert_eq!(game.zone(ZoneType::Battlefield, PlayerId(0)).len(), 1);
1740 assert_eq!(game.card(cid).zone, ZoneType::Battlefield);
1741 }
1742
1743 #[test]
1744 fn state_based_actions_lethal_damage() {
1745 let mut game = GameState::new(&["Alice", "Bob"], 20);
1746 let cid = make_creature(&mut game, "Bear", PlayerId(0), 2, 2);
1747 game.move_card(cid, ZoneType::Battlefield, PlayerId(0));
1748
1749 game.deal_damage_to_card(cid, 2);
1750 assert!(game.check_state_based_actions());
1751 assert_eq!(game.zone(ZoneType::Graveyard, PlayerId(0)).len(), 1);
1752 }
1753
1754 #[test]
1755 fn state_based_actions_zero_life() {
1756 let mut game = GameState::new(&["Alice", "Bob"], 20);
1757 game.deal_damage_to_player(PlayerId(0), 20);
1758 game.check_state_based_actions();
1759 assert!(game.player(PlayerId(0)).has_lost);
1760 assert!(game.game_over);
1761 assert_eq!(game.winner, Some(PlayerId(1)));
1762 }
1763
1764 #[test]
1765 fn draw_card() {
1766 let mut game = GameState::new(&["Alice", "Bob"], 20);
1767 let cid = make_creature(&mut game, "Bear", PlayerId(0), 2, 2);
1768 game.move_card(cid, ZoneType::Library, PlayerId(0));
1769
1770 let drawn = game.draw_card(PlayerId(0));
1771 assert_eq!(drawn, Some(cid));
1772 assert_eq!(game.card(cid).zone, ZoneType::Hand);
1773 }
1774
1775 #[test]
1776 fn tap_untap() {
1777 let mut game = GameState::new(&["Alice", "Bob"], 20);
1778 let cid = make_creature(&mut game, "Bear", PlayerId(0), 2, 2);
1779 game.move_card(cid, ZoneType::Battlefield, PlayerId(0));
1780
1781 assert!(game.tap(cid));
1782 assert!(game.card(cid).tapped);
1783 assert!(!game.tap(cid)); assert!(game.untap(cid));
1785 assert!(!game.card(cid).tapped);
1786 }
1787
1788 #[test]
1789 fn stun_counter_replaces_untap() {
1790 let mut game = GameState::new(&["Alice", "Bob"], 20);
1791 let cid = make_creature(&mut game, "Bear", PlayerId(0), 2, 2);
1792 game.move_card(cid, ZoneType::Battlefield, PlayerId(0));
1793 game.tap(cid);
1794 game.card_mut(cid)
1795 .add_counter(&CounterType::Named("STUN".to_string()), 1);
1796
1797 assert!(!game.untap(cid));
1798 assert!(game.card(cid).tapped);
1799 assert_eq!(
1800 game.card(cid)
1801 .counter_count(&CounterType::Named("STUN".to_string())),
1802 0
1803 );
1804 }
1805}