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