manabrew_engine/ability/effects/
mill_effect.rs1use 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#[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 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 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}