Skip to main content

manabrew_engine/ability/effects/
repeat_effect.rs

1//! Repeat effect — execute a sub-ability loop.
2//!
3//! Mirrors the Java `RepeatEffect` behavior for the common repeat cases:
4//! run the configured sub-ability at least once, then continue while the
5//! repeat conditions still hold.
6
7use super::EffectContext;
8use crate::ability::ability_ir::DefinedRef;
9use crate::parsing::compare::compare_expr;
10use crate::spellability::{build_spell_ability, SpellAbility};
11
12/// Struct form of this effect so it can participate in the
13/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
14/// `RepeatEffect` class extending `SpellAbilityEffect`.
15#[manabrew_engine_macros::spell_effect(RepeatEffect)]
16fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
17    let Some(source_id) = sa.source else {
18        return;
19    };
20    let Some(sub_svar_name) = sa.ir.repeat_sub_ability.clone() else {
21        return;
22    };
23    let Some(sub_text) = ctx
24        .game
25        .card(source_id)
26        .get_s_var(&sub_svar_name)
27        .map(str::to_string)
28    else {
29        return;
30    };
31
32    let max_repeat = sa
33        .ir
34        .max_repeat
35        .as_ref()
36        .map(|_| super::resolve_numeric_svar(ctx.game, sa, "MaxRepeat", 0).max(0));
37    if max_repeat == Some(0) {
38        return;
39    }
40
41    let mut count = 0i32;
42    loop {
43        let mut sub_sa = build_spell_ability(ctx.game, source_id, &sub_text, sa.activating_player);
44        // Propagate parent targets so `Defined$ Targeted` in the sub-ability
45        // resolves to the parent's chosen target (e.g. Tyrant of Discord's
46        // Repeat calls a DBSac sub whose `Defined$ Targeted` must point to the
47        // opponent the parent spell targeted).
48        if sub_sa.target_chosen.target_card.is_none() {
49            sub_sa.target_chosen.target_card = sa.target_chosen.target_card;
50        }
51        if sub_sa.target_chosen.target_player.is_none() {
52            sub_sa.target_chosen.target_player = sa.target_chosen.target_player;
53        }
54        resolve_sub_chain(ctx, sub_sa);
55        count += 1;
56
57        if ctx.game.game_over {
58            break;
59        }
60        if max_repeat.is_some_and(|max| count >= max) {
61            break;
62        }
63        let should_continue = check_repeat_conditions(ctx, sa);
64        if !should_continue {
65            break;
66        }
67    }
68
69    if let Some(sub_sa) = sa.sub_ability.as_deref() {
70        resolve_sub_chain(ctx, sub_sa.clone());
71    }
72}
73
74fn check_repeat_conditions(ctx: &mut EffectContext, sa: &SpellAbility) -> bool {
75    if sa.ir.repeat_check_svar {
76        let svar_value = super::resolve_numeric_svar(ctx.game, sa, "RepeatCheckSVar", 0);
77        let compare = sa.ir.repeat_svar_compare.as_deref().unwrap_or("GE1");
78        if !compare_expr(svar_value, compare) {
79            return false;
80        }
81    }
82
83    if let (Some(defined), Some(present), Some(compare)) = (
84        sa.ir.repeat_defined.as_deref(),
85        sa.ir.repeat_present.as_deref(),
86        sa.ir.repeat_compare.as_deref(),
87    ) {
88        let Some(source_id) = sa.source else {
89            return false;
90        };
91        let source = ctx.game.card(source_id);
92        let defined_ref = DefinedRef::parse(defined);
93        let cards: Vec<_> = if matches!(defined_ref, DefinedRef::Imprinted) {
94            source.imprinted_cards.clone()
95        } else if matches!(defined_ref, DefinedRef::Remembered) {
96            source.remembered_cards.clone()
97        } else {
98            Vec::new()
99        };
100
101        let present_count = if present.eq_ignore_ascii_case("Card.sharesNameWith Remembered") {
102            let remembered_names: std::collections::HashSet<String> = source
103                .remembered_cards
104                .iter()
105                .map(|&cid| ctx.game.card(cid).card_name.clone())
106                .collect();
107            cards
108                .into_iter()
109                .filter(|&cid| remembered_names.contains(&ctx.game.card(cid).card_name))
110                .count() as i32
111        } else {
112            let selector = crate::parsing::cached_compiled_selector(present);
113            cards
114                .into_iter()
115                .filter(|&cid| {
116                    super::matches_valid_cards_for_sa(
117                        ctx.game,
118                        sa,
119                        ctx.game.card(cid),
120                        Some(&selector),
121                        present,
122                    )
123                })
124                .count() as i32
125        };
126
127        if !compare_expr(present_count, compare) {
128            return false;
129        }
130    }
131
132    if sa.ir.repeat_optional {
133        let decider = sa
134            .ir
135            .repeat_optional_decider
136            .as_deref()
137            .and_then(|defined| {
138                crate::ability::ability_utils::resolve_defined_players_with_sa(
139                    defined,
140                    sa,
141                    sa.activating_player,
142                    ctx.game,
143                )
144                .into_iter()
145                .next()
146            })
147            .unwrap_or(sa.activating_player);
148        return ctx.agents[decider.index()].confirm_action(
149            decider,
150            Some("Repeat"),
151            "Do you want to repeat this process again?",
152            &[],
153            sa.source,
154            sa.api,
155        );
156    }
157
158    true
159}
160
161fn resolve_sub_chain(ctx: &mut EffectContext, initial: SpellAbility) {
162    let parent_target_card = initial.target_chosen.target_card;
163    let parent_target_player = initial.target_chosen.target_player;
164    let mut cur_opt: Option<SpellAbility> = Some(initial);
165    while let Some(mut cur_sa) = cur_opt {
166        if cur_sa.target_chosen.target_card.is_none() {
167            cur_sa.target_chosen.target_card = parent_target_card;
168        }
169        if cur_sa.target_chosen.target_player.is_none() {
170            cur_sa.target_chosen.target_player = parent_target_player;
171        }
172        super::resolve_effect(ctx, &cur_sa);
173        cur_opt = if super::sub_ability_handled_internally(&cur_sa) {
174            None
175        } else {
176            cur_sa.sub_ability.map(|b| *b)
177        };
178        if ctx.game.game_over {
179            break;
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::ability::spell_ability_effect::SpellAbilityEffect;
188    use crate::agent::{PlayOption, PlayerAgent, TargetChoice};
189    use crate::card::Card;
190    use crate::combat::DefenderId;
191    use crate::game::GameState;
192    use crate::ids::{CardId, PlayerId};
193    use crate::mana::ManaPool;
194    use crate::player::actions::PlayerAction;
195    use crate::trigger::handler::TriggerHandler;
196    use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
197
198    struct RepeatAgent {
199        confirmations: Vec<bool>,
200    }
201
202    impl PlayerAgent for RepeatAgent {
203        fn mulligan_decision(&mut self, _: PlayerId, _: &[CardId], _: u32) -> bool {
204            true
205        }
206        fn choose_action(
207            &mut self,
208            player: PlayerId,
209            action_space: Option<&crate::agent::PriorityActionSpace>,
210            request_action_space: &mut dyn FnMut() -> crate::agent::PriorityActionSpace,
211        ) -> PlayerAction {
212            PlayerAction::PassPriority
213        }
214        fn choose_land_or_spell(&mut self, _: PlayerId) -> Option<bool> {
215            None
216        }
217        fn choose_attackers(
218            &mut self,
219            _: PlayerId,
220            _: &[CardId],
221            _: &[DefenderId],
222        ) -> Vec<(CardId, DefenderId)> {
223            vec![]
224        }
225        fn choose_blockers(
226            &mut self,
227            _: PlayerId,
228            _: &[CardId],
229            _: &[CardId],
230            _: Option<usize>,
231        ) -> Vec<(CardId, CardId)> {
232            vec![]
233        }
234        fn choose_target_player(
235            &mut self,
236            _: PlayerId,
237            valid: &[PlayerId],
238            _: Option<&SpellAbility>,
239        ) -> Option<PlayerId> {
240            valid.first().copied()
241        }
242        fn choose_target_card(
243            &mut self,
244            _: PlayerId,
245            valid: &[CardId],
246            _: Option<&SpellAbility>,
247        ) -> Option<CardId> {
248            valid.first().copied()
249        }
250        fn choose_target_any(
251            &mut self,
252            _: PlayerId,
253            valid_players: &[PlayerId],
254            valid_cards: &[CardId],
255            _: Option<&SpellAbility>,
256        ) -> TargetChoice {
257            if let Some(&pid) = valid_players.first() {
258                TargetChoice::Player(pid)
259            } else if let Some(&cid) = valid_cards.first() {
260                TargetChoice::Card(cid)
261            } else {
262                TargetChoice::None
263            }
264        }
265        fn choose_targets_for(
266            &mut self,
267            _: &mut SpellAbility,
268            _: &GameState,
269            _: &[ManaPool],
270        ) -> bool {
271            true
272        }
273        fn confirm_action(
274            &mut self,
275            _: PlayerId,
276            _: Option<&str>,
277            _: &str,
278            _: &[String],
279            _: Option<crate::ids::CardId>,
280            _: Option<crate::ability::api_type::ApiType>,
281        ) -> bool {
282            if self.confirmations.is_empty() {
283                false
284            } else {
285                self.confirmations.remove(0)
286            }
287        }
288    }
289
290    #[test]
291    fn repeat_optional_executes_until_declined() {
292        let mut game = GameState::new(&["P1", "P2"], 20);
293        let p0 = PlayerId(0);
294        let p1 = PlayerId(1);
295        let source = game.create_card(Card::new(
296            CardId(0),
297            "Repeat Source".into(),
298            p0,
299            CardTypeLine::parse("Instant"),
300            ManaCost::parse("1 B"),
301            ColorSet::BLACK,
302            None,
303            None,
304            vec![],
305            vec![],
306        ));
307        game.card_mut(source).svars.insert(
308            "Loop".to_string(),
309            "DB$ LoseLife | Defined$ Opponent | LifeAmount$ 1".to_string(),
310        );
311
312        let sa = build_spell_ability(
313            &game,
314            source,
315            "SP$ Repeat | RepeatSubAbility$ Loop | RepeatOptional$ True",
316            p0,
317        );
318
319        let mut agents: Vec<Box<dyn PlayerAgent>> = vec![
320            Box::new(RepeatAgent {
321                confirmations: vec![true, false],
322            }),
323            Box::new(RepeatAgent {
324                confirmations: vec![],
325            }),
326        ];
327        let mut pools = vec![ManaPool::new(), ManaPool::new()];
328        let mut trigger_handler = TriggerHandler::default();
329        let mut rng = crate::game_rng::ThreadRngAdapter;
330        let start_life = game.player(p1).life;
331
332        RepeatEffect::resolve(
333            &mut EffectContext {
334                game: &mut game,
335                combat: None,
336                agents: &mut agents,
337                trigger_handler: &mut trigger_handler,
338                token_templates: &Default::default(),
339                token_art_variants: &Default::default(),
340                token_fallback: &Default::default(),
341                edition_dates: &Default::default(),
342                mana_pools: &mut pools,
343                parent_target_card: None,
344                rng: &mut rng,
345            },
346            &sa,
347        );
348
349        assert_eq!(game.player(p1).life, start_life - 2);
350    }
351
352    #[test]
353    fn repeat_executes_parent_subability_after_loop() {
354        let mut game = GameState::new(&["P1", "P2"], 20);
355        let p0 = PlayerId(0);
356        let p1 = PlayerId(1);
357        let source = game.create_card(Card::new(
358            CardId(0),
359            "Repeat Source".into(),
360            p0,
361            CardTypeLine::parse("Instant"),
362            ManaCost::parse("1 B"),
363            ColorSet::BLACK,
364            None,
365            None,
366            vec![],
367            vec![],
368        ));
369        game.card_mut(source).svars.insert(
370            "Loop".to_string(),
371            "DB$ LoseLife | Defined$ Opponent | LifeAmount$ 1".to_string(),
372        );
373        game.card_mut(source).svars.insert(
374            "After".to_string(),
375            "DB$ LoseLife | Defined$ Opponent | LifeAmount$ 3".to_string(),
376        );
377
378        let sa = build_spell_ability(
379            &game,
380            source,
381            "SP$ Repeat | RepeatSubAbility$ Loop | MaxRepeat$ 1 | SubAbility$ After",
382            p0,
383        );
384
385        let mut agents: Vec<Box<dyn PlayerAgent>> = vec![
386            Box::new(RepeatAgent {
387                confirmations: vec![],
388            }),
389            Box::new(RepeatAgent {
390                confirmations: vec![],
391            }),
392        ];
393        let mut pools = vec![ManaPool::new(), ManaPool::new()];
394        let mut trigger_handler = TriggerHandler::default();
395        let mut rng = crate::game_rng::ThreadRngAdapter;
396        let start_life = game.player(p1).life;
397
398        RepeatEffect::resolve(
399            &mut EffectContext {
400                game: &mut game,
401                combat: None,
402                agents: &mut agents,
403                trigger_handler: &mut trigger_handler,
404                token_templates: &Default::default(),
405                token_art_variants: &Default::default(),
406                token_fallback: &Default::default(),
407                edition_dates: &Default::default(),
408                mana_pools: &mut pools,
409                parent_target_card: None,
410                rng: &mut rng,
411            },
412            &sa,
413        );
414
415        assert_eq!(game.player(p1).life, start_life - 4);
416    }
417}