Skip to main content

manabrew_engine/agent/
mod.rs

1use crate::agent::notification::GameNotification;
2use crate::card::CounterType;
3use crate::combat::DefenderId;
4use crate::cost::payment_decision::PaymentDecision;
5use crate::cost::CostPart;
6use crate::game::GameState;
7use crate::ids::{CardId, PlayerId};
8use crate::mana::ManaPool;
9use crate::player::actions::PlayerAction;
10use crate::spellability::SpellAbility;
11
12pub mod attach_ai;
13pub mod creature_evaluator;
14pub mod game_log;
15pub mod notification;
16pub mod types;
17
18pub use game_log::*;
19pub use types::*;
20
21/// A held pass-priority-until target: keep auto-passing until `player` reaches
22/// `phase` in turn order.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct PassUntilTarget {
25    pub player: PlayerId,
26    pub phase: forge_foundation::PhaseType,
27}
28
29/// Trait for player decision-making. Decouples the engine from UI/AI.
30/// Implementations can be interactive (prompt user), AI, or network-driven.
31pub trait PlayerAgent {
32    /// Called before each agent decision point with the current game state.
33    /// Override this to capture snapshots for a UI or network layer.
34    fn snapshot_state(&mut self, _game: &GameState, _mana_pools: &[ManaPool]) {}
35
36    /// Poll and clear any pending snapshot-restore request from this agent.
37    fn take_restore_request(&mut self) -> Option<u64> {
38        None
39    }
40
41    fn reveal_cards(
42        &mut self,
43        _game: &GameState,
44        _player: PlayerId,
45        _cards: &[CardId],
46        _zone: forge_foundation::ZoneType,
47        _owner: PlayerId,
48        _message_prefix: Option<&str>,
49    ) {
50    }
51
52    /// Returns the `(player, phase)` slot this player auto-passes until.
53    /// A stop is genuinely `(player, phase)`: "pass until Player2's end" is
54    /// distinct from "pass until my end". The declaration is HELD across
55    /// priority windows — the engine does not consume it each pass — and is
56    /// cleared only when the target is reached or a meaningful event occurs.
57    /// `None` = no standing pass-until (prompt normally).
58    fn get_pass_until(&self) -> Option<PassUntilTarget> {
59        None
60    }
61
62    /// Clear the pass-until declaration (target reached, cast, attackers
63    /// declared, …).
64    fn clear_pass_until(&mut self) {}
65
66    /// Choose whether to keep the current opening hand or mulligan.
67    /// `mulligan_count` is the number of mulligans already taken this game.
68    /// Returns true to keep, false to mulligan.
69    fn mulligan_decision(&mut self, player: PlayerId, hand: &[CardId], mulligan_count: u32)
70        -> bool;
71
72    /// Fire the mulligan prompt without blocking for a response.
73    /// Default: no-op. UI agents override to decouple prompt dispatch from
74    /// response collection so multiple players can be prompted in parallel.
75    fn mulligan_decision_send(
76        &mut self,
77        _player: PlayerId,
78        _hand: &[CardId],
79        _mulligan_count: u32,
80    ) {
81    }
82
83    /// Block waiting for the mulligan response previously sent via
84    /// `mulligan_decision_send`. Default falls back to the blocking
85    /// `mulligan_decision` so agents that don't split send/recv still work.
86    fn mulligan_decision_recv(
87        &mut self,
88        player: PlayerId,
89        hand: &[CardId],
90        mulligan_count: u32,
91    ) -> bool {
92        self.mulligan_decision(player, hand, mulligan_count)
93    }
94
95    /// London Mulligan: after keeping, choose `count` cards from hand to put
96    /// on the bottom of the library. Returns exactly `count` card IDs.
97    /// Default: picks the first `count` cards (suitable for simple AI agents).
98    fn choose_cards_to_bottom(
99        &mut self,
100        _player: PlayerId,
101        hand: &[CardId],
102        count: usize,
103    ) -> Vec<CardId> {
104        hand.iter().copied().take(count).collect()
105    }
106
107    /// Fire the put-back prompt without blocking. Default: no-op.
108    fn choose_cards_to_bottom_send(&mut self, _player: PlayerId, _hand: &[CardId], _count: usize) {}
109
110    /// Block waiting for the put-back response. Default falls back to the
111    /// blocking `choose_cards_to_bottom`.
112    fn choose_cards_to_bottom_recv(
113        &mut self,
114        player: PlayerId,
115        hand: &[CardId],
116        count: usize,
117    ) -> Vec<CardId> {
118        self.choose_cards_to_bottom(player, hand, count)
119    }
120
121    /// Choose a main-phase action: play a card from hand, tap a land for mana, untap a land,
122    /// activate an ability, or pass.
123    /// `tappable_lands` lists untapped lands available for tapping.
124    /// `untappable_lands` lists source IDs whose most recent mana action can be undone.
125    /// `activatable` lists (card_id, ability_index) pairs for activated abilities that can be used.
126    fn choose_action(
127        &mut self,
128        player: PlayerId,
129        action_space: Option<&PriorityActionSpace>,
130        request_action_space: &mut dyn FnMut() -> PriorityActionSpace,
131    ) -> PlayerAction;
132
133    /// Choose attackers from available creatures, assigning each to a defender.
134    /// `possible_defenders` lists valid attack targets (opponent players + their planeswalkers).
135    /// Returns (attacker, defender) pairs.
136    fn choose_attackers(
137        &mut self,
138        player: PlayerId,
139        available: &[CardId],
140        possible_defenders: &[DefenderId],
141    ) -> Vec<(CardId, DefenderId)>;
142
143    /// Choose which attackers to exert.
144    /// Input is the subset of already-declared attackers that can pay an Exert
145    /// optional attack cost. Return a subset of `attackers`.
146    /// Default: choose none.
147    fn exert_attackers(&mut self, _player: PlayerId, _attackers: &[CardId]) -> Vec<CardId> {
148        vec![]
149    }
150
151    /// Choose which attackers to enlist.
152    /// Input is the subset of already-declared attackers that can pay an Enlist
153    /// optional attack cost. Return a subset of `attackers`.
154    /// Default: choose none.
155    fn enlist_attackers(&mut self, _player: PlayerId, _attackers: &[CardId]) -> Vec<CardId> {
156        vec![]
157    }
158
159    /// Choose blockers. Returns pairs of (blocker, attacker).
160    /// `max_blockers` is the BlockRestrict limit (if any) — agent should stop after this many.
161    fn choose_blockers(
162        &mut self,
163        player: PlayerId,
164        attackers: &[CardId],
165        available_blockers: &[CardId],
166        max_blockers: Option<usize>,
167    ) -> Vec<(CardId, CardId)>;
168
169    /// Choose one attacker for a specific blocker during sequential declaration.
170    ///
171    /// Return `Some(attacker_id)` to assign this blocker, or `None` to leave it
172    /// unassigned. Default behavior maps through `choose_blockers` for the single
173    /// blocker, preserving existing agent behavior when not overridden.
174    fn choose_blocker_for(
175        &mut self,
176        player: PlayerId,
177        attackers: &[CardId],
178        blocker: CardId,
179    ) -> Option<CardId> {
180        let pairs = self.choose_blockers(player, attackers, &[blocker], None);
181        pairs
182            .into_iter()
183            .find_map(|(b, a)| if b == blocker { Some(a) } else { None })
184    }
185
186    /// Choose the order in which an attacker assigns damage to its blockers.
187    /// The attacker must assign lethal damage to each blocker in order before
188    /// assigning damage to the next one.
189    /// Returns a permutation of `blockers` in the desired assignment order.
190    /// Default: return blockers as-is (no reordering).
191    fn choose_damage_assignment_order(
192        &mut self,
193        _player: PlayerId,
194        _attacker: CardId,
195        blockers: &[CardId],
196    ) -> Vec<CardId> {
197        blockers.to_vec()
198    }
199
200    /// Choose exact combat damage assignment for one blocked attacker.
201    ///
202    /// `blockers_in_order` are in assignment order. `defender_id` is provided
203    /// only when damage can legally be assigned to the defender (e.g. trample).
204    ///
205    /// Return pairs of `(assignee, damage)` where:
206    /// - `Some(card_id)` assigns to a blocker
207    /// - `None` assigns to defender
208    ///
209    fn assign_combat_damage(
210        &mut self,
211        game: &GameState,
212        _player: PlayerId,
213        attacker: CardId,
214        blockers_in_order: &[CardId],
215        defender_id: Option<DefenderId>,
216        damage_to_assign: i32,
217    ) -> Vec<(Option<CardId>, i32)> {
218        let mut out: Vec<(Option<CardId>, i32)> = Vec::new();
219        if damage_to_assign <= 0 {
220            return out;
221        }
222
223        let mut dmg_left = damage_to_assign;
224        let has_deathtouch = game.card(attacker).has_deathtouch();
225        let can_assign_to_defender = defender_id.is_some() && game.card(attacker).has_trample();
226        let mut last_blocker: Option<CardId> = None;
227
228        for &blocker_id in blockers_in_order {
229            if dmg_left <= 0 {
230                break;
231            }
232            if game.card(blocker_id).zone != forge_foundation::ZoneType::Battlefield {
233                continue;
234            }
235            if crate::staticability::static_ability_colorless_damage_source::target_is_protected_from_source(
236                &game.cards,
237                game.card(blocker_id),
238                game.card(attacker),
239            ) {
240                continue;
241            }
242            last_blocker = Some(blocker_id);
243
244            let blocker_card = game.card(blocker_id);
245            let is_indestructible = blocker_card.has_keyword("Indestructible");
246            let attacker_has_wither =
247                game.card(attacker).has_wither() || game.card(attacker).has_infect();
248            let lethal = if is_indestructible && !attacker_has_wither {
249                // Can't kill by damage — assign all remaining (mirrors maxDamage + 1)
250                dmg_left + 1
251            } else if has_deathtouch {
252                1
253            } else if blocker_card.type_line.is_planeswalker() {
254                blocker_card
255                    .counter_count(&crate::card::CounterType::Loyalty)
256                    .max(0)
257            } else {
258                (blocker_card.toughness() - blocker_card.damage).max(0)
259            };
260            let assign = lethal.min(dmg_left);
261            if assign > 0 {
262                out.push((Some(blocker_id), assign));
263                dmg_left -= assign;
264            }
265        }
266
267        if dmg_left > 0 {
268            if can_assign_to_defender {
269                out.push((None, dmg_left));
270            } else if let Some(last) = last_blocker {
271                if let Some((_, amount)) = out
272                    .iter_mut()
273                    .find(|(assignee, _)| assignee.map(|id| id == last).unwrap_or(false))
274                {
275                    *amount += dmg_left;
276                } else {
277                    out.push((Some(last), dmg_left));
278                }
279            }
280        }
281        out
282    }
283
284    fn choose_targets_for(
285        &mut self,
286        sa: &mut SpellAbility,
287        game: &GameState,
288        mana_pools: &[ManaPool],
289    ) -> bool;
290
291    /// Choose a target player (e.g. for Lightning Bolt targeting a player).
292    /// `sa` is the active spell ability context (source card, API type, etc.) for UI display.
293    fn choose_target_player(
294        &mut self,
295        player: PlayerId,
296        valid: &[PlayerId],
297        sa: Option<&SpellAbility>,
298    ) -> Option<PlayerId>;
299
300    /// Choose a target card (e.g. for Lightning Bolt targeting a creature).
301    fn choose_target_card(
302        &mut self,
303        player: PlayerId,
304        valid: &[CardId],
305        sa: Option<&SpellAbility>,
306    ) -> Option<CardId>;
307
308    /// Choose a target card from a specific zone (e.g. Raise Dead from graveyard).
309    fn choose_target_card_from_zone(
310        &mut self,
311        player: PlayerId,
312        _zone: forge_foundation::ZoneType,
313        valid: &[CardId],
314        sa: Option<&SpellAbility>,
315    ) -> Option<CardId> {
316        self.choose_target_card(player, valid, sa)
317    }
318
319    /// Choose a target that can be a player or a card (e.g. "any target").
320    fn choose_target_any(
321        &mut self,
322        player: PlayerId,
323        valid_players: &[PlayerId],
324        valid_cards: &[CardId],
325        sa: Option<&SpellAbility>,
326    ) -> TargetChoice;
327
328    /// Choose one permanent to sacrifice/select from the valid options.
329    /// `sa` is the active spell ability context for UI display.
330    /// Default picks the first (used by AI agents).
331    fn choose_sacrifice(
332        &mut self,
333        _player: PlayerId,
334        valid: &[CardId],
335        _source: Option<CardId>,
336    ) -> Option<CardId> {
337        valid.first().copied()
338    }
339
340    /// Distribute the looked-at Scry cards across the zones. Returns one ordered
341    /// pile per zone — `[top, bottom]` — where the last id in each pile is placed
342    /// on top of that pile. Default: keep everything on top, nothing to bottom.
343    fn choose_scry(
344        &mut self,
345        _game: &GameState,
346        _player: PlayerId,
347        _source: Option<CardId>,
348        cards: &[CardId],
349    ) -> Vec<Vec<CardId>> {
350        vec![cards.to_vec(), vec![]]
351    }
352
353    /// Distribute the looked-at Surveil cards: `[top, graveyard]` ordered piles.
354    /// Default: keep everything on top, nothing milled.
355    fn choose_surveil(
356        &mut self,
357        _game: &GameState,
358        _player: PlayerId,
359        _source: Option<CardId>,
360        cards: &[CardId],
361    ) -> Vec<Vec<CardId>> {
362        vec![cards.to_vec(), vec![]]
363    }
364
365    /// Choose up to `max` cards from `valid` to move to the destination zone (Dig effect).
366    /// `optional` means the player is not required to choose any.
367    /// Default: take first `max` cards.
368    fn choose_dig(
369        &mut self,
370        _game: &GameState,
371        _player: PlayerId,
372        valid: &[CardId],
373        max: usize,
374        _optional: bool,
375    ) -> Vec<CardId> {
376        valid.iter().copied().take(max).collect()
377    }
378
379    /// Choose an ordering for the top N cards being put back on the library (Ponder/Reorder).
380    /// Returns the cards in desired order: index 0 will be placed deepest, last will be on top.
381    /// Default: keep original order.
382    fn choose_reorder_library(
383        &mut self,
384        _game: &GameState,
385        _player: PlayerId,
386        cards: &[CardId],
387    ) -> Vec<CardId> {
388        cards.to_vec()
389    }
390
391    /// Choose which cards to discard from hand (for SP$ Discard effects).
392    /// `hand` is the full hand, `num` is how many must be discarded.
393    /// Default: discard the first `num` cards.
394    fn choose_discard(&mut self, _player: PlayerId, hand: &[CardId], num: usize) -> Vec<CardId> {
395        hand.iter().copied().take(num).collect()
396    }
397
398    /// Choose any number of cards to discard (for `AnyNumber$ True` on
399    /// SP$/DB$ Discard). The agent may pick 0..=hand.len() cards.
400    /// Default: discard `min` cards (the minimum forced amount).
401    fn choose_discard_any_number(
402        &mut self,
403        _player: PlayerId,
404        hand: &[CardId],
405        min: usize,
406        max: usize,
407    ) -> Vec<CardId> {
408        let _ = max;
409        hand.iter().copied().take(min).collect()
410    }
411
412    /// Choose cards to discard at random (for Mode$ Random discard, e.g. Hypnotic Specter).
413    /// The engine calls this instead of `choose_discard` when the discard is random.
414    /// Default: discard the first `num` cards (same as choose_discard).
415    /// Deterministic agents should override this to use their seeded RNG.
416    fn choose_random_discard(
417        &mut self,
418        _player: PlayerId,
419        hand: &[CardId],
420        num: usize,
421    ) -> Vec<CardId> {
422        hand.iter().copied().take(num).collect()
423    }
424
425    /// Choose a target spell on the stack (for SP$ Counter effects).
426    /// `valid` is a slice of stack entry IDs.
427    /// Default: target the first (topmost) spell.
428    fn choose_target_spell(
429        &mut self,
430        _player: PlayerId,
431        valid: &[u32],
432        _source: Option<CardId>,
433    ) -> Option<u32> {
434        valid.first().copied()
435    }
436
437    /// Choose N modes for a modal spell (SP$ Charm / Commands).
438    ///
439    /// `descriptions` — human-readable description of each mode.
440    /// `min` — minimum number of modes to choose.
441    /// `max` — maximum number of modes to choose.
442    ///
443    /// Returns indices into `descriptions` of the chosen modes, in order.
444    /// Default: choose the first `min` modes (index 0, 1, …).
445    fn choose_mode(
446        &mut self,
447        _player: PlayerId,
448        descriptions: &[String],
449        min: usize,
450        _max: usize,
451        _source_card_id: Option<CardId>,
452    ) -> Vec<usize> {
453        (0..min.min(descriptions.len())).collect()
454    }
455
456    fn choose_spell_abilities_for_effect(
457        &mut self,
458        _player: PlayerId,
459        abilities: &[SpellAbility],
460        num: usize,
461    ) -> Vec<usize> {
462        (0..num.min(abilities.len())).collect()
463    }
464
465    /// Choose exactly one entity (Card or Player) from a candidate list.
466    fn choose_single_entity_for_effect(
467        &mut self,
468        _player: PlayerId,
469        valid: &[GameEntity],
470        _is_optional: bool,
471    ) -> Option<GameEntity> {
472        valid.first().copied()
473    }
474
475    fn get_ability_to_play(
476        &mut self,
477        _player: PlayerId,
478        abilities: &[SpellAbility],
479    ) -> Option<usize> {
480        if abilities.is_empty() {
481            None
482        } else {
483            Some(0)
484        }
485    }
486
487    /// Choose which legendary permanent to keep when the legend rule applies.
488    /// `duplicates` contains all legendaries with the same name controlled by this player.
489    /// Returns the CardId of the one to keep; the rest are sacrificed.
490    fn choose_legend_keep(&mut self, _player: PlayerId, duplicates: &[CardId]) -> CardId {
491        duplicates[0]
492    }
493
494    /// Choose whether an optional triggered ability fires.
495    /// `description` is the trigger text shown to the player.
496    /// `source` is the engine card id of the source card (for UI display).
497    /// `api` is the spell ability API type.
498    /// Returns true to allow the trigger, false to decline.
499    /// Default: always allow (non-interactive agents accept all optional triggers).
500    fn choose_optional_trigger(
501        &mut self,
502        _player: PlayerId,
503        _description: &str,
504        _source: Option<CardId>,
505        _api: Option<crate::ability::api_type::ApiType>,
506    ) -> bool {
507        true
508    }
509
510    fn confirm_replacement_effect(
511        &mut self,
512        _player: PlayerId,
513        _question: &str,
514        _effect_description: &str,
515        _source: Option<CardId>,
516    ) -> bool {
517        true
518    }
519
520    /// Generic confirmation hook for optional effect prompts that don't yet
521    /// have a dedicated typed callback in the Rust agent interface.
522    ///
523    /// Returns true to accept/confirm, false to decline.
524    fn confirm_action(
525        &mut self,
526        _player: PlayerId,
527        _mode: Option<&str>,
528        _message: &str,
529        _options: &[String],
530        _source: Option<CardId>,
531        _api: Option<crate::ability::api_type::ApiType>,
532    ) -> bool {
533        false
534    }
535
536    fn confirm_payment(
537        &mut self,
538        player: PlayerId,
539        cost_kind: &str,
540        message: &str,
541        source: Option<CardId>,
542        api: Option<crate::ability::api_type::ApiType>,
543    ) -> bool {
544        let _ = (player, cost_kind, message, source, api);
545        true
546    }
547
548    fn pay_cost_to_prevent_effect(
549        &mut self,
550        player: PlayerId,
551        cost_kind: &str,
552        message: &str,
553        source: Option<CardId>,
554        api: Option<crate::ability::api_type::ApiType>,
555        can_pay: bool,
556        targets: &[GameEntity],
557        effect_text: &str,
558    ) -> bool {
559        let _ = (targets, effect_text);
560        if !can_pay {
561            return false;
562        }
563        self.confirm_payment(player, cost_kind, message, source, api)
564    }
565
566    fn choose_binary(
567        &mut self,
568        player: PlayerId,
569        question: &str,
570        kind: BinaryChoiceKind,
571        _default_choice: Option<bool>,
572        source: Option<CardId>,
573        api: Option<crate::ability::api_type::ApiType>,
574    ) -> bool {
575        let (left, right) = kind.labels();
576        self.confirm_action(
577            player,
578            Some(kind.as_str()),
579            question,
580            &[right.to_string(), left.to_string()],
581            source,
582            api,
583        )
584    }
585
586    /// Choose whether to pay the kicker cost for a spell.
587    /// `kicker_cost` is the mana cost string (e.g. "W", "2 R").
588    /// `source` is the name of the spell being cast (for UI display).
589    /// Returns true to kick, false to cast without kicker.
590    /// Default: don't kick (AI default).
591    fn choose_kicker(
592        &mut self,
593        _player: PlayerId,
594        _kicker_cost: &str,
595        _source: Option<CardId>,
596    ) -> bool {
597        false
598    }
599
600    /// Assist: another player asks if we'll help pay generic mana.
601    /// Returns how much generic mana to pay (0 = decline). Default: decline.
602    fn help_pay_assist(&mut self, _player: PlayerId, _card_name: &str, _max_generic: u32) -> u32 {
603        0
604    }
605
606    /// Choose whether to pay the buyback cost for a spell.
607    /// Returns true to pay buyback, false to cast normally.
608    /// Default: don't pay buyback.
609    fn choose_buyback(
610        &mut self,
611        _player: PlayerId,
612        _buyback_cost: &str,
613        _source: Option<CardId>,
614    ) -> bool {
615        false
616    }
617
618    /// Choose how many times to pay the multikicker cost.
619    /// `max_kicks` is the maximum affordable.
620    /// Returns the number of times to kick (0 to max_kicks).
621    /// Default: 0 (don't multikick).
622    fn choose_multikicker(
623        &mut self,
624        _player: PlayerId,
625        _cost: &str,
626        _max_kicks: u32,
627        _source: Option<CardId>,
628    ) -> u32 {
629        0
630    }
631
632    /// Choose how many times to pay the replicate cost.
633    /// `max_replicates` is the maximum affordable.
634    /// Returns the number of replicates.
635    /// Default: 0.
636    fn choose_replicate(
637        &mut self,
638        _player: PlayerId,
639        _cost: &str,
640        _max_replicates: u32,
641        _source: Option<CardId>,
642    ) -> u32 {
643        0
644    }
645
646    /// Choose a color (for ChooseColorEffect).
647    /// `valid_colors` lists the legal color choices (e.g. ["White","Blue","Black","Red","Green"]).
648    /// Default: pick the first valid color.
649    fn choose_color(&mut self, _player: PlayerId, valid_colors: &[String]) -> Option<String> {
650        valid_colors.first().cloned()
651    }
652
653    /// Choose one or more colors.
654    fn choose_colors(
655        &mut self,
656        _player: PlayerId,
657        valid_colors: &[String],
658        min: usize,
659        max: usize,
660    ) -> Vec<String> {
661        let hi = max.min(valid_colors.len());
662        let lo = min.min(hi);
663        valid_colors.iter().take(lo).cloned().collect()
664    }
665
666    /// Choose cards for an effect (ChooseCardEffect, CloneEffect, etc.).
667    /// `valid` lists eligible card IDs, `min`/`max` are the selection bounds.
668    /// Default: pick up to `max` from the front of `valid`.
669    fn choose_cards_for_effect(
670        &mut self,
671        _player: PlayerId,
672        valid: &[CardId],
673        _min: usize,
674        max: usize,
675    ) -> Vec<CardId> {
676        valid.iter().copied().take(max).collect()
677    }
678
679    /// Choose cards to tap for a `tapXType` cost that has a total-power floor
680    /// such as Crew. `card_powers` carries the effective tap-power value for
681    /// each candidate under the active ability; `card_sort_powers` carries the
682    /// normal net power value used by Forge's deterministic cost plumbing when
683    /// ordering candidates.
684    fn choose_tap_type_for_cost(
685        &mut self,
686        player: PlayerId,
687        valid: &[CardId],
688        _min_total_power: i32,
689        _card_powers: &[(CardId, i32)],
690        _card_sort_powers: &[(CardId, i32)],
691        _sa: Option<&SpellAbility>,
692    ) -> Vec<CardId> {
693        self.choose_cards_for_effect(player, valid, 1, valid.len())
694    }
695
696    /// Choose game entities (players and/or permanents) for an effect like Proliferate.
697    fn choose_entities_for_effect(
698        &mut self,
699        _player: PlayerId,
700        candidates: &[GameEntity],
701        _min: usize,
702        max: usize,
703    ) -> Vec<GameEntity> {
704        candidates.iter().copied().take(max).collect()
705    }
706
707    /// Choose a single card for hidden-origin zone changes (e.g. library search).
708    fn choose_single_card_for_zone_change(
709        &mut self,
710        _game: &GameState,
711        player: PlayerId,
712        valid: &[CardId],
713        _select_prompt: &str,
714        _is_optional: bool,
715    ) -> Option<CardId> {
716        self.choose_cards_for_effect(player, valid, 1, 1)
717            .into_iter()
718            .next()
719    }
720
721    /// Choose multiple cards for hidden-origin zone changes (e.g. tutor multi-select).
722    fn choose_cards_for_zone_change(
723        &mut self,
724        _game: &GameState,
725        player: PlayerId,
726        valid: &[CardId],
727        min: usize,
728        max: usize,
729        _select_prompt: &str,
730    ) -> Vec<CardId> {
731        self.choose_cards_for_effect(player, valid, min, max)
732    }
733
734    /// Choose a creature/card type (for ChooseType effect).
735    /// `type_category` is "Creature", "Card", "Land", etc.
736    /// `valid_types` lists the legal type choices.
737    /// Default: pick the first valid type.
738    fn choose_type(
739        &mut self,
740        _player: PlayerId,
741        _type_category: &str,
742        valid_types: &[String],
743    ) -> Option<String> {
744        valid_types.first().cloned()
745    }
746
747    /// Choose a counter type.
748    fn choose_counter_type(
749        &mut self,
750        _player: PlayerId,
751        options: &[CounterType],
752        _prompt: &str,
753    ) -> Option<CounterType> {
754        options.first().cloned()
755    }
756
757    /// Choose a card name (for NameCard effect).
758    /// `valid_names` lists the legal card name choices (for ChooseFromList mode).
759    /// Default: pick the first valid name.
760    fn choose_card_name(&mut self, _player: PlayerId, valid_names: &[String]) -> Option<String> {
761        valid_names.first().cloned()
762    }
763
764    /// Choose a number within `[min, max]`. `title`/`description` present the
765    /// choice and `source` is the card driving it (shown in the prompt).
766    /// Default: pick the minimum.
767    fn choose_number(
768        &mut self,
769        _player: PlayerId,
770        _source: Option<CardId>,
771        _title: &str,
772        _description: Option<&str>,
773        min: i32,
774        _max: i32,
775    ) -> Option<i32> {
776        Some(min)
777    }
778
779    /// Choose how many times to pay an optional keyword cost.
780    /// Default: decline optional keyword costs.
781    fn choose_number_for_keyword_cost(
782        &mut self,
783        _player: PlayerId,
784        _max: i32,
785        _prompt: &str,
786        _source: Option<CardId>,
787    ) -> i32 {
788        0
789    }
790
791    /// Choose one number from an explicit list of legal rolled values.
792    fn choose_number_from_list(
793        &mut self,
794        _player: PlayerId,
795        choices: &[i32],
796        _message: &str,
797        _source_card_id: Option<CardId>,
798    ) -> Option<i32> {
799        choices.first().copied()
800    }
801
802    /// Choose one die result from a rolled list to ignore.
803    fn choose_roll_to_ignore(
804        &mut self,
805        _player: PlayerId,
806        rolls: &[i32],
807        _source: Option<CardId>,
808    ) -> Option<i32> {
809        rolls.first().copied()
810    }
811
812    /// Choose one rolled result to exchange with a card's power or toughness.
813    fn choose_roll_to_swap(
814        &mut self,
815        _player: PlayerId,
816        rolls: &[i32],
817        _source: Option<CardId>,
818    ) -> Option<i32> {
819        rolls.first().copied()
820    }
821
822    /// Choose one or more dice to reroll from the current natural roll list.
823    fn choose_dice_to_reroll(
824        &mut self,
825        _player: PlayerId,
826        _rolls: &[i32],
827        _source: Option<CardId>,
828    ) -> Vec<i32> {
829        vec![]
830    }
831
832    /// Choose one rolled result to increment or decrement by 1.
833    fn choose_roll_to_modify(
834        &mut self,
835        _player: PlayerId,
836        rolls: &[i32],
837        _source: Option<CardId>,
838    ) -> Option<i32> {
839        rolls.first().copied()
840    }
841
842    /// Choose whether a swap should use power or toughness.
843    fn choose_roll_swap_value(
844        &mut self,
845        _player: PlayerId,
846        _current_result: i32,
847        _power: i32,
848        _toughness: i32,
849        _source: Option<CardId>,
850    ) -> Option<RollSwapChoice> {
851        Some(RollSwapChoice::Power)
852    }
853
854    /// Choose heads or tails for a coin flip.
855    /// Returns true for heads, false for tails.
856    /// Default: always call heads.
857    fn flip_coin_call(&mut self, _player: PlayerId) -> bool {
858        true
859    }
860
861    /// Choose whether to pay life instead of mana for a Phyrexian mana shard.
862    /// Returns true to pay 2 life, false to pay the color.
863    /// Default: always pay color (never pay life).
864    fn choose_phyrexian_pay_life(
865        &mut self,
866        _player: PlayerId,
867        _color: &str,
868        _source: Option<CardId>,
869    ) -> bool {
870        false
871    }
872
873    /// Pay an attack cost for a creature (Propaganda, Ghostly Prison).
874    /// Called in a loop: tap lands to build mana, then Pay or Decline.
875    fn pay_combat_cost(
876        &mut self,
877        _player: PlayerId,
878        _attacker: CardId,
879        _cost: i32,
880        _description: &str,
881        _mana_ability_options: &[ManaAbilityOption],
882        _tappable_lands: &[CardId],
883        _untappable_lands: &[CardId],
884        _mana_pool_total: i32,
885    ) -> CombatCostAction {
886        CombatCostAction::Decline
887    }
888
889    /// Choose graveyard cards to exile for Delve (reduces generic cost).
890    /// `valid` lists graveyard card IDs, `max` is the maximum that can be exiled.
891    /// Default: exile max cards (maximize cost reduction). The interactive UI
892    /// resolves delve inside the mana-payment session, not via this callback.
893    fn choose_delve(
894        &mut self,
895        _player: PlayerId,
896        valid: &[CardId],
897        max: usize,
898        _source: Option<CardId>,
899    ) -> Vec<CardId> {
900        valid.iter().copied().take(max).collect()
901    }
902
903    /// Choose artifacts to tap for Improvise (each pays {1} generic).
904    /// `untapped_artifacts` lists available artifacts to tap.
905    /// Default: don't improvise (AI default — auto-tap handles mana).
906    fn choose_improvise(
907        &mut self,
908        _player: PlayerId,
909        _untapped_artifacts: &[CardId],
910        _remaining_cost: &forge_foundation::ManaCost,
911        _source: Option<CardId>,
912    ) -> Vec<CardId> {
913        vec![]
914    }
915
916    /// Choose creatures to tap for Convoke (each pays {1} or a matching colored mana).
917    /// `untapped_creatures` lists available creatures to tap.
918    /// Default: don't convoke (AI default — auto-tap handles mana).
919    fn choose_convoke(
920        &mut self,
921        _player: PlayerId,
922        _untapped_creatures: &[CardId],
923        _remaining_cost: &forge_foundation::ManaCost,
924        _source: Option<CardId>,
925    ) -> Vec<CardId> {
926        vec![]
927    }
928
929    /// Pay a mana cost within a single payment session.
930    /// Called in a loop for manual interaction: tap lands to build mana, then
931    /// `Pay { auto: false }` or `Cancel`. Agents can also return
932    /// `Pay { auto: true }` to delegate the rest of the session to engine
933    /// auto-pay.
934    /// Default: always cancel.
935    fn pay_mana_cost(
936        &mut self,
937        _player: PlayerId,
938        _card_id: CardId,
939        _card_name: &str,
940        _mana_cost: &str,
941        _mana_cost_display: &str,
942        _mana_cost_checkpoint: &str,
943        _can_confirm_from_pool: bool,
944        _allow_reserved_source_reuse: bool,
945        _reserved_sacrifices: &[CardId],
946        _mana_ability_options: &[ManaAbilityOption],
947        _tappable_lands: &[CardId],
948        _untappable_lands: &[CardId],
949        _mana_pool: &ManaPool,
950    ) -> ManaCostAction {
951        ManaCostAction::AttemptedAndFailed
952    }
953
954    /// Block until this agent acknowledges a display-only prompt that
955    /// requires UI dwell time (e.g. dice roll animations). Default
956    /// implementation is a no-op — only human-driven transports need
957    /// to wait for an ack.
958    ///
959    /// Used to make multi-agent broadcasts run their UI in parallel:
960    /// the broadcast loop dispatches the prompt to every agent in one
961    /// pass (so all clients receive it simultaneously), then a second
962    /// pass calls `await_display_ack` on each agent so the engine
963    /// blocks until the slowest player finishes their animation.
964    fn await_display_ack(&mut self) {}
965
966    /// Decide how to pay a single cost part.
967    fn decide_cost_part(
968        &mut self,
969        _player: PlayerId,
970        _source: CardId,
971        _cost_part: &CostPart,
972        _game: &GameState,
973    ) -> Option<PaymentDecision> {
974        // TODO: Implement default decisions per CostPart variant,
975        None
976    }
977
978    /// Whether this agent pays each cost part immediately after deciding (true)
979    /// or batches all decisions first, then pays (false).
980    fn pays_right_after_decision(&self) -> bool {
981        false
982    }
983
984    /// Reorder cost parts before payment (for human players to choose payment order).
985    fn order_cost_parts(&mut self, parts: Vec<CostPart>) -> Vec<CostPart> {
986        parts
987    }
988
989    /// Specify mana color distribution for combo/any mana production.
990    /// `available_colors` lists which colors can be produced.
991    /// `amount` is the total mana to distribute across colors.
992    /// Returns a list of color letters (e.g. ["W", "W", "U"]) totaling `amount`.
993    /// Default: picks the color with least mana in pool for each unit (AI heuristic).
994    fn specify_mana_combo(
995        &mut self,
996        _player: PlayerId,
997        available_colors: &[String],
998        amount: usize,
999        _source: Option<CardId>,
1000        _express_choice: Option<u16>,
1001    ) -> Vec<String> {
1002        // Default AI: pick first available color for all
1003        if let Some(first) = available_colors.first() {
1004            vec![first.clone(); amount]
1005        } else {
1006            vec!["C".to_string(); amount]
1007        }
1008    }
1009
1010    /// Choose whether to play a land or cast a spell when both are possible.
1011    /// Returns true for land, false for spell, None to pass.
1012    fn choose_land_or_spell(&mut self, player: PlayerId) -> Option<bool>;
1013
1014    /// Receive engine notifications for UI/game-log observers.
1015    /// Default is a no-op so simple agents do not need to handle them.
1016    fn notify(&mut self, _event: GameNotification) {}
1017
1018    /// Choose which replacement effect to apply when multiple effects match the same event.
1019    fn choose_single_replacement_effect(
1020        &mut self,
1021        _player: PlayerId,
1022        _descriptions: &[String],
1023    ) -> usize {
1024        0
1025    }
1026}
1027
1028/// A simple agent that always passes priority and makes no choices.
1029/// Useful for testing.
1030pub struct PassAgent;
1031
1032impl PlayerAgent for PassAgent {
1033    fn choose_targets_for(
1034        &mut self,
1035        _sa: &mut SpellAbility,
1036        _game: &GameState,
1037        _mana_pools: &[ManaPool],
1038    ) -> bool {
1039        true
1040    }
1041
1042    fn mulligan_decision(
1043        &mut self,
1044        _player: PlayerId,
1045        _hand: &[CardId],
1046        _mulligan_count: u32,
1047    ) -> bool {
1048        true
1049    }
1050
1051    fn choose_action(
1052        &mut self,
1053        _player: PlayerId,
1054        _action_space: Option<&PriorityActionSpace>,
1055        _request_action_space: &mut dyn FnMut() -> PriorityActionSpace,
1056    ) -> PlayerAction {
1057        PlayerAction::PassPriority
1058    }
1059
1060    fn choose_attackers(
1061        &mut self,
1062        _player: PlayerId,
1063        _available: &[CardId],
1064        _possible_defenders: &[DefenderId],
1065    ) -> Vec<(CardId, DefenderId)> {
1066        Vec::new() // no attackers
1067    }
1068
1069    fn choose_blockers(
1070        &mut self,
1071        _player: PlayerId,
1072        _attackers: &[CardId],
1073        _available_blockers: &[CardId],
1074        _max_blockers: Option<usize>,
1075    ) -> Vec<(CardId, CardId)> {
1076        Vec::new() // no blockers
1077    }
1078
1079    fn choose_target_player(
1080        &mut self,
1081        _player: PlayerId,
1082        valid: &[PlayerId],
1083        _sa: Option<&SpellAbility>,
1084    ) -> Option<PlayerId> {
1085        valid.first().copied()
1086    }
1087
1088    fn choose_target_card(
1089        &mut self,
1090        _player: PlayerId,
1091        valid: &[CardId],
1092        _sa: Option<&SpellAbility>,
1093    ) -> Option<CardId> {
1094        valid.first().copied()
1095    }
1096
1097    fn choose_target_any(
1098        &mut self,
1099        _player: PlayerId,
1100        valid_players: &[PlayerId],
1101        valid_cards: &[CardId],
1102        _sa: Option<&SpellAbility>,
1103    ) -> TargetChoice {
1104        if let Some(&pid) = valid_players.first() {
1105            TargetChoice::Player(pid)
1106        } else if let Some(&cid) = valid_cards.first() {
1107            TargetChoice::Card(cid)
1108        } else {
1109            TargetChoice::None
1110        }
1111    }
1112
1113    fn choose_sacrifice(
1114        &mut self,
1115        _player: PlayerId,
1116        valid: &[CardId],
1117        _source: Option<CardId>,
1118    ) -> Option<CardId> {
1119        valid.first().copied()
1120    }
1121
1122    fn choose_land_or_spell(&mut self, _player: PlayerId) -> Option<bool> {
1123        None
1124    }
1125}