Skip to main content

manabrew_engine/game_loop/
priority.rs

1use super::*;
2use crate::player::actions::player_action::STATIC_ALTERNATIVE_ABILITY_INDEX;
3use crate::player::actions::{PlayerAction, PlayerActionOutcome};
4use crate::player::PlayerController;
5
6impl GameLoop {
7    fn describe_priority_action(
8        &self,
9        game: &GameState,
10        action: MainPhaseAction,
11        ability_idx: Option<usize>,
12    ) -> String {
13        let card_name_or_id = |card_id: CardId| -> String {
14            game.cards
15                .get(card_id.index())
16                .map(|c| c.card_name.clone())
17                .unwrap_or_else(|| format!("CardId({})", card_id.0))
18        };
19        match action {
20            MainPhaseAction::Pass => "Pass".to_string(),
21            MainPhaseAction::Play(play) => {
22                format!("Play {}", card_name_or_id(play.card_id))
23            }
24            MainPhaseAction::ActivateMana(card_id, _, _) => {
25                format!("Activate mana ({})", card_name_or_id(card_id))
26            }
27            MainPhaseAction::UntapMana(card_id) => {
28                format!("Untap mana ({})", card_name_or_id(card_id))
29            }
30            MainPhaseAction::ActivateAbility(card_id, _) => {
31                let idx = ability_idx.unwrap_or_default();
32                format!("Activate ability {} ({})", idx, card_name_or_id(card_id))
33            }
34        }
35    }
36
37    pub fn priority_round(
38        &mut self,
39        game: &mut GameState,
40        agents: &mut [Box<dyn PlayerAgent>],
41        is_main_phase: bool,
42    ) {
43        let _perf_scope =
44            crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::Priority);
45        let mut priority_player = game.active_player();
46        let mut last_notified_priority: Option<PlayerId> = None;
47        let mut passed_count = 0;
48        let num_players = game.players.len();
49        while passed_count < num_players {
50            if game.game_over {
51                return;
52            }
53            self.with_shared_state_mutation(game, agents, |_this, game, _agents| {
54                game.turn.priority_player = priority_player;
55            });
56
57            if last_notified_priority != Some(priority_player) {
58                self.notify_priority_changed(game, agents, priority_player);
59                last_notified_priority = Some(priority_player);
60            }
61            if game.game_over {
62                return;
63            }
64
65            loop {
66                let sba_changed = super::check_sba(game, &mut self.trigger_handler, agents);
67                if game.game_over {
68                    return;
69                }
70                let stack_before = game.stack.len();
71                self.with_shared_state_mutation(game, agents, |this, game, agents| {
72                    this.process_triggers(game, agents);
73                });
74                let triggers_added = game.stack.len() > stack_before;
75                // Keep looping while either SBA changed state or new triggers were added
76                if !sba_changed && !triggers_added {
77                    break;
78                }
79            }
80            if game.game_over {
81                return;
82            }
83
84            if let Some(target) = agents[priority_player.index()].get_pass_until() {
85                let current_phase = game.turn.phase;
86                let active = game.active_player();
87                let has_declared_attackers = self.combat.has_attackers();
88                let is_active_combat = has_declared_attackers
89                    && matches!(
90                        current_phase,
91                        forge_foundation::PhaseType::CombatDeclareAttackers
92                            | forge_foundation::PhaseType::CombatDeclareBlockers
93                            | forge_foundation::PhaseType::CombatFirstStrikeDamage
94                            | forge_foundation::PhaseType::CombatDamage
95                            | forge_foundation::PhaseType::CombatEnd
96                    );
97                let reached = (active == target.player && !current_phase.is_before(target.phase))
98                    || !game.player(target.player).is_alive();
99                if reached {
100                    agents[priority_player.index()].clear_pass_until();
101                } else if !is_active_combat && game.stack.is_empty() {
102                    self.log_priority_pass(game, priority_player);
103                    passed_count += 1;
104                    priority_player = game.next_player(priority_player);
105                    self.with_shared_state_mutation(game, agents, |_this, game, _agents| {
106                        game.turn.priority_player = priority_player;
107                    });
108                    continue;
109                }
110            }
111
112            let mut action_space = if self.provide_priority_action_space {
113                crate::staticability::layer::apply_continuous_effects(game);
114                Some(self.action_space(game, priority_player, is_main_phase))
115            } else {
116                None
117            };
118            if action_space.as_ref().is_some_and(|space| space.is_empty()) {
119                self.invalidate_mana_undo_for_player(priority_player);
120                self.log_priority_pass(game, priority_player);
121                passed_count += 1;
122                priority_player = game.next_player(priority_player);
123                self.with_shared_state_mutation(game, agents, |_this, game, _agents| {
124                    game.turn.priority_player = priority_player;
125                });
126                continue;
127            }
128            self.log_waiting_for_priority(game, priority_player);
129            let action = {
130                let _perf_scope = crate::perf::ParamsLookupScopeGuard::enter(
131                    crate::perf::ParamsLookupScope::PriorityChoice,
132                );
133                {
134                    let _perf_scope = crate::perf::ParamsLookupScopeGuard::enter(
135                        crate::perf::ParamsLookupScope::PrioritySnapshot,
136                    );
137                    crate::perf::increment_priority_snapshot();
138                    let agent = agents[priority_player.index()].as_mut();
139                    let mut controller = PlayerController::new(game, priority_player, agent);
140                    controller.snapshot_state(&self.mana_pools);
141                }
142                if self.is_aborted() {
143                    game.game_over = true;
144                    return;
145                }
146                let mut request_action_space = || {
147                    crate::staticability::layer::apply_continuous_effects(game);
148                    self.action_space(game, priority_player, is_main_phase)
149                };
150                agents[priority_player.index()].choose_action(
151                    priority_player,
152                    action_space.as_ref(),
153                    &mut request_action_space,
154                )
155            };
156
157            if action == PlayerAction::Concede {
158                let _ = agents[priority_player.index()].take_restore_request();
159                self.with_shared_state_mutation(game, agents, |_this, game, _agents| {
160                    crate::player::concede(game, priority_player);
161                });
162                if game.alive_players().len() > 1 {
163                    agents[priority_player.index()].snapshot_state(game, &self.mana_pools);
164                    agents[priority_player.index()]
165                        .notify(crate::agent::notification::GameNotification::GameOver);
166                }
167                passed_count = 0;
168                priority_player = game.next_player(priority_player);
169                self.with_shared_state_mutation(game, agents, |_this, game, _agents| {
170                    game.turn.priority_player = priority_player;
171                });
172                continue;
173            }
174
175            if self.apply_pending_snapshot_restore(game, agents) {
176                passed_count = 0;
177                priority_player = game.turn.priority_player;
178                continue;
179            }
180
181            let priority_action = if action == PlayerAction::PassPriority {
182                MainPhaseAction::Pass
183            } else {
184                if action_space.is_none() {
185                    crate::staticability::layer::apply_continuous_effects(game);
186                    action_space = Some(self.action_space(game, priority_player, is_main_phase));
187                }
188                let action_space = action_space
189                    .as_ref()
190                    .expect("non-pass priority action requires action space");
191                let agent = agents[priority_player.index()].as_mut();
192                let mut controller = PlayerController::new(game, priority_player, agent);
193                let activatable_ids: Vec<(CardId, usize)> = action_space
194                    .activatable
195                    .iter()
196                    .map(|a| (a.card_id, a.ability_index))
197                    .collect();
198                match action.run(
199                    &mut controller,
200                    &action_space.playable,
201                    &action_space.tappable_lands,
202                    &action_space.untappable_lands,
203                    &activatable_ids,
204                ) {
205                    PlayerActionOutcome::Priority(action) => action,
206                    PlayerActionOutcome::Pending | PlayerActionOutcome::Target(_) => {
207                        crate::agent::notify_all_agents(
208                            agents,
209                            crate::agent::GameLogEvent::warning(
210                                "Illegal action ignored: unsupported priority action",
211                            )
212                            .with_player(priority_player),
213                        );
214                        passed_count += 1;
215                        priority_player = game.next_player(priority_player);
216                        self.with_shared_state_mutation(game, agents, |_this, game, _agents| {
217                            game.turn.priority_player = priority_player;
218                        });
219                        continue;
220                    }
221                }
222            };
223            let _perf_scope = crate::perf::ParamsLookupScopeGuard::enter(
224                crate::perf::ParamsLookupScope::PriorityExecution,
225            );
226            match priority_action {
227                MainPhaseAction::Pass => {
228                    self.invalidate_mana_undo_for_player(priority_player);
229                    self.log_priority_pass(game, priority_player);
230                    passed_count += 1;
231                    priority_player = game.next_player(priority_player);
232                    self.with_shared_state_mutation(game, agents, |_this, game, _agents| {
233                        game.turn.priority_player = priority_player;
234                    });
235                }
236                MainPhaseAction::Play(play) => {
237                    agents[priority_player.index()].clear_pass_until();
238                    let action_space = action_space
239                        .as_ref()
240                        .expect("play priority action requires action space");
241                    self.invalidate_mana_undo_for_player(priority_player);
242                    self.log_priority_response(
243                        game,
244                        priority_player,
245                        &self.describe_priority_action(game, priority_action, None),
246                    );
247                    if !action_space.playable.contains(&play) {
248                        crate::agent::notify_all_agents(
249                            agents,
250                            crate::agent::GameLogEvent::warning(
251                                "Illegal action ignored: unplayable card",
252                            )
253                            .with_player(priority_player),
254                        );
255                        passed_count += 1;
256                        priority_player = game.next_player(priority_player);
257                        self.with_shared_state_mutation(game, agents, |_this, game, _agents| {
258                            game.turn.priority_player = priority_player;
259                        });
260                        continue;
261                    }
262
263                    // Room UnlockDoor: route through the activated-ability branch of
264                    // play_spell_ability. Java models this as a StaticAbilityApiBased.
265                    if play.mode == crate::agent::PlayCardMode::UnlockDoor {
266                        let unlock_ab_idx = game
267                            .card(play.card_id)
268                            .activated_abilities
269                            .iter()
270                            .find(|ab| ab.is_unlock_door)
271                            .map(|ab| ab.ability_index);
272                        if let Some(ability_idx) = unlock_ab_idx {
273                            let played = self.with_shared_state_mutation(
274                                game,
275                                agents,
276                                |this, game, agents| {
277                                    let ability_text = game
278                                        .card(play.card_id)
279                                        .activated_abilities
280                                        .iter()
281                                        .find(|ab| ab.ability_index == ability_idx)
282                                        .map(|ab| ab.ability_text.clone())?;
283                                    let mut sa = crate::spellability::build_spell_ability(
284                                        game,
285                                        play.card_id,
286                                        &ability_text,
287                                        priority_player,
288                                    );
289                                    sa.is_activated = true;
290                                    this.play_spell_ability(
291                                        game,
292                                        agents,
293                                        priority_player,
294                                        PreparedSpellAbility {
295                                            spell_ability: sa,
296                                            activated_ability_index: Some(ability_idx),
297                                            static_alternative_cost_prepared: false,
298                                        },
299                                    )
300                                },
301                            );
302                            if played.is_some() {
303                                self.with_shared_state_mutation(
304                                    game,
305                                    agents,
306                                    |this, game, agents| {
307                                        this.process_triggers(game, agents);
308                                    },
309                                );
310                                passed_count = 0;
311                            }
312                            // Whether activated or not, skip the normal play_card path
313                            continue;
314                        }
315                    }
316
317                    let origin_zone = game.card_current_zone(play.card_id);
318                    let played =
319                        self.with_shared_state_mutation(game, agents, |this, game, agents| {
320                            let card_name = game.card(play.card_id).card_name.clone();
321                            if game.card(play.card_id).is_land()
322                                || play.mode == crate::agent::PlayCardMode::BackFaceLand
323                            {
324                                this.play_land(
325                                    game,
326                                    agents,
327                                    priority_player,
328                                    play.card_id,
329                                    &card_name,
330                                    play.mode,
331                                )
332                                .map(|(card_id, card_name)| PlaySpellAbilityResult::CardPlayed {
333                                    card_id,
334                                    card_name,
335                                })
336                            } else if let Some(result) = this.play_special_card_action(
337                                game,
338                                agents,
339                                priority_player,
340                                play.card_id,
341                                play.mode,
342                            ) {
343                                result.map(|(card_id, card_name)| {
344                                    PlaySpellAbilityResult::CardPlayed { card_id, card_name }
345                                })
346                            } else {
347                                let prepared = this.prepare_card_spell_ability(
348                                    game,
349                                    priority_player,
350                                    play.card_id,
351                                    play,
352                                )?;
353                                this.play_spell_ability(game, agents, priority_player, prepared)
354                            }
355                        });
356                    if let Some(PlaySpellAbilityResult::CardPlayed {
357                        card_id: played_id,
358                        card_name: played_name,
359                    }) = played
360                    {
361                        let set_code = game.card(played_id).set_code.clone().unwrap_or_default();
362                        for agent in agents.iter_mut() {
363                            agent.snapshot_state(game, &self.mana_pools);
364                            agent.notify(
365                                crate::agent::notification::GameNotification::CardPlayed {
366                                    player: priority_player,
367                                    card_id: played_id,
368                                    card_name: played_name.clone(),
369                                    set_code: set_code.clone(),
370                                },
371                            );
372                        }
373                        // Process SpellCast / BecomesTarget triggers immediately so they
374                        // go on the stack ABOVE the spell (resolving before it).
375                        // Mirrors Java's MagicStack.addAndUnfreeze() which runs waiting
376                        // triggers right after the spell is placed on the stack.
377                        self.with_shared_state_mutation(game, agents, |this, game, agents| {
378                            let current_zone = game.card_current_zone(played_id);
379                            if current_zone != origin_zone {
380                                let mut trigger_list =
381                                    crate::card::card_zone_table::CardZoneTable::default();
382                                trigger_list.put(Some(origin_zone), Some(current_zone), played_id);
383                                trigger_list.trigger_changes_zone_all(
384                                    &mut this.trigger_handler,
385                                    game,
386                                    None,
387                                );
388                            }
389                            this.process_triggers(game, agents);
390                        });
391                        passed_count = 0;
392                    } else {
393                        crate::agent::notify_all_agents(
394                            agents,
395                            crate::agent::GameLogEvent::warning("Card play failed")
396                                .with_player(priority_player),
397                        );
398                    }
399                }
400                MainPhaseAction::ActivateMana(land_id, requested_ability_idx, express_choice) => {
401                    let action_space = action_space
402                        .as_ref()
403                        .expect("mana priority action requires action space");
404                    self.log_priority_response(
405                        game,
406                        priority_player,
407                        &self.describe_priority_action(game, priority_action, None),
408                    );
409                    if !action_space.tappable_lands.contains(&land_id) {
410                        crate::agent::notify_all_agents(
411                            agents,
412                            crate::agent::GameLogEvent::warning(
413                                "Illegal action ignored: permanent can't tap for mana",
414                            )
415                            .with_player(priority_player),
416                        );
417                        passed_count += 1;
418                        priority_player = game.next_player(priority_player);
419                        self.with_shared_state_mutation(game, agents, |_this, game, _agents| {
420                            game.turn.priority_player = priority_player;
421                        });
422                        continue;
423                    }
424                    let undo_record = self.begin_mana_undo_action(game, priority_player, land_id);
425                    let pool_snapshot = self.pool(priority_player).begin_tap_tracking();
426
427                    let mana_abs: Vec<_> = {
428                        let c = game.card(land_id);
429                        c.activated_abilities
430                            .iter()
431                            .filter(|ab| ab.is_mana_ability)
432                            .cloned()
433                            .collect()
434                    };
435                    if !mana_abs.is_empty() {
436                        // Separate tap-cost mana abilities from non-tap-cost ones.
437                        // Dual lands (e.g. Breeding Pool = Forest Island) generate
438                        // separate {T}: Add {G} and {T}: Add {U} abilities.  The
439                        // player must choose ONE; we must not fire both.
440                        let (tap_abs, non_tap_abs): (Vec<_>, Vec<_>) =
441                            mana_abs.iter().partition(|ab| {
442                                ab.cost
443                                    .parts
444                                    .iter()
445                                    .any(|p| matches!(p, crate::cost::CostPart::Tap))
446                            });
447
448                        let chosen_ab: Option<crate::ability::activated::ActivatedAbility> =
449                            if let Some(req_idx) = requested_ability_idx {
450                                tap_abs
451                                    .iter()
452                                    .chain(non_tap_abs.iter())
453                                    .find(|ab| ab.ability_index == req_idx)
454                                    .map(|ab| (*ab).clone())
455                            } else if tap_abs.len() <= 1 {
456                                tap_abs.first().map(|ab| (*ab).clone()).or_else(|| {
457                                    if non_tap_abs.len() == 1 {
458                                        Some((*non_tap_abs[0]).clone())
459                                    } else {
460                                        None
461                                    }
462                                })
463                            } else {
464                                // Multiple tap-cost abilities — ask the player to choose a color.
465                                let mut color_options: Vec<(String, usize)> = Vec::new();
466                                for (i, ab) in tap_abs.iter().enumerate() {
467                                    if let Some(produced_ir) = ab.produced_ir.as_ref() {
468                                        let chosen_colors =
469                                            game.card(land_id).chosen_colors.clone();
470                                        let names = produced_ir.to_color_names(&chosen_colors);
471                                        for name in names {
472                                            if !color_options.iter().any(|(n, _)| *n == name) {
473                                                color_options.push((name, i));
474                                            }
475                                        }
476                                    }
477                                }
478                                let color_names: Vec<String> =
479                                    color_options.iter().map(|(n, _)| n.clone()).collect();
480                                let chosen_idx = if color_names.len() == 1 {
481                                    Some(0usize)
482                                } else {
483                                    agents[priority_player.index()]
484                                        .choose_color(priority_player, &color_names)
485                                        .and_then(|chosen| {
486                                            color_options.iter().position(|(n, _)| *n == chosen)
487                                        })
488                                };
489                                chosen_idx.and_then(|ci| {
490                                    let (_, ab_idx) = &color_options[ci];
491                                    tap_abs.get(*ab_idx).map(|ab| (*ab).clone())
492                                })
493                            };
494
495                        let chosen_idx = chosen_ab.as_ref().map(|ab| ab.ability_index);
496                        let chosen_is_tap = chosen_ab
497                            .as_ref()
498                            .map(|ab| {
499                                ab.cost
500                                    .parts
501                                    .iter()
502                                    .any(|p| matches!(p, crate::cost::CostPart::Tap))
503                            })
504                            .unwrap_or(false);
505
506                        if let Some(ab) = chosen_ab {
507                            self.with_shared_state_mutation(game, agents, |this, game, agents| {
508                                this.resolve_mana_ability(
509                                    game,
510                                    agents,
511                                    priority_player,
512                                    land_id,
513                                    &ab,
514                                    express_choice,
515                                );
516                            });
517                        }
518
519                        if chosen_is_tap {
520                            for ab in &non_tap_abs {
521                                if Some(ab.ability_index) == chosen_idx {
522                                    continue;
523                                }
524                                if !ab.cost.parts.is_empty() {
525                                    continue;
526                                }
527                                let ab = (*ab).clone();
528                                self.with_shared_state_mutation(
529                                    game,
530                                    agents,
531                                    |this, game, agents| {
532                                        this.resolve_mana_ability(
533                                            game,
534                                            agents,
535                                            priority_player,
536                                            land_id,
537                                            &ab,
538                                            None,
539                                        );
540                                    },
541                                );
542                            }
543                        }
544                    } else {
545                        // Legacy fallback for lands with no parsed mana abilities
546                        self.with_shared_state_mutation(game, agents, |this, game, agents| {
547                            let atom_opt = {
548                                let c = game.card(land_id);
549                                if c.is_land() && !c.tapped {
550                                    basic_land_mana_atom(c)
551                                } else {
552                                    None
553                                }
554                            };
555                            if let Some(atom) = atom_opt {
556                                game.tap(land_id);
557                                this.pool_mut(priority_player).add(atom, 1);
558                                this.trigger_handler.run_trigger(
559                                    TriggerType::Taps,
560                                    RunParams {
561                                        card: Some(land_id),
562                                        player: Some(priority_player),
563                                        ..Default::default()
564                                    },
565                                    false,
566                                );
567                                this.trigger_handler.run_trigger(
568                                    TriggerType::TapsForMana,
569                                    RunParams {
570                                        card: Some(land_id),
571                                        player: Some(priority_player),
572                                        ..Default::default()
573                                    },
574                                    false,
575                                );
576                                // Resolve mana triggers inline (e.g. Utopia Sprawl).
577                                let pending = this.trigger_handler.run_waiting_triggers(game);
578                                if !pending.is_empty() {
579                                    this.mark_mana_undo_disqualified();
580                                }
581                                for pt in pending {
582                                    this.resolve_single_effect(
583                                        game,
584                                        agents,
585                                        &pt.entry.spell_ability,
586                                        None,
587                                    );
588                                }
589                            }
590                        });
591                    }
592
593                    // Record ALL mana produced by this tap for rollback — single snapshot
594                    // covers base ability + granted abilities + aura triggers.
595                    let produced = self.pool(priority_player).end_tap_tracking(&pool_snapshot);
596                    let produced_count = produced.len();
597                    if !produced.is_empty() {
598                        self.with_shared_state_mutation(game, agents, |_this, game, _agents| {
599                            game.card_mut(land_id).last_mana_produced = Some(produced);
600                        });
601                    }
602                    self.finish_mana_undo_action(undo_record, produced_count);
603                    passed_count = 0;
604                }
605                MainPhaseAction::UntapMana(land_id) => {
606                    let action_space = action_space
607                        .as_ref()
608                        .expect("mana undo priority action requires action space");
609                    self.log_priority_response(
610                        game,
611                        priority_player,
612                        &self.describe_priority_action(game, priority_action, None),
613                    );
614                    if !action_space.untappable_lands.contains(&land_id) {
615                        crate::agent::notify_all_agents(
616                            agents,
617                            crate::agent::GameLogEvent::warning(
618                                "Illegal action ignored: land can't be untapped for mana rollback",
619                            )
620                            .with_player(priority_player),
621                        );
622                        passed_count += 1;
623                        priority_player = game.next_player(priority_player);
624                        self.with_shared_state_mutation(game, agents, |_this, game, _agents| {
625                            game.turn.priority_player = priority_player;
626                        });
627                        continue;
628                    }
629                    self.with_shared_state_mutation(game, agents, |this, game, _agents| {
630                        this.undo_mana_action(game, priority_player, land_id);
631                    });
632                    passed_count = 0;
633                }
634                MainPhaseAction::ActivateAbility(card_id, ability_idx) => {
635                    agents[priority_player.index()].clear_pass_until();
636                    let action_space = action_space
637                        .as_ref()
638                        .expect("ability priority action requires action space");
639                    self.invalidate_mana_undo_for_player(priority_player);
640                    self.log_priority_response(
641                        game,
642                        priority_player,
643                        &self.describe_priority_action(game, priority_action, Some(ability_idx)),
644                    );
645                    if !action_space
646                        .activatable
647                        .iter()
648                        .any(|a| a.card_id == card_id && a.ability_index == ability_idx)
649                    {
650                        crate::agent::notify_all_agents(
651                            agents,
652                            crate::agent::GameLogEvent::warning(
653                                "Illegal action ignored: ability not activatable",
654                            )
655                            .with_player(priority_player),
656                        );
657                        passed_count += 1;
658                        priority_player = game.next_player(priority_player);
659                        self.with_shared_state_mutation(game, agents, |_this, game, _agents| {
660                            game.turn.priority_player = priority_player;
661                        });
662                        continue;
663                    }
664                    let activated =
665                        self.with_shared_state_mutation(game, agents, |this, game, agents| {
666                            if ability_idx == STATIC_ALTERNATIVE_ABILITY_INDEX {
667                                let can_play_sorcery = is_main_phase
668                                    && priority_player == game.active_player()
669                                    && game.stack.is_empty();
670                                let (ab, sa) = this.prepare_static_alternative_activated_ability(
671                                    game,
672                                    priority_player,
673                                    card_id,
674                                    can_play_sorcery,
675                                )?;
676                                return this
677                                    .play_prepared_activated_ability_on_stack(
678                                        game,
679                                        agents,
680                                        priority_player,
681                                        card_id,
682                                        &ab,
683                                        sa,
684                                    )
685                                    .then_some(PlaySpellAbilityResult::AbilityActivated);
686                            }
687                            let ability_text = game
688                                .card(card_id)
689                                .activated_abilities
690                                .iter()
691                                .find(|ab| ab.ability_index == ability_idx)
692                                .map(|ab| ab.ability_text.clone())?;
693                            let mut sa = crate::spellability::build_spell_ability(
694                                game,
695                                card_id,
696                                &ability_text,
697                                priority_player,
698                            );
699                            sa.is_activated = true;
700                            this.play_spell_ability(
701                                game,
702                                agents,
703                                priority_player,
704                                PreparedSpellAbility {
705                                    spell_ability: sa,
706                                    activated_ability_index: Some(ability_idx),
707                                    static_alternative_cost_prepared: false,
708                                },
709                            )
710                        });
711                    if activated.is_some() {
712                        // Process triggers immediately after ability activation so
713                        // they go on the stack above the ability (mirroring the
714                        // Play arm and Java's addAndUnfreeze behaviour).
715                        self.with_shared_state_mutation(game, agents, |this, game, agents| {
716                            this.process_triggers(game, agents);
717                        });
718                        passed_count = 0;
719                    }
720                }
721            }
722        }
723        self.with_shared_state_mutation(game, agents, |_this, game, _agents| {
724            game.turn.priority_player = game.active_player();
725        });
726    }
727}