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