use forge_foundation::ZoneType;
use super::{
emit_zone_trigger, matches_change_type, resolve_defined_player, resolve_numeric_svar,
EffectContext,
};
use crate::parsing::keys;
#[manabrew_engine_macros::spell_effect(DigMultipleEffect)]
fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
let dig_num = resolve_numeric_svar(ctx.game, sa, "DigNum", 1).max(0) as usize;
let optional = sa.ir.optional;
let change_all = sa
.ir
.change_num_text
.as_deref()
.map(|s| s.eq_ignore_ascii_case("All"))
.unwrap_or(false);
let any_number = sa
.ir
.change_num_text
.as_deref()
.map(|s| s.eq_ignore_ascii_case("Any"))
.unwrap_or(false);
let change_num = if change_all || any_number {
dig_num
} else {
resolve_numeric_svar(ctx.game, sa, keys::CHANGE_NUM, 1).max(0) as usize
};
let dest_zone1 = sa.destination_zone().unwrap_or(ZoneType::Hand);
let lib_position1: i32 = sa
.library_position()
.and_then(|s| s.parse().ok())
.unwrap_or(-1);
let dest_zone2 = sa.ir.destination_zone_2.unwrap_or(ZoneType::Library);
let lib_position2: i32 = sa
.library_position_2()
.and_then(|s| s.parse().ok())
.unwrap_or(-1);
let change_valid = sa
.ir
.change_valid
.as_deref()
.map(|s| s.to_string())
.unwrap_or_default();
let dig_player = sa
.target_chosen
.target_player
.or_else(|| {
sa.defined()
.and_then(|d| resolve_defined_player(d, sa.activating_player, ctx.game))
})
.unwrap_or(sa.activating_player);
let lib_len = ctx.game.cards_in_zone(ZoneType::Library, dig_player).len();
if lib_len == 0 {
return;
}
let count = dig_num.min(lib_len);
let mut top_n = ctx
.game
.take_top_cards_from_zone(ZoneType::Library, dig_player, count);
top_n.reverse();
let valid: Vec<_> = if change_valid.is_empty() {
top_n.clone()
} else {
top_n
.iter()
.copied()
.filter(|&id| matches_change_type(ctx.game.card(id), &change_valid, &[]))
.collect()
};
let may_be_skipped = sa.ir.prompt_to_skip_optional_ability;
if optional && may_be_skipped && !valid.is_empty() {
let _source_name = sa.source.map(|cid| ctx.game.card(cid).card_name.clone());
let prompt = sa
.ir
.optional_ability_prompt
.as_deref()
.unwrap_or("Would you like to proceed with this optional ability?");
let accepted = ctx.agents[dig_player.index()].confirm_action(
dig_player,
None,
prompt,
&[],
sa.source,
Some(crate::ability::api_type::ApiType::Dig),
);
if !accepted {
top_n.reverse();
for card_id in top_n {
ctx.game
.add_card_to_zone(ZoneType::Library, dig_player, card_id);
}
return;
}
}
let max_take = change_num.min(valid.len());
let chosen = ctx.agents[sa.activating_player.index()].choose_dig(
ctx.game,
sa.activating_player,
&valid,
max_take,
optional || any_number,
);
let chosen: Vec<_> = chosen
.into_iter()
.filter(|id| valid.contains(id))
.take(max_take)
.collect();
let rest: Vec<_> = top_n
.iter()
.copied()
.filter(|id| !chosen.contains(id))
.collect();
for &id in &chosen {
let owner = ctx.game.card(id).owner;
let dest_owner = if dest_zone1 == ZoneType::Battlefield {
sa.activating_player
} else {
owner
};
ctx.move_card(id, dest_zone1, dest_owner);
if dest_zone1 == ZoneType::Library {
match lib_position1 {
pos if pos < 0 => {
ctx.game
.reorder_card_in_zone(ZoneType::Library, dest_owner, id, 0)
}
0 => {}
pos => {
let len = ctx.game.cards_in_zone(ZoneType::Library, dest_owner).len();
let from_top = pos as usize;
let index = len.saturating_sub(from_top + 1);
ctx.game
.reorder_card_in_zone(ZoneType::Library, dest_owner, id, index);
}
}
}
emit_zone_trigger(ctx.trigger_handler, id, ZoneType::Library, dest_zone1);
}
for &id in &rest {
let owner = ctx.game.card(id).owner;
if dest_zone2 == ZoneType::Library {
if lib_position2 == 0 {
ctx.game.add_card_to_zone(ZoneType::Library, owner, id);
ctx.game.card_mut(id).set_zone(ZoneType::Library);
} else {
ctx.game
.add_card_to_zone_bottom(ZoneType::Library, owner, id);
ctx.game.card_mut(id).set_zone(ZoneType::Library);
}
} else {
let dest_owner = if dest_zone2 == ZoneType::Battlefield {
sa.activating_player
} else {
owner
};
ctx.move_card(id, dest_zone2, dest_owner);
emit_zone_trigger(ctx.trigger_handler, id, ZoneType::Library, dest_zone2);
}
}
}
#[cfg(test)]
mod tests {
use crate::ability::spell_ability_effect::SpellAbilityEffect;
use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
use crate::ability::effects::EffectContext;
use crate::agent::{PassAgent, PlayerAgent};
use crate::card::Card;
use crate::combat::DefenderId;
use crate::game::GameState;
use crate::ids::{CardId, PlayerId};
use crate::mana::ManaPool;
use crate::spellability::SpellAbility;
use crate::trigger::handler::TriggerHandler;
use std::collections::HashMap;
fn make_land(game: &mut GameState, owner: PlayerId) -> CardId {
let c = Card::new(
CardId(0),
"Island".into(),
owner,
CardTypeLine::parse("Basic Land Island"),
ManaCost::parse(""),
ColorSet::COLORLESS,
None,
None,
vec![],
vec![],
);
game.create_card(c)
}
struct TakeFirstAgent;
impl PlayerAgent for TakeFirstAgent {
fn mulligan_decision(&mut self, _: PlayerId, _: &[CardId], _: u32) -> bool {
true
}
fn choose_action(
&mut self,
player: PlayerId,
action_space: Option<&crate::agent::PriorityActionSpace>,
request_action_space: &mut dyn FnMut() -> crate::agent::PriorityActionSpace,
) -> crate::player::actions::PlayerAction {
crate::player::actions::PlayerAction::PassPriority
}
fn choose_attackers(
&mut self,
_: PlayerId,
_: &[CardId],
_: &[DefenderId],
) -> Vec<(CardId, DefenderId)> {
vec![]
}
fn choose_blockers(
&mut self,
_: PlayerId,
_: &[CardId],
_: &[CardId],
_: Option<usize>,
) -> Vec<(CardId, CardId)> {
vec![]
}
fn choose_target_player(
&mut self,
_: PlayerId,
v: &[PlayerId],
_sa: Option<&crate::spellability::SpellAbility>,
) -> Option<PlayerId> {
v.first().copied()
}
fn choose_target_card(
&mut self,
_: PlayerId,
v: &[CardId],
_sa: Option<&crate::spellability::SpellAbility>,
) -> Option<CardId> {
v.first().copied()
}
fn choose_target_any(
&mut self,
_: PlayerId,
vp: &[PlayerId],
vc: &[CardId],
_sa: Option<&crate::spellability::SpellAbility>,
) -> crate::agent::TargetChoice {
vp.first()
.copied()
.map(crate::agent::TargetChoice::Player)
.or_else(|| vc.first().copied().map(crate::agent::TargetChoice::Card))
.unwrap_or(crate::agent::TargetChoice::None)
}
fn choose_land_or_spell(&mut self, _: PlayerId) -> Option<bool> {
None
}
fn choose_dig(
&mut self,
_game: &GameState,
_player: PlayerId,
cards: &[CardId],
max: usize,
_optional: bool,
) -> Vec<CardId> {
cards.iter().copied().take(max).collect()
}
fn choose_targets_for(
&mut self,
_sa: &mut SpellAbility,
_game: &GameState,
_mana_pools: &[ManaPool],
) -> bool {
false
}
}
#[test]
fn dig_moves_chosen_to_hand() {
let mut game = GameState::new(&["Alice", "Bob"], 20);
let p0 = PlayerId(0);
let a = make_land(&mut game, p0);
let b = make_land(&mut game, p0);
let c = make_land(&mut game, p0);
game.replace_zone_cards(ZoneType::Library, p0, vec![a, b, c]);
let sa = SpellAbility::new_simple(
None,
p0,
"SP$ Dig | DigNum$ 3 | ChangeNum$ 1 | DestinationZone2$ Graveyard | NoReveal$ True",
);
let mut trigger_handler = TriggerHandler::new();
let mut agents: Vec<Box<dyn PlayerAgent>> =
vec![Box::new(TakeFirstAgent), Box::new(PassAgent)];
let mut mana_pools = vec![ManaPool::default(), ManaPool::default()];
let token_templates = HashMap::new();
let templates_variants: HashMap<(String, String), usize> = HashMap::new();
let token_fallback: HashMap<String, String> = HashMap::new();
let edition_dates: HashMap<String, String> = HashMap::new();
let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
let mut ctx = EffectContext {
game: &mut game,
combat: None,
agents: &mut agents,
trigger_handler: &mut trigger_handler,
token_templates: &token_templates,
token_art_variants: &templates_variants,
token_fallback: &token_fallback,
edition_dates: &edition_dates,
mana_pools: &mut mana_pools,
parent_target_card: None,
rng: &mut rng_adapter,
};
super::DigMultipleEffect::resolve(&mut ctx, &sa);
assert_eq!(ctx.game.cards_in_zone(ZoneType::Hand, p0).len(), 1);
assert_eq!(ctx.game.cards_in_zone(ZoneType::Graveyard, p0).len(), 2);
assert_eq!(ctx.game.cards_in_zone(ZoneType::Library, p0).len(), 0);
}
}