Skip to main content

manabrew_engine/ability/effects/
mill_effect.rs

1use forge_foundation::ZoneType;
2
3use super::{emit_zone_trigger, resolve_defined_player, resolve_numeric_svar, EffectContext};
4use crate::ability::ability_ir::EffectIr;
5use crate::event::RunParams;
6use crate::parsing::keys;
7use crate::replacement::replacement_handler::{apply_replacements, ReplacementEvent};
8use crate::replacement::ReplacementResult;
9use crate::spellability::SpellAbility;
10use crate::trigger::TriggerType;
11
12/// Mirrors Java's `MillEffect.java`.
13///
14/// `SP$ Mill | NumCards$ N | Defined$ You`
15/// Moves the top N cards of the target player's library to their graveyard.
16/// Struct form of this effect so it can participate in the
17/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
18/// `MillEffect` class extending `SpellAbilityEffect`.
19#[manabrew_engine_macros::spell_effect(MillEffect)]
20fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
21    let num = resolve_mill_amount(ctx, sa).max(0) as usize;
22
23    let millers: Vec<crate::ids::PlayerId> = if let Some(tp) = sa.target_chosen.target_player {
24        vec![tp]
25    } else if let Some(d) = sa.defined() {
26        let players = crate::ability::ability_utils::resolve_defined_players_with_sa(
27            d,
28            sa,
29            sa.activating_player,
30            ctx.game,
31        );
32        if players.is_empty() {
33            resolve_defined_player(d, sa.activating_player, ctx.game)
34                .map(|pid| vec![pid])
35                .unwrap_or_else(|| vec![sa.activating_player])
36        } else {
37            players
38        }
39    } else {
40        vec![sa.activating_player]
41    };
42
43    let mut all_milled: Vec<crate::ids::CardId> = Vec::new();
44    for target in millers {
45        // Run Mill replacement effects before milling.
46        let mut event = ReplacementEvent::Mill {
47            player: target,
48            count: num as i32,
49        };
50        let result = apply_replacements(ctx.game, &mut event);
51        if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
52            continue;
53        }
54        let num = if let ReplacementEvent::Mill { count, .. } = event {
55            count.max(0) as usize
56        } else {
57            num
58        };
59
60        if num == 0 {
61            continue;
62        }
63
64        let lib = ctx.game.cards_in_zone(ZoneType::Library, target);
65        let mut milled_cards: Vec<crate::ids::CardId> =
66            lib.iter().rev().take(num).copied().collect();
67        if milled_cards.len() > 1 {
68            ctx.agents[target.index()].snapshot_state(ctx.game, ctx.mana_pools);
69            let reordered =
70                ctx.agents[target.index()].choose_reorder_library(ctx.game, target, &milled_cards);
71            if reordered.len() == milled_cards.len()
72                && milled_cards.iter().all(|id| reordered.contains(id))
73            {
74                milled_cards = reordered;
75            }
76        }
77
78        for &card_id in &milled_cards {
79            ctx.move_card(card_id, ZoneType::Graveyard, target);
80            emit_zone_trigger(
81                ctx.trigger_handler,
82                card_id,
83                ZoneType::Library,
84                ZoneType::Graveyard,
85            );
86            // Fire Milled trigger per card
87            ctx.trigger_handler.run_trigger(
88                TriggerType::Milled,
89                RunParams {
90                    card: Some(card_id),
91                    player: Some(target),
92                    ..Default::default()
93                },
94                false,
95            );
96        }
97
98        if !milled_cards.is_empty() {
99            ctx.trigger_handler.run_trigger(
100                TriggerType::MilledOnce,
101                RunParams {
102                    player: Some(target),
103                    cards: Some(milled_cards.clone()),
104                    ..Default::default()
105                },
106                false,
107            );
108        }
109        all_milled.extend(milled_cards);
110    }
111
112    if sa.ir.remember_milled {
113        if let Some(source_id) = sa.source {
114            ctx.game
115                .card_mut(source_id)
116                .add_remembered_cards(all_milled.iter().copied());
117        }
118    }
119    if sa.ir.imprint {
120        if let Some(source_id) = sa.source {
121            ctx.game
122                .card_mut(source_id)
123                .add_imprinted_cards(all_milled.iter().copied());
124        }
125    }
126
127    if !all_milled.is_empty() {
128        ctx.trigger_handler.run_trigger(
129            TriggerType::MilledAll,
130            RunParams {
131                cards: Some(all_milled),
132                ..Default::default()
133            },
134            false,
135        );
136    }
137}
138
139fn resolve_mill_amount(ctx: &EffectContext, sa: &SpellAbility) -> i32 {
140    if let Some(EffectIr::Mill(ir)) = &sa.ir.effect {
141        if let Some(amount) = &ir.amount {
142            let resolved = amount.resolve_for_spell_ability(ctx.game, sa, 1);
143            #[cfg(debug_assertions)]
144            debug_assert_eq!(
145                resolved,
146                resolve_numeric_svar(ctx.game, sa, keys::NUM_CARDS, 1),
147                "compiled Mill amount diverged from string params"
148            );
149            return resolved;
150        }
151    }
152
153    resolve_numeric_svar(ctx.game, sa, keys::NUM_CARDS, 1)
154}
155
156#[cfg(test)]
157mod tests {
158    use crate::ability::spell_ability_effect::SpellAbilityEffect;
159    use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
160
161    use crate::ability::effects::EffectContext;
162    use crate::agent::PassAgent;
163    use crate::card::Card;
164    use crate::game::GameState;
165    use crate::ids::{CardId, PlayerId};
166    use crate::mana::ManaPool;
167    use crate::spellability::SpellAbility;
168    use crate::trigger::handler::TriggerHandler;
169    use std::collections::HashMap;
170
171    fn make_land(game: &mut GameState, owner: PlayerId) -> CardId {
172        let c = Card::new(
173            CardId(0),
174            "Island".into(),
175            owner,
176            CardTypeLine::parse("Basic Land Island"),
177            ManaCost::parse(""),
178            ColorSet::COLORLESS,
179            None,
180            None,
181            vec![],
182            vec![],
183        );
184        game.create_card(c)
185    }
186
187    #[test]
188    fn mill_moves_cards_to_graveyard() {
189        let mut game = GameState::new(&["Alice", "Bob"], 20);
190        let p0 = PlayerId(0);
191        for _ in 0..3 {
192            let id = make_land(&mut game, p0);
193            game.move_card(id, ZoneType::Library, p0);
194        }
195        assert_eq!(game.cards_in_zone(ZoneType::Library, p0).len(), 3);
196
197        let sa = SpellAbility::new_simple(None, p0, "SP$ Mill | NumCards$ 2 | Defined$ You");
198        let mut trigger_handler = TriggerHandler::new();
199        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
200            vec![Box::new(PassAgent), Box::new(PassAgent)];
201        let mut mana_pools = vec![ManaPool::default(), ManaPool::default()];
202        let token_templates = HashMap::new();
203        let templates_variants: HashMap<(String, String), usize> = HashMap::new();
204        let token_fallback: HashMap<String, String> = HashMap::new();
205        let edition_dates: HashMap<String, String> = HashMap::new();
206        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
207        let mut ctx = EffectContext {
208            game: &mut game,
209            combat: None,
210            agents: &mut agents,
211            trigger_handler: &mut trigger_handler,
212            token_templates: &token_templates,
213            token_art_variants: &templates_variants,
214            token_fallback: &token_fallback,
215            edition_dates: &edition_dates,
216            mana_pools: &mut mana_pools,
217            parent_target_card: None,
218            rng: &mut rng_adapter,
219        };
220
221        super::MillEffect::resolve(&mut ctx, &sa);
222
223        assert_eq!(ctx.game.cards_in_zone(ZoneType::Library, p0).len(), 1);
224        assert_eq!(ctx.game.cards_in_zone(ZoneType::Graveyard, p0).len(), 2);
225    }
226
227    #[test]
228    fn mill_does_not_exceed_library_size() {
229        let mut game = GameState::new(&["Alice", "Bob"], 20);
230        let p0 = PlayerId(0);
231        let id = make_land(&mut game, p0);
232        game.move_card(id, ZoneType::Library, p0);
233
234        let sa = SpellAbility::new_simple(None, p0, "SP$ Mill | NumCards$ 5 | Defined$ You");
235        let mut trigger_handler = TriggerHandler::new();
236        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
237            vec![Box::new(PassAgent), Box::new(PassAgent)];
238        let mut mana_pools = vec![ManaPool::default(), ManaPool::default()];
239        let token_templates = HashMap::new();
240        let templates_variants: HashMap<(String, String), usize> = HashMap::new();
241        let token_fallback: HashMap<String, String> = HashMap::new();
242        let edition_dates: HashMap<String, String> = HashMap::new();
243        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
244        let mut ctx = EffectContext {
245            game: &mut game,
246            combat: None,
247            agents: &mut agents,
248            trigger_handler: &mut trigger_handler,
249            token_templates: &token_templates,
250            token_art_variants: &templates_variants,
251            token_fallback: &token_fallback,
252            edition_dates: &edition_dates,
253            mana_pools: &mut mana_pools,
254            parent_target_card: None,
255            rng: &mut rng_adapter,
256        };
257
258        super::MillEffect::resolve(&mut ctx, &sa);
259
260        assert_eq!(ctx.game.cards_in_zone(ZoneType::Library, p0).len(), 0);
261        assert_eq!(ctx.game.cards_in_zone(ZoneType::Graveyard, p0).len(), 1);
262    }
263}