Skip to main content

manabrew_engine/replacement/
replacement_handler.rs

1//! Replacement effect dispatcher.
2//!
3//! Mirrors the Java Forge `ReplacementHandler.java` in
4//! `forge/game/replacement/`.
5//!
6//! The entry point is [`apply_replacements`], which accepts a mutable
7//! [`ReplacementEvent`] and iterates through the five CR 616 layers
8//! (CantHappen → Control → Copy → Transform → Other), applying the first
9//! matching effect in each layer.
10
11use 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// ── ReplacementEvent ──────────────────────────────────────────────────────────
31
32/// A game event that may be subject to one or more replacement effects.
33///
34/// Each variant carries the mutable parameters of the event so that a
35/// replacement effect can modify them (e.g. reduce damage to 0, redirect a
36/// zone move, etc.).
37///
38/// Mirrors the `runParams` map passed to `ReplacementHandler.run()` in Java,
39/// but typed for safety.
40#[derive(Debug, Clone)]
41pub enum ReplacementEvent {
42    /// A card is being drawn by a player.
43    /// `extra_draws` is incremented by replacements (e.g. Alhammarret's Archive DrawTwo).
44    /// `is_first_in_draw_step` is true for the first draw in the draw step (not extra draws).
45    Draw {
46        player: PlayerId,
47        extra_draws: i32,
48        is_first_in_draw_step: bool,
49    },
50
51    /// Damage is being dealt to a permanent.
52    DamageToCard {
53        target: CardId,
54        amount: i32,
55        /// The card dealing the damage, if known.
56        source: Option<CardId>,
57        /// Whether this is combat damage.
58        is_combat: bool,
59    },
60
61    /// Damage is being dealt to a player.
62    DamageToPlayer {
63        target: PlayerId,
64        amount: i32,
65        /// The card dealing the damage, if known.
66        source: Option<CardId>,
67        /// Whether this is combat damage.
68        is_combat: bool,
69    },
70
71    /// A permanent is being destroyed (lethal damage or destroy effect).
72    Destroy { target: CardId },
73
74    /// A card is moving between zones.
75    Moved {
76        card: CardId,
77        origin: ZoneType,
78        destination: ZoneType,
79        /// True when this move originates from a discard action.
80        /// Mirrors Java's `AbilityKey.Discard` in replacement params.
81        is_discard: bool,
82    },
83
84    /// A player is gaining life.
85    GainLife { player: PlayerId, amount: i32 },
86
87    /// Token(s) are being created.
88    /// `is_effect` is `true` when created by a spell/ability effect, `false` for game rules.
89    CreateToken {
90        player: PlayerId,
91        count: i32,
92        is_effect: bool,
93    },
94
95    /// Counter(s) are being added to a permanent.
96    /// `is_effect` is `true` when placed by a spell/ability, `false` for ETB keywords/game rules.
97    AddCounter {
98        target: CardId,
99        counter_type: CounterType,
100        count: i32,
101        is_effect: bool,
102    },
103
104    /// A player is losing the game.
105    GameLoss {
106        player: PlayerId,
107        reason: GameLossReason,
108    },
109
110    /// A player is winning the game.
111    GameWin { player: PlayerId },
112
113    /// A spell is being countered.
114    Counter { card: CardId },
115
116    /// Mana is being produced (for doublers like Mirari's Wake, Nyxbloom Ancient).
117    /// `mana` is the produced mana string (e.g. "G" or "U U") that may be modified.
118    ProduceMana {
119        source: CardId,
120        activator: PlayerId,
121        mana: String,
122    },
123
124    /// A permanent is being tapped.
125    Tap { card: CardId },
126
127    /// A permanent is being untapped.
128    Untap {
129        card: CardId,
130        player: Option<PlayerId>,
131    },
132
133    /// Life is being reduced (distinct from damage).
134    LifeReduced {
135        player: PlayerId,
136        amount: i32,
137        is_damage: bool,
138    },
139
140    /// Counter(s) are being removed from a permanent.
141    RemoveCounter {
142        target: CardId,
143        counter_type: CounterType,
144        count: i32,
145    },
146
147    /// Damage has been dealt (post-damage).
148    DealtDamage {
149        target: CardId,
150        amount: i32,
151        source: Option<CardId>,
152    },
153
154    /// Multiple cards are being drawn.
155    DrawCards { player: PlayerId, count: i32 },
156
157    /// Cards are being milled.
158    Mill { player: PlayerId, count: i32 },
159
160    /// Life is being paid as a cost.
161    PayLife { player: PlayerId, amount: i32 },
162
163    /// A player is scrying.
164    Scry { player: PlayerId, count: i32 },
165
166    /// An aura/equipment is being attached.
167    Attached { card: CardId, target: CardId },
168
169    /// A phase is beginning.
170    BeginPhase {
171        player: PlayerId,
172        phase: forge_foundation::PhaseType,
173    },
174
175    /// A turn is beginning.
176    BeginTurn { player: PlayerId },
177
178    /// A creature is exploring.
179    Explore { card: CardId },
180
181    /// Blockers are being declared.
182    DeclareBlocker { player: PlayerId },
183
184    /// Damage is being assigned before dealing.
185    AssignDealDamage { card: CardId },
186
187    /// A DFC is transforming.
188    Transform { card: CardId },
189
190    /// A face-down card is turning face up.
191    TurnFaceUp { card: CardId },
192
193    /// A spell is being copied.
194    CopySpell { player: PlayerId, count: i32 },
195
196    /// A player is proliferating.
197    Proliferate { player: PlayerId, count: i32 },
198
199    /// Cascade is triggering.
200    Cascade { player: PlayerId },
201
202    /// A player is learning.
203    Learn { player: PlayerId },
204
205    /// Mana is being lost.
206    LoseMana { player: PlayerId },
207
208    /// Dice are being rolled.
209    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    /// The planar die is being rolled.
219    RollPlanarDice { player: PlayerId },
220
221    /// A planar dice result is being applied.
222    PlanarDiceResult { player: PlayerId },
223
224    /// A player is planeswalking.
225    Planeswalk { player: PlayerId },
226
227    /// A scheme is being set in motion.
228    SetInMotion { player: PlayerId },
229
230    /// A contraption is being assembled.
231    AssembleContraption { player: PlayerId },
232}
233
234// ── ReplacementHandler struct ─────────────────────────────────────────────────
235
236use 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
248/// Struct-based replacement handler with loop prevention and optional agent access.
249///
250/// Mirrors Java `ReplacementHandler` class. The `has_run` set prevents infinite
251/// loops when effects re-trigger themselves.
252///
253/// Usage:
254/// - `ReplacementHandler::new().run(game, Some(agents), event)` — with agent choice
255/// - `apply_replacements(game, event)` — free function wrapper (no agent, auto-selects first)
256pub struct ReplacementHandler {
257    /// Tracks (card_id, layer, effect_index) tuples that have already been applied
258    /// during this handler invocation, to prevent infinite re-application.
259    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    /// Apply all applicable replacement effects to a game event.
276    ///
277    /// Processes effects in CR 616 layer order:
278    /// CantHappen → Control → Copy → Transform → Other.
279    ///
280    /// When `agents` is `Some` and multiple effects match in the same layer,
281    /// the affected player's agent is asked to choose via
282    /// `choose_single_replacement_effect()`.
283    ///
284    /// Mirrors Java `ReplacementHandler.run(ReplacementType, Map<AbilityKey,Object>)`.
285    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                    // Java preserves Updated unless a later replacement fully
310                    // replaces the event during the re-run.
311                    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    /// Apply one CR 616 layer of replacement effects.
325    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
398/// Determine which player is "affected" by the event (for choosing among effects).
399fn 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
444// ── Public free-function API (backward compatibility) ─────────────────────────
445
446/// Apply all applicable replacement effects to a game event.
447///
448/// Free function wrapper — auto-selects first effect (no agent prompt).
449/// Used by callers that don't have access to agents (e.g. `action.rs`).
450///
451/// Mirrors Java `ReplacementHandler.run(ReplacementType, Map<AbilityKey,Object>)`.
452pub 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
457/// Apply Moved replacement effects (Rest in Peace, Leyline of the Void) for a card
458/// being moved to the Graveyard. Uses agents for proper RNG consumption when
459/// multiple effects apply. Returns the final destination zone.
460///
461/// If `dest` is not `Graveyard`, returns `dest` unchanged (no replacement needed).
462pub 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
719/// Handle `DB$ ReplaceToken` SVars.
720///
721/// Mirrors Java `ReplaceTokenEffect.resolve()`.
722/// - `Type$ Amount`: multiplies token count using `Amount$` param (default `Twice`).
723fn 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            // Get current count from event
734            let current = replacement_event_amount(event)?;
735            // Amount$ param, defaults to "Twice" per Java
736            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            // Other types (AddToken, ReplaceToken, ReplaceController) not yet needed
743            None
744        }
745    }
746}
747
748/// Handle `DB$ ReplaceCounter` SVars.
749///
750/// Mirrors Java `ReplaceCounterEffect.resolve()`.
751/// Applies `Amount$` expression to the counter count.
752fn execute_replace_counter_chain(
753    amount_expr: &str,
754    event: &mut ReplacementEvent,
755    game: &GameState,
756    source_card_id: CardId,
757) -> Option<ReplacementResult> {
758    // Ensure event has a valid amount before resolving
759    let _current = replacement_event_amount(event)?;
760    let value = resolve_replace_value(amount_expr, game, source_card_id, event)?;
761    if value <= 0 {
762        // Java removes the counter entry when value <= 0
763        set_replacement_event_amount(event, 0);
764    } else {
765        set_replacement_event_amount(event, value);
766    }
767    Some(ReplacementResult::Updated)
768}
769
770/// Simple math evaluation mirroring Java `AbilityUtils.doXMath()`.
771///
772/// Handles common expressions: "Twice", "Thrice", "Half", integer literals,
773/// and `Plus.N` / `Minus.N` style ops.
774fn 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            // Try resolving as an SVar or ReplaceCount$ expression
787            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
820// ── Scan-parity helper functions ──────────────────────────────────────────────
821
822/// Check if any `CantHappen`-layer replacement effect would prevent this event.
823///
824/// Walks all battlefield cards' replacement effects, filtering by the
825/// `CantHappen` layer. Returns `true` if at least one effect matches.
826///
827/// Mirrors Java `ReplacementHandler.cantHappenCheck()`.
828pub fn cant_happen_check(game: &GameState, event: &ReplacementEvent) -> bool {
829    let effects = collect_effects(game, event, ReplacementLayer::CantHappen);
830    !effects.is_empty()
831}
832
833/// Apply replacement effects specifically for damage events.
834///
835/// Delegates to `apply_replacements`. This is a convenience entry point
836/// matching Java's batch-damage processing path in `ReplacementHandler`.
837///
838/// Mirrors Java `ReplacementHandler.runReplaceDamage()`.
839pub fn run_replace_damage(game: &mut GameState, event: &mut ReplacementEvent) -> ReplacementResult {
840    apply_replacements(game, event)
841}
842
843/// Parse a raw `R$` replacement-effect line. Re-export of
844/// `replacement_effect::parse_replacement_effect` for scan parity.
845///
846/// Mirrors Java `ReplacementHandler.parseReplacement()`.
847pub fn parse_replacement(raw: &str) -> Option<ReplacementEffect> {
848    super::replacement_effect::parse_replacement_effect(raw)
849}
850
851/// Check if any `BeginPhase` replacement effect in the `Control` layer would
852/// skip the given phase for the given player.
853///
854/// Walks all battlefield cards looking for matching `BeginPhase` effects
855/// with `Layer$ Control` (or `CantHappen`) whose `ActivePhases$` includes
856/// the target phase and whose `ValidPlayer$` matches the player.
857///
858/// Mirrors Java `ReplacementHandler.wouldPhaseBeSkipped()`.
859pub 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            // Must be Control or CantHappen layer to skip a phase
869            if re.layer != ReplacementLayer::Control && re.layer != ReplacementLayer::CantHappen {
870                continue;
871            }
872            // Check ActivePhases$ matches the target phase
873            if !re.matches_phase(phase) {
874                continue;
875            }
876            // Check ValidPlayer$ matches the player
877            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
888/// Check if any `BeginTurn` replacement effect in the `Other` layer would
889/// skip an extra turn for the given player.
890///
891/// Mirrors Java `ReplacementHandler.wouldExtraTurnBeSkipped()`.
892pub 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            // Check ValidPlayer$ matches the player
905            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
916/// Walk all battlefield cards and collect `(card_id, effect_index)` pairs for
917/// replacement effects matching the given event type and optional layer.
918///
919/// This is the scan-parity equivalent of Java's `ReplacementHandler.visit()`.
920pub 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            // Layer filter
935            if let Some(target_layer) = layer {
936                if re.layer != target_layer {
937                    continue;
938                }
939            }
940            // Zone filter — effect must be active in the card's current zone
941            if !re.active_in_zone(card.zone) {
942                continue;
943            }
944            // Mode check — effect's event must match the requested event
945            if !re.mode_check(event) {
946                continue;
947            }
948            // has_run check (always false in our architecture)
949            if re.has_run() {
950                continue;
951            }
952            // Requirements check
953            if !re.requirements_check(game, card) {
954                continue;
955            }
956
957            result.push((card_id, current_idx));
958        }
959    }
960
961    result
962}
963
964// ── Internal helpers ──────────────────────────────────────────────────────────
965
966/// Collect all `(source_card_id, effect, effect_index)` triples that apply to `event` in `layer`.
967///
968/// Iterates over every card in the game and checks each of its replacement
969/// effects.  Only effects whose source card is in an active zone are included.
970///
971/// The `effect_index` is a global unique index used for `has_run` tracking.
972///
973/// Mirrors `ReplacementHandler.getReplacementList()`.
974fn 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        // Format/mechanic-specific replacements
984        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            // Layer filter.
1031            if re.layer != layer {
1032                continue;
1033            }
1034            // Zone filter — effect is only active when host is in a valid zone.
1035            if !re.active_in_zone(card.zone) {
1036                continue;
1037            }
1038            if !re.requirements_check(game, card) {
1039                continue;
1040            }
1041            // Dispatch to per-type module for applicability check.
1042            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                // Format/mechanic-specific replacements
1059                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
1131/// Execute a single replacement effect, mutating the event parameters.
1132///
1133/// Dispatches to the per-type module's `execute()` function.
1134///
1135/// Mirrors `ReplacementHandler.executeReplacement()`.
1136fn 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        // Format/mechanic-specific replacements
1147        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        // Format/mechanic-specific replacements
1222        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    // `description(host, game)` already handles CARDNAME/NICKNAME + text changes.
1291    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// ── Tests ─────────────────────────────────────────────────────────────────────
1304
1305#[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    // ── Test helpers ──────────────────────────────────────────────────────
1315
1316    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), // placeholder; create_card assigns real ID
1328            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    // ── Draw replacement tests ────────────────────────────────────────────
1446
1447    #[test]
1448    fn draw_skip_for_controller() {
1449        let mut game = make_game();
1450        let alice = PlayerId(0);
1451        // Card with "skip your draw step" effect.
1452        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        // Bob's draw is not affected by Alice's effect.
1483        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        // Card stays in hand, not battlefield.
1506        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    // ── Damage prevention tests ───────────────────────────────────────────
1518
1519    #[test]
1520    fn damage_to_player_prevented() {
1521        let mut game = make_game();
1522        let alice = PlayerId(0);
1523        // "Prevent all damage dealt to you."
1524        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        // Amount should be zeroed out.
1541        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        // Zero damage — effect should not apply.
1561        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    // ── Destroy replacement tests ─────────────────────────────────────────
1793
1794    #[test]
1795    fn indestructible_destroy_replaced() {
1796        let mut game = make_game();
1797        let alice = PlayerId(0);
1798        // Indestructible: "If ~ would be destroyed, instead it isn't."
1799        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        // Indestructible creature — protects only itself.
1817        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    // ── Moved replacement tests ───────────────────────────────────────────
1833
1834    #[test]
1835    fn moved_destination_updated_to_exile() {
1836        let mut game = make_game();
1837        let alice = PlayerId(0);
1838        // "If ~ would be put into a graveyard from the battlefield, exile it instead."
1839        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        // Card moving from Hand → Graveyard: Origin doesn't match Battlefield.
1875        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    // ── No effects test ───────────────────────────────────────────────────
1953
1954    #[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}