manabrew_engine/ability/effects/
dig_multiple_effect.rs1use forge_foundation::ZoneType;
2
3use super::{
4 emit_zone_trigger, matches_change_type, resolve_defined_player, resolve_numeric_svar,
5 EffectContext,
6};
7use crate::parsing::keys;
8
9#[manabrew_engine_macros::spell_effect(DigMultipleEffect)]
20fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
21 let dig_num = resolve_numeric_svar(ctx.game, sa, "DigNum", 1).max(0) as usize;
22 let optional = sa.ir.optional;
23 let change_all = sa
24 .ir
25 .change_num_text
26 .as_deref()
27 .map(|s| s.eq_ignore_ascii_case("All"))
28 .unwrap_or(false);
29 let any_number = sa
30 .ir
31 .change_num_text
32 .as_deref()
33 .map(|s| s.eq_ignore_ascii_case("Any"))
34 .unwrap_or(false);
35 let change_num = if change_all || any_number {
36 dig_num
37 } else {
38 resolve_numeric_svar(ctx.game, sa, keys::CHANGE_NUM, 1).max(0) as usize
39 };
40
41 let dest_zone1 = sa.destination_zone().unwrap_or(ZoneType::Hand);
42 let lib_position1: i32 = sa
43 .library_position()
44 .and_then(|s| s.parse().ok())
45 .unwrap_or(-1);
46 let dest_zone2 = sa.ir.destination_zone_2.unwrap_or(ZoneType::Library);
47
48 let lib_position2: i32 = sa
50 .library_position_2()
51 .and_then(|s| s.parse().ok())
52 .unwrap_or(-1);
53
54 let change_valid = sa
55 .ir
56 .change_valid
57 .as_deref()
58 .map(|s| s.to_string())
59 .unwrap_or_default();
60
61 let dig_player = sa
63 .target_chosen
64 .target_player
65 .or_else(|| {
66 sa.defined()
67 .and_then(|d| resolve_defined_player(d, sa.activating_player, ctx.game))
68 })
69 .unwrap_or(sa.activating_player);
70
71 let lib_len = ctx.game.cards_in_zone(ZoneType::Library, dig_player).len();
72 if lib_len == 0 {
73 return;
74 }
75
76 let count = dig_num.min(lib_len);
77
78 let mut top_n = ctx
80 .game
81 .take_top_cards_from_zone(ZoneType::Library, dig_player, count);
82 top_n.reverse();
86
87 let valid: Vec<_> = if change_valid.is_empty() {
89 top_n.clone()
90 } else {
91 top_n
92 .iter()
93 .copied()
94 .filter(|&id| matches_change_type(ctx.game.card(id), &change_valid, &[]))
95 .collect()
96 };
97
98 let may_be_skipped = sa.ir.prompt_to_skip_optional_ability;
101 if optional && may_be_skipped && !valid.is_empty() {
102 let source_name = sa.source.map(|cid| ctx.game.card(cid).card_name.clone());
103 let prompt = sa
104 .ir
105 .optional_ability_prompt
106 .as_deref()
107 .unwrap_or("Would you like to proceed with this optional ability?");
108 let accepted = ctx.agents[dig_player.index()].confirm_action(
109 dig_player,
110 None,
111 prompt,
112 &[],
113 sa.source,
114 Some(crate::ability::api_type::ApiType::Dig),
115 );
116 if !accepted {
117 top_n.reverse();
119 for card_id in top_n {
120 ctx.game
121 .add_card_to_zone(ZoneType::Library, dig_player, card_id);
122 }
123 return;
124 }
125 }
126
127 let max_take = change_num.min(valid.len());
129 let chosen = ctx.agents[sa.activating_player.index()].choose_dig(
130 ctx.game,
131 sa.activating_player,
132 &valid,
133 max_take,
134 optional || any_number,
135 );
136
137 let chosen: Vec<_> = chosen
138 .into_iter()
139 .filter(|id| valid.contains(id))
140 .take(max_take)
141 .collect();
142
143 let rest: Vec<_> = top_n
144 .iter()
145 .copied()
146 .filter(|id| !chosen.contains(id))
147 .collect();
148
149 for &id in &chosen {
151 let owner = ctx.game.card(id).owner;
152 let dest_owner = if dest_zone1 == ZoneType::Battlefield {
153 sa.activating_player
154 } else {
155 owner
156 };
157 ctx.move_card(id, dest_zone1, dest_owner);
158 if dest_zone1 == ZoneType::Library {
159 match lib_position1 {
160 pos if pos < 0 => {
161 ctx.game
162 .reorder_card_in_zone(ZoneType::Library, dest_owner, id, 0)
163 }
164 0 => {}
165 pos => {
166 let len = ctx.game.cards_in_zone(ZoneType::Library, dest_owner).len();
167 let from_top = pos as usize;
168 let index = len.saturating_sub(from_top + 1);
169 ctx.game
170 .reorder_card_in_zone(ZoneType::Library, dest_owner, id, index);
171 }
172 }
173 }
174 emit_zone_trigger(ctx.trigger_handler, id, ZoneType::Library, dest_zone1);
175 }
176
177 for &id in &rest {
179 let owner = ctx.game.card(id).owner;
180 if dest_zone2 == ZoneType::Library {
181 if lib_position2 == 0 {
184 ctx.game.add_card_to_zone(ZoneType::Library, owner, id);
186 ctx.game.card_mut(id).set_zone(ZoneType::Library);
187 } else {
188 ctx.game
190 .add_card_to_zone_bottom(ZoneType::Library, owner, id);
191 ctx.game.card_mut(id).set_zone(ZoneType::Library);
192 }
193 } else {
194 let dest_owner = if dest_zone2 == ZoneType::Battlefield {
195 sa.activating_player
196 } else {
197 owner
198 };
199 ctx.move_card(id, dest_zone2, dest_owner);
200 emit_zone_trigger(ctx.trigger_handler, id, ZoneType::Library, dest_zone2);
201 }
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use crate::ability::spell_ability_effect::SpellAbilityEffect;
208 use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
209
210 use crate::ability::effects::EffectContext;
211 use crate::agent::{PassAgent, PlayerAgent};
212 use crate::card::Card;
213 use crate::combat::DefenderId;
214 use crate::game::GameState;
215 use crate::ids::{CardId, PlayerId};
216 use crate::mana::ManaPool;
217 use crate::spellability::SpellAbility;
218 use crate::trigger::handler::TriggerHandler;
219 use std::collections::HashMap;
220
221 fn make_land(game: &mut GameState, owner: PlayerId) -> CardId {
222 let c = Card::new(
223 CardId(0),
224 "Island".into(),
225 owner,
226 CardTypeLine::parse("Basic Land Island"),
227 ManaCost::parse(""),
228 ColorSet::COLORLESS,
229 None,
230 None,
231 vec![],
232 vec![],
233 );
234 game.create_card(c)
235 }
236
237 struct TakeFirstAgent;
239 impl PlayerAgent for TakeFirstAgent {
240 fn mulligan_decision(&mut self, _: PlayerId, _: &[CardId], _: u32) -> bool {
241 true
242 }
243 fn choose_action(
244 &mut self,
245 player: PlayerId,
246 action_space: Option<&crate::agent::PriorityActionSpace>,
247 request_action_space: &mut dyn FnMut() -> crate::agent::PriorityActionSpace,
248 ) -> crate::player::actions::PlayerAction {
249 crate::player::actions::PlayerAction::PassPriority
250 }
251 fn choose_attackers(
252 &mut self,
253 _: PlayerId,
254 _: &[CardId],
255 _: &[DefenderId],
256 ) -> Vec<(CardId, DefenderId)> {
257 vec![]
258 }
259 fn choose_blockers(
260 &mut self,
261 _: PlayerId,
262 _: &[CardId],
263 _: &[CardId],
264 _: Option<usize>,
265 ) -> Vec<(CardId, CardId)> {
266 vec![]
267 }
268 fn choose_target_player(
269 &mut self,
270 _: PlayerId,
271 v: &[PlayerId],
272 _sa: Option<&crate::spellability::SpellAbility>,
273 ) -> Option<PlayerId> {
274 v.first().copied()
275 }
276 fn choose_target_card(
277 &mut self,
278 _: PlayerId,
279 v: &[CardId],
280 _sa: Option<&crate::spellability::SpellAbility>,
281 ) -> Option<CardId> {
282 v.first().copied()
283 }
284 fn choose_target_any(
285 &mut self,
286 _: PlayerId,
287 vp: &[PlayerId],
288 vc: &[CardId],
289 _sa: Option<&crate::spellability::SpellAbility>,
290 ) -> crate::agent::TargetChoice {
291 vp.first()
292 .copied()
293 .map(crate::agent::TargetChoice::Player)
294 .or_else(|| vc.first().copied().map(crate::agent::TargetChoice::Card))
295 .unwrap_or(crate::agent::TargetChoice::None)
296 }
297 fn choose_land_or_spell(&mut self, _: PlayerId) -> Option<bool> {
298 None
299 }
300 fn choose_dig(
301 &mut self,
302 _game: &GameState,
303 _player: PlayerId,
304 cards: &[CardId],
305 max: usize,
306 _optional: bool,
307 ) -> Vec<CardId> {
308 cards.iter().copied().take(max).collect()
309 }
310 fn choose_targets_for(
311 &mut self,
312 _sa: &mut SpellAbility,
313 _game: &GameState,
314 _mana_pools: &[ManaPool],
315 ) -> bool {
316 false
317 }
318 }
319
320 #[test]
321 fn dig_moves_chosen_to_hand() {
322 let mut game = GameState::new(&["Alice", "Bob"], 20);
323 let p0 = PlayerId(0);
324
325 let a = make_land(&mut game, p0);
326 let b = make_land(&mut game, p0);
327 let c = make_land(&mut game, p0);
328 game.replace_zone_cards(ZoneType::Library, p0, vec![a, b, c]);
330
331 let sa = SpellAbility::new_simple(
333 None,
334 p0,
335 "SP$ Dig | DigNum$ 3 | ChangeNum$ 1 | DestinationZone2$ Graveyard | NoReveal$ True",
336 );
337 let mut trigger_handler = TriggerHandler::new();
338 let mut agents: Vec<Box<dyn PlayerAgent>> =
339 vec![Box::new(TakeFirstAgent), Box::new(PassAgent)];
340 let mut mana_pools = vec![ManaPool::default(), ManaPool::default()];
341 let token_templates = HashMap::new();
342 let templates_variants: HashMap<(String, String), usize> = HashMap::new();
343 let token_fallback: HashMap<String, String> = HashMap::new();
344 let edition_dates: HashMap<String, String> = HashMap::new();
345 let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
346 let mut ctx = EffectContext {
347 game: &mut game,
348 combat: None,
349 agents: &mut agents,
350 trigger_handler: &mut trigger_handler,
351 token_templates: &token_templates,
352 token_art_variants: &templates_variants,
353 token_fallback: &token_fallback,
354 edition_dates: &edition_dates,
355 mana_pools: &mut mana_pools,
356 parent_target_card: None,
357 rng: &mut rng_adapter,
358 };
359
360 super::DigMultipleEffect::resolve(&mut ctx, &sa);
361
362 assert_eq!(ctx.game.cards_in_zone(ZoneType::Hand, p0).len(), 1);
364 assert_eq!(ctx.game.cards_in_zone(ZoneType::Graveyard, p0).len(), 2);
365 assert_eq!(ctx.game.cards_in_zone(ZoneType::Library, p0).len(), 0);
366 }
367}