1use std::collections::{HashMap, HashSet};
12
13use forge_foundation::{PhaseType, ZoneType};
14
15use crate::card::Card;
16use crate::card::CounterType;
17use crate::card_trait_base::CardTrait;
18use crate::game::GameState;
19use crate::game_rng::GameRng;
20use crate::ids::{CardId, PlayerId};
21use crate::mana::ManaPool;
22use crate::replacement::replacement_effect::{
23 resolve_replace_with_chain, GameLossReason, ReplacementChainIr, ReplacementEffect,
24};
25use crate::replacement::replacement_layer::ReplacementLayer;
26use crate::replacement::replacement_result::ReplacementResult;
27use crate::replacement::replacement_type::ReplacementType;
28use crate::trigger::TriggerHandler;
29
30#[derive(Debug, Clone)]
41pub enum ReplacementEvent {
42 Draw {
46 player: PlayerId,
47 extra_draws: i32,
48 is_first_in_draw_step: bool,
49 },
50
51 DamageToCard {
53 target: CardId,
54 amount: i32,
55 source: Option<CardId>,
57 is_combat: bool,
59 },
60
61 DamageToPlayer {
63 target: PlayerId,
64 amount: i32,
65 source: Option<CardId>,
67 is_combat: bool,
69 },
70
71 Destroy { target: CardId },
73
74 Moved {
76 card: CardId,
77 origin: ZoneType,
78 destination: ZoneType,
79 is_discard: bool,
82 },
83
84 GainLife { player: PlayerId, amount: i32 },
86
87 CreateToken {
90 player: PlayerId,
91 count: i32,
92 is_effect: bool,
93 },
94
95 AddCounter {
98 target: CardId,
99 counter_type: CounterType,
100 count: i32,
101 is_effect: bool,
102 },
103
104 GameLoss {
106 player: PlayerId,
107 reason: GameLossReason,
108 },
109
110 GameWin { player: PlayerId },
112
113 Counter { card: CardId },
115
116 ProduceMana {
119 source: CardId,
120 activator: PlayerId,
121 mana: String,
122 },
123
124 Tap { card: CardId },
126
127 Untap {
129 card: CardId,
130 player: Option<PlayerId>,
131 },
132
133 LifeReduced {
135 player: PlayerId,
136 amount: i32,
137 is_damage: bool,
138 },
139
140 RemoveCounter {
142 target: CardId,
143 counter_type: CounterType,
144 count: i32,
145 },
146
147 DealtDamage {
149 target: CardId,
150 amount: i32,
151 source: Option<CardId>,
152 },
153
154 DrawCards { player: PlayerId, count: i32 },
156
157 Mill { player: PlayerId, count: i32 },
159
160 PayLife { player: PlayerId, amount: i32 },
162
163 Scry { player: PlayerId, count: i32 },
165
166 Attached { card: CardId, target: CardId },
168
169 BeginPhase {
171 player: PlayerId,
172 phase: forge_foundation::PhaseType,
173 },
174
175 BeginTurn { player: PlayerId },
177
178 Explore { card: CardId },
180
181 DeclareBlocker { player: PlayerId },
183
184 AssignDealDamage { card: CardId },
186
187 Transform { card: CardId },
189
190 TurnFaceUp { card: CardId },
192
193 CopySpell { player: PlayerId, count: i32 },
195
196 Proliferate { player: PlayerId, count: i32 },
198
199 Cascade { player: PlayerId },
201
202 Learn { player: PlayerId },
204
205 LoseMana { player: PlayerId },
207
208 RollDice {
210 player: PlayerId,
211 sides: i32,
212 number: i32,
213 ignore: i32,
214 ignore_chosen: HashMap<PlayerId, i32>,
215 dice_pt_exchanges: HashSet<CardId>,
216 },
217
218 RollPlanarDice { player: PlayerId },
220
221 PlanarDiceResult { player: PlayerId },
223
224 Planeswalk { player: PlayerId },
226
227 SetInMotion { player: PlayerId },
229
230 AssembleContraption { player: PlayerId },
232}
233
234use crate::agent::PlayerAgent;
237
238pub struct ReplacementRuntime<'a> {
239 pub trigger_handler: &'a mut TriggerHandler,
240 pub token_templates: &'a HashMap<String, Card>,
241 pub token_art_variants: &'a HashMap<(String, String), usize>,
242 pub token_fallback: &'a HashMap<String, String>,
243 pub edition_dates: &'a HashMap<String, String>,
244 pub mana_pools: &'a mut Vec<ManaPool>,
245 pub rng: &'a mut dyn GameRng,
246}
247
248pub struct ReplacementHandler {
257 has_run: HashSet<(CardId, ReplacementLayer, usize)>,
260}
261
262impl Default for ReplacementHandler {
263 fn default() -> Self {
264 Self::new()
265 }
266}
267
268impl ReplacementHandler {
269 pub fn new() -> Self {
270 Self {
271 has_run: HashSet::new(),
272 }
273 }
274
275 pub fn run(
286 &mut self,
287 game: &mut GameState,
288 mut agents: Option<&mut [Box<dyn PlayerAgent>]>,
289 mut runtime: Option<&mut ReplacementRuntime<'_>>,
290 event: &mut ReplacementEvent,
291 ) -> ReplacementResult {
292 for layer in [
293 ReplacementLayer::CantHappen,
294 ReplacementLayer::Control,
295 ReplacementLayer::Copy,
296 ReplacementLayer::Transform,
297 ReplacementLayer::Other,
298 ] {
299 let result = self.run_layer(
300 game,
301 agents.as_deref_mut(),
302 runtime.as_deref_mut(),
303 event,
304 layer,
305 );
306 match result {
307 ReplacementResult::NotReplaced => continue,
308 ReplacementResult::Updated => {
309 return match self.run(game, agents, runtime, event) {
312 ReplacementResult::NotReplaced | ReplacementResult::Updated => {
313 ReplacementResult::Updated
314 }
315 other => other,
316 };
317 }
318 other => return other,
319 }
320 }
321 ReplacementResult::NotReplaced
322 }
323
324 fn run_layer(
326 &mut self,
327 game: &mut GameState,
328 mut agents: Option<&mut [Box<dyn PlayerAgent>]>,
329 mut runtime: Option<&mut ReplacementRuntime<'_>>,
330 event: &mut ReplacementEvent,
331 layer: ReplacementLayer,
332 ) -> ReplacementResult {
333 let effects = collect_effects(game, event, layer);
334 let mut declined_effects: HashSet<(CardId, usize)> = HashSet::new();
335
336 if effects.is_empty() {
337 return ReplacementResult::NotReplaced;
338 }
339
340 loop {
341 let eligible: Vec<_> = effects
342 .iter()
343 .filter(|(card_id, _re, effect_idx)| {
344 !self.has_run.contains(&(*card_id, layer, *effect_idx))
345 && !declined_effects.contains(&(*card_id, *effect_idx))
346 })
347 .cloned()
348 .collect();
349
350 if eligible.is_empty() {
351 return ReplacementResult::NotReplaced;
352 }
353
354 let chosen_idx = if !eligible.is_empty() && layer != ReplacementLayer::CantHappen {
355 if let Some(agents) = agents.as_deref_mut() {
356 let descriptions: Vec<String> = eligible
357 .iter()
358 .map(|(card_id, re, _)| {
359 let host = game.card(*card_id);
360 let card_name = &host.card_name;
361 let desc = re.description(host, game);
362 format!("{card_name}: {desc}")
363 })
364 .collect();
365
366 let affected_player = affected_player_for_event(event, game);
367 let agent = &mut agents[affected_player.index()];
368 agent
369 .choose_single_replacement_effect(affected_player, &descriptions)
370 .min(eligible.len() - 1)
371 } else {
372 0
373 }
374 } else {
375 0
376 };
377
378 let (source_card_id, ref effect, effect_idx) = eligible[chosen_idx];
379 self.has_run.insert((source_card_id, layer, effect_idx));
380 let result = execute_effect(
381 game,
382 source_card_id,
383 effect,
384 event,
385 agents.as_deref_mut(),
386 runtime.as_deref_mut(),
387 );
388 if result == ReplacementResult::NotReplaced {
389 self.has_run.remove(&(source_card_id, layer, effect_idx));
390 declined_effects.insert((source_card_id, effect_idx));
391 continue;
392 }
393 return result;
394 }
395 }
396}
397
398fn affected_player_for_event(event: &ReplacementEvent, game: &GameState) -> PlayerId {
400 match event {
401 ReplacementEvent::Draw { player, .. } => *player,
402 ReplacementEvent::DamageToCard { target, .. } => game.cards[target.index()].controller,
403 ReplacementEvent::DamageToPlayer { target, .. } => *target,
404 ReplacementEvent::Destroy { target } => game.cards[target.index()].controller,
405 ReplacementEvent::Moved { card, .. } => game.cards[card.index()].controller,
406 ReplacementEvent::GainLife { player, .. } => *player,
407 ReplacementEvent::CreateToken { player, .. } => *player,
408 ReplacementEvent::AddCounter { target, .. } => game.cards[target.index()].controller,
409 ReplacementEvent::GameLoss { player, .. } => *player,
410 ReplacementEvent::GameWin { player } => *player,
411 ReplacementEvent::Counter { card } => game.cards[card.index()].controller,
412 ReplacementEvent::ProduceMana { activator, .. } => *activator,
413 ReplacementEvent::Tap { card } => game.cards[card.index()].controller,
414 ReplacementEvent::Untap { card, .. } => game.cards[card.index()].controller,
415 ReplacementEvent::LifeReduced { player, .. } => *player,
416 ReplacementEvent::RemoveCounter { target, .. } => game.cards[target.index()].controller,
417 ReplacementEvent::DealtDamage { target, .. } => game.cards[target.index()].controller,
418 ReplacementEvent::DrawCards { player, .. } => *player,
419 ReplacementEvent::Mill { player, .. } => *player,
420 ReplacementEvent::PayLife { player, .. } => *player,
421 ReplacementEvent::Scry { player, .. } => *player,
422 ReplacementEvent::Attached { card, .. } => game.cards[card.index()].controller,
423 ReplacementEvent::BeginPhase { player, .. } => *player,
424 ReplacementEvent::BeginTurn { player } => *player,
425 ReplacementEvent::Explore { card } => game.cards[card.index()].controller,
426 ReplacementEvent::DeclareBlocker { player } => *player,
427 ReplacementEvent::AssignDealDamage { card } => game.cards[card.index()].controller,
428 ReplacementEvent::Transform { card } => game.cards[card.index()].controller,
429 ReplacementEvent::TurnFaceUp { card } => game.cards[card.index()].controller,
430 ReplacementEvent::CopySpell { player, .. } => *player,
431 ReplacementEvent::Proliferate { player, .. } => *player,
432 ReplacementEvent::Cascade { player } => *player,
433 ReplacementEvent::Learn { player } => *player,
434 ReplacementEvent::LoseMana { player } => *player,
435 ReplacementEvent::RollDice { player, .. } => *player,
436 ReplacementEvent::RollPlanarDice { player } => *player,
437 ReplacementEvent::PlanarDiceResult { player } => *player,
438 ReplacementEvent::Planeswalk { player } => *player,
439 ReplacementEvent::SetInMotion { player } => *player,
440 ReplacementEvent::AssembleContraption { player } => *player,
441 }
442}
443
444pub fn apply_replacements(game: &mut GameState, event: &mut ReplacementEvent) -> ReplacementResult {
453 let mut handler = ReplacementHandler::new();
454 handler.run(game, None, None, event)
455}
456
457pub fn apply_moved_replacement(
463 game: &mut GameState,
464 card_id: CardId,
465 dest: ZoneType,
466 agents: Option<&mut [Box<dyn crate::agent::PlayerAgent>]>,
467) -> ZoneType {
468 if dest != ZoneType::Graveyard {
469 return dest;
470 }
471 let src_zone = game.cards[card_id.index()].zone;
472 let mut event = ReplacementEvent::Moved {
473 card: card_id,
474 origin: src_zone,
475 destination: ZoneType::Graveyard,
476 is_discard: false,
477 };
478 let mut handler = ReplacementHandler::new();
479 handler.run(game, agents, None, &mut event);
480 if let ReplacementEvent::Moved { destination, .. } = event {
481 destination
482 } else {
483 ZoneType::Graveyard
484 }
485}
486
487pub fn apply_replacements_with_agents(
488 game: &mut GameState,
489 agents: &mut [Box<dyn PlayerAgent>],
490 event: &mut ReplacementEvent,
491) -> ReplacementResult {
492 let mut handler = ReplacementHandler::new();
493 handler.run(game, Some(agents), None, event)
494}
495
496pub fn apply_replacements_with_agents_and_runtime(
497 game: &mut GameState,
498 agents: &mut [Box<dyn PlayerAgent>],
499 runtime: &mut ReplacementRuntime<'_>,
500 event: &mut ReplacementEvent,
501) -> ReplacementResult {
502 let mut handler = ReplacementHandler::new();
503 handler.run(game, Some(agents), Some(runtime), event)
504}
505
506pub(crate) fn replacement_event_amount(event: &ReplacementEvent) -> Option<i32> {
507 match event {
508 ReplacementEvent::DamageToCard { amount, .. } => Some(*amount),
509 ReplacementEvent::DamageToPlayer { amount, .. } => Some(*amount),
510 ReplacementEvent::GainLife { amount, .. } => Some(*amount),
511 ReplacementEvent::LifeReduced { amount, .. } => Some(*amount),
512 ReplacementEvent::CreateToken { count, .. } => Some(*count),
513 ReplacementEvent::AddCounter { count, .. } => Some(*count),
514 ReplacementEvent::DrawCards { count, .. } => Some(*count),
515 ReplacementEvent::Mill { count, .. } => Some(*count),
516 ReplacementEvent::PayLife { amount, .. } => Some(*amount),
517 ReplacementEvent::Scry { count, .. } => Some(*count),
518 ReplacementEvent::CopySpell { count, .. } => Some(*count),
519 ReplacementEvent::Proliferate { count, .. } => Some(*count),
520 ReplacementEvent::RollDice { number, .. } => Some(*number),
521 _ => None,
522 }
523}
524
525pub(crate) fn set_replacement_event_amount(event: &mut ReplacementEvent, value: i32) -> bool {
526 match event {
527 ReplacementEvent::DamageToCard { amount, .. } => *amount = value.max(0),
528 ReplacementEvent::DamageToPlayer { amount, .. } => *amount = value.max(0),
529 ReplacementEvent::GainLife { amount, .. } => *amount = value.max(0),
530 ReplacementEvent::LifeReduced { amount, .. } => *amount = value.max(0),
531 ReplacementEvent::CreateToken { count, .. } => *count = value.max(0),
532 ReplacementEvent::AddCounter { count, .. } => *count = value.max(0),
533 ReplacementEvent::DrawCards { count, .. } => *count = value.max(0),
534 ReplacementEvent::Mill { count, .. } => *count = value.max(0),
535 ReplacementEvent::PayLife { amount, .. } => *amount = value.max(0),
536 ReplacementEvent::Scry { count, .. } => *count = value.max(0),
537 ReplacementEvent::CopySpell { count, .. } => *count = value.max(0),
538 ReplacementEvent::Proliferate { count, .. } => *count = value.max(0),
539 ReplacementEvent::RollDice { number, .. } => *number = value.max(0),
540 _ => return false,
541 }
542 true
543}
544
545fn amount_after_math(mut amount: i32, ops: &str) -> i32 {
546 if ops.is_empty() {
547 return amount;
548 }
549 let parts: Vec<&str> = ops.split('.').collect();
550 let op = parts.first().copied().unwrap_or("");
551 let rhs = parts
552 .get(1)
553 .and_then(|value| value.parse::<i32>().ok())
554 .unwrap_or(0);
555 if op.contains("Plus") {
556 amount += rhs;
557 } else if op.contains("Minus") {
558 amount -= rhs;
559 } else if op.contains("Twice") {
560 amount *= 2;
561 } else if op.contains("Thrice") {
562 amount *= 3;
563 } else if op.contains("HalfUp") {
564 amount = ((amount as f64) / 2.0).ceil() as i32;
565 } else if op.contains("HalfDown") {
566 amount = ((amount as f64) / 2.0).floor() as i32;
567 }
568 amount
569}
570
571pub(crate) fn resolve_replace_value(
572 expr: &str,
573 game: &GameState,
574 source_card_id: CardId,
575 event: &ReplacementEvent,
576) -> Option<i32> {
577 let expr = expr.trim();
578 if let Ok(value) = expr.parse::<i32>() {
579 return Some(value);
580 }
581 if let Some(svar) = game.card(source_card_id).svars.get(expr) {
582 return resolve_replace_value(svar, game, source_card_id, event);
583 }
584 let rest = expr.strip_prefix("ReplaceCount$")?;
585 let (field, ops) = rest.split_once('/').unwrap_or((rest, ""));
586 let base = match field {
587 "DamageAmount" | "LifeGained" | "Amount" | "Number" | "TokenNum" | "CounterNum" => {
588 replacement_event_amount(event)?
589 }
590 "Ignore" => match event {
591 ReplacementEvent::RollDice { ignore, .. } => *ignore,
592 _ => return None,
593 },
594 _ => return None,
595 };
596 Some(amount_after_math(base, ops))
597}
598
599pub(crate) fn execute_replace_with_numeric_update(
600 effect: &ReplacementEffect,
601 event: &mut ReplacementEvent,
602 game: &GameState,
603 source_card_id: CardId,
604 var_name: &str,
605) -> Option<ReplacementResult> {
606 let chain = resolve_replace_with_chain(effect, game.card(source_card_id))?;
607 execute_replace_effect_ir(&chain, event, game, source_card_id, Some(var_name))
608}
609
610pub(crate) fn execute_replace_effect_ir(
611 chain: &ReplacementChainIr,
612 event: &mut ReplacementEvent,
613 game: &GameState,
614 source_card_id: CardId,
615 required_var_name: Option<&str>,
616) -> Option<ReplacementResult> {
617 match chain {
618 ReplacementChainIr::ReplaceToken {
619 token_type,
620 amount_expr,
621 } => execute_replace_token_chain(
622 token_type.as_deref(),
623 amount_expr.as_deref(),
624 event,
625 game,
626 source_card_id,
627 ),
628 ReplacementChainIr::ReplaceCounter { amount_expr } => {
629 execute_replace_counter_chain(amount_expr, event, game, source_card_id)
630 }
631 ReplacementChainIr::ReplaceEffect {
632 var_name,
633 var_type,
634 var_key,
635 var_value,
636 sub_ability,
637 } => {
638 let mut updated = false;
639 if let Some(var_name) = var_name.as_deref() {
640 if required_var_name.is_none_or(|required| required == var_name) {
641 match var_name {
642 "DicePTExchanges" => {
643 if var_type.as_deref() == Some("CardSet") {
644 if let Some(var_value) = var_value.as_deref() {
645 if let Some(card_id) =
646 resolve_replace_card_key(var_value, source_card_id)
647 {
648 if let ReplacementEvent::RollDice {
649 dice_pt_exchanges,
650 ..
651 } = event
652 {
653 dice_pt_exchanges.insert(card_id);
654 updated = true;
655 }
656 }
657 }
658 }
659 }
660 _ => {
661 if let Some(var_value) = var_value.as_deref() {
662 if let Some(value) =
663 resolve_replace_value(var_value, game, source_card_id, event)
664 {
665 match var_name {
666 "Ignore" => {
667 if let ReplacementEvent::RollDice { ignore, .. } = event
668 {
669 *ignore = value.max(0);
670 updated = true;
671 }
672 }
673 "IgnoreChosen" => {
674 if var_type.as_deref() == Some("Map") {
675 if let Some(var_key) = var_key.as_deref() {
676 if let Some(player) = resolve_replace_player_key(
677 var_key,
678 game,
679 source_card_id,
680 event,
681 ) {
682 if let ReplacementEvent::RollDice {
683 ignore_chosen,
684 ..
685 } = event
686 {
687 ignore_chosen
688 .insert(player, value.max(0));
689 updated = true;
690 }
691 }
692 }
693 }
694 }
695 _ => {
696 if set_replacement_event_amount(event, value) {
697 updated = true;
698 }
699 }
700 }
701 }
702 }
703 }
704 }
705 }
706 }
707
708 if let Some(sub_ability) = sub_ability.as_deref() {
709 updated |=
710 execute_replace_effect_ir(sub_ability, event, game, source_card_id, None)
711 .is_some();
712 }
713
714 updated.then_some(ReplacementResult::Updated)
715 }
716 }
717}
718
719fn execute_replace_token_chain(
724 token_type: Option<&str>,
725 amount_expr: Option<&str>,
726 event: &mut ReplacementEvent,
727 game: &GameState,
728 source_card_id: CardId,
729) -> Option<ReplacementResult> {
730 let token_type = token_type.unwrap_or("Amount");
731 match token_type {
732 "Amount" => {
733 let current = replacement_event_amount(event)?;
735 let amount_expr = amount_expr.unwrap_or("Twice");
737 let new_value = do_x_math(current, amount_expr, game, source_card_id, event);
738 set_replacement_event_amount(event, new_value);
739 Some(ReplacementResult::Updated)
740 }
741 _ => {
742 None
744 }
745 }
746}
747
748fn execute_replace_counter_chain(
753 amount_expr: &str,
754 event: &mut ReplacementEvent,
755 game: &GameState,
756 source_card_id: CardId,
757) -> Option<ReplacementResult> {
758 let _current = replacement_event_amount(event)?;
760 let value = resolve_replace_value(amount_expr, game, source_card_id, event)?;
761 if value <= 0 {
762 set_replacement_event_amount(event, 0);
764 } else {
765 set_replacement_event_amount(event, value);
766 }
767 Some(ReplacementResult::Updated)
768}
769
770fn do_x_math(
775 base: i32,
776 expr: &str,
777 game: &GameState,
778 source_card_id: CardId,
779 event: &ReplacementEvent,
780) -> i32 {
781 match expr {
782 "Twice" => base * 2,
783 "Thrice" => base * 3,
784 "Half" => ((base as f64) / 2.0).ceil() as i32,
785 _ => {
786 if let Some(val) = resolve_replace_value(expr, game, source_card_id, event) {
788 val
789 } else {
790 base
791 }
792 }
793 }
794}
795
796fn resolve_replace_player_key(
797 expr: &str,
798 game: &GameState,
799 source_card_id: CardId,
800 event: &ReplacementEvent,
801) -> Option<PlayerId> {
802 match expr.trim() {
803 "You" => Some(game.card(source_card_id).controller),
804 "Affected" => match event {
805 ReplacementEvent::RollDice { player, .. } => Some(*player),
806 _ => None,
807 },
808 "Opponent" => Some(game.opponent_of(game.card(source_card_id).controller)),
809 _ => None,
810 }
811}
812
813fn resolve_replace_card_key(expr: &str, source_card_id: CardId) -> Option<CardId> {
814 match expr.trim() {
815 "Self" => Some(source_card_id),
816 _ => None,
817 }
818}
819
820pub fn cant_happen_check(game: &GameState, event: &ReplacementEvent) -> bool {
829 let effects = collect_effects(game, event, ReplacementLayer::CantHappen);
830 !effects.is_empty()
831}
832
833pub fn run_replace_damage(game: &mut GameState, event: &mut ReplacementEvent) -> ReplacementResult {
840 apply_replacements(game, event)
841}
842
843pub fn parse_replacement(raw: &str) -> Option<ReplacementEffect> {
848 super::replacement_effect::parse_replacement_effect(raw)
849}
850
851pub fn would_phase_be_skipped(game: &GameState, player: PlayerId, phase: PhaseType) -> bool {
860 for card in game.cards.iter() {
861 if card.zone != ZoneType::Battlefield {
862 continue;
863 }
864 for re in &card.replacement_effects {
865 if re.event != ReplacementType::BeginPhase {
866 continue;
867 }
868 if re.layer != ReplacementLayer::Control && re.layer != ReplacementLayer::CantHappen {
870 continue;
871 }
872 if !re.matches_phase(phase) {
874 continue;
875 }
876 if let Some(vp) = re.ir.valid_player_selector.as_ref() {
878 if !re.matches_compiled_valid_player(vp, player, card) {
879 continue;
880 }
881 }
882 return true;
883 }
884 }
885 false
886}
887
888pub fn would_extra_turn_be_skipped(game: &GameState, player: PlayerId) -> bool {
893 for card in game.cards.iter() {
894 if card.zone != ZoneType::Battlefield {
895 continue;
896 }
897 for re in &card.replacement_effects {
898 if re.event != ReplacementType::BeginTurn {
899 continue;
900 }
901 if re.layer != ReplacementLayer::Other && re.layer != ReplacementLayer::CantHappen {
902 continue;
903 }
904 if let Some(vp) = re.ir.valid_player_selector.as_ref() {
906 if !re.matches_compiled_valid_player(vp, player, card) {
907 continue;
908 }
909 }
910 return true;
911 }
912 }
913 false
914}
915
916pub fn visit(
921 game: &GameState,
922 event: &ReplacementType,
923 layer: Option<ReplacementLayer>,
924) -> Vec<(CardId, usize)> {
925 let mut result = Vec::new();
926 let mut global_idx = 0usize;
927
928 for (i, card) in game.cards.iter().enumerate() {
929 let card_id = CardId(i as u32);
930 for re in &card.replacement_effects {
931 let current_idx = global_idx;
932 global_idx += 1;
933
934 if let Some(target_layer) = layer {
936 if re.layer != target_layer {
937 continue;
938 }
939 }
940 if !re.active_in_zone(card.zone) {
942 continue;
943 }
944 if !re.mode_check(event) {
946 continue;
947 }
948 if re.has_run() {
950 continue;
951 }
952 if !re.requirements_check(game, card) {
954 continue;
955 }
956
957 result.push((card_id, current_idx));
958 }
959 }
960
961 result
962}
963
964fn collect_effects(
975 game: &GameState,
976 event: &ReplacementEvent,
977 layer: ReplacementLayer,
978) -> Vec<(CardId, ReplacementEffect, usize)> {
979 let _perf_scope =
980 crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::Replacement);
981 use super::{
982 replace_add_counter,
983 replace_assemble_contraption,
985 replace_assign_deal_damage,
986 replace_attached,
987 replace_begin_phase,
988 replace_begin_turn,
989 replace_cascade,
990 replace_copy_spell,
991 replace_counter,
992 replace_damage,
993 replace_dealt_damage,
994 replace_declare_blocker,
995 replace_destroy,
996 replace_draw,
997 replace_draw_cards,
998 replace_explore,
999 replace_gain_life,
1000 replace_game_loss,
1001 replace_game_win,
1002 replace_learn,
1003 replace_life_reduced,
1004 replace_lose_mana,
1005 replace_mill,
1006 replace_moved,
1007 replace_pay_life,
1008 replace_planar_dice_result,
1009 replace_planeswalk,
1010 replace_produce_mana,
1011 replace_proliferate,
1012 replace_remove_counter,
1013 replace_roll_dice,
1014 replace_roll_planar_dice,
1015 replace_scry,
1016 replace_set_in_motion,
1017 replace_tap,
1018 replace_token,
1019 replace_transform,
1020 replace_turn_face_up,
1021 replace_untap,
1022 };
1023
1024 let mut result = Vec::new();
1025 for (i, card) in game.cards.iter().enumerate() {
1026 let card_id = CardId(i as u32);
1027
1028 for (effect_idx_in_card, re) in card.replacement_effects.iter().enumerate() {
1029 let current_idx = effect_idx_in_card;
1030 if re.layer != layer {
1032 continue;
1033 }
1034 if !re.active_in_zone(card.zone) {
1036 continue;
1037 }
1038 if !re.requirements_check(game, card) {
1039 continue;
1040 }
1041 let applies = match re.event {
1043 ReplacementType::DamageDone => replace_damage::can_replace(re, event, game, card),
1044 ReplacementType::Draw => replace_draw::can_replace(re, event, game, card),
1045 ReplacementType::Destroy => replace_destroy::can_replace(re, event, game, card),
1046 ReplacementType::Moved => replace_moved::can_replace(re, event, game, card),
1047 ReplacementType::GainLife => replace_gain_life::can_replace(re, event, game, card),
1048 ReplacementType::CreateToken => replace_token::can_replace(re, event, game, card),
1049 ReplacementType::AddCounter => {
1050 replace_add_counter::can_replace(re, event, game, card)
1051 }
1052 ReplacementType::GameLoss => replace_game_loss::can_replace(re, event, game, card),
1053 ReplacementType::GameWin => replace_game_win::can_replace(re, event, game, card),
1054 ReplacementType::Counter => replace_counter::can_replace(re, event, game, card),
1055 ReplacementType::ProduceMana => {
1056 replace_produce_mana::can_replace(re, event, game, card)
1057 }
1058 ReplacementType::DrawCards => {
1060 replace_draw_cards::can_replace(re, event, game, card)
1061 }
1062 ReplacementType::AssembleContraption => {
1063 replace_assemble_contraption::can_replace(re, event, game, card)
1064 }
1065 ReplacementType::AssignDealDamage => {
1066 replace_assign_deal_damage::can_replace(re, event, game, card)
1067 }
1068 ReplacementType::Attached => replace_attached::can_replace(re, event, game, card),
1069 ReplacementType::BeginPhase => {
1070 replace_begin_phase::can_replace(re, event, game, card)
1071 }
1072 ReplacementType::BeginTurn => {
1073 replace_begin_turn::can_replace(re, event, game, card)
1074 }
1075 ReplacementType::Cascade => replace_cascade::can_replace(re, event, game, card),
1076 ReplacementType::CopySpell => {
1077 replace_copy_spell::can_replace(re, event, game, card)
1078 }
1079 ReplacementType::DealtDamage => {
1080 replace_dealt_damage::can_replace(re, event, game, card)
1081 }
1082 ReplacementType::DeclareBlocker => {
1083 replace_declare_blocker::can_replace(re, event, game, card)
1084 }
1085 ReplacementType::Explore => replace_explore::can_replace(re, event, game, card),
1086 ReplacementType::Learn => replace_learn::can_replace(re, event, game, card),
1087 ReplacementType::LifeReduced => {
1088 replace_life_reduced::can_replace(re, event, game, card)
1089 }
1090 ReplacementType::LoseMana => replace_lose_mana::can_replace(re, event, game, card),
1091 ReplacementType::Mill => replace_mill::can_replace(re, event, game, card),
1092 ReplacementType::PayLife => replace_pay_life::can_replace(re, event, game, card),
1093 ReplacementType::PlanarDiceResult => {
1094 replace_planar_dice_result::can_replace(re, event, game, card)
1095 }
1096 ReplacementType::Planeswalk => {
1097 replace_planeswalk::can_replace(re, event, game, card)
1098 }
1099 ReplacementType::Proliferate => {
1100 replace_proliferate::can_replace(re, event, game, card)
1101 }
1102 ReplacementType::RemoveCounter => {
1103 replace_remove_counter::can_replace(re, event, game, card)
1104 }
1105 ReplacementType::RollDice => replace_roll_dice::can_replace(re, event, game, card),
1106 ReplacementType::RollPlanarDice => {
1107 replace_roll_planar_dice::can_replace(re, event, game, card)
1108 }
1109 ReplacementType::Scry => replace_scry::can_replace(re, event, game, card),
1110 ReplacementType::SetInMotion => {
1111 replace_set_in_motion::can_replace(re, event, game, card)
1112 }
1113 ReplacementType::Tap => replace_tap::can_replace(re, event, game, card),
1114 ReplacementType::Transform => replace_transform::can_replace(re, event, game, card),
1115 ReplacementType::TurnFaceUp => {
1116 replace_turn_face_up::can_replace(re, event, game, card)
1117 }
1118 ReplacementType::Untap => replace_untap::can_replace(re, event, game, card),
1119 ReplacementType::Other(_) => false,
1120 };
1121
1122 if applies {
1123 result.push((card_id, re.clone(), current_idx));
1124 }
1125 }
1126 }
1127
1128 result
1129}
1130
1131fn execute_effect(
1137 game: &mut GameState,
1138 card_id: CardId,
1139 effect: &ReplacementEffect,
1140 event: &mut ReplacementEvent,
1141 mut agents: Option<&mut [Box<dyn PlayerAgent>]>,
1142 runtime: Option<&mut ReplacementRuntime<'_>>,
1143) -> ReplacementResult {
1144 use super::{
1145 replace_add_counter,
1146 replace_assemble_contraption,
1148 replace_assign_deal_damage,
1149 replace_attached,
1150 replace_begin_phase,
1151 replace_begin_turn,
1152 replace_cascade,
1153 replace_copy_spell,
1154 replace_counter,
1155 replace_damage,
1156 replace_dealt_damage,
1157 replace_declare_blocker,
1158 replace_destroy,
1159 replace_draw,
1160 replace_draw_cards,
1161 replace_explore,
1162 replace_gain_life,
1163 replace_game_loss,
1164 replace_game_win,
1165 replace_learn,
1166 replace_life_reduced,
1167 replace_lose_mana,
1168 replace_mill,
1169 replace_moved,
1170 replace_pay_life,
1171 replace_planar_dice_result,
1172 replace_planeswalk,
1173 replace_produce_mana,
1174 replace_proliferate,
1175 replace_remove_counter,
1176 replace_roll_dice,
1177 replace_roll_planar_dice,
1178 replace_scry,
1179 replace_set_in_motion,
1180 replace_tap,
1181 replace_token,
1182 replace_transform,
1183 replace_turn_face_up,
1184 replace_untap,
1185 };
1186
1187 if effect.ir.optional {
1188 let decider = optional_decider_for_effect(effect, game, card_id, event)
1189 .unwrap_or_else(|| affected_player_for_event(event, game));
1190 let host = game.card(card_id);
1191 let question = replacement_question(effect, host, game, event);
1192 let confirmed = if let Some(agents) = agents.as_deref_mut() {
1193 agents[decider.index()].confirm_replacement_effect(
1194 decider,
1195 &question,
1196 &effect.description(host, game),
1197 Some(card_id),
1198 )
1199 } else {
1200 true
1201 };
1202 if !confirmed {
1203 return ReplacementResult::NotReplaced;
1204 }
1205 }
1206
1207 match effect.event {
1208 ReplacementType::DamageDone => replace_damage::execute(effect, event, game, card_id),
1209 ReplacementType::Draw => replace_draw::execute(effect, event, game, card_id),
1210 ReplacementType::Destroy => replace_destroy::execute(effect, event, game, card_id),
1211 ReplacementType::Moved => {
1212 replace_moved::execute(effect, event, game, card_id, agents, runtime)
1213 }
1214 ReplacementType::GainLife => replace_gain_life::execute(effect, event, game, card_id),
1215 ReplacementType::CreateToken => replace_token::execute(effect, event, game, card_id),
1216 ReplacementType::AddCounter => replace_add_counter::execute(effect, event, game, card_id),
1217 ReplacementType::GameLoss => replace_game_loss::execute(effect, event, game, card_id),
1218 ReplacementType::GameWin => replace_game_win::execute(effect, event, game, card_id),
1219 ReplacementType::Counter => replace_counter::execute(effect, event, game, card_id),
1220 ReplacementType::ProduceMana => replace_produce_mana::execute(effect, event, game, card_id),
1221 ReplacementType::DrawCards => replace_draw_cards::execute(effect, event, game, card_id),
1223 ReplacementType::AssembleContraption => {
1224 replace_assemble_contraption::execute(effect, event, game, card_id)
1225 }
1226 ReplacementType::AssignDealDamage => {
1227 replace_assign_deal_damage::execute(effect, event, game, card_id)
1228 }
1229 ReplacementType::Attached => replace_attached::execute(effect, event, game, card_id),
1230 ReplacementType::BeginPhase => replace_begin_phase::execute(effect, event, game, card_id),
1231 ReplacementType::BeginTurn => replace_begin_turn::execute(effect, event, game, card_id),
1232 ReplacementType::Cascade => replace_cascade::execute(effect, event, game, card_id),
1233 ReplacementType::CopySpell => replace_copy_spell::execute(effect, event, game, card_id),
1234 ReplacementType::DealtDamage => replace_dealt_damage::execute(effect, event, game, card_id),
1235 ReplacementType::DeclareBlocker => {
1236 replace_declare_blocker::execute(effect, event, game, card_id)
1237 }
1238 ReplacementType::Explore => replace_explore::execute(effect, event, game, card_id),
1239 ReplacementType::Learn => replace_learn::execute(effect, event, game, card_id),
1240 ReplacementType::LifeReduced => replace_life_reduced::execute(effect, event, game, card_id),
1241 ReplacementType::LoseMana => replace_lose_mana::execute(effect, event, game, card_id),
1242 ReplacementType::Mill => replace_mill::execute(effect, event, game, card_id),
1243 ReplacementType::PayLife => replace_pay_life::execute(effect, event, game, card_id),
1244 ReplacementType::PlanarDiceResult => {
1245 replace_planar_dice_result::execute(effect, event, game, card_id)
1246 }
1247 ReplacementType::Planeswalk => replace_planeswalk::execute(effect, event, game, card_id),
1248 ReplacementType::Proliferate => replace_proliferate::execute(effect, event, game, card_id),
1249 ReplacementType::RemoveCounter => {
1250 replace_remove_counter::execute(effect, event, game, card_id)
1251 }
1252 ReplacementType::RollDice => replace_roll_dice::execute(effect, event, game, card_id),
1253 ReplacementType::RollPlanarDice => {
1254 replace_roll_planar_dice::execute(effect, event, game, card_id)
1255 }
1256 ReplacementType::Scry => replace_scry::execute(effect, event, game, card_id),
1257 ReplacementType::SetInMotion => {
1258 replace_set_in_motion::execute(effect, event, game, card_id)
1259 }
1260 ReplacementType::Tap => replace_tap::execute(effect, event, game, card_id),
1261 ReplacementType::Transform => replace_transform::execute(effect, event, game, card_id),
1262 ReplacementType::TurnFaceUp => replace_turn_face_up::execute(effect, event, game, card_id),
1263 ReplacementType::Untap => replace_untap::execute(effect, event, game, card_id),
1264 ReplacementType::Other(_) => ReplacementResult::NotReplaced,
1265 }
1266}
1267
1268fn optional_decider_for_effect(
1269 effect: &ReplacementEffect,
1270 game: &GameState,
1271 source_card_id: CardId,
1272 event: &ReplacementEvent,
1273) -> Option<PlayerId> {
1274 let expr = effect.ir.optional_decider_text.as_deref()?.trim();
1275 match expr {
1276 "You" | "Controller" => Some(game.card(source_card_id).controller),
1277 "Owner" => Some(game.card(source_card_id).owner),
1278 "Affected" | "AffectedController" => Some(affected_player_for_event(event, game)),
1279 "Opponent" => Some(game.opponent_of(game.card(source_card_id).controller)),
1280 _ => None,
1281 }
1282}
1283
1284fn replacement_question(
1285 effect: &ReplacementEffect,
1286 host: &crate::card::Card,
1287 game: &GameState,
1288 event: &ReplacementEvent,
1289) -> String {
1290 let desc = effect.description(host, game);
1292 match event {
1293 ReplacementEvent::Moved { card, .. } => format!(
1294 "Apply {} to {}?\n{}",
1295 host.card_name,
1296 game.card(*card).card_name,
1297 desc
1298 ),
1299 _ => format!("Apply {}?\n{}", host.card_name, desc),
1300 }
1301}
1302
1303#[cfg(test)]
1306mod tests {
1307 use super::*;
1308 use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
1309
1310 use crate::agent::PlayerAgent;
1311 use crate::card::Card;
1312 use crate::ids::{CardId, PlayerId};
1313
1314 fn make_game() -> GameState {
1317 GameState::new(&["Alice", "Bob"], 20)
1318 }
1319
1320 fn add_creature_with_abilities(
1321 game: &mut GameState,
1322 owner: PlayerId,
1323 name: &str,
1324 abilities: Vec<String>,
1325 ) -> CardId {
1326 let card = Card::new(
1327 CardId(0), name.to_string(),
1329 owner,
1330 CardTypeLine::parse("Creature - Bear"),
1331 ManaCost::parse("1 G"),
1332 ColorSet::GREEN,
1333 Some(2),
1334 Some(2),
1335 vec![],
1336 abilities,
1337 );
1338 game.create_card(card)
1339 }
1340
1341 fn put_on_battlefield(game: &mut GameState, card_id: CardId, owner: PlayerId) {
1342 game.move_card(card_id, ZoneType::Battlefield, owner);
1343 }
1344
1345 struct ConfirmReplacementAgent {
1346 confirm: bool,
1347 }
1348
1349 impl PlayerAgent for ConfirmReplacementAgent {
1350 fn mulligan_decision(
1351 &mut self,
1352 _player: PlayerId,
1353 _hand: &[CardId],
1354 _mulligan_count: u32,
1355 ) -> bool {
1356 true
1357 }
1358
1359 fn choose_action(
1360 &mut self,
1361 player: PlayerId,
1362 action_space: Option<&crate::agent::PriorityActionSpace>,
1363 request_action_space: &mut dyn FnMut() -> crate::agent::PriorityActionSpace,
1364 ) -> crate::player::actions::PlayerAction {
1365 crate::player::actions::PlayerAction::PassPriority
1366 }
1367
1368 fn choose_attackers(
1369 &mut self,
1370 _player: PlayerId,
1371 _available: &[CardId],
1372 _possible_defenders: &[crate::combat::DefenderId],
1373 ) -> Vec<(CardId, crate::combat::DefenderId)> {
1374 vec![]
1375 }
1376
1377 fn choose_blockers(
1378 &mut self,
1379 _player: PlayerId,
1380 _attackers: &[CardId],
1381 _available_blockers: &[CardId],
1382 _max_blockers: Option<usize>,
1383 ) -> Vec<(CardId, CardId)> {
1384 vec![]
1385 }
1386
1387 fn choose_target_player(
1388 &mut self,
1389 _player: PlayerId,
1390 valid: &[PlayerId],
1391 _sa: Option<&crate::spellability::SpellAbility>,
1392 ) -> Option<PlayerId> {
1393 valid.first().copied()
1394 }
1395
1396 fn choose_target_card(
1397 &mut self,
1398 _player: PlayerId,
1399 valid: &[CardId],
1400 _sa: Option<&crate::spellability::SpellAbility>,
1401 ) -> Option<CardId> {
1402 valid.first().copied()
1403 }
1404
1405 fn choose_target_any(
1406 &mut self,
1407 _player: PlayerId,
1408 valid_players: &[PlayerId],
1409 valid_cards: &[CardId],
1410 _sa: Option<&crate::spellability::SpellAbility>,
1411 ) -> crate::agent::TargetChoice {
1412 if let Some(player) = valid_players.first() {
1413 crate::agent::TargetChoice::Player(*player)
1414 } else if let Some(card) = valid_cards.first() {
1415 crate::agent::TargetChoice::Card(*card)
1416 } else {
1417 crate::agent::TargetChoice::None
1418 }
1419 }
1420
1421 fn choose_land_or_spell(&mut self, _player: PlayerId) -> Option<bool> {
1422 Some(true)
1423 }
1424
1425 fn confirm_replacement_effect(
1426 &mut self,
1427 _player: PlayerId,
1428 _question: &str,
1429 _effect_description: &str,
1430 _source: Option<crate::ids::CardId>,
1431 ) -> bool {
1432 self.confirm
1433 }
1434
1435 fn choose_targets_for(
1436 &mut self,
1437 _sa: &mut crate::spellability::SpellAbility,
1438 _game: &GameState,
1439 _mana_pools: &[ManaPool],
1440 ) -> bool {
1441 false
1442 }
1443 }
1444
1445 #[test]
1448 fn draw_skip_for_controller() {
1449 let mut game = make_game();
1450 let alice = PlayerId(0);
1451 let cid = add_creature_with_abilities(
1453 &mut game,
1454 alice,
1455 "SkipDraw",
1456 vec!["R$ Event$ Draw | ValidPlayer$ You | Prevent$ True".to_string()],
1457 );
1458 put_on_battlefield(&mut game, cid, alice);
1459
1460 let mut event = ReplacementEvent::Draw {
1461 player: alice,
1462 extra_draws: 0,
1463 is_first_in_draw_step: false,
1464 };
1465 let result = apply_replacements(&mut game, &mut event);
1466 assert_eq!(result, ReplacementResult::Skipped);
1467 }
1468
1469 #[test]
1470 fn draw_not_skipped_for_opponent() {
1471 let mut game = make_game();
1472 let alice = PlayerId(0);
1473 let bob = PlayerId(1);
1474 let cid = add_creature_with_abilities(
1475 &mut game,
1476 alice,
1477 "SkipDraw",
1478 vec!["R$ Event$ Draw | ValidPlayer$ You | Prevent$ True".to_string()],
1479 );
1480 put_on_battlefield(&mut game, cid, alice);
1481
1482 let mut event = ReplacementEvent::Draw {
1484 player: bob,
1485 extra_draws: 0,
1486 is_first_in_draw_step: false,
1487 };
1488 let result = apply_replacements(&mut game, &mut event);
1489 assert_eq!(result, ReplacementResult::NotReplaced);
1490 }
1491
1492 #[test]
1493 fn draw_not_applied_when_card_not_on_battlefield() {
1494 let mut game = make_game();
1495 let alice = PlayerId(0);
1496 let cid = add_creature_with_abilities(
1497 &mut game,
1498 alice,
1499 "SkipDraw",
1500 vec![
1501 "R$ Event$ Draw | ActiveZones$ Battlefield | ValidPlayer$ You | Prevent$ True"
1502 .to_string(),
1503 ],
1504 );
1505 game.move_card(cid, ZoneType::Hand, alice);
1507
1508 let mut event = ReplacementEvent::Draw {
1509 player: alice,
1510 extra_draws: 0,
1511 is_first_in_draw_step: false,
1512 };
1513 let result = apply_replacements(&mut game, &mut event);
1514 assert_eq!(result, ReplacementResult::NotReplaced);
1515 }
1516
1517 #[test]
1520 fn damage_to_player_prevented() {
1521 let mut game = make_game();
1522 let alice = PlayerId(0);
1523 let cid = add_creature_with_abilities(
1525 &mut game,
1526 alice,
1527 "Shield",
1528 vec!["R$ Event$ DamageDone | ValidTarget$ Player | Prevent$ True".to_string()],
1529 );
1530 put_on_battlefield(&mut game, cid, alice);
1531
1532 let mut event = ReplacementEvent::DamageToPlayer {
1533 target: alice,
1534 amount: 5,
1535 source: None,
1536 is_combat: false,
1537 };
1538 let result = apply_replacements(&mut game, &mut event);
1539 assert_eq!(result, ReplacementResult::Prevented);
1540 if let ReplacementEvent::DamageToPlayer { amount, .. } = event {
1542 assert_eq!(amount, 0);
1543 } else {
1544 panic!("unexpected event type");
1545 }
1546 }
1547
1548 #[test]
1549 fn damage_zero_not_replaced() {
1550 let mut game = make_game();
1551 let alice = PlayerId(0);
1552 let cid = add_creature_with_abilities(
1553 &mut game,
1554 alice,
1555 "Shield",
1556 vec!["R$ Event$ DamageDone | ValidTarget$ Player | Prevent$ True".to_string()],
1557 );
1558 put_on_battlefield(&mut game, cid, alice);
1559
1560 let mut event = ReplacementEvent::DamageToPlayer {
1562 target: alice,
1563 amount: 0,
1564 source: None,
1565 is_combat: false,
1566 };
1567 let result = apply_replacements(&mut game, &mut event);
1568 assert_eq!(result, ReplacementResult::NotReplaced);
1569 }
1570
1571 #[test]
1572 fn damage_to_card_prevented() {
1573 let mut game = make_game();
1574 let alice = PlayerId(0);
1575 let shield = add_creature_with_abilities(
1576 &mut game,
1577 alice,
1578 "Shield",
1579 vec!["R$ Event$ DamageDone | ValidTarget$ Card | Prevent$ True".to_string()],
1580 );
1581 put_on_battlefield(&mut game, shield, alice);
1582
1583 let target_creature = add_creature_with_abilities(&mut game, alice, "Target", vec![]);
1584 put_on_battlefield(&mut game, target_creature, alice);
1585
1586 let mut event = ReplacementEvent::DamageToCard {
1587 target: target_creature,
1588 amount: 3,
1589 source: None,
1590 is_combat: false,
1591 };
1592 let result = apply_replacements(&mut game, &mut event);
1593 assert_eq!(result, ReplacementResult::Prevented);
1594 if let ReplacementEvent::DamageToCard { amount, .. } = event {
1595 assert_eq!(amount, 0);
1596 } else {
1597 panic!("unexpected event type");
1598 }
1599 }
1600
1601 #[test]
1602 fn damage_replace_with_can_increase_amount() {
1603 let mut game = make_game();
1604 let alice = PlayerId(0);
1605 let bob = PlayerId(1);
1606
1607 let replacer = add_creature_with_abilities(
1608 &mut game,
1609 alice,
1610 "Boss",
1611 vec![
1612 "R$ Event$ DamageDone | ActiveZones$ Battlefield | ValidSource$ Card.YouCtrl | ValidTarget$ Player.Opponent | ReplaceWith$ DmgPlus".to_string(),
1613 ],
1614 );
1615 game.card_mut(replacer).set_s_var(
1616 "DmgPlus",
1617 "DB$ ReplaceEffect | VarName$ DamageAmount | VarValue$ ReplaceCount$DamageAmount/Plus.1",
1618 );
1619 put_on_battlefield(&mut game, replacer, alice);
1620
1621 let source = add_creature_with_abilities(&mut game, alice, "Source", vec![]);
1622 put_on_battlefield(&mut game, source, alice);
1623
1624 let mut event = ReplacementEvent::DamageToPlayer {
1625 target: bob,
1626 amount: 2,
1627 source: Some(source),
1628 is_combat: false,
1629 };
1630 let _ = apply_replacements(&mut game, &mut event);
1631 if let ReplacementEvent::DamageToPlayer { amount, .. } = event {
1632 assert_eq!(amount, 3);
1633 } else {
1634 panic!("unexpected event type");
1635 }
1636 }
1637
1638 #[test]
1639 fn damage_replace_with_respects_max_speed() {
1640 let mut game = make_game();
1641 let alice = PlayerId(0);
1642 let bob = PlayerId(1);
1643 game.player_mut(alice).speed = 4;
1644
1645 let replacer = add_creature_with_abilities(
1646 &mut game,
1647 alice,
1648 "Far Fortune",
1649 vec![
1650 "R$ Event$ DamageDone | MaxSpeed$ True | ActiveZones$ Battlefield | ValidSource$ Card.YouCtrl | ValidTarget$ Player.Opponent | ReplaceWith$ DmgPlus".to_string(),
1651 ],
1652 );
1653 game.card_mut(replacer).set_s_var(
1654 "DmgPlus",
1655 "DB$ ReplaceEffect | VarName$ DamageAmount | VarValue$ ReplaceCount$DamageAmount/Plus.1",
1656 );
1657 put_on_battlefield(&mut game, replacer, alice);
1658
1659 let source = add_creature_with_abilities(&mut game, alice, "Source", vec![]);
1660 put_on_battlefield(&mut game, source, alice);
1661
1662 let mut event = ReplacementEvent::DamageToPlayer {
1663 target: bob,
1664 amount: 1,
1665 source: Some(source),
1666 is_combat: false,
1667 };
1668 let _ = apply_replacements(&mut game, &mut event);
1669 if let ReplacementEvent::DamageToPlayer { amount, .. } = event {
1670 assert_eq!(amount, 2);
1671 } else {
1672 panic!("unexpected event type");
1673 }
1674 }
1675
1676 #[test]
1677 fn roll_dice_replace_with_updates_number_and_ignore() {
1678 let mut game = make_game();
1679 let alice = PlayerId(0);
1680
1681 let replacer = add_creature_with_abilities(
1682 &mut game,
1683 alice,
1684 "Barbarian Class",
1685 vec![
1686 "R$ Event$ RollDice | ActiveZones$ Battlefield | ValidPlayer$ You | ReplaceWith$ PlusRoll".to_string(),
1687 ],
1688 );
1689 game.card_mut(replacer).set_s_var(
1690 "PlusRoll",
1691 "DB$ ReplaceEffect | VarName$ Number | VarValue$ ReplaceCount$Number/Plus.1 | SubAbility$ IgnoreLowest",
1692 );
1693 game.card_mut(replacer).set_s_var(
1694 "IgnoreLowest",
1695 "DB$ ReplaceEffect | VarName$ Ignore | VarValue$ ReplaceCount$Ignore/Plus.1",
1696 );
1697 put_on_battlefield(&mut game, replacer, alice);
1698
1699 let mut event = ReplacementEvent::RollDice {
1700 player: alice,
1701 sides: 20,
1702 number: 1,
1703 ignore: 0,
1704 ignore_chosen: HashMap::new(),
1705 dice_pt_exchanges: HashSet::new(),
1706 };
1707 let result = apply_replacements(&mut game, &mut event);
1708 assert_eq!(result, ReplacementResult::Updated);
1709 if let ReplacementEvent::RollDice { number, ignore, .. } = event {
1710 assert_eq!(number, 2);
1711 assert_eq!(ignore, 1);
1712 } else {
1713 panic!("unexpected event type");
1714 }
1715 }
1716
1717 #[test]
1718 fn roll_dice_replace_with_updates_ignore_chosen_map() {
1719 let mut game = make_game();
1720 let alice = PlayerId(0);
1721
1722 let replacer = add_creature_with_abilities(
1723 &mut game,
1724 alice,
1725 "Bamboozling Beeble",
1726 vec![
1727 "R$ Event$ RollDice | ActiveZones$ Battlefield | ValidPlayer$ You | ReplaceWith$ RigRoll".to_string(),
1728 ],
1729 );
1730 game.card_mut(replacer).set_s_var(
1731 "RigRoll",
1732 "DB$ ReplaceEffect | VarName$ IgnoreChosen | VarType$ Map | VarKey$ You | VarValue$ 1",
1733 );
1734 put_on_battlefield(&mut game, replacer, alice);
1735
1736 let mut event = ReplacementEvent::RollDice {
1737 player: alice,
1738 sides: 6,
1739 number: 2,
1740 ignore: 0,
1741 ignore_chosen: HashMap::new(),
1742 dice_pt_exchanges: HashSet::new(),
1743 };
1744 let result = apply_replacements(&mut game, &mut event);
1745 assert_eq!(result, ReplacementResult::Updated);
1746 if let ReplacementEvent::RollDice { ignore_chosen, .. } = event {
1747 assert_eq!(ignore_chosen.get(&alice), Some(&1));
1748 } else {
1749 panic!("unexpected event type");
1750 }
1751 }
1752
1753 #[test]
1754 fn roll_dice_replace_with_updates_dice_pt_exchange_set() {
1755 let mut game = make_game();
1756 let alice = PlayerId(0);
1757
1758 let replacer = add_creature_with_abilities(
1759 &mut game,
1760 alice,
1761 "Vedalken Squirrel-Whacker",
1762 vec![
1763 "R$ Event$ RollDice | ActiveZones$ Battlefield | ValidPlayer$ You | ValidSides$ 6 | ReplaceWith$ SwapRoll".to_string(),
1764 ],
1765 );
1766 game.card_mut(replacer).set_s_var(
1767 "SwapRoll",
1768 "DB$ ReplaceEffect | VarName$ DicePTExchanges | VarType$ CardSet | VarValue$ Self",
1769 );
1770 put_on_battlefield(&mut game, replacer, alice);
1771
1772 let mut event = ReplacementEvent::RollDice {
1773 player: alice,
1774 sides: 6,
1775 number: 2,
1776 ignore: 0,
1777 ignore_chosen: HashMap::new(),
1778 dice_pt_exchanges: HashSet::new(),
1779 };
1780 let result = apply_replacements(&mut game, &mut event);
1781 assert_eq!(result, ReplacementResult::Updated);
1782 if let ReplacementEvent::RollDice {
1783 dice_pt_exchanges, ..
1784 } = event
1785 {
1786 assert!(dice_pt_exchanges.contains(&replacer));
1787 } else {
1788 panic!("unexpected event type");
1789 }
1790 }
1791
1792 #[test]
1795 fn indestructible_destroy_replaced() {
1796 let mut game = make_game();
1797 let alice = PlayerId(0);
1798 let cid = add_creature_with_abilities(
1800 &mut game,
1801 alice,
1802 "Indestructible Bear",
1803 vec!["R$ Event$ Destroy | ValidCard$ Card.Self".to_string()],
1804 );
1805 put_on_battlefield(&mut game, cid, alice);
1806
1807 let mut event = ReplacementEvent::Destroy { target: cid };
1808 let result = apply_replacements(&mut game, &mut event);
1809 assert_eq!(result, ReplacementResult::Replaced);
1810 }
1811
1812 #[test]
1813 fn destroy_not_replaced_for_other_card() {
1814 let mut game = make_game();
1815 let alice = PlayerId(0);
1816 let indestructible = add_creature_with_abilities(
1818 &mut game,
1819 alice,
1820 "Indestructible Bear",
1821 vec!["R$ Event$ Destroy | ValidCard$ Card.Self".to_string()],
1822 );
1823 let other = add_creature_with_abilities(&mut game, alice, "Mortal Bear", vec![]);
1824 put_on_battlefield(&mut game, indestructible, alice);
1825 put_on_battlefield(&mut game, other, alice);
1826
1827 let mut event = ReplacementEvent::Destroy { target: other };
1828 let result = apply_replacements(&mut game, &mut event);
1829 assert_eq!(result, ReplacementResult::NotReplaced);
1830 }
1831
1832 #[test]
1835 fn moved_destination_updated_to_exile() {
1836 let mut game = make_game();
1837 let alice = PlayerId(0);
1838 let cid = add_creature_with_abilities(
1840 &mut game,
1841 alice,
1842 "Exile Bear",
1843 vec!["R$ Event$ Moved | Destination$ Graveyard | Origin$ Battlefield | ValidCard$ Card.Self | NewDestination$ Exile".to_string()],
1844 );
1845 put_on_battlefield(&mut game, cid, alice);
1846
1847 let mut event = ReplacementEvent::Moved {
1848 card: cid,
1849 origin: ZoneType::Battlefield,
1850 destination: ZoneType::Graveyard,
1851 is_discard: false,
1852 };
1853 let result = apply_replacements(&mut game, &mut event);
1854 assert_eq!(result, ReplacementResult::Updated);
1855 if let ReplacementEvent::Moved { destination, .. } = event {
1856 assert_eq!(destination, ZoneType::Exile);
1857 } else {
1858 panic!("unexpected event type");
1859 }
1860 }
1861
1862 #[test]
1863 fn moved_not_replaced_wrong_origin() {
1864 let mut game = make_game();
1865 let alice = PlayerId(0);
1866 let cid = add_creature_with_abilities(
1867 &mut game,
1868 alice,
1869 "Exile Bear",
1870 vec!["R$ Event$ Moved | Destination$ Graveyard | Origin$ Battlefield | ValidCard$ Card.Self | NewDestination$ Exile".to_string()],
1871 );
1872 put_on_battlefield(&mut game, cid, alice);
1873
1874 let mut event = ReplacementEvent::Moved {
1876 card: cid,
1877 origin: ZoneType::Hand,
1878 destination: ZoneType::Graveyard,
1879 is_discard: false,
1880 };
1881 let result = apply_replacements(&mut game, &mut event);
1882 assert_eq!(result, ReplacementResult::NotReplaced);
1883 }
1884
1885 #[test]
1886 fn declined_optional_replacement_falls_through_to_next_candidate() {
1887 let mut game = make_game();
1888 let alice = PlayerId(0);
1889 let cid = add_creature_with_abilities(
1890 &mut game,
1891 alice,
1892 "Optional Bear",
1893 vec![
1894 "R$ Event$ Moved | Destination$ Graveyard | Origin$ Battlefield | ValidCard$ Card.Self | Optional$ True | OptionalDecider$ You | NewDestination$ Command".to_string(),
1895 "R$ Event$ Moved | Destination$ Graveyard | Origin$ Battlefield | ValidCard$ Card.Self | NewDestination$ Exile".to_string(),
1896 ],
1897 );
1898 put_on_battlefield(&mut game, cid, alice);
1899
1900 let mut agents: Vec<Box<dyn PlayerAgent>> = vec![
1901 Box::new(ConfirmReplacementAgent { confirm: false }),
1902 Box::new(ConfirmReplacementAgent { confirm: false }),
1903 ];
1904 let mut event = ReplacementEvent::Moved {
1905 card: cid,
1906 origin: ZoneType::Battlefield,
1907 destination: ZoneType::Graveyard,
1908 is_discard: false,
1909 };
1910 let result = apply_replacements_with_agents(&mut game, agents.as_mut_slice(), &mut event);
1911 assert_eq!(result, ReplacementResult::Updated);
1912 if let ReplacementEvent::Moved { destination, .. } = event {
1913 assert_eq!(destination, ZoneType::Exile);
1914 } else {
1915 panic!("unexpected event type");
1916 }
1917 }
1918
1919 #[test]
1920 fn accepted_optional_replacement_updates_destination() {
1921 let mut game = make_game();
1922 let alice = PlayerId(0);
1923 let cid = add_creature_with_abilities(
1924 &mut game,
1925 alice,
1926 "Optional Bear",
1927 vec![
1928 "R$ Event$ Moved | Destination$ Graveyard | Origin$ Battlefield | ValidCard$ Card.Self | Optional$ True | OptionalDecider$ You | NewDestination$ Command".to_string(),
1929 ],
1930 );
1931 put_on_battlefield(&mut game, cid, alice);
1932
1933 let mut agents: Vec<Box<dyn PlayerAgent>> = vec![
1934 Box::new(ConfirmReplacementAgent { confirm: true }),
1935 Box::new(ConfirmReplacementAgent { confirm: true }),
1936 ];
1937 let mut event = ReplacementEvent::Moved {
1938 card: cid,
1939 origin: ZoneType::Battlefield,
1940 destination: ZoneType::Graveyard,
1941 is_discard: false,
1942 };
1943 let result = apply_replacements_with_agents(&mut game, agents.as_mut_slice(), &mut event);
1944 assert_eq!(result, ReplacementResult::Updated);
1945 if let ReplacementEvent::Moved { destination, .. } = event {
1946 assert_eq!(destination, ZoneType::Command);
1947 } else {
1948 panic!("unexpected event type");
1949 }
1950 }
1951
1952 #[test]
1955 fn no_effects_returns_not_replaced() {
1956 let mut game = make_game();
1957 let mut event = ReplacementEvent::Draw {
1958 player: PlayerId(0),
1959 extra_draws: 0,
1960 is_first_in_draw_step: false,
1961 };
1962 let result = apply_replacements(&mut game, &mut event);
1963 assert_eq!(result, ReplacementResult::NotReplaced);
1964 }
1965}