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 move_battlefield_card_to_graveyard_for_sba(
865 &mut self,
866 cid: CardId,
867 trigger_handler: &mut Option<&mut TriggerHandler>,
868 agents: &mut Option<&mut [Box<dyn PlayerAgent>]>,
869 ) {
870 let owner = self.card(cid).owner;
871 let mut moved_event = ReplacementEvent::Moved {
872 card: cid,
873 origin: ZoneType::Battlefield,
874 destination: ZoneType::Graveyard,
875 is_discard: false,
876 };
877 if let Some(agents) = agents.as_deref_mut() {
878 apply_replacements_with_agents(self, agents, &mut moved_event);
879 } else {
880 apply_replacements(self, &mut moved_event);
881 }
882 let final_dest = if let ReplacementEvent::Moved { destination, .. } = moved_event {
883 destination
884 } else {
885 ZoneType::Graveyard
886 };
887 let old_zone = self.card(cid).zone;
888 if let Some(handler) = trigger_handler.as_deref_mut() {
891 let lki_p1p1 = *self
892 .card(cid)
893 .counters
894 .get(&CounterType::P1P1)
895 .unwrap_or(&0);
896 let lki_power = self.card(cid).power();
897 let lki_toughness = self.card(cid).toughness();
898 let lki_counters = self.card(cid).counters.clone();
899 self.card_mut(cid).lki_counters = Some(lki_counters);
900 self.card_mut(cid)
901 .set_lki_power_toughness(Some(lki_power), Some(lki_toughness));
902 crate::ability::effects::emit_zone_trigger_with_lki_counters(
903 handler,
904 cid,
905 old_zone,
906 final_dest,
907 lki_p1p1,
908 lki_power,
909 lki_toughness,
910 );
911 handler.flush_waiting_triggers(self);
912 }
913 self.move_card_without_replacement(cid, final_dest, owner);
914 }
915
916 fn check_state_based_actions_impl(
917 &mut self,
918 mut trigger_handler: Option<&mut TriggerHandler>,
919 mut legend_keep_fn: Option<&mut dyn FnMut(PlayerId, &[CardId]) -> CardId>,
920 mut agents: Option<&mut [Box<dyn PlayerAgent>]>,
921 ) -> bool {
922 self.pre_sba_battlefield = self
927 .cards
928 .iter()
929 .filter(|c| c.zone == ZoneType::Battlefield)
930 .map(|c| c.id)
931 .collect();
932
933 let mut any_changes = false;
934 let mut newly_lost_players: Vec<PlayerId> = Vec::new();
935
936 for pid in self.player_order.clone() {
938 if self.player(pid).tried_to_draw_from_empty_library && self.player(pid).is_alive() {
939 self.player_mut(pid).tried_to_draw_from_empty_library = false;
940 let mut event = ReplacementEvent::GameLoss {
941 player: pid,
942 reason: GameLossReason::Milled,
943 };
944 let result = apply_replacements(self, &mut event);
945 if result != ReplacementResult::Replaced && !self.player(pid).has_lost {
946 self.player_mark_lost(pid, GameLossReason::Milled);
947 newly_lost_players.push(pid);
948 any_changes = true;
949 }
950 }
951 if self.player(pid).life <= 0 && self.player(pid).is_alive() {
952 let mut event = ReplacementEvent::GameLoss {
953 player: pid,
954 reason: GameLossReason::LifeReachedZero,
955 };
956 let result = apply_replacements(self, &mut event);
957 if result != ReplacementResult::Replaced && !self.player(pid).has_lost {
958 self.player_mark_lost(pid, GameLossReason::LifeReachedZero);
959 newly_lost_players.push(pid);
960 any_changes = true;
961 }
962 }
963 if self.player(pid).poison_counters >= 10 && self.player(pid).is_alive() {
965 let mut event = ReplacementEvent::GameLoss {
966 player: pid,
967 reason: GameLossReason::Poisoned,
968 };
969 let result = apply_replacements(self, &mut event);
970 if result != ReplacementResult::Replaced {
971 if !self.player(pid).has_lost {
972 self.player_mark_lost(pid, GameLossReason::Poisoned);
973 newly_lost_players.push(pid);
974 }
975 any_changes = true;
976 }
977 }
978 if self.player(pid).commander_damage_enabled {
980 let commander_dmg_entries: Vec<(u32, i32)> = self
981 .player(pid)
982 .commander_damage_received
983 .iter()
984 .map(|(&k, &v)| (k, v))
985 .collect();
986 for (_card_raw_id, dmg) in commander_dmg_entries {
987 if dmg >= 21 && self.player(pid).is_alive() && !self.player(pid).has_lost {
988 self.player_mark_lost(pid, GameLossReason::CommanderDamage);
989 newly_lost_players.push(pid);
990 any_changes = true;
991 }
992 }
993 }
994
995 if self.player(pid).speed == 0
998 && self
999 .cards_in_zone(ZoneType::Battlefield, pid)
1000 .iter()
1001 .any(|&cid| self.card(cid).has_keyword("Start your engines"))
1002 {
1003 self.increase_player_speed(pid, None);
1004 any_changes = true;
1005 }
1006 }
1007
1008 if !newly_lost_players.is_empty() {
1009 for pid in &newly_lost_players {
1010 self.stack.remove_instances_controlled_by(*pid);
1011 }
1012 if let Some(handler) = trigger_handler.as_deref_mut() {
1013 for pid in &newly_lost_players {
1014 handler.run_trigger(
1015 TriggerType::LosesGame,
1016 RunParams {
1017 player: Some(*pid),
1018 ..Default::default()
1019 },
1020 false,
1021 );
1022 handler.on_player_lost(*pid);
1023 }
1024 }
1025 }
1026
1027 let battlefield_cards: Vec<CardId> = self
1029 .player_order
1030 .clone()
1031 .iter()
1032 .flat_map(|&pid| self.cards_in_zone(ZoneType::Battlefield, pid).to_vec())
1033 .collect();
1034
1035 for cid in battlefield_cards {
1036 let (is_creature, zero_toughness, lethal, should_die) = {
1037 let card = &self.cards[cid.index()];
1038 let is_creature = card.is_creature();
1039 let zero_toughness = card.toughness() <= 0;
1040 let lethal = card.lethal_damage() || card.has_deathtouch_damage;
1041 let should_die = zero_toughness || lethal;
1042 (is_creature, zero_toughness, lethal, should_die)
1043 };
1044 if is_creature && should_die {
1045 self.cards[cid.index()].has_deathtouch_damage = false;
1048 if lethal
1052 && !zero_toughness
1053 && self.cards[cid.index()].has_keyword("Indestructible")
1054 {
1055 continue;
1056 }
1057 let has_umbra = self.cards[cid.index()].attachments.iter().any(|&aid| {
1060 aid.index() < self.cards.len()
1061 && self.cards[aid.index()].zone == ZoneType::Battlefield
1062 && (self.cards[aid.index()].has_keyword("Umbra armor")
1063 || self.cards[aid.index()].has_keyword("Totem armor"))
1064 });
1065 if has_umbra && !zero_toughness {
1066 let umbra_id =
1068 self.cards[cid.index()]
1069 .attachments
1070 .iter()
1071 .copied()
1072 .find(|&aid| {
1073 aid.index() < self.cards.len()
1074 && self.cards[aid.index()].zone == ZoneType::Battlefield
1075 && (self.cards[aid.index()].has_keyword("Umbra armor")
1076 || self.cards[aid.index()].has_keyword("Totem armor"))
1077 });
1078 if let Some(umbra_id) = umbra_id {
1079 self.cards[cid.index()].damage = 0;
1081 self.cards[cid.index()].has_deathtouch_damage = false;
1082 let umbra_owner = self.cards[umbra_id.index()].owner;
1084 let old_zone = self.cards[umbra_id.index()].zone;
1085 self.move_card(umbra_id, ZoneType::Graveyard, umbra_owner);
1086 if let Some(handler) = trigger_handler.as_deref_mut() {
1087 crate::ability::effects::emit_zone_trigger(
1088 handler,
1089 umbra_id,
1090 old_zone,
1091 ZoneType::Graveyard,
1092 );
1093 }
1094 any_changes = true;
1095 continue; }
1097 }
1098
1099 if zero_toughness {
1100 self.move_battlefield_card_to_graveyard_for_sba(
1101 cid,
1102 &mut trigger_handler,
1103 &mut agents,
1104 );
1105 any_changes = true;
1106 continue;
1107 }
1108
1109 let mut destroy_event = ReplacementEvent::Destroy { target: cid };
1112 let result = apply_replacements(self, &mut destroy_event);
1113 if result != ReplacementResult::Replaced {
1114 self.move_battlefield_card_to_graveyard_for_sba(
1115 cid,
1116 &mut trigger_handler,
1117 &mut agents,
1118 );
1119 any_changes = true;
1123 } else {
1124 }
1127 }
1128 }
1129
1130 let battlefield_cards: Vec<CardId> = self
1131 .player_order
1132 .clone()
1133 .iter()
1134 .flat_map(|&pid| self.cards_in_zone(ZoneType::Battlefield, pid).to_vec())
1135 .collect();
1136
1137 for cid in battlefield_cards {
1138 let should_put_in_graveyard = {
1139 let card = self.card(cid);
1140 card.type_line.is_planeswalker() && card.counter_count(&CounterType::Loyalty) <= 0
1141 };
1142 if !should_put_in_graveyard {
1143 continue;
1144 }
1145
1146 self.move_battlefield_card_to_graveyard_for_sba(cid, &mut trigger_handler, &mut agents);
1147 any_changes = true;
1148 }
1149
1150 for &pid in &self.player_order.clone() {
1152 let battlefield = self.cards_in_zone(ZoneType::Battlefield, pid).to_vec();
1153 for cid in battlefield {
1154 let p1 = self.card(cid).counter_count(&CounterType::P1P1);
1155 let m1 = self.card(cid).counter_count(&CounterType::M1M1);
1156 if p1 > 0 && m1 > 0 {
1157 let cancel = p1.min(m1);
1158 self.card_mut(cid)
1159 .remove_counter(&CounterType::P1P1, cancel);
1160 self.card_mut(cid)
1161 .remove_counter(&CounterType::M1M1, cancel);
1162 any_changes = true;
1163 }
1164 }
1165 }
1166
1167 for &pid in &self.player_order.clone() {
1169 let mut commander_candidates = self.cards_in_zone(ZoneType::Graveyard, pid).to_vec();
1170 commander_candidates.extend(self.cards_in_zone(ZoneType::Exile, pid).iter().copied());
1171 for cid in commander_candidates {
1172 if !self.card(cid).can_move_to_command_zone() {
1173 continue;
1174 }
1175 self.card_mut(cid).move_to_command_zone = false;
1176 let accepted = if let Some(agents) = agents.as_deref_mut() {
1177 let name = self.card(cid).card_name.clone();
1178 let message = format!(
1179 "{}: 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.",
1180 name
1181 );
1182 agents[pid.index()].confirm_action(
1183 pid,
1184 Some("ChangeZoneToAltDestination"),
1185 &message,
1186 &[],
1187 Some(cid),
1188 None,
1189 )
1190 } else {
1191 false
1192 };
1193 if accepted {
1194 self.move_card_without_replacement(cid, ZoneType::Command, pid);
1195 any_changes = true;
1196 }
1197 }
1198 }
1199
1200 for &pid in &self.player_order.clone() {
1204 let battlefield = self.cards_in_zone(ZoneType::Battlefield, pid).to_vec();
1205 let mut by_name: std::collections::BTreeMap<String, Vec<CardId>> =
1206 std::collections::BTreeMap::new();
1207 for cid in battlefield {
1208 let c = self.card(cid);
1209 if !c.type_line.is_legendary() {
1210 continue;
1211 }
1212 if crate::staticability::static_ability_ignore_legend_rule::ignore_legend_rule(
1213 &self.cards,
1214 c,
1215 ) {
1216 continue;
1217 }
1218 by_name.entry(c.card_name.clone()).or_default().push(cid);
1219 }
1220 for (_name, ids) in by_name {
1221 if ids.len() <= 1 {
1222 continue;
1223 }
1224 let keep = if let Some(ref mut chooser) = legend_keep_fn {
1227 chooser(pid, &ids)
1228 } else if let Some(agents) = agents.as_deref_mut() {
1229 agents[pid.index()].choose_legend_keep(pid, &ids)
1230 } else {
1231 ids[0]
1232 };
1233 for cid in ids {
1234 if cid == keep {
1235 continue;
1236 }
1237 let owner = self.card(cid).owner;
1238 let old_zone = self.card(cid).zone;
1239 if let Some(agents) = agents.as_deref_mut() {
1240 self.move_card_with_agents(cid, ZoneType::Graveyard, owner, agents);
1241 } else {
1242 self.move_card(cid, ZoneType::Graveyard, owner);
1243 }
1244 if let Some(handler) = trigger_handler.as_deref_mut() {
1245 crate::ability::effects::emit_zone_trigger(
1246 handler,
1247 cid,
1248 old_zone,
1249 ZoneType::Graveyard,
1250 );
1251 }
1252 any_changes = true;
1253 }
1254 }
1255 }
1256
1257 {
1261 let aura_ids: Vec<CardId> = self
1262 .cards
1263 .iter()
1264 .filter(|c| {
1265 c.zone == ZoneType::Battlefield
1266 && c.type_line.has_subtype("Aura")
1267 && !c.type_line.is_creature() })
1269 .filter(|c| {
1270 match (c.attached_to, c.attached_to_player) {
1271 (None, None) => true, (None, Some(player_id)) => {
1273 if player_id.index() >= self.players.len() {
1274 return true;
1275 }
1276 let player = &self.players[player_id.index()];
1277 let enchant_type = c
1278 .keywords
1279 .iter_strings()
1280 .find_map(|kw| {
1281 crate::keyword::extract_keyword_cost_str(kw, "Enchant")
1282 })
1283 .unwrap_or_default();
1284 player.has_lost || !enchant_type.eq_ignore_ascii_case("Player")
1285 }
1286 (Some(host_id), _) => {
1287 if host_id.index() >= self.cards.len() {
1288 return true; }
1290 let host = &self.cards[host_id.index()];
1291 let enchant_type = c
1296 .keywords
1297 .iter_strings()
1298 .find_map(|kw| {
1299 crate::keyword::extract_keyword_cost_str(kw, "Enchant")
1300 })
1301 .unwrap_or_default();
1302 !crate::parsing::enchant_type_matches_card(enchant_type, host, Some(c))
1303 || !can_attachment_remain_attached(&self.cards, c, host, true)
1304 }
1305 }
1306 })
1307 .map(|c| c.id)
1308 .collect();
1309
1310 for aura_id in aura_ids {
1311 let owner = self.card(aura_id).owner;
1312 let old_zone = self.card(aura_id).zone;
1313 self.move_card(aura_id, ZoneType::Graveyard, owner);
1314 if let Some(handler) = trigger_handler.as_deref_mut() {
1315 crate::ability::effects::emit_zone_trigger(
1316 handler,
1317 aura_id,
1318 old_zone,
1319 ZoneType::Graveyard,
1320 );
1321 }
1322 any_changes = true;
1323 }
1324 }
1325
1326 let alive = self.alive_players();
1328 if alive.len() <= 1 {
1329 self.game_over = true;
1330 if alive.len() == 1 {
1331 self.winner = Some(alive[0]);
1332 }
1333 }
1334
1335 any_changes
1336 }
1337
1338 pub fn untap_all(&mut self, player: PlayerId) {
1341 let cards: Vec<CardId> = self.cards_in_zone(ZoneType::Battlefield, player).to_vec();
1342 for cid in cards {
1343 self.untap_during_untap_step(cid, player);
1345 }
1346 }
1347
1348 pub fn draw_card(&mut self, player: PlayerId) -> Option<CardId> {
1356 self.player_draw_one(player)
1357 }
1358
1359 pub fn draw_card_with_agents(
1361 &mut self,
1362 player: PlayerId,
1363 agents: &mut [Box<dyn crate::agent::PlayerAgent>],
1364 ) -> Option<CardId> {
1365 self.player_draw_one_internal(player, false, Some(agents))
1366 }
1367
1368 pub fn draw_cards(&mut self, player: PlayerId, n: usize) -> Vec<CardId> {
1370 self.player_draw_cards(player, n)
1371 }
1372
1373 pub fn shuffle_library(&mut self, player: PlayerId, rng: &mut impl rand::Rng) {
1375 self.shuffle_zone_cards_with_rand(ZoneType::Library, player, rng);
1376 }
1377
1378 pub fn new_turn_for_player(&mut self, player: PlayerId) {
1380 self.player_new_turn(player);
1381 for pid in &self.player_order.clone() {
1386 if *pid != player {
1387 self.player_reset_drawn_this_turn(*pid);
1388 let p = self.player_mut(*pid);
1389 p.life_started_this_turn_with = p.life;
1390 p.life_gained_this_turn = 0;
1391 p.life_gained_by_team_this_turn = 0;
1392 p.life_gained_times_this_turn = 0;
1393 p.life_lost_last_turn = p.life_lost_this_turn;
1394 p.life_lost_this_turn = 0;
1395 }
1396 }
1397
1398 let all_card_ids: Vec<CardId> = (0..self.cards.len()).map(|i| CardId(i as u32)).collect();
1399 for cid in all_card_ids {
1400 if self.cards[cid.index()].zone == ZoneType::Battlefield {
1401 self.cards[cid.index()].started_turn_tapped = self.cards[cid.index()].tapped;
1402 }
1403 if self.cards[cid.index()].controller == player {
1404 self.cards[cid.index()].new_turn();
1405 } else {
1406 self.cards[cid.index()].clear_global_turn_state();
1407 }
1408 }
1409 }
1410
1411 pub fn tap(&mut self, card_id: CardId) -> bool {
1414 let card = &self.cards[card_id.index()];
1415 if card.tapped {
1416 return false;
1417 }
1418 let mut event = ReplacementEvent::Tap { card: card_id };
1420 let result = apply_replacements(self, &mut event);
1421 if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
1422 return false; }
1424 self.cards[card_id.index()].tapped = true;
1425 true
1426 }
1427
1428 pub fn untap(&mut self, card_id: CardId) -> bool {
1431 self.untap_internal(card_id, None)
1432 }
1433
1434 pub fn untap_during_untap_step(&mut self, card_id: CardId, player: PlayerId) -> bool {
1435 self.untap_internal(card_id, Some(player))
1436 }
1437
1438 fn untap_internal(&mut self, card_id: CardId, player: Option<PlayerId>) -> bool {
1439 let card = &self.cards[card_id.index()];
1440 if !card.tapped {
1441 return false;
1442 }
1443 let stun = CounterType::Named("STUN".to_string());
1444 if card.counter_count(&stun) > 0 && card.can_remove_counters(&stun) {
1445 self.cards[card_id.index()].remove_counter(&stun, 1);
1448 return false;
1449 }
1450 let mut event = ReplacementEvent::Untap {
1452 card: card_id,
1453 player,
1454 };
1455 let result = apply_replacements(self, &mut event);
1456 if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
1457 return false; }
1459 self.cards[card_id.index()].tapped = false;
1460 crate::ability::effects::control_gain_effect::untap_hook(self, card_id);
1462 true
1463 }
1464
1465 pub fn change_controller(&mut self, card_id: CardId, new_controller: PlayerId) {
1469 let card = &self.cards[card_id.index()];
1470 if card.controller == new_controller {
1471 return;
1472 }
1473 let old_controller = card.controller;
1474 let zone = card.zone;
1475
1476 if zone != ZoneType::None {
1478 self.remove_card_from_zone(zone, old_controller, card_id);
1479 self.add_card_to_zone(zone, new_controller, card_id);
1480 }
1481 self.cards[card_id.index()].controller = new_controller;
1482 }
1483
1484 pub fn attach_to(&mut self, aura_id: CardId, target_id: CardId) {
1488 self.detach(aura_id);
1490 self.cards[aura_id.index()].attached_to = Some(target_id);
1491 self.cards[aura_id.index()].attached_to_player = None;
1492 self.cards[aura_id.index()].attached_this_turn = true;
1493 self.cards[target_id.index()].attachments.push(aura_id);
1494 }
1495
1496 pub fn attach_to_player(&mut self, aura_id: CardId, player_id: PlayerId) {
1497 self.detach(aura_id);
1498 self.cards[aura_id.index()].attached_to = None;
1499 self.cards[aura_id.index()].attached_to_player = Some(player_id);
1500 self.cards[aura_id.index()].attached_this_turn = true;
1501 }
1502
1503 pub fn detach(&mut self, aura_id: CardId) {
1506 if let Some(host_id) = self.cards[aura_id.index()].attached_to.take() {
1507 self.cards[host_id.index()]
1508 .attachments
1509 .retain(|&a| a != aura_id);
1510 self.cards[aura_id.index()].is_bestowed = false;
1512 }
1513 self.cards[aura_id.index()].attached_to_player = None;
1514 }
1515
1516 pub fn put_on_bottom_of_library(&mut self, card_id: CardId, owner: PlayerId) {
1519 let card = &self.cards[card_id.index()];
1520 let src_zone = card.zone;
1521 let src_owner = card.controller;
1522
1523 if src_zone != ZoneType::None {
1524 self.remove_card_from_zone(src_zone, src_owner, card_id);
1525 }
1526
1527 self.cards[card_id.index()].zone = ZoneType::Library;
1528 self.assign_zone_timestamp(card_id);
1529 self.add_card_to_zone_bottom(ZoneType::Library, owner, card_id);
1530 }
1531
1532 pub fn remove_from_stack(&mut self, entry_id: u32) -> bool {
1535 self.stack.remove_by_id(entry_id).is_some()
1536 }
1537}
1538
1539fn can_attachment_remain_attached(
1540 cards: &[Card],
1541 attachment: &Card,
1542 target: &Card,
1543 check_sba: bool,
1544) -> bool {
1545 if target.zone != ZoneType::Battlefield {
1546 return true;
1547 }
1548 if crate::staticability::static_ability_cant_attach::cant_attach(
1549 cards, attachment, target, check_sba,
1550 ) {
1551 return false;
1552 }
1553 !crate::staticability::static_ability_colorless_damage_source::target_is_protected_from_source(
1554 cards, target, attachment,
1555 )
1556}
1557
1558#[cfg(test)]
1559mod tests {
1560 use super::*;
1561 use crate::card::Card;
1562 use crate::player::RegisteredPlayer;
1563 use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
1564
1565 fn make_creature(game: &mut GameState, name: &str, owner: PlayerId, p: i32, t: i32) -> CardId {
1566 let card = Card::new(
1567 CardId(0),
1568 name.to_string(),
1569 owner,
1570 CardTypeLine::parse("Creature Bear"),
1571 ManaCost::parse("1 G"),
1572 ColorSet::GREEN,
1573 Some(p),
1574 Some(t),
1575 vec![],
1576 vec![],
1577 );
1578 game.create_card(card)
1579 }
1580
1581 #[test]
1582 fn move_card_to_battlefield() {
1583 let mut game = GameState::new(&["Alice", "Bob"], 20);
1584 let cid = make_creature(&mut game, "Bear", PlayerId(0), 2, 2);
1585 game.move_card(cid, ZoneType::Hand, PlayerId(0));
1586 assert_eq!(game.zone(ZoneType::Hand, PlayerId(0)).len(), 1);
1587
1588 game.move_card(cid, ZoneType::Battlefield, PlayerId(0));
1589 assert_eq!(game.zone(ZoneType::Hand, PlayerId(0)).len(), 0);
1590 assert_eq!(game.zone(ZoneType::Battlefield, PlayerId(0)).len(), 1);
1591 assert_eq!(game.card(cid).zone, ZoneType::Battlefield);
1592 }
1593
1594 #[test]
1595 fn state_based_actions_lethal_damage() {
1596 let mut game = GameState::new(&["Alice", "Bob"], 20);
1597 let cid = make_creature(&mut game, "Bear", PlayerId(0), 2, 2);
1598 game.move_card(cid, ZoneType::Battlefield, PlayerId(0));
1599
1600 game.deal_damage_to_card(cid, 2);
1601 assert!(game.check_state_based_actions());
1602 assert_eq!(game.zone(ZoneType::Graveyard, PlayerId(0)).len(), 1);
1603 }
1604
1605 #[test]
1606 fn state_based_actions_zero_life() {
1607 let mut game = GameState::new(&["Alice", "Bob"], 20);
1608 game.deal_damage_to_player(PlayerId(0), 20);
1609 game.check_state_based_actions();
1610 assert!(game.player(PlayerId(0)).has_lost);
1611 assert!(game.game_over);
1612 assert_eq!(game.winner, Some(PlayerId(1)));
1613 }
1614
1615 #[test]
1616 fn draw_card() {
1617 let mut game = GameState::new(&["Alice", "Bob"], 20);
1618 let cid = make_creature(&mut game, "Bear", PlayerId(0), 2, 2);
1619 game.move_card(cid, ZoneType::Library, PlayerId(0));
1620
1621 let drawn = game.draw_card(PlayerId(0));
1622 assert_eq!(drawn, Some(cid));
1623 assert_eq!(game.card(cid).zone, ZoneType::Hand);
1624 }
1625
1626 #[test]
1627 fn tap_untap() {
1628 let mut game = GameState::new(&["Alice", "Bob"], 20);
1629 let cid = make_creature(&mut game, "Bear", PlayerId(0), 2, 2);
1630 game.move_card(cid, ZoneType::Battlefield, PlayerId(0));
1631
1632 assert!(game.tap(cid));
1633 assert!(game.card(cid).tapped);
1634 assert!(!game.tap(cid)); assert!(game.untap(cid));
1636 assert!(!game.card(cid).tapped);
1637 }
1638
1639 #[test]
1640 fn stun_counter_replaces_untap() {
1641 let mut game = GameState::new(&["Alice", "Bob"], 20);
1642 let cid = make_creature(&mut game, "Bear", PlayerId(0), 2, 2);
1643 game.move_card(cid, ZoneType::Battlefield, PlayerId(0));
1644 game.tap(cid);
1645 game.card_mut(cid)
1646 .add_counter(&CounterType::Named("STUN".to_string()), 1);
1647
1648 assert!(!game.untap(cid));
1649 assert!(game.card(cid).tapped);
1650 assert_eq!(
1651 game.card(cid)
1652 .counter_count(&CounterType::Named("STUN".to_string())),
1653 0
1654 );
1655 }
1656}