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