Skip to main content

manabrew_engine/
game_loop.rs

1use std::collections::{HashMap, VecDeque};
2use std::hash::{DefaultHasher, Hasher};
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::Arc;
5
6#[path = "game_loop/mana_action_undo.rs"]
7mod mana_action_undo;
8#[path = "game_loop/trigger_replacement_base.rs"]
9pub mod trigger_replacement_base;
10
11use forge_foundation::{PhaseType, ZoneType};
12
13// Comment test 2
14use crate::ability::effects::{self, EffectContext};
15use crate::agent::{CombatCostAction, MainPhaseAction, ManaCostAction, PlayerAgent};
16use crate::card::Card;
17use crate::combat::{self, CombatState};
18use crate::cost::{self, parse_cost, CostPart};
19use crate::event::RunParams;
20use crate::game::GameState;
21use crate::game_log::GameLog;
22use crate::game_log_entry_type::GameLogEntryType;
23use crate::game_rng::{GameRng, ThreadRngAdapter};
24use crate::game_snapshot::GameSnapshot;
25use crate::ids::{CardId, PlayerId};
26use crate::mana::{self, basic_land_mana_atom, ManaPool};
27use crate::parsing::{keys, Params};
28use crate::spellability::target_restrictions;
29use crate::spellability::{SpellAbility, StackEntry};
30use crate::staticability::layer::apply_continuous_effects;
31use crate::trigger::handler::TriggerHandler;
32use crate::trigger::TriggerType;
33use mana_action_undo::ManaUndoRecord;
34
35// ── GameLoop ────────────────────────────────────────────────────────
36
37/// Drives a complete game from setup through game over.
38pub struct GameLoop {
39    pub mana_pools: Vec<ManaPool>,
40    pub combat: CombatState,
41    pub trigger_handler: TriggerHandler,
42    pub game_log: GameLog,
43    /// Token templates keyed by their script filename stem (e.g. "r_1_1_goblin").
44    /// Populated at game start by the Tauri layer; used by the Token effect handler.
45    pub token_templates: HashMap<String, Card>,
46    /// Token art variant counts: (token_script, edition_code) → count.
47    /// Used for game-RNG parity with Java. When Java creates a token, it calls
48    /// `Aggregates.random(collection)` on a Set of art variants, which consumes
49    /// `nextInt()` once per element. Rust needs to consume the same number of
50    /// RNG calls to keep the game RNG in sync.
51    pub token_art_variants: HashMap<(String, String), usize>,
52    /// Token fallback codes: edition_code → fallback_edition_code.
53    pub token_fallback: HashMap<String, String>,
54    /// Edition release dates: edition_code → "YYYY-MM-DD".
55    pub edition_dates: HashMap<String, String>,
56    /// Pluggable RNG for game effects (shuffles, coin flips, dice rolls).
57    /// Default: ThreadRngAdapter (non-deterministic). For parity testing,
58    /// replace with a JavaRandom-backed implementation.
59    pub game_rng: Box<dyn GameRng>,
60    /// Enables Java-parity snapshot rollback support (`stash_game_state` / `restore_game_state`).
61    pub experimental_restore_snapshot: bool,
62    /// Last stashed snapshot used by rollback flows.
63    previous_game_state: Option<GameSnapshot>,
64    /// Rolling checkpoint history for UI rewind/debug.
65    checkpoints: VecDeque<(u64, String, GameSnapshot)>,
66    next_checkpoint_id: u64,
67    reserved_sacrifice_stack: Vec<Vec<CardId>>,
68    reserved_source_reuse_stack: Vec<bool>,
69    /// Per-player stack of reversible mana actions. The UI consumes this
70    /// through `untappableLandIds`; legality is owned by the engine.
71    mana_undo_stacks: Vec<Vec<ManaUndoRecord>>,
72    mana_undo_disqualified: bool,
73    /// Cooperative shutdown signal. When the host (e.g. Tauri's
74    /// `GameManager::end_game`) flips this flag we short-circuit the
75    /// outer `run()` loop and bail out. Prevents the engine from
76    /// continuing to tick after the user has conceded or returned to
77    /// the main menu — the previous behavior kept the game running
78    /// silently and drove a visible log/prompt loop on the frontend.
79    pub abort_signal: Option<Arc<AtomicBool>>,
80    /// Whether the engine precomputes priority action space before calling
81    /// `PlayerAgent::choose_action`. UI agents use the default `true`; parity
82    /// disables it and requests action space explicitly only when needed.
83    pub provide_priority_action_space: bool,
84}
85
86#[derive(Debug, Clone)]
87pub(crate) struct PreparedSpellAbility {
88    pub spell_ability: SpellAbility,
89    pub activated_ability_index: Option<usize>,
90    pub static_alternative_cost_prepared: bool,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub(crate) enum SpellAbilityLogEventKind {
95    Stack,
96    Action,
97}
98
99#[derive(Debug, Clone)]
100pub(crate) struct StackPushContext {
101    pub source_card: CardId,
102    pub entry: StackEntry,
103    pub pending_stack_id: Option<u32>,
104    pub stack_log_name: String,
105    pub stack_message: String,
106    pub target_card: Option<CardId>,
107    pub event_kind: SpellAbilityLogEventKind,
108    pub move_source_to_stack: bool,
109    pub register_source_trigger: bool,
110}
111
112#[derive(Debug, Clone)]
113pub(crate) struct PostStackTriggerContext {
114    pub source_card: CardId,
115    pub cast_trigger: TriggerType,
116    pub emit_ability_activated: bool,
117    pub emit_waterbend: bool,
118    pub waterbend_cards: Vec<CardId>,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub(crate) enum PlaySpellAbilityResult {
123    CardPlayed { card_id: CardId, card_name: String },
124    AbilityActivated,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128enum TurnMachineState {
129    Untap,
130    Upkeep,
131    Draw,
132    Main1,
133    Combat,
134    Main2,
135    EndOfTurn,
136    Cleanup,
137    Done,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141enum TurnEvent {
142    EnterPhase {
143        phase: PhaseType,
144        emit_phase_trigger: bool,
145    },
146    PriorityWindow {
147        is_main_phase: bool,
148    },
149    UntapStep,
150    DrawStep,
151    CombatStep,
152    CleanupStep,
153    AdvanceTurn,
154}
155
156impl GameLoop {
157    pub fn new(num_players: usize) -> Self {
158        GameLoop {
159            mana_pools: (0..num_players).map(|_| ManaPool::new()).collect(),
160            combat: CombatState::new(),
161            trigger_handler: TriggerHandler::new(),
162            game_log: GameLog::new(),
163            token_templates: HashMap::new(),
164            token_art_variants: HashMap::new(),
165            token_fallback: HashMap::new(),
166            edition_dates: HashMap::new(),
167            game_rng: Box::new(ThreadRngAdapter),
168            experimental_restore_snapshot: false,
169            previous_game_state: None,
170            checkpoints: VecDeque::new(),
171            next_checkpoint_id: 1,
172            reserved_sacrifice_stack: Vec::new(),
173            reserved_source_reuse_stack: Vec::new(),
174            mana_undo_stacks: (0..num_players).map(|_| Vec::new()).collect(),
175            mana_undo_disqualified: false,
176            abort_signal: None,
177            provide_priority_action_space: true,
178        }
179    }
180
181    pub fn set_provide_priority_action_space(&mut self, provide: bool) {
182        self.provide_priority_action_space = provide;
183    }
184
185    /// Install a cooperative abort signal. When the host flips the flag
186    /// the outer `run()` loop exits before the next turn so the game
187    /// thread can wind down cleanly instead of continuing to drive
188    /// prompts at a frontend that has already unmounted.
189    pub fn set_abort_signal(&mut self, signal: Arc<AtomicBool>) {
190        self.abort_signal = Some(signal);
191    }
192
193    fn is_aborted(&self) -> bool {
194        self.abort_signal
195            .as_ref()
196            .map(|s| s.load(Ordering::Relaxed))
197            .unwrap_or(false)
198    }
199
200    /// Register a token template by its script filename stem (e.g. "r_1_1_goblin").
201    /// Called at game start by the Tauri layer for every token script in the token DB.
202    pub fn register_token(&mut self, script_name: impl Into<String>, template: Card) {
203        self.token_templates.insert(script_name.into(), template);
204    }
205
206    /// Get the number of art variants for a token in a given edition.
207    /// Follows TokenFallbackCode chains. Returns 1 if not found.
208    pub fn token_art_variant_count(&self, token_script: &str, edition_code: &str) -> usize {
209        let key = (token_script.to_lowercase(), edition_code.to_uppercase());
210        if let Some(&count) = self.token_art_variants.get(&key) {
211            return count;
212        }
213        if let Some(fallback) = self.token_fallback.get(&edition_code.to_uppercase()) {
214            return self.token_art_variant_count(token_script, fallback);
215        }
216        1
217    }
218
219    pub fn pool(&self, pid: PlayerId) -> &ManaPool {
220        &self.mana_pools[pid.index()]
221    }
222
223    pub fn pool_mut(&mut self, pid: PlayerId) -> &mut ManaPool {
224        &mut self.mana_pools[pid.index()]
225    }
226
227    pub(crate) fn move_card_with_runtime(
228        &mut self,
229        game: &mut GameState,
230        card_id: CardId,
231        dest_zone: ZoneType,
232        dest_owner: PlayerId,
233        agents: &mut [Box<dyn PlayerAgent>],
234    ) {
235        let mut runtime = crate::replacement::replacement_handler::ReplacementRuntime {
236            trigger_handler: &mut self.trigger_handler,
237            token_templates: &self.token_templates,
238            token_art_variants: &self.token_art_variants,
239            token_fallback: &self.token_fallback,
240            edition_dates: &self.edition_dates,
241            mana_pools: &mut self.mana_pools,
242            rng: &mut *self.game_rng,
243        };
244        game.move_card_with_agents_and_replacement_runtime(
245            card_id,
246            dest_zone,
247            dest_owner,
248            agents,
249            &mut runtime,
250        );
251    }
252
253    /// Create a game snapshot. Set `include_stack` false for copy-without-stack flows.
254    pub fn make_snapshot(&self, game: &GameState, include_stack: bool) -> GameSnapshot {
255        GameSnapshot::capture(
256            game,
257            &self.mana_pools,
258            &self.combat,
259            &self.trigger_handler,
260            include_stack,
261        )
262    }
263
264    /// Restore a previously captured snapshot.
265    pub fn restore_snapshot(&mut self, game: &mut GameState, snapshot: &GameSnapshot) {
266        snapshot.restore_game_state(
267            game,
268            &mut self.mana_pools,
269            &mut self.combat,
270            &mut self.trigger_handler,
271        );
272    }
273
274    /// Stash the current state if snapshot rollback is enabled.
275    pub fn stash_game_state(&mut self, game: &GameState) {
276        if self.experimental_restore_snapshot {
277            self.previous_game_state = Some(self.make_snapshot(game, true));
278        }
279    }
280
281    /// Restore from the previously stashed state if available and enabled.
282    pub fn restore_game_state(&mut self, game: &mut GameState) -> bool {
283        if !self.experimental_restore_snapshot {
284            return false;
285        }
286        let Some(snapshot) = self.previous_game_state.as_ref() else {
287            return false;
288        };
289        crate::perf::increment(crate::perf::Metric::SnapshotClones, 1);
290        let snapshot = snapshot.clone();
291        self.restore_snapshot(game, &snapshot);
292        true
293    }
294
295    pub fn restore_checkpoint(&mut self, game: &mut GameState, checkpoint_id: u64) -> bool {
296        let Some((_, _, snapshot)) = self
297            .checkpoints
298            .iter()
299            .find(|(id, _, _)| *id == checkpoint_id)
300        else {
301            return false;
302        };
303        crate::perf::increment(crate::perf::Metric::SnapshotClones, 1);
304        let snapshot = snapshot.clone();
305        self.restore_snapshot(game, &snapshot);
306        true
307    }
308
309    fn record_checkpoint(&mut self, game: &GameState, include_stack: bool) -> (u64, String) {
310        let checkpoint_id = self.next_checkpoint_id;
311        self.next_checkpoint_id += 1;
312        let label = format!(
313            "Turn {} {}",
314            game.turn.turn_number,
315            game.turn.phase.script_name()
316        );
317        let snap = self.make_snapshot(game, include_stack);
318        self.checkpoints
319            .push_back((checkpoint_id, label.clone(), snap));
320        while self.checkpoints.len() > 256 {
321            self.checkpoints.pop_front();
322        }
323        (checkpoint_id, label)
324    }
325
326    pub(crate) fn apply_pending_snapshot_restore(
327        &mut self,
328        game: &mut GameState,
329        agents: &mut [Box<dyn PlayerAgent>],
330    ) -> bool {
331        let mut requested = None;
332        for agent in agents.iter_mut() {
333            if let Some(id) = agent.take_restore_request() {
334                requested = Some(id);
335            }
336        }
337        let Some(checkpoint_id) = requested else {
338            return false;
339        };
340        let restored = self.restore_checkpoint(game, checkpoint_id);
341        if restored {
342            for agent in agents.iter_mut() {
343                agent.snapshot_state(game, &self.mana_pools);
344                agent.notify(crate::agent::notification::GameNotification::StateChanged);
345            }
346        }
347        restored
348    }
349
350    /// Get untapped lands on the battlefield for a player.
351    pub fn get_tappable_lands(&self, game: &GameState, player: PlayerId) -> Vec<CardId> {
352        game.cards_in_zone(ZoneType::Battlefield, player)
353            .to_vec()
354            .into_iter()
355            .filter(|&cid| {
356                let c = game.card(cid);
357                c.is_land() && !c.tapped
358            })
359            .collect()
360    }
361
362    /// Get the top reversible mana source for a player, if any.
363    pub fn get_untappable_lands(
364        &self,
365        _game: &GameState,
366        player: PlayerId,
367        _pool_snapshot: &ManaPool,
368    ) -> Vec<CardId> {
369        self.undoable_mana_sources(player)
370    }
371
372    /// Set up the game: roll for first player, shuffle libraries, draw
373    /// opening hands, run mulligans.
374    pub fn setup(
375        &mut self,
376        game: &mut GameState,
377        agents: &mut [Box<dyn PlayerAgent>],
378        rng: &mut impl rand::Rng,
379    ) {
380        self.roll_for_first_player(game, agents, rng);
381
382        for &pid in &game.player_order.clone() {
383            game.shuffle_library(pid, rng);
384            game.draw_cards(pid, 7);
385        }
386
387        let first_player = game.active_player();
388        crate::mulligan::run_london_mulligans(
389            game,
390            agents,
391            rng,
392            first_player,
393            &self.mana_pools,
394            &self.game_log,
395        );
396    }
397
398    /// Each player rolls a d20; the highest roller goes first. Ties are
399    /// broken by rerolling among the tied players (resolved internally —
400    /// only the final round is surfaced to the UI).
401    ///
402    /// Emits a single `FirstPlayerRoll` notification so the frontend can
403    /// animate every player's die side-by-side. Mutates
404    /// `game.turn.active_player` and `priority_player` to the winner.
405    pub fn roll_for_first_player(
406        &mut self,
407        game: &mut GameState,
408        agents: &mut [Box<dyn PlayerAgent>],
409        rng: &mut impl rand::Rng,
410    ) {
411        const SIDES: i32 = 20;
412        let players: Vec<PlayerId> = game.player_order.clone();
413        if players.len() < 2 {
414            return;
415        }
416
417        // Run the tiebreak loop silently; only the final, decisive round
418        // is broadcast to the UI.
419        let mut contenders: Vec<PlayerId> = players.clone();
420        let (final_rolls, winner) = loop {
421            let rolls: Vec<(PlayerId, i32)> = contenders
422                .iter()
423                .map(|&pid| (pid, rng.gen_range(1..=SIDES)))
424                .collect();
425
426            for (pid, value) in &rolls {
427                let player_name = game.player(*pid).name.clone();
428                self.game_log.log(
429                    GameLogEntryType::Info,
430                    0,
431                    format!("{player_name} rolls a {value} (d{SIDES})"),
432                );
433            }
434
435            let highest = rolls.iter().map(|(_, v)| *v).max().unwrap_or(0);
436            let top: Vec<PlayerId> = rolls
437                .iter()
438                .filter(|(_, v)| *v == highest)
439                .map(|(p, _)| *p)
440                .collect();
441            if top.len() == 1 {
442                break (rolls, top[0]);
443            }
444            self.game_log.log(
445                GameLogEntryType::Info,
446                0,
447                "Tie — rerolling among tied players".to_string(),
448            );
449            contenders = top;
450        };
451
452        let winner_name = game.player(winner).name.clone();
453        self.game_log.log(
454            GameLogEntryType::Info,
455            0,
456            format!("{winner_name} goes first"),
457        );
458
459        for agent in agents.iter_mut() {
460            agent.snapshot_state(game, &self.mana_pools);
461        }
462        crate::agent::game_log::broadcast_notification(
463            agents,
464            crate::agent::notification::GameNotification::FirstPlayerRoll {
465                sides: SIDES,
466                rolls: final_rolls,
467                winner,
468            },
469        );
470        // Wait for every human-driven transport to finish its animation
471        // (in parallel — the prompt was already dispatched to all of
472        // them). AI transports skip this no-op.
473        for agent in agents.iter_mut() {
474            agent.await_display_ack();
475        }
476
477        game.turn.active_player = winner;
478        game.turn.priority_player = winner;
479    }
480
481    /// Run generic "opening hand" actions before the game begins.
482    ///
483    /// Mirrors Java's `GameAction.runOpeningHandActions()`: gather every
484    /// `MayEffectFromOpeningHand` keyword in hand, ask the controller whether
485    /// to use it, and resolve the referenced SVar immediately.
486    pub fn run_opening_hand_actions(
487        &mut self,
488        game: &mut GameState,
489        agents: &mut [Box<dyn PlayerAgent>],
490    ) {
491        let first_player = game.active_player();
492        let mut takes_action = first_player;
493        let mut new_first = first_player;
494
495        loop {
496            let usable = self.collect_opening_hand_actions(game, takes_action, first_player);
497            for mut sa in usable {
498                let Some(source_id) = sa.source else {
499                    continue;
500                };
501                if game.card(source_id).zone != ZoneType::Hand {
502                    continue;
503                }
504
505                agents[takes_action.index()].snapshot_state(game, &self.mana_pools);
506                let card_name = game.card(source_id).card_name.clone();
507                let prompt = sa
508                    .ir
509                    .spell_description_text
510                    .as_deref()
511                    .unwrap_or("Use opening hand effect?");
512                let accepted = agents[takes_action.index()].confirm_action(
513                    takes_action,
514                    Some("FromOpeningHand"),
515                    prompt,
516                    &[],
517                    Some(source_id),
518                    sa.api,
519                );
520                if !accepted {
521                    continue;
522                }
523
524                if sa.uses_targeting() && !sa.setup_targets(game, agents, &self.mana_pools) {
525                    continue;
526                }
527
528                let becomes_starting_player = sa.ir.become_starting_player;
529                let entry = StackEntry {
530                    id: 0,
531                    spell_ability: sa,
532                    is_pending_cast: false,
533                    is_creature_spell: false,
534                    is_permanent_spell: false,
535                    cast_from_zone: Some(ZoneType::Hand),
536                    optional_trigger_decider: None,
537                    optional_trigger_description: None,
538                    optional_trigger_source_name: None,
539                };
540                self.resolve_spell_effect(game, agents, &entry);
541                apply_continuous_effects(game);
542
543                if becomes_starting_player {
544                    new_first = takes_action;
545                }
546            }
547
548            takes_action = game.next_player(takes_action);
549            if takes_action == first_player {
550                break;
551            }
552        }
553
554        if new_first != first_player {
555            game.turn.active_player = new_first;
556            game.turn.priority_player = new_first;
557        }
558    }
559
560    fn collect_opening_hand_actions(
561        &self,
562        game: &GameState,
563        player: PlayerId,
564        first_player: PlayerId,
565    ) -> Vec<SpellAbility> {
566        let mut usable = Vec::new();
567
568        for &card_id in game.cards_in_zone(ZoneType::Hand, player) {
569            let card = game.card(card_id);
570            for kw in card.keywords.as_string_list() {
571                if !kw.starts_with("MayEffectFromOpeningHand") {
572                    continue;
573                }
574                let split: Vec<&str> = kw.split(':').collect();
575                let Some(effect_name) = split.get(1).copied() else {
576                    continue;
577                };
578                if split.get(2).copied() == Some("!PlayFirst") && first_player == player {
579                    continue;
580                }
581                let Some(raw) = card.svars.get(effect_name) else {
582                    continue;
583                };
584                usable.push(crate::spellability::build_spell_ability(
585                    game, card_id, raw, player,
586                ));
587            }
588        }
589
590        usable
591    }
592
593    /// Run the full game until someone wins or loses.
594    /// Returns the winner's PlayerId.
595    pub fn run(
596        &mut self,
597        game: &mut GameState,
598        agents: &mut [Box<dyn PlayerAgent>],
599        rng: &mut impl rand::Rng,
600        max_turns: u32,
601    ) -> Option<PlayerId> {
602        self.setup(game, agents, rng);
603        self.run_opening_hand_actions(game, agents);
604
605        self.trigger_handler.reset_active_triggers(game);
606        self.trigger_handler
607            .run_trigger(TriggerType::NewGame, RunParams::default(), true);
608
609        while !game.game_over && game.turn.turn_number <= max_turns {
610            if self.is_aborted() {
611                // Host requested a shutdown (user conceded / returned to
612                // menu). Mark the game as over without picking a winner
613                // so the agent thread can fall through and drop.
614                game.game_over = true;
615                break;
616            }
617            self.run_turn(game, agents, rng);
618        }
619
620        game.winner
621    }
622
623    /// Run a single turn.
624    pub fn run_turn(
625        &mut self,
626        game: &mut GameState,
627        agents: &mut [Box<dyn PlayerAgent>],
628        _rng: &mut impl rand::Rng,
629    ) {
630        let _perf_scope =
631            crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::GameLoop);
632        let active = game.active_player();
633        let active_name = game.player(active).name.clone();
634
635        // SkipTurn (issue #22): if the active player has skip_turns > 0, skip entirely.
636        if game.player(active).skip_turns > 0 {
637            game.player_decrement_skip_turns(active);
638            self.log_turn_skipped(game, active, game.player(active).skip_turns);
639            // Still advance turn state so the next player gets their turn
640            game.turn.next_player_turn(&game.player_order.clone());
641            return;
642        }
643
644        game.new_turn_for_player(active);
645        self.log_turn_begin(&active_name, game.turn.turn_number);
646
647        // Snapshot + notify all agents of the turn change (display-only, before any actions)
648        let turn_number = game.turn.turn_number;
649        for agent in agents.iter_mut() {
650            agent.snapshot_state(game, &self.mana_pools);
651        }
652        let (checkpoint_id, label) = self.record_checkpoint(game, true);
653        for agent in agents.iter_mut() {
654            agent.notify(
655                crate::agent::notification::GameNotification::SnapshotCreated {
656                    checkpoint_id,
657                    label: label.clone(),
658                },
659            );
660        }
661        for agent in agents.iter_mut() {
662            agent.notify(crate::agent::notification::GameNotification::TurnChanged {
663                active_player: active,
664                turn_number,
665            });
666        }
667
668        // Recompute continuous static effects for the new turn.
669        apply_continuous_effects(game);
670        // Rebuild active triggers after statics so granted triggers are included.
671        self.trigger_handler.reset_active_triggers(game);
672
673        self.run_turn_state_machine(game, agents);
674    }
675
676    pub(crate) fn log_turn_begin(&self, player_name: &str, turn_number: u32) {
677        self.game_log.log(
678            GameLogEntryType::TurnBegin,
679            0,
680            format!("{player_name} turn begins (turn {turn_number})"),
681        );
682    }
683
684    pub(crate) fn log_turn_skipped(
685        &self,
686        game: &GameState,
687        player: PlayerId,
688        remaining_skip_turns: i32,
689    ) {
690        self.game_log.log(
691            GameLogEntryType::TurnSkip,
692            0,
693            format!(
694                "{} turn skipped (remaining skip-turn effects: {})",
695                game.player(player).name,
696                remaining_skip_turns
697            ),
698        );
699    }
700
701    pub(crate) fn log_phase_begin(&self, phase: PhaseType) {
702        self.game_log.log(
703            GameLogEntryType::PhaseBegin,
704            1,
705            format!("Phase {}", phase.script_name()),
706        );
707    }
708
709    pub(crate) fn log_waiting_for_priority(&self, game: &GameState, player: PlayerId) {
710        self.game_log.log(
711            GameLogEntryType::PriorityWaiting,
712            2,
713            format!("Waiting for {} priority response", game.player(player).name),
714        );
715    }
716
717    pub(crate) fn log_priority_response(&self, game: &GameState, player: PlayerId, action: &str) {
718        self.game_log.log(
719            GameLogEntryType::PriorityResponse,
720            2,
721            format!("{} responded with {}", game.player(player).name, action),
722        );
723    }
724
725    pub(crate) fn log_priority_pass(&self, game: &GameState, player: PlayerId) {
726        self.game_log.log(
727            GameLogEntryType::PriorityPass,
728            2,
729            format!("{} passed priority", game.player(player).name),
730        );
731    }
732
733    pub(crate) fn log_stack_push(&self, item_name: &str, player_name: &str) {
734        self.game_log.log(
735            GameLogEntryType::StackPush,
736            2,
737            format!("{item_name} pushed to stack ({player_name})"),
738        );
739    }
740
741    pub(crate) fn log_stack_resolved_item(&self, item_name: &str) {
742        self.game_log.log(
743            GameLogEntryType::StackResolve,
744            2,
745            format!("{item_name} resolved"),
746        );
747    }
748}
749
750/// Helper: run SBA with trigger handler and legend-rule agent callback.
751/// Mirrors Java's GameAction.checkStateEffects() + handleLegendRule() which
752/// delegates the "keep which legendary?" choice to the player controller.
753fn check_sba(
754    game: &mut GameState,
755    trigger_handler: &mut TriggerHandler,
756    agents: &mut [Box<dyn PlayerAgent>],
757) -> bool {
758    let _perf_scope =
759        crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::PrioritySba);
760    let result = game.check_state_based_actions_with_trigger_agents(Some(trigger_handler), agents);
761    if result {
762        // Flush triggers fired during SBA before re-registering. This preserves
763        // triggers from Animate effects (pump_trigger_count) that were active
764        // when creatures died.
765        trigger_handler.flush_waiting_triggers(game);
766        // Re-register triggers after SBA may have moved cards between zones.
767        // This ensures triggers with non-Battlefield active zones (e.g.
768        // TriggerZones$ Graveyard) are registered when cards die.
769        trigger_handler.reset_active_triggers(game);
770    }
771    result
772}
773
774mod action_space;
775mod cast_spell;
776mod combat_phase;
777mod cost_payment;
778mod game_action;
779pub(crate) use game_action::{fire_sacrificed_once_for_batch, perform_sacrifice};
780pub(crate) mod mana_payment;
781mod phase_handler;
782mod playability;
783mod priority;
784mod stack_resolution;
785mod state_observer;
786mod trigger_handler;
787
788#[cfg(test)]
789mod tests {
790    use std::sync::atomic::{AtomicBool, Ordering};
791    use std::sync::{Arc, Mutex};
792
793    use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
794    use rand::SeedableRng;
795
796    use crate::agent::{PlayCardMode, PlayerAgent, TargetChoice};
797    use crate::card::Card;
798    use crate::player::actions::PlayerAction;
799
800    use super::*;
801
802    struct RecordingPassAgent {
803        phases_seen: Arc<Mutex<Vec<PhaseType>>>,
804        bad_priority_seen: Arc<AtomicBool>,
805        last_phase: Option<PhaseType>,
806        last_priority: Option<PlayerId>,
807    }
808
809    struct InvalidPlayAgent;
810    struct OpeningHandAgent {
811        accept: bool,
812    }
813
814    impl PlayerAgent for InvalidPlayAgent {
815        fn mulligan_decision(
816            &mut self,
817            _player: PlayerId,
818            _hand: &[CardId],
819            _mulligan_count: u32,
820        ) -> bool {
821            true
822        }
823
824        fn choose_action(
825            &mut self,
826            player: PlayerId,
827            action_space: Option<&crate::agent::PriorityActionSpace>,
828            request_action_space: &mut dyn FnMut() -> crate::agent::PriorityActionSpace,
829        ) -> PlayerAction {
830            PlayerAction::CastSpell(crate::agent::PlayOption {
831                card_id: CardId(u32::MAX),
832                mode: PlayCardMode::Normal,
833                alt_cost_index: 0,
834            })
835        }
836
837        fn choose_attackers(
838            &mut self,
839            _player: PlayerId,
840            _available: &[CardId],
841            _possible_defenders: &[crate::combat::DefenderId],
842        ) -> Vec<(CardId, crate::combat::DefenderId)> {
843            Vec::new()
844        }
845
846        fn choose_blockers(
847            &mut self,
848            _player: PlayerId,
849            _attackers: &[CardId],
850            _available_blockers: &[CardId],
851            _max_blockers: Option<usize>,
852        ) -> Vec<(CardId, CardId)> {
853            Vec::new()
854        }
855
856        fn choose_target_player(
857            &mut self,
858            _player: PlayerId,
859            valid: &[PlayerId],
860            _sa: Option<&crate::spellability::SpellAbility>,
861        ) -> Option<PlayerId> {
862            valid.first().copied()
863        }
864
865        fn choose_target_card(
866            &mut self,
867            _player: PlayerId,
868            valid: &[CardId],
869            _sa: Option<&crate::spellability::SpellAbility>,
870        ) -> Option<CardId> {
871            valid.first().copied()
872        }
873
874        fn choose_target_any(
875            &mut self,
876            _player: PlayerId,
877            valid_players: &[PlayerId],
878            valid_cards: &[CardId],
879            _sa: Option<&crate::spellability::SpellAbility>,
880        ) -> TargetChoice {
881            if let Some(&pid) = valid_players.first() {
882                TargetChoice::Player(pid)
883            } else if let Some(&cid) = valid_cards.first() {
884                TargetChoice::Card(cid)
885            } else {
886                TargetChoice::None
887            }
888        }
889
890        fn choose_land_or_spell(&mut self, _player: PlayerId) -> Option<bool> {
891            None
892        }
893
894        fn choose_targets_for(
895            &mut self,
896            _sa: &mut crate::spellability::SpellAbility,
897            _game: &GameState,
898            _mana_pools: &[ManaPool],
899        ) -> bool {
900            false
901        }
902
903        fn notify(&mut self, _message: crate::agent::notification::GameNotification) {}
904    }
905
906    impl RecordingPassAgent {
907        fn new(
908            phases_seen: Arc<Mutex<Vec<PhaseType>>>,
909            bad_priority_seen: Arc<AtomicBool>,
910        ) -> Self {
911            Self {
912                phases_seen,
913                bad_priority_seen,
914                last_phase: None,
915                last_priority: None,
916            }
917        }
918    }
919
920    impl PlayerAgent for RecordingPassAgent {
921        fn snapshot_state(&mut self, game: &GameState, _mana_pools: &[ManaPool]) {
922            self.last_phase = Some(game.turn.phase);
923            self.last_priority = Some(game.turn.priority_player);
924        }
925
926        fn mulligan_decision(
927            &mut self,
928            _player: PlayerId,
929            _hand: &[CardId],
930            _mulligan_count: u32,
931        ) -> bool {
932            true
933        }
934
935        fn choose_action(
936            &mut self,
937            player: PlayerId,
938            action_space: Option<&crate::agent::PriorityActionSpace>,
939            request_action_space: &mut dyn FnMut() -> crate::agent::PriorityActionSpace,
940        ) -> PlayerAction {
941            if self.last_priority != Some(player) {
942                self.bad_priority_seen.store(true, Ordering::SeqCst);
943            }
944            if let Some(phase) = self.last_phase {
945                self.phases_seen.lock().unwrap().push(phase);
946            }
947            PlayerAction::PassPriority
948        }
949
950        fn choose_attackers(
951            &mut self,
952            _player: PlayerId,
953            _available: &[CardId],
954            _possible_defenders: &[crate::combat::DefenderId],
955        ) -> Vec<(CardId, crate::combat::DefenderId)> {
956            Vec::new()
957        }
958
959        fn choose_blockers(
960            &mut self,
961            _player: PlayerId,
962            _attackers: &[CardId],
963            _available_blockers: &[CardId],
964            _max_blockers: Option<usize>,
965        ) -> Vec<(CardId, CardId)> {
966            Vec::new()
967        }
968
969        fn choose_target_player(
970            &mut self,
971            _player: PlayerId,
972            valid: &[PlayerId],
973            _sa: Option<&crate::spellability::SpellAbility>,
974        ) -> Option<PlayerId> {
975            valid.first().copied()
976        }
977
978        fn choose_target_card(
979            &mut self,
980            _player: PlayerId,
981            valid: &[CardId],
982            _sa: Option<&crate::spellability::SpellAbility>,
983        ) -> Option<CardId> {
984            valid.first().copied()
985        }
986
987        fn choose_target_any(
988            &mut self,
989            _player: PlayerId,
990            valid_players: &[PlayerId],
991            valid_cards: &[CardId],
992            _sa: Option<&crate::spellability::SpellAbility>,
993        ) -> TargetChoice {
994            if let Some(&pid) = valid_players.first() {
995                TargetChoice::Player(pid)
996            } else if let Some(&cid) = valid_cards.first() {
997                TargetChoice::Card(cid)
998            } else {
999                TargetChoice::None
1000            }
1001        }
1002
1003        fn choose_land_or_spell(&mut self, _player: PlayerId) -> Option<bool> {
1004            None
1005        }
1006
1007        fn choose_targets_for(
1008            &mut self,
1009            _sa: &mut crate::spellability::SpellAbility,
1010            _game: &GameState,
1011            _mana_pools: &[ManaPool],
1012        ) -> bool {
1013            false
1014        }
1015
1016        fn notify(&mut self, _message: crate::agent::notification::GameNotification) {}
1017    }
1018
1019    impl PlayerAgent for OpeningHandAgent {
1020        fn mulligan_decision(
1021            &mut self,
1022            _player: PlayerId,
1023            _hand: &[CardId],
1024            _mulligan_count: u32,
1025        ) -> bool {
1026            true
1027        }
1028
1029        fn choose_action(
1030            &mut self,
1031            player: PlayerId,
1032            action_space: Option<&crate::agent::PriorityActionSpace>,
1033            request_action_space: &mut dyn FnMut() -> crate::agent::PriorityActionSpace,
1034        ) -> PlayerAction {
1035            PlayerAction::PassPriority
1036        }
1037
1038        fn choose_attackers(
1039            &mut self,
1040            _player: PlayerId,
1041            _available: &[CardId],
1042            _possible_defenders: &[crate::combat::DefenderId],
1043        ) -> Vec<(CardId, crate::combat::DefenderId)> {
1044            Vec::new()
1045        }
1046
1047        fn choose_blockers(
1048            &mut self,
1049            _player: PlayerId,
1050            _attackers: &[CardId],
1051            _available_blockers: &[CardId],
1052            _max_blockers: Option<usize>,
1053        ) -> Vec<(CardId, CardId)> {
1054            Vec::new()
1055        }
1056
1057        fn choose_target_player(
1058            &mut self,
1059            _player: PlayerId,
1060            valid: &[PlayerId],
1061            _sa: Option<&crate::spellability::SpellAbility>,
1062        ) -> Option<PlayerId> {
1063            valid.first().copied()
1064        }
1065
1066        fn choose_target_card(
1067            &mut self,
1068            _player: PlayerId,
1069            valid: &[CardId],
1070            _sa: Option<&crate::spellability::SpellAbility>,
1071        ) -> Option<CardId> {
1072            valid.first().copied()
1073        }
1074
1075        fn choose_target_any(
1076            &mut self,
1077            _player: PlayerId,
1078            valid_players: &[PlayerId],
1079            valid_cards: &[CardId],
1080            _sa: Option<&crate::spellability::SpellAbility>,
1081        ) -> TargetChoice {
1082            if let Some(&pid) = valid_players.first() {
1083                TargetChoice::Player(pid)
1084            } else if let Some(&cid) = valid_cards.first() {
1085                TargetChoice::Card(cid)
1086            } else {
1087                TargetChoice::None
1088            }
1089        }
1090
1091        fn choose_land_or_spell(&mut self, _player: PlayerId) -> Option<bool> {
1092            None
1093        }
1094
1095        fn confirm_action(
1096            &mut self,
1097            _player: PlayerId,
1098            _mode: Option<&str>,
1099            _message: &str,
1100            _options: &[String],
1101            _source: Option<crate::ids::CardId>,
1102            _api: Option<crate::ability::api_type::ApiType>,
1103        ) -> bool {
1104            self.accept
1105        }
1106
1107        fn choose_targets_for(
1108            &mut self,
1109            _sa: &mut crate::spellability::SpellAbility,
1110            _game: &GameState,
1111            _mana_pools: &[ManaPool],
1112        ) -> bool {
1113            false
1114        }
1115
1116        fn notify(&mut self, _message: crate::agent::notification::GameNotification) {}
1117    }
1118
1119    fn zero_cost_instant(owner: PlayerId) -> Card {
1120        Card::new(
1121            CardId(0),
1122            "Test Instant".to_string(),
1123            owner,
1124            CardTypeLine::parse("Instant"),
1125            ManaCost::no_cost(),
1126            ColorSet::COLORLESS,
1127            None,
1128            None,
1129            vec![],
1130            vec![],
1131        )
1132    }
1133
1134    fn mana_land(owner: PlayerId, name: &str, produced: &str) -> Card {
1135        Card::new(
1136            CardId(0),
1137            name.to_string(),
1138            owner,
1139            CardTypeLine::parse("Land"),
1140            ManaCost::no_cost(),
1141            ColorSet::COLORLESS,
1142            None,
1143            None,
1144            vec![],
1145            vec![format!(
1146                "AB$ Mana | Cost$ T | Produced$ {} | SpellDescription$ Add mana.",
1147                produced
1148            )],
1149        )
1150    }
1151
1152    fn vanilla_spell(owner: PlayerId, name: &str, cost: &str) -> Card {
1153        Card::new(
1154            CardId(0),
1155            name.to_string(),
1156            owner,
1157            CardTypeLine::parse("Sorcery"),
1158            ManaCost::parse(cost),
1159            ColorSet::COLORLESS,
1160            None,
1161            None,
1162            vec![],
1163            vec![],
1164        )
1165    }
1166
1167    fn evoked_etb_creature(owner: PlayerId) -> Card {
1168        let mut card = Card::new(
1169            CardId(0),
1170            "Mulldrifter Test".to_string(),
1171            owner,
1172            CardTypeLine::parse("Creature - Elemental"),
1173            ManaCost::parse("4 U"),
1174            ColorSet::BLUE,
1175            Some(2),
1176            Some(2),
1177            vec!["Evoke:2 U".to_string()],
1178            vec![],
1179        );
1180
1181        let mut next_trigger_id = 0;
1182        let etb_draw = crate::trigger::parse_trigger(
1183            "Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigDraw | TriggerDescription$ When CARDNAME enters the battlefield, draw two cards.",
1184            &mut next_trigger_id,
1185        )
1186        .expect("valid ETB trigger");
1187        card.add_trigger(etb_draw);
1188        card.base_trigger_count = card.triggers.len();
1189        card.svars.insert(
1190            "TrigDraw".to_string(),
1191            "DB$ Draw | NumCards$ 2 | Defined$ You".to_string(),
1192        );
1193        card
1194    }
1195
1196    fn activated_permanent(
1197        owner: PlayerId,
1198        name: &str,
1199        type_line: &str,
1200        abilities: Vec<&str>,
1201    ) -> Card {
1202        Card::new(
1203            CardId(0),
1204            name.to_string(),
1205            owner,
1206            CardTypeLine::parse(type_line),
1207            ManaCost::no_cost(),
1208            ColorSet::COLORLESS,
1209            None,
1210            None,
1211            vec![],
1212            abilities.into_iter().map(|s| s.to_string()).collect(),
1213        )
1214    }
1215
1216    fn opening_hand_card(owner: PlayerId, name: &str, keyword: &str, svar_text: &str) -> Card {
1217        let mut card = Card::new(
1218            CardId(0),
1219            name.to_string(),
1220            owner,
1221            CardTypeLine::parse("Enchantment"),
1222            ManaCost::parse("2 W"),
1223            ColorSet::WHITE,
1224            None,
1225            None,
1226            vec![keyword.to_string()],
1227            vec![],
1228        );
1229        card.svars
1230            .insert("FromHand".to_string(), svar_text.to_string());
1231        card
1232    }
1233
1234    #[test]
1235    fn priority_round_ignores_illegal_actions() {
1236        let p0 = PlayerId(0);
1237        let p1 = PlayerId(1);
1238        let mut game = GameState::new(&["A", "B"], 20);
1239
1240        let c0 = game.create_card(zero_cost_instant(p0));
1241        let c1 = game.create_card(zero_cost_instant(p1));
1242        game.move_card(c0, ZoneType::Hand, p0);
1243        game.move_card(c1, ZoneType::Hand, p1);
1244
1245        game.turn.active_player = p0;
1246        game.turn.priority_player = p0;
1247        game.turn.phase = PhaseType::Upkeep;
1248
1249        let seen = Arc::new(Mutex::new(Vec::new()));
1250        let bad = Arc::new(AtomicBool::new(false));
1251        let mut agents: Vec<Box<dyn PlayerAgent>> = vec![
1252            Box::new(InvalidPlayAgent),
1253            Box::new(RecordingPassAgent::new(seen, bad)),
1254        ];
1255
1256        let mut game_loop = GameLoop::new(2);
1257        game_loop.priority_round(&mut game, &mut agents, false);
1258
1259        assert!(game.stack.is_empty());
1260        assert!(game.cards_in_zone(ZoneType::Hand, p0).contains(&c0));
1261        assert!(game.cards_in_zone(ZoneType::Hand, p1).contains(&c1));
1262        assert_eq!(game.turn.priority_player, game.active_player());
1263    }
1264
1265    #[test]
1266    fn action_space_excludes_nonland_mana_abilities_from_main_actions() {
1267        let p0 = PlayerId(0);
1268        let mut game = GameState::new(&["A", "B"], 20);
1269
1270        let goose = game.create_card(activated_permanent(
1271            p0,
1272            "Gilded Goose",
1273            "Creature - Bird",
1274            vec![
1275                "AB$ Token | Cost$ 1 G T | TokenScript$ c_a_food_sac | TokenOwner$ You | SpellDescription$ Create a Food Token.",
1276                "AB$ Mana | Cost$ T Sac<1/Food> | Produced$ Any | SpellDescription$ Add one mana of any color.",
1277            ],
1278        ));
1279        let food = game.create_card(activated_permanent(
1280            p0,
1281            "Food Token",
1282            "Artifact Food",
1283            vec!["AB$ GainLife | Cost$ 2 T Sac<1/CARDNAME> | LifeAmount$ 3 | SpellDescription$ You gain 3 life."],
1284        ));
1285        let forest = game.create_card(mana_land(p0, "Forest", "G"));
1286        let island = game.create_card(mana_land(p0, "Island", "U"));
1287
1288        for cid in [goose, food, forest, island] {
1289            game.move_card(cid, ZoneType::Battlefield, p0);
1290            game.card_mut(cid).summoning_sick = false;
1291        }
1292
1293        game.turn.turn_number = 20;
1294        game.turn.active_player = p0;
1295        game.turn.priority_player = p0;
1296        game.turn.phase = PhaseType::Main1;
1297
1298        let gl = GameLoop::new(2);
1299        let action_space = gl.action_space(&game, p0, true);
1300
1301        let has = |cid, idx| {
1302            action_space
1303                .activatable
1304                .iter()
1305                .any(|a| a.card_id == cid && a.ability_index == idx)
1306        };
1307        assert!(has(food, 0));
1308        assert!(has(goose, 0));
1309        assert!(!has(goose, 1));
1310    }
1311
1312    #[test]
1313    fn evoke_keeps_etb_triggers_when_spell_resolves() {
1314        let p0 = PlayerId(0);
1315        let _p1 = PlayerId(1);
1316        let mut game = GameState::new(&["A", "B"], 20);
1317
1318        let evoked = game.create_card(evoked_etb_creature(p0));
1319        game.move_card(evoked, ZoneType::Stack, p0);
1320
1321        let mut sa = SpellAbility::new_simple(Some(evoked), p0, "SP$ Permanent");
1322        sa.alt_cost = Some(crate::spellability::AlternativeCost::Evoke);
1323
1324        game.stack.push(StackEntry {
1325            id: 1,
1326            spell_ability: sa,
1327            is_pending_cast: false,
1328            is_creature_spell: true,
1329            is_permanent_spell: true,
1330            cast_from_zone: Some(ZoneType::Hand),
1331            optional_trigger_decider: None,
1332            optional_trigger_description: None,
1333            optional_trigger_source_name: None,
1334        });
1335
1336        let mut gl = GameLoop::new(2);
1337        let mut agents: Vec<Box<dyn PlayerAgent>> = vec![
1338            Box::new(RecordingPassAgent::new(
1339                Arc::new(Mutex::new(Vec::new())),
1340                Arc::new(AtomicBool::new(false)),
1341            )),
1342            Box::new(RecordingPassAgent::new(
1343                Arc::new(Mutex::new(Vec::new())),
1344                Arc::new(AtomicBool::new(false)),
1345            )),
1346        ];
1347
1348        gl.resolve_stack(&mut game, &mut agents);
1349        gl.process_triggers(&mut game, &mut agents);
1350
1351        assert_eq!(game.card(evoked).zone, ZoneType::Battlefield);
1352        assert!(
1353            game.stack
1354                .iter()
1355                .any(|entry| entry.spell_ability.api
1356                    == Some(crate::ability::api_type::ApiType::Draw)),
1357            "ETB draw trigger should be on stack for an evoked creature"
1358        );
1359        assert!(
1360            game.stack.iter().any(|entry| entry.spell_ability.api
1361                == Some(crate::ability::api_type::ApiType::Sacrifice)),
1362            "Evoke sacrifice trigger should be on stack"
1363        );
1364    }
1365
1366    #[test]
1367    fn opening_hand_action_resolves_generic_keyword_effect() {
1368        let p0 = PlayerId(0);
1369        let p1 = PlayerId(1);
1370        let mut game = GameState::new(&["A", "B"], 20);
1371        game.turn.active_player = p0;
1372        game.turn.priority_player = p0;
1373
1374        let card_id = game.create_card(opening_hand_card(
1375            p0,
1376            "Opening Hand Test",
1377            "MayEffectFromOpeningHand:FromHand",
1378            "DB$ ChangeZone | Defined$ Self | Origin$ Hand | Destination$ Battlefield | SpellDescription$ Test opening hand action.",
1379        ));
1380        game.move_card(card_id, ZoneType::Hand, p0);
1381
1382        let mut agents: Vec<Box<dyn PlayerAgent>> = vec![
1383            Box::new(OpeningHandAgent { accept: true }),
1384            Box::new(OpeningHandAgent { accept: true }),
1385        ];
1386
1387        let mut game_loop = GameLoop::new(2);
1388        game_loop.run_opening_hand_actions(&mut game, &mut agents);
1389
1390        assert_eq!(game.card(card_id).zone, ZoneType::Battlefield);
1391        assert!(game
1392            .cards_in_zone(ZoneType::Battlefield, p0)
1393            .contains(&card_id));
1394        assert!(!game.cards_in_zone(ZoneType::Hand, p0).contains(&card_id));
1395        let _ = p1;
1396    }
1397
1398    #[test]
1399    fn opening_hand_action_respects_not_play_first_restriction() {
1400        let p0 = PlayerId(0);
1401        let p1 = PlayerId(1);
1402        let mut game = GameState::new(&["A", "B"], 20);
1403        game.turn.active_player = p0;
1404        game.turn.priority_player = p0;
1405
1406        let card_id = game.create_card(opening_hand_card(
1407            p0,
1408            "Opening Hand Skip Test",
1409            "MayEffectFromOpeningHand:FromHand:!PlayFirst",
1410            "DB$ ChangeZone | Defined$ Self | Origin$ Hand | Destination$ Battlefield | SpellDescription$ Test opening hand action.",
1411        ));
1412        game.move_card(card_id, ZoneType::Hand, p0);
1413
1414        let mut agents: Vec<Box<dyn PlayerAgent>> = vec![
1415            Box::new(OpeningHandAgent { accept: true }),
1416            Box::new(OpeningHandAgent { accept: true }),
1417        ];
1418
1419        let mut game_loop = GameLoop::new(2);
1420        game_loop.run_opening_hand_actions(&mut game, &mut agents);
1421
1422        assert_eq!(game.card(card_id).zone, ZoneType::Hand);
1423        assert!(game.cards_in_zone(ZoneType::Hand, p0).contains(&card_id));
1424        let _ = p1;
1425    }
1426}