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 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 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 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 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 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 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 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 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 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 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}