manabrew_engine/ability/effects/
surveil_effect.rs1use forge_foundation::ZoneType;
2
3use super::{emit_zone_trigger, resolve_defined_player, resolve_numeric_svar, EffectContext};
4use crate::event::RunParams;
5use crate::trigger::TriggerType;
6
7#[manabrew_engine_macros::spell_effect(SurveilEffect)]
16fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
17 let num = resolve_numeric_svar(ctx.game, sa, "Amount", 1).max(0) as usize;
18
19 let target = sa
20 .defined()
21 .and_then(|d| resolve_defined_player(d, sa.activating_player, ctx.game))
22 .unwrap_or(sa.activating_player);
23
24 if sa.ir.optional {
25 let source_name = sa.source.map(|cid| ctx.game.card(cid).card_name.as_str());
26 let accepted = ctx.agents[target.index()].confirm_action(
27 target,
28 None,
29 "Do you want to surveil?",
30 &[],
31 sa.source,
32 Some(crate::ability::api_type::ApiType::Surveil),
33 );
34 if !accepted {
35 return;
36 }
37 }
38
39 let lib_len = ctx.game.cards_in_zone(ZoneType::Library, target).len();
40 if lib_len == 0 || num == 0 {
41 return;
42 }
43
44 let count = num.min(lib_len);
45
46 let mut top_n = ctx
48 .game
49 .take_top_cards_from_zone(ZoneType::Library, target, count);
50 top_n.reverse();
52
53 let piles = ctx.agents[target.index()].choose_surveil(ctx.game, target, sa.source, &top_n);
55 let (keep_top, graveyard) = super::split_scry_piles(&top_n, &piles);
56
57 for &id in &graveyard {
59 ctx.move_card(id, ZoneType::Graveyard, target);
60 emit_zone_trigger(
61 ctx.trigger_handler,
62 id,
63 ZoneType::Library,
64 ZoneType::Graveyard,
65 );
66 }
67
68 for &id in keep_top.iter().rev() {
71 ctx.game.add_card_to_zone(ZoneType::Library, target, id);
72 }
73
74 ctx.trigger_handler.run_trigger(
76 TriggerType::Surveil,
77 RunParams {
78 player: Some(target),
79 ..Default::default()
80 },
81 false,
82 );
83}
84
85#[cfg(test)]
86mod tests {
87 use crate::ability::spell_ability_effect::SpellAbilityEffect;
88 use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
89
90 use crate::ability::effects::EffectContext;
91 use crate::agent::{PassAgent, PlayerAgent};
92 use crate::card::Card;
93 use crate::combat::DefenderId;
94 use crate::game::GameState;
95 use crate::ids::{CardId, PlayerId};
96 use crate::mana::ManaPool;
97 use crate::spellability::SpellAbility;
98 use crate::trigger::handler::TriggerHandler;
99 use std::collections::HashMap;
100
101 fn make_land(game: &mut GameState, owner: PlayerId) -> CardId {
102 let c = Card::new(
103 CardId(0),
104 "Island".into(),
105 owner,
106 CardTypeLine::parse("Basic Land Island"),
107 ManaCost::parse(""),
108 ColorSet::COLORLESS,
109 None,
110 None,
111 vec![],
112 vec![],
113 );
114 game.create_card(c)
115 }
116
117 struct GraveyardAllAgent;
119 impl PlayerAgent for GraveyardAllAgent {
120 fn mulligan_decision(&mut self, _: PlayerId, _: &[CardId], _: u32) -> bool {
121 true
122 }
123 fn choose_action(
124 &mut self,
125 player: PlayerId,
126 action_space: Option<&crate::agent::PriorityActionSpace>,
127 request_action_space: &mut dyn FnMut() -> crate::agent::PriorityActionSpace,
128 ) -> crate::player::actions::PlayerAction {
129 crate::player::actions::PlayerAction::PassPriority
130 }
131 fn choose_attackers(
132 &mut self,
133 _: PlayerId,
134 _: &[CardId],
135 _: &[DefenderId],
136 ) -> Vec<(CardId, DefenderId)> {
137 vec![]
138 }
139 fn choose_blockers(
140 &mut self,
141 _: PlayerId,
142 _: &[CardId],
143 _: &[CardId],
144 _: Option<usize>,
145 ) -> Vec<(CardId, CardId)> {
146 vec![]
147 }
148 fn choose_target_player(
149 &mut self,
150 _: PlayerId,
151 v: &[PlayerId],
152 _sa: Option<&crate::spellability::SpellAbility>,
153 ) -> Option<PlayerId> {
154 v.first().copied()
155 }
156 fn choose_target_card(
157 &mut self,
158 _: PlayerId,
159 v: &[CardId],
160 _sa: Option<&crate::spellability::SpellAbility>,
161 ) -> Option<CardId> {
162 v.first().copied()
163 }
164 fn choose_target_any(
165 &mut self,
166 _: PlayerId,
167 vp: &[PlayerId],
168 vc: &[CardId],
169 _sa: Option<&crate::spellability::SpellAbility>,
170 ) -> crate::agent::TargetChoice {
171 vp.first()
172 .copied()
173 .map(crate::agent::TargetChoice::Player)
174 .or_else(|| vc.first().copied().map(crate::agent::TargetChoice::Card))
175 .unwrap_or(crate::agent::TargetChoice::None)
176 }
177 fn choose_land_or_spell(&mut self, _: PlayerId) -> Option<bool> {
178 None
179 }
180 fn choose_surveil(
181 &mut self,
182 _game: &GameState,
183 _player: PlayerId,
184 _source: Option<CardId>,
185 cards: &[CardId],
186 ) -> Vec<Vec<CardId>> {
187 vec![vec![], cards.to_vec()] }
189 fn choose_targets_for(
190 &mut self,
191 _sa: &mut SpellAbility,
192 _game: &GameState,
193 _mana_pools: &[ManaPool],
194 ) -> bool {
195 false
196 }
197 }
198
199 #[test]
200 fn surveil_sends_chosen_to_graveyard() {
201 let mut game = GameState::new(&["Alice", "Bob"], 20);
202 let p0 = PlayerId(0);
203
204 let a = make_land(&mut game, p0);
205 let b = make_land(&mut game, p0);
206 let c = make_land(&mut game, p0);
207 game.replace_zone_cards(ZoneType::Library, p0, vec![a, b, c]);
208
209 let sa = SpellAbility::new_simple(None, p0, "SP$ Surveil | Amount$ 2");
211 let mut trigger_handler = TriggerHandler::new();
212 let mut agents: Vec<Box<dyn PlayerAgent>> =
213 vec![Box::new(GraveyardAllAgent), Box::new(PassAgent)];
214 let mut mana_pools = vec![ManaPool::default(), ManaPool::default()];
215 let token_templates = HashMap::new();
216 let templates_variants: HashMap<(String, String), usize> = HashMap::new();
217 let token_fallback: HashMap<String, String> = HashMap::new();
218 let edition_dates: HashMap<String, String> = HashMap::new();
219 let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
220 let mut ctx = EffectContext {
221 game: &mut game,
222 combat: None,
223 agents: &mut agents,
224 trigger_handler: &mut trigger_handler,
225 token_templates: &token_templates,
226 token_art_variants: &templates_variants,
227 token_fallback: &token_fallback,
228 edition_dates: &edition_dates,
229 mana_pools: &mut mana_pools,
230 parent_target_card: None,
231 rng: &mut rng_adapter,
232 };
233
234 super::SurveilEffect::resolve(&mut ctx, &sa);
235
236 assert_eq!(ctx.game.cards_in_zone(ZoneType::Library, p0).len(), 1);
237 assert_eq!(ctx.game.cards_in_zone(ZoneType::Graveyard, p0).len(), 2);
238 }
239
240 #[test]
241 fn surveil_keep_all_on_top_with_pass_agent() {
242 let mut game = GameState::new(&["Alice", "Bob"], 20);
243 let p0 = PlayerId(0);
244
245 let a = make_land(&mut game, p0);
246 let b = make_land(&mut game, p0);
247 game.replace_zone_cards(ZoneType::Library, p0, vec![a, b]);
248
249 let sa = SpellAbility::new_simple(None, p0, "SP$ Surveil | Amount$ 2");
250 let mut trigger_handler = TriggerHandler::new();
251 let mut agents: Vec<Box<dyn PlayerAgent>> = vec![Box::new(PassAgent), Box::new(PassAgent)];
252 let mut mana_pools = vec![ManaPool::default(), ManaPool::default()];
253 let token_templates = HashMap::new();
254 let templates_variants: HashMap<(String, String), usize> = HashMap::new();
255 let token_fallback: HashMap<String, String> = HashMap::new();
256 let edition_dates: HashMap<String, String> = HashMap::new();
257 let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
258 let mut ctx = EffectContext {
259 game: &mut game,
260 combat: None,
261 agents: &mut agents,
262 trigger_handler: &mut trigger_handler,
263 token_templates: &token_templates,
264 token_art_variants: &templates_variants,
265 token_fallback: &token_fallback,
266 edition_dates: &edition_dates,
267 mana_pools: &mut mana_pools,
268 parent_target_card: None,
269 rng: &mut rng_adapter,
270 };
271
272 super::SurveilEffect::resolve(&mut ctx, &sa);
273
274 let lib = ctx.game.cards_in_zone(ZoneType::Library, p0);
276 assert_eq!(lib.len(), 2);
277 assert_eq!(ctx.game.cards_in_zone(ZoneType::Graveyard, p0).len(), 0);
278 }
279}