1use forge_foundation::ZoneType;
2
3use super::{
4 emit_zone_trigger, matches_change_type, resolve_defined_player, resolve_numeric_svar,
5 EffectContext,
6};
7use crate::agent::{notify_all_agents, GameLogEvent};
8use crate::card::card_zone_table::CardZoneTable;
9use crate::parsing::keys;
10
11#[manabrew_engine_macros::spell_effect(DigEffect)]
22fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
23 let dig_num = resolve_numeric_svar(ctx.game, sa, "DigNum", 1).max(0) as usize;
24 let optional = sa.ir.optional;
25 let skip_reorder = sa.ir.skip_reorder;
26 let rest_random_order = sa.ir.rest_random_order;
27 let change_all = sa
28 .ir
29 .change_num_text
30 .as_deref()
31 .map(|s| s.eq_ignore_ascii_case("All"))
32 .unwrap_or(false);
33 let any_number = sa
34 .ir
35 .change_num_text
36 .as_deref()
37 .map(|s| s.eq_ignore_ascii_case("Any"))
38 .unwrap_or(false);
39 let change_num = if change_all || any_number {
40 dig_num
41 } else {
42 resolve_numeric_svar(ctx.game, sa, keys::CHANGE_NUM, 1).max(0) as usize
43 };
44
45 let dest_zone1 = sa.destination_zone().unwrap_or(ZoneType::Hand);
46 let lib_position1: i32 = sa
47 .library_position()
48 .and_then(|s| s.parse().ok())
49 .unwrap_or(-1);
50 let dest_zone2 = sa.ir.destination_zone_2.unwrap_or(ZoneType::Library);
51
52 let lib_position2: i32 = sa
54 .library_position_2()
55 .and_then(|s| s.parse().ok())
56 .unwrap_or(-1);
57
58 let change_valid = sa
59 .ir
60 .change_valid
61 .as_deref()
62 .map(|s| s.to_string())
63 .unwrap_or_default();
64
65 let dig_player = sa
67 .target_chosen
68 .target_player
69 .or_else(|| {
70 sa.defined()
71 .and_then(|d| resolve_defined_player(d, sa.activating_player, ctx.game))
72 })
73 .unwrap_or(sa.activating_player);
74
75 let lib_len = ctx.game.cards_in_zone(ZoneType::Library, dig_player).len();
76 if lib_len == 0 {
77 return;
78 }
79
80 let count = dig_num.min(lib_len);
81
82 let mut top_n = ctx
84 .game
85 .take_top_cards_from_zone(ZoneType::Library, dig_player, count);
86 top_n.reverse();
90
91 let valid: Vec<_> = if change_valid.is_empty() {
93 top_n.clone()
94 } else {
95 top_n
96 .iter()
97 .copied()
98 .filter(|&id| matches_change_type(ctx.game.card(id), &change_valid, &[]))
99 .collect()
100 };
101
102 let may_be_skipped = sa.ir.prompt_to_skip_optional_ability;
105 if optional && may_be_skipped && !valid.is_empty() {
106 let source_name = sa.source.map(|cid| ctx.game.card(cid).card_name.clone());
107 let prompt = sa
108 .ir
109 .optional_ability_prompt
110 .as_deref()
111 .unwrap_or("Would you like to proceed with this optional ability?");
112 let accepted = ctx.agents[dig_player.index()].confirm_action(
113 dig_player,
114 None,
115 prompt,
116 &[],
117 sa.source,
118 Some(crate::ability::api_type::ApiType::Dig),
119 );
120 if !accepted {
121 top_n.reverse();
123 for card_id in top_n {
124 ctx.game
125 .add_card_to_zone(ZoneType::Library, dig_player, card_id);
126 }
127 return;
128 }
129 }
130
131 if sa.ir.reveal_true {
132 for &card_id in &top_n {
133 notify_all_agents(
134 ctx.agents,
135 GameLogEvent::rule("Reveal Library cards")
136 .with_player(dig_player)
137 .with_card(card_id),
138 );
139 }
140 }
141
142 let max_take = change_num.min(valid.len());
146 let chosen = if change_all {
147 valid.clone()
148 } else if valid.is_empty() {
149 Vec::new()
150 } else {
151 ctx.agents[sa.activating_player.index()].choose_dig(
152 ctx.game,
153 sa.activating_player,
154 &valid,
155 max_take,
156 optional || any_number,
157 )
158 };
159
160 let mut chosen: Vec<_> = chosen
161 .into_iter()
162 .filter(|id| valid.contains(id))
163 .take(max_take)
164 .collect();
165 chosen.reverse();
168
169 let mut rest: Vec<_> = top_n
170 .iter()
171 .copied()
172 .filter(|id| !chosen.contains(id))
173 .collect();
174
175 if rest_random_order && rest.len() > 1 {
186 for i in (1..rest.len()).rev() {
187 let j = ctx.rng.next_int((i + 1) as i32) as usize;
188 rest.swap(i, j);
189 }
190 } else if !skip_reorder
191 && rest.len() > 1
192 && (dest_zone2 == ZoneType::Library || dest_zone2 == ZoneType::Graveyard)
193 {
194 ctx.agents[sa.activating_player.index()].snapshot_state(ctx.game, ctx.mana_pools);
195 let reordered = ctx.agents[sa.activating_player.index()].choose_reorder_library(
196 ctx.game,
197 sa.activating_player,
198 &rest,
199 );
200 if reordered.len() == rest.len() && rest.iter().all(|id| reordered.contains(id)) {
201 rest = reordered;
202 }
203 }
204
205 let mut zone_movements = CardZoneTable::default();
206
207 for &id in &chosen {
209 let owner = ctx.game.card(id).owner;
210 let dest_owner = if dest_zone1 == ZoneType::Battlefield {
211 sa.activating_player
212 } else {
213 owner
214 };
215 ctx.move_card(id, dest_zone1, dest_owner);
216 zone_movements.put(Some(ZoneType::Library), Some(dest_zone1), id);
217 if dest_zone1 == ZoneType::Library {
218 match lib_position1 {
219 pos if pos < 0 => {
220 ctx.game
221 .reorder_card_in_zone(ZoneType::Library, dest_owner, id, 0)
222 }
223 0 => {}
224 pos => {
225 let len = ctx.game.cards_in_zone(ZoneType::Library, dest_owner).len();
226 let from_top = pos as usize;
227 let index = len.saturating_sub(from_top + 1);
228 ctx.game
229 .reorder_card_in_zone(ZoneType::Library, dest_owner, id, index);
230 }
231 }
232 }
233 if sa.param_is_true(keys::IMPRINT) {
234 if let Some(source_id) = sa.source {
235 ctx.game.card_mut(source_id).add_imprinted_card(id);
236 }
237 }
238 if sa.is_remember_changed() {
239 if let Some(source_id) = sa.source {
240 ctx.game.card_mut(source_id).add_remembered_card(id);
241 }
242 }
243 if dest_zone1 == ZoneType::Battlefield {
244 ctx.trigger_handler.register_active_trigger(ctx.game, id);
245 let _ = super::add_to_combat(ctx, sa, id, keys::ATTACKING);
246 }
247 emit_zone_trigger(ctx.trigger_handler, id, ZoneType::Library, dest_zone1);
248 }
249
250 for &id in &rest {
252 let owner = ctx.game.card(id).owner;
253 if dest_zone2 == ZoneType::Library {
254 if lib_position2 == 0 {
257 ctx.game.add_card_to_zone(ZoneType::Library, owner, id);
259 ctx.game.card_mut(id).set_zone(ZoneType::Library);
260 } else {
261 ctx.game
263 .add_card_to_zone_bottom(ZoneType::Library, owner, id);
264 ctx.game.card_mut(id).set_zone(ZoneType::Library);
265 }
266 zone_movements.put(Some(ZoneType::Library), Some(ZoneType::Library), id);
267 } else {
268 let dest_owner = if dest_zone2 == ZoneType::Battlefield {
269 sa.activating_player
270 } else {
271 owner
272 };
273 ctx.move_card(id, dest_zone2, dest_owner);
274 zone_movements.put(Some(ZoneType::Library), Some(dest_zone2), id);
275 if dest_zone2 == ZoneType::Battlefield {
276 ctx.trigger_handler.register_active_trigger(ctx.game, id);
277 }
278 emit_zone_trigger(ctx.trigger_handler, id, ZoneType::Library, dest_zone2);
279 }
280 }
281
282 if !zone_movements.all_cards().is_empty() {
283 zone_movements.trigger_changes_zone_all(ctx.trigger_handler, ctx.game, Some(sa));
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use crate::ability::spell_ability_effect::SpellAbilityEffect;
290 use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
291
292 use crate::ability::effects::EffectContext;
293 use crate::agent::{PassAgent, PlayerAgent};
294 use crate::card::Card;
295 use crate::combat::DefenderId;
296 use crate::game::GameState;
297 use crate::ids::{CardId, PlayerId};
298 use crate::mana::ManaPool;
299 use crate::spellability::SpellAbility;
300 use crate::trigger::handler::TriggerHandler;
301 use std::collections::HashMap;
302
303 fn make_land(game: &mut GameState, owner: PlayerId) -> CardId {
304 let c = Card::new(
305 CardId(0),
306 "Island".into(),
307 owner,
308 CardTypeLine::parse("Basic Land Island"),
309 ManaCost::parse(""),
310 ColorSet::COLORLESS,
311 None,
312 None,
313 vec![],
314 vec![],
315 );
316 game.create_card(c)
317 }
318
319 struct TakeFirstAgent;
321 impl PlayerAgent for TakeFirstAgent {
322 fn mulligan_decision(&mut self, _: PlayerId, _: &[CardId], _: u32) -> bool {
323 true
324 }
325 fn choose_action(
326 &mut self,
327 player: PlayerId,
328 action_space: Option<&crate::agent::PriorityActionSpace>,
329 request_action_space: &mut dyn FnMut() -> crate::agent::PriorityActionSpace,
330 ) -> crate::player::actions::PlayerAction {
331 crate::player::actions::PlayerAction::PassPriority
332 }
333 fn choose_attackers(
334 &mut self,
335 _: PlayerId,
336 _: &[CardId],
337 _: &[DefenderId],
338 ) -> Vec<(CardId, DefenderId)> {
339 vec![]
340 }
341 fn choose_blockers(
342 &mut self,
343 _: PlayerId,
344 _: &[CardId],
345 _: &[CardId],
346 _: Option<usize>,
347 ) -> Vec<(CardId, CardId)> {
348 vec![]
349 }
350 fn choose_target_player(
351 &mut self,
352 _: PlayerId,
353 v: &[PlayerId],
354 _sa: Option<&crate::spellability::SpellAbility>,
355 ) -> Option<PlayerId> {
356 v.first().copied()
357 }
358 fn choose_target_card(
359 &mut self,
360 _: PlayerId,
361 v: &[CardId],
362 _sa: Option<&crate::spellability::SpellAbility>,
363 ) -> Option<CardId> {
364 v.first().copied()
365 }
366 fn choose_target_any(
367 &mut self,
368 _: PlayerId,
369 vp: &[PlayerId],
370 vc: &[CardId],
371 _sa: Option<&crate::spellability::SpellAbility>,
372 ) -> crate::agent::TargetChoice {
373 vp.first()
374 .copied()
375 .map(crate::agent::TargetChoice::Player)
376 .or_else(|| vc.first().copied().map(crate::agent::TargetChoice::Card))
377 .unwrap_or(crate::agent::TargetChoice::None)
378 }
379 fn choose_land_or_spell(&mut self, _: PlayerId) -> Option<bool> {
380 None
381 }
382 fn choose_dig(
383 &mut self,
384 _game: &GameState,
385 _player: PlayerId,
386 cards: &[CardId],
387 max: usize,
388 _optional: bool,
389 ) -> Vec<CardId> {
390 cards.iter().copied().take(max).collect()
391 }
392 fn choose_targets_for(
393 &mut self,
394 _sa: &mut SpellAbility,
395 _game: &GameState,
396 _mana_pools: &[ManaPool],
397 ) -> bool {
398 false
399 }
400 }
401
402 #[test]
403 fn dig_moves_chosen_to_hand() {
404 let mut game = GameState::new(&["Alice", "Bob"], 20);
405 let p0 = PlayerId(0);
406
407 let a = make_land(&mut game, p0);
408 let b = make_land(&mut game, p0);
409 let c = make_land(&mut game, p0);
410 game.replace_zone_cards(ZoneType::Library, p0, vec![a, b, c]);
412
413 let sa = SpellAbility::new_simple(
415 None,
416 p0,
417 "SP$ Dig | DigNum$ 3 | ChangeNum$ 1 | DestinationZone2$ Graveyard | NoReveal$ True",
418 );
419 let mut trigger_handler = TriggerHandler::new();
420 let mut agents: Vec<Box<dyn PlayerAgent>> =
421 vec![Box::new(TakeFirstAgent), Box::new(PassAgent)];
422 let mut mana_pools = vec![ManaPool::default(), ManaPool::default()];
423 let token_templates = HashMap::new();
424 let templates_variants: HashMap<(String, String), usize> = HashMap::new();
425 let token_fallback: HashMap<String, String> = HashMap::new();
426 let edition_dates: HashMap<String, String> = HashMap::new();
427 let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
428 let mut ctx = EffectContext {
429 game: &mut game,
430 combat: None,
431 agents: &mut agents,
432 trigger_handler: &mut trigger_handler,
433 token_templates: &token_templates,
434 token_art_variants: &templates_variants,
435 token_fallback: &token_fallback,
436 edition_dates: &edition_dates,
437 mana_pools: &mut mana_pools,
438 parent_target_card: None,
439 rng: &mut rng_adapter,
440 };
441
442 super::DigEffect::resolve(&mut ctx, &sa);
443
444 assert_eq!(ctx.game.cards_in_zone(ZoneType::Hand, p0).len(), 1);
446 assert_eq!(ctx.game.cards_in_zone(ZoneType::Graveyard, p0).len(), 2);
447 assert_eq!(ctx.game.cards_in_zone(ZoneType::Library, p0).len(), 0);
448 }
449}