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    fn set_targeting_cancellable(&mut self, _cancellable: bool) {}
292
293    /// Choose a target player (e.g. for Lightning Bolt targeting a player).
294    /// `sa` is the active spell ability context (source card, API type, etc.) for UI display.
295    fn choose_target_player(
296        &mut self,
297        player: PlayerId,
298        valid: &[PlayerId],
299        sa: Option<&SpellAbility>,
300    ) -> Option<PlayerId>;
301
302    /// Choose a target card (e.g. for Lightning Bolt targeting a creature).
303    fn choose_target_card(
304        &mut self,
305        player: PlayerId,
306        valid: &[CardId],
307        sa: Option<&SpellAbility>,
308    ) -> Option<CardId>;
309
310    /// Choose a target card from a specific zone (e.g. Raise Dead from graveyard).
311    fn choose_target_card_from_zone(
312        &mut self,
313        player: PlayerId,
314        _zone: forge_foundation::ZoneType,
315        valid: &[CardId],
316        sa: Option<&SpellAbility>,
317    ) -> Option<CardId> {
318        self.choose_target_card(player, valid, sa)
319    }
320
321    /// Choose a target that can be a player or a card (e.g. "any target").
322    fn choose_target_any(
323        &mut self,
324        player: PlayerId,
325        valid_players: &[PlayerId],
326        valid_cards: &[CardId],
327        sa: Option<&SpellAbility>,
328    ) -> TargetChoice;
329
330    /// Choose one permanent to sacrifice/select from the valid options.
331    /// `sa` is the active spell ability context for UI display.
332    /// Default picks the first (used by AI agents).
333    fn choose_sacrifice(
334        &mut self,
335        _player: PlayerId,
336        valid: &[CardId],
337        _source: Option<CardId>,
338    ) -> Option<CardId> {
339        valid.first().copied()
340    }
341
342    /// Distribute the looked-at Scry cards across the zones. Returns one ordered
343    /// pile per zone — `[top, bottom]` — where the last id in each pile is placed
344    /// on top of that pile. Default: keep everything on top, nothing to bottom.
345    fn choose_scry(
346        &mut self,
347        _game: &GameState,
348        _player: PlayerId,
349        _source: Option<CardId>,
350        cards: &[CardId],
351    ) -> Vec<Vec<CardId>> {
352        vec![cards.to_vec(), vec![]]
353    }
354
355    /// Distribute the looked-at Surveil cards: `[top, graveyard]` ordered piles.
356    /// Default: keep everything on top, nothing milled.
357    fn choose_surveil(
358        &mut self,
359        _game: &GameState,
360        _player: PlayerId,
361        _source: Option<CardId>,
362        cards: &[CardId],
363    ) -> Vec<Vec<CardId>> {
364        vec![cards.to_vec(), vec![]]
365    }
366
367    /// Choose up to `max` cards from `valid` to move to the destination zone (Dig effect).
368    /// `optional` means the player is not required to choose any.
369    /// Default: take first `max` cards.
370    fn choose_dig(
371        &mut self,
372        _game: &GameState,
373        _player: PlayerId,
374        valid: &[CardId],
375        max: usize,
376        _optional: bool,
377    ) -> Vec<CardId> {
378        valid.iter().copied().take(max).collect()
379    }
380
381    /// Choose an ordering for the top N cards being put back on the library (Ponder/Reorder).
382    /// Returns the cards in desired order: index 0 will be placed deepest, last will be on top.
383    /// Default: keep original order.
384    fn choose_reorder_library(
385        &mut self,
386        _game: &GameState,
387        _player: PlayerId,
388        cards: &[CardId],
389    ) -> Vec<CardId> {
390        cards.to_vec()
391    }
392
393    /// Choose which cards to discard from hand (for SP$ Discard effects).
394    /// `hand` is the full hand, `num` is how many must be discarded.
395    /// Default: discard the first `num` cards.
396    fn choose_discard(&mut self, _player: PlayerId, hand: &[CardId], num: usize) -> Vec<CardId> {
397        hand.iter().copied().take(num).collect()
398    }
399
400    /// Choose any number of cards to discard (for `AnyNumber$ True` on
401    /// SP$/DB$ Discard). The agent may pick 0..=hand.len() cards.
402    /// Default: discard `min` cards (the minimum forced amount).
403    fn choose_discard_any_number(
404        &mut self,
405        _player: PlayerId,
406        hand: &[CardId],
407        min: usize,
408        max: usize,
409    ) -> Vec<CardId> {
410        let _ = max;
411        hand.iter().copied().take(min).collect()
412    }
413
414    /// Choose cards to discard at random (for Mode$ Random discard, e.g. Hypnotic Specter).
415    /// The engine calls this instead of `choose_discard` when the discard is random.
416    /// Default: discard the first `num` cards (same as choose_discard).
417    /// Deterministic agents should override this to use their seeded RNG.
418    fn choose_random_discard(
419        &mut self,
420        _player: PlayerId,
421        hand: &[CardId],
422        num: usize,
423    ) -> Vec<CardId> {
424        hand.iter().copied().take(num).collect()
425    }
426
427    /// Choose a target spell on the stack (for SP$ Counter effects).
428    /// `valid` is a slice of stack entry IDs.
429    /// Default: target the first (topmost) spell.
430    fn choose_target_spell(
431        &mut self,
432        _player: PlayerId,
433        valid: &[u32],
434        _source: Option<CardId>,
435    ) -> Option<u32> {
436        valid.first().copied()
437    }
438
439    /// Choose N modes for a modal spell (SP$ Charm / Commands).
440    ///
441    /// `descriptions` — human-readable description of each mode.
442    /// `min` — minimum number of modes to choose.
443    /// `max` — maximum number of modes to choose.
444    ///
445    /// Returns indices into `descriptions` of the chosen modes, in order.
446    /// Default: choose the first `min` modes (index 0, 1, …).
447    fn choose_mode(
448        &mut self,
449        _player: PlayerId,
450        descriptions: &[String],
451        min: usize,
452        _max: usize,
453        _source_card_id: Option<CardId>,
454    ) -> Vec<usize> {
455        (0..min.min(descriptions.len())).collect()
456    }
457
458    fn choose_spell_abilities_for_effect(
459        &mut self,
460        _player: PlayerId,
461        abilities: &[SpellAbility],
462        num: usize,
463    ) -> Vec<usize> {
464        (0..num.min(abilities.len())).collect()
465    }
466
467    /// Choose exactly one entity (Card or Player) from a candidate list.
468    fn choose_single_entity_for_effect(
469        &mut self,
470        _player: PlayerId,
471        valid: &[GameEntity],
472        _is_optional: bool,
473    ) -> Option<GameEntity> {
474        valid.first().copied()
475    }
476
477    fn get_ability_to_play(
478        &mut self,
479        _player: PlayerId,
480        abilities: &[SpellAbility],
481    ) -> Option<usize> {
482        if abilities.is_empty() {
483            None
484        } else {
485            Some(0)
486        }
487    }
488
489    /// Choose which legendary permanent to keep when the legend rule applies.
490    /// `duplicates` contains all legendaries with the same name controlled by this player.
491    /// Returns the CardId of the one to keep; the rest are sacrificed.
492    fn choose_legend_keep(&mut self, _player: PlayerId, duplicates: &[CardId]) -> CardId {
493        duplicates[0]
494    }
495
496    /// Choose whether an optional triggered ability fires.
497    /// `description` is the trigger text shown to the player.
498    /// `source` is the engine card id of the source card (for UI display).
499    /// `api` is the spell ability API type.
500    /// Returns true to allow the trigger, false to decline.
501    /// Default: always allow (non-interactive agents accept all optional triggers).
502    fn choose_optional_trigger(
503        &mut self,
504        _player: PlayerId,
505        _description: &str,
506        _source: Option<CardId>,
507        _api: Option<crate::ability::api_type::ApiType>,
508    ) -> bool {
509        true
510    }
511
512    fn confirm_replacement_effect(
513        &mut self,
514        _player: PlayerId,
515        _question: &str,
516        _effect_description: &str,
517        _source: Option<CardId>,
518    ) -> bool {
519        true
520    }
521
522    /// Generic confirmation hook for optional effect prompts that don't yet
523    /// have a dedicated typed callback in the Rust agent interface.
524    ///
525    /// Returns true to accept/confirm, false to decline.
526    fn confirm_action(
527        &mut self,
528        _player: PlayerId,
529        _mode: Option<&str>,
530        _message: &str,
531        _options: &[String],
532        _source: Option<CardId>,
533        _api: Option<crate::ability::api_type::ApiType>,
534    ) -> bool {
535        false
536    }
537
538    fn confirm_payment(
539        &mut self,
540        player: PlayerId,
541        cost_kind: &str,
542        message: &str,
543        source: Option<CardId>,
544        api: Option<crate::ability::api_type::ApiType>,
545    ) -> bool {
546        let _ = (player, cost_kind, message, source, api);
547        true
548    }
549
550    fn pay_cost_to_prevent_effect(
551        &mut self,
552        player: PlayerId,
553        cost_kind: &str,
554        message: &str,
555        source: Option<CardId>,
556        api: Option<crate::ability::api_type::ApiType>,
557        can_pay: bool,
558        targets: &[GameEntity],
559        effect_text: &str,
560    ) -> bool {
561        let _ = (targets, effect_text);
562        if !can_pay {
563            return false;
564        }
565        self.confirm_payment(player, cost_kind, message, source, api)
566    }
567
568    fn choose_binary(
569        &mut self,
570        player: PlayerId,
571        question: &str,
572        kind: BinaryChoiceKind,
573        _default_choice: Option<bool>,
574        source: Option<CardId>,
575        api: Option<crate::ability::api_type::ApiType>,
576    ) -> bool {
577        let (left, right) = kind.labels();
578        self.confirm_action(
579            player,
580            Some(kind.as_str()),
581            question,
582            &[right.to_string(), left.to_string()],
583            source,
584            api,
585        )
586    }
587
588    /// Choose whether to pay the kicker cost for a spell.
589    /// `kicker_cost` is the mana cost string (e.g. "W", "2 R").
590    /// `source` is the name of the spell being cast (for UI display).
591    /// Returns true to kick, false to cast without kicker.
592    /// Default: don't kick (AI default).
593    fn choose_kicker(
594        &mut self,
595        _player: PlayerId,
596        _kicker_cost: &str,
597        _source: Option<CardId>,
598    ) -> bool {
599        false
600    }
601
602    /// Assist: another player asks if we'll help pay generic mana.
603    /// Returns how much generic mana to pay (0 = decline). Default: decline.
604    fn help_pay_assist(&mut self, _player: PlayerId, _card_name: &str, _max_generic: u32) -> u32 {
605        0
606    }
607
608    /// Choose whether to pay the buyback cost for a spell.
609    /// Returns true to pay buyback, false to cast normally.
610    /// Default: don't pay buyback.
611    fn choose_buyback(
612        &mut self,
613        _player: PlayerId,
614        _buyback_cost: &str,
615        _source: Option<CardId>,
616    ) -> bool {
617        false
618    }
619
620    /// Choose how many times to pay the multikicker cost.
621    /// `max_kicks` is the maximum affordable.
622    /// Returns the number of times to kick (0 to max_kicks).
623    /// Default: 0 (don't multikick).
624    fn choose_multikicker(
625        &mut self,
626        _player: PlayerId,
627        _cost: &str,
628        _max_kicks: u32,
629        _source: Option<CardId>,
630    ) -> u32 {
631        0
632    }
633
634    /// Choose how many times to pay the replicate cost.
635    /// `max_replicates` is the maximum affordable.
636    /// Returns the number of replicates.
637    /// Default: 0.
638    fn choose_replicate(
639        &mut self,
640        _player: PlayerId,
641        _cost: &str,
642        _max_replicates: u32,
643        _source: Option<CardId>,
644    ) -> u32 {
645        0
646    }
647
648    /// Choose a color (for ChooseColorEffect).
649    /// `valid_colors` lists the legal color choices (e.g. ["White","Blue","Black","Red","Green"]).
650    /// Default: pick the first valid color.
651    fn choose_color(&mut self, _player: PlayerId, valid_colors: &[String]) -> Option<String> {
652        valid_colors.first().cloned()
653    }
654
655    /// Choose one or more colors.
656    fn choose_colors(
657        &mut self,
658        _player: PlayerId,
659        valid_colors: &[String],
660        min: usize,
661        max: usize,
662    ) -> Vec<String> {
663        let hi = max.min(valid_colors.len());
664        let lo = min.min(hi);
665        valid_colors.iter().take(lo).cloned().collect()
666    }
667
668    /// Choose cards for an effect (ChooseCardEffect, CloneEffect, etc.).
669    /// `valid` lists eligible card IDs, `min`/`max` are the selection bounds.
670    /// Default: pick up to `max` from the front of `valid`.
671    fn choose_cards_for_effect(
672        &mut self,
673        _player: PlayerId,
674        valid: &[CardId],
675        _min: usize,
676        max: usize,
677    ) -> Vec<CardId> {
678        valid.iter().copied().take(max).collect()
679    }
680
681    /// Choose cards to tap for a `tapXType` cost that has a total-power floor
682    /// such as Crew. `card_powers` carries the effective tap-power value for
683    /// each candidate under the active ability; `card_sort_powers` carries the
684    /// normal net power value used by Forge's deterministic cost plumbing when
685    /// ordering candidates.
686    fn choose_tap_type_for_cost(
687        &mut self,
688        player: PlayerId,
689        valid: &[CardId],
690        _min_total_power: i32,
691        _card_powers: &[(CardId, i32)],
692        _card_sort_powers: &[(CardId, i32)],
693        _sa: Option<&SpellAbility>,
694    ) -> Vec<CardId> {
695        self.choose_cards_for_effect(player, valid, 1, valid.len())
696    }
697
698    /// Choose game entities (players and/or permanents) for an effect like Proliferate.
699    fn choose_entities_for_effect(
700        &mut self,
701        _player: PlayerId,
702        candidates: &[GameEntity],
703        _min: usize,
704        max: usize,
705    ) -> Vec<GameEntity> {
706        candidates.iter().copied().take(max).collect()
707    }
708
709    /// Choose a single card for hidden-origin zone changes (e.g. library search).
710    fn choose_single_card_for_zone_change(
711        &mut self,
712        _game: &GameState,
713        player: PlayerId,
714        valid: &[CardId],
715        _select_prompt: &str,
716        _is_optional: bool,
717    ) -> Option<CardId> {
718        self.choose_cards_for_effect(player, valid, 1, 1)
719            .into_iter()
720            .next()
721    }
722
723    /// Choose multiple cards for hidden-origin zone changes (e.g. tutor multi-select).
724    fn choose_cards_for_zone_change(
725        &mut self,
726        _game: &GameState,
727        player: PlayerId,
728        valid: &[CardId],
729        min: usize,
730        max: usize,
731        _select_prompt: &str,
732    ) -> Vec<CardId> {
733        self.choose_cards_for_effect(player, valid, min, max)
734    }
735
736    /// Choose a creature/card type (for ChooseType effect).
737    /// `type_category` is "Creature", "Card", "Land", etc.
738    /// `valid_types` lists the legal type choices.
739    /// Default: pick the first valid type.
740    fn choose_type(
741        &mut self,
742        _player: PlayerId,
743        _type_category: &str,
744        valid_types: &[String],
745    ) -> Option<String> {
746        valid_types.first().cloned()
747    }
748
749    /// Choose a counter type.
750    fn choose_counter_type(
751        &mut self,
752        _player: PlayerId,
753        options: &[CounterType],
754        _prompt: &str,
755    ) -> Option<CounterType> {
756        options.first().cloned()
757    }
758
759    /// Choose a card name (for NameCard effect).
760    /// `valid_names` lists the legal card name choices (for ChooseFromList mode).
761    /// Default: pick the first valid name.
762    fn choose_card_name(&mut self, _player: PlayerId, valid_names: &[String]) -> Option<String> {
763        valid_names.first().cloned()
764    }
765
766    /// Choose a number within `[min, max]`. `title`/`description` present the
767    /// choice and `source` is the card driving it (shown in the prompt).
768    /// Default: pick the minimum.
769    fn choose_number(
770        &mut self,
771        _player: PlayerId,
772        _source: Option<CardId>,
773        _title: &str,
774        _description: Option<&str>,
775        min: i32,
776        _max: i32,
777    ) -> Option<i32> {
778        Some(min)
779    }
780
781    /// Choose how many times to pay an optional keyword cost.
782    /// Default: decline optional keyword costs.
783    fn choose_number_for_keyword_cost(
784        &mut self,
785        _player: PlayerId,
786        _max: i32,
787        _prompt: &str,
788        _source: Option<CardId>,
789    ) -> i32 {
790        0
791    }
792
793    /// Choose one number from an explicit list of legal rolled values.
794    fn choose_number_from_list(
795        &mut self,
796        _player: PlayerId,
797        choices: &[i32],
798        _message: &str,
799        _source_card_id: Option<CardId>,
800    ) -> Option<i32> {
801        choices.first().copied()
802    }
803
804    /// Choose one die result from a rolled list to ignore.
805    fn choose_roll_to_ignore(
806        &mut self,
807        _player: PlayerId,
808        rolls: &[i32],
809        _source: Option<CardId>,
810    ) -> Option<i32> {
811        rolls.first().copied()
812    }
813
814    /// Choose one rolled result to exchange with a card's power or toughness.
815    fn choose_roll_to_swap(
816        &mut self,
817        _player: PlayerId,
818        rolls: &[i32],
819        _source: Option<CardId>,
820    ) -> Option<i32> {
821        rolls.first().copied()
822    }
823
824    /// Choose one or more dice to reroll from the current natural roll list.
825    fn choose_dice_to_reroll(
826        &mut self,
827        _player: PlayerId,
828        _rolls: &[i32],
829        _source: Option<CardId>,
830    ) -> Vec<i32> {
831        vec![]
832    }
833
834    /// Choose one rolled result to increment or decrement by 1.
835    fn choose_roll_to_modify(
836        &mut self,
837        _player: PlayerId,
838        rolls: &[i32],
839        _source: Option<CardId>,
840    ) -> Option<i32> {
841        rolls.first().copied()
842    }
843
844    /// Choose whether a swap should use power or toughness.
845    fn choose_roll_swap_value(
846        &mut self,
847        _player: PlayerId,
848        _current_result: i32,
849        _power: i32,
850        _toughness: i32,
851        _source: Option<CardId>,
852    ) -> Option<RollSwapChoice> {
853        Some(RollSwapChoice::Power)
854    }
855
856    /// Choose heads or tails for a coin flip.
857    /// Returns true for heads, false for tails.
858    /// Default: always call heads.
859    fn flip_coin_call(&mut self, _player: PlayerId) -> bool {
860        true
861    }
862
863    /// Choose whether to pay life instead of mana for a Phyrexian mana shard.
864    /// Returns true to pay 2 life, false to pay the color.
865    /// Default: always pay color (never pay life).
866    fn choose_phyrexian_pay_life(
867        &mut self,
868        _player: PlayerId,
869        _color: &str,
870        _source: Option<CardId>,
871    ) -> bool {
872        false
873    }
874
875    /// Pay an attack cost for a creature (Propaganda, Ghostly Prison).
876    /// Called in a loop: tap lands to build mana, then Pay or Decline.
877    fn pay_combat_cost(
878        &mut self,
879        _player: PlayerId,
880        _attacker: CardId,
881        _cost: i32,
882        _description: &str,
883        _mana_ability_options: &[ManaAbilityOption],
884        _tappable_lands: &[CardId],
885        _untappable_lands: &[CardId],
886        _mana_pool_total: i32,
887    ) -> CombatCostAction {
888        CombatCostAction::Decline
889    }
890
891    /// Choose graveyard cards to exile for Delve (reduces generic cost).
892    /// `valid` lists graveyard card IDs, `max` is the maximum that can be exiled.
893    /// Default: exile max cards (maximize cost reduction). The interactive UI
894    /// resolves delve inside the mana-payment session, not via this callback.
895    fn choose_delve(
896        &mut self,
897        _player: PlayerId,
898        valid: &[CardId],
899        max: usize,
900        _source: Option<CardId>,
901    ) -> Vec<CardId> {
902        valid.iter().copied().take(max).collect()
903    }
904
905    /// Choose artifacts to tap for Improvise (each pays {1} generic).
906    /// `untapped_artifacts` lists available artifacts to tap.
907    /// Default: don't improvise (AI default — auto-tap handles mana).
908    fn choose_improvise(
909        &mut self,
910        _player: PlayerId,
911        _untapped_artifacts: &[CardId],
912        _remaining_cost: &forge_foundation::ManaCost,
913        _source: Option<CardId>,
914    ) -> Vec<CardId> {
915        vec![]
916    }
917
918    /// Choose creatures to tap for Convoke (each pays {1} or a matching colored mana).
919    /// `untapped_creatures` lists available creatures to tap.
920    /// Default: don't convoke (AI default — auto-tap handles mana).
921    fn choose_convoke(
922        &mut self,
923        _player: PlayerId,
924        _untapped_creatures: &[CardId],
925        _remaining_cost: &forge_foundation::ManaCost,
926        _source: Option<CardId>,
927    ) -> Vec<CardId> {
928        vec![]
929    }
930
931    /// Pay a mana cost within a single payment session.
932    /// Called in a loop for manual interaction: tap lands to build mana, then
933    /// `Pay { auto: false }` or `Cancel`. Agents can also return
934    /// `Pay { auto: true }` to delegate the rest of the session to engine
935    /// auto-pay.
936    /// Default: always cancel.
937    fn pay_mana_cost(
938        &mut self,
939        _player: PlayerId,
940        _card_id: CardId,
941        _card_name: &str,
942        _mana_cost: &str,
943        _mana_cost_display: &str,
944        _mana_cost_checkpoint: &str,
945        _can_confirm_from_pool: bool,
946        _allow_reserved_source_reuse: bool,
947        _reserved_sacrifices: &[CardId],
948        _mana_ability_options: &[ManaAbilityOption],
949        _tappable_lands: &[CardId],
950        _untappable_lands: &[CardId],
951        _mana_pool: &ManaPool,
952    ) -> ManaCostAction {
953        ManaCostAction::AttemptedAndFailed
954    }
955
956    /// Block until this agent acknowledges a display-only prompt that
957    /// requires UI dwell time (e.g. dice roll animations). Default
958    /// implementation is a no-op — only human-driven transports need
959    /// to wait for an ack.
960    ///
961    /// Used to make multi-agent broadcasts run their UI in parallel:
962    /// the broadcast loop dispatches the prompt to every agent in one
963    /// pass (so all clients receive it simultaneously), then a second
964    /// pass calls `await_display_ack` on each agent so the engine
965    /// blocks until the slowest player finishes their animation.
966    fn await_display_ack(&mut self) {}
967
968    /// Decide how to pay a single cost part.
969    fn decide_cost_part(
970        &mut self,
971        _player: PlayerId,
972        _source: CardId,
973        _cost_part: &CostPart,
974        _game: &GameState,
975    ) -> Option<PaymentDecision> {
976        // TODO: Implement default decisions per CostPart variant,
977        None
978    }
979
980    /// Whether this agent pays each cost part immediately after deciding (true)
981    /// or batches all decisions first, then pays (false).
982    fn pays_right_after_decision(&self) -> bool {
983        false
984    }
985
986    /// Reorder cost parts before payment (for human players to choose payment order).
987    fn order_cost_parts(&mut self, parts: Vec<CostPart>) -> Vec<CostPart> {
988        parts
989    }
990
991    /// Specify mana color distribution for combo/any mana production.
992    /// `available_colors` lists which colors can be produced.
993    /// `amount` is the total mana to distribute across colors.
994    /// Returns a list of color letters (e.g. ["W", "W", "U"]) totaling `amount`.
995    /// Default: picks the color with least mana in pool for each unit (AI heuristic).
996    fn specify_mana_combo(
997        &mut self,
998        _player: PlayerId,
999        available_colors: &[String],
1000        amount: usize,
1001        _source: Option<CardId>,
1002        _express_choice: Option<u16>,
1003    ) -> Vec<String> {
1004        // Default AI: pick first available color for all
1005        if let Some(first) = available_colors.first() {
1006            vec![first.clone(); amount]
1007        } else {
1008            vec!["C".to_string(); amount]
1009        }
1010    }
1011
1012    /// Choose whether to play a land or cast a spell when both are possible.
1013    /// Returns true for land, false for spell, None to pass.
1014    fn choose_land_or_spell(&mut self, player: PlayerId) -> Option<bool>;
1015
1016    /// Receive engine notifications for UI/game-log observers.
1017    /// Default is a no-op so simple agents do not need to handle them.
1018    fn notify(&mut self, _event: GameNotification) {}
1019
1020    /// Choose which replacement effect to apply when multiple effects match the same event.
1021    fn choose_single_replacement_effect(
1022        &mut self,
1023        _player: PlayerId,
1024        _descriptions: &[String],
1025    ) -> usize {
1026        0
1027    }
1028}
1029
1030/// A simple agent that always passes priority and makes no choices.
1031/// Useful for testing.
1032pub struct PassAgent;
1033
1034impl PlayerAgent for PassAgent {
1035    fn choose_targets_for(
1036        &mut self,
1037        _sa: &mut SpellAbility,
1038        _game: &GameState,
1039        _mana_pools: &[ManaPool],
1040    ) -> bool {
1041        true
1042    }
1043
1044    fn mulligan_decision(
1045        &mut self,
1046        _player: PlayerId,
1047        _hand: &[CardId],
1048        _mulligan_count: u32,
1049    ) -> bool {
1050        true
1051    }
1052
1053    fn choose_action(
1054        &mut self,
1055        _player: PlayerId,
1056        _action_space: Option<&PriorityActionSpace>,
1057        _request_action_space: &mut dyn FnMut() -> PriorityActionSpace,
1058    ) -> PlayerAction {
1059        PlayerAction::PassPriority
1060    }
1061
1062    fn choose_attackers(
1063        &mut self,
1064        _player: PlayerId,
1065        _available: &[CardId],
1066        _possible_defenders: &[DefenderId],
1067    ) -> Vec<(CardId, DefenderId)> {
1068        Vec::new() // no attackers
1069    }
1070
1071    fn choose_blockers(
1072        &mut self,
1073        _player: PlayerId,
1074        _attackers: &[CardId],
1075        _available_blockers: &[CardId],
1076        _max_blockers: Option<usize>,
1077    ) -> Vec<(CardId, CardId)> {
1078        Vec::new() // no blockers
1079    }
1080
1081    fn choose_target_player(
1082        &mut self,
1083        _player: PlayerId,
1084        valid: &[PlayerId],
1085        _sa: Option<&SpellAbility>,
1086    ) -> Option<PlayerId> {
1087        valid.first().copied()
1088    }
1089
1090    fn choose_target_card(
1091        &mut self,
1092        _player: PlayerId,
1093        valid: &[CardId],
1094        _sa: Option<&SpellAbility>,
1095    ) -> Option<CardId> {
1096        valid.first().copied()
1097    }
1098
1099    fn choose_target_any(
1100        &mut self,
1101        _player: PlayerId,
1102        valid_players: &[PlayerId],
1103        valid_cards: &[CardId],
1104        _sa: Option<&SpellAbility>,
1105    ) -> TargetChoice {
1106        if let Some(&pid) = valid_players.first() {
1107            TargetChoice::Player(pid)
1108        } else if let Some(&cid) = valid_cards.first() {
1109            TargetChoice::Card(cid)
1110        } else {
1111            TargetChoice::None
1112        }
1113    }
1114
1115    fn choose_sacrifice(
1116        &mut self,
1117        _player: PlayerId,
1118        valid: &[CardId],
1119        _source: Option<CardId>,
1120    ) -> Option<CardId> {
1121        valid.first().copied()
1122    }
1123
1124    fn choose_land_or_spell(&mut self, _player: PlayerId) -> Option<bool> {
1125        None
1126    }
1127}