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