Skip to main content

manabrew_engine/mana/
auto_pay.rs

1use forge_foundation::ManaCost;
2
3use crate::game::GameState;
4use crate::ids::{CardId, PlayerId};
5
6use super::{
7    auto_tap_lands_trace, auto_tap_lands_with_chooser, AutoTapChoice, ManaPayCallbackFn,
8    ManaPaymentContext, ManaPool, SacrificeChooser,
9};
10
11pub struct AutoPayResult {
12    pub tapped: Vec<CardId>,
13    pub choices: Vec<AutoTapChoice>,
14    pub life_paid: i32,
15    pub colors_spent: u16,
16    pub paying_mana: Vec<u16>,
17    /// True when auto-tap produced taps but the resulting pool couldn't
18    /// satisfy the full cost — i.e. the cast must cancel. Java's
19    /// `AutoPay.payManaCostWithTrace` records the partial taps before
20    /// appending its `Cancel` marker; mirroring that lets the parity trace
21    /// stay byte-aligned with Java's `[TapLand …, Cancel]` shape and the
22    /// agent observe the same RNG-consumption pattern on the next priority
23    /// loop iteration.
24    pub cancelled: bool,
25}
26
27/// Deterministic auto-pay entrypoint used by parity AI paths.
28///
29/// This mirrors the harness `AutoPay.java` flow at a high level:
30/// 1) auto-activate legal mana sources for the required spell cost
31/// 2) auto-activate additional sources for commander tax
32/// 3) pay exactly the required mana from pool with engine restriction checks
33pub fn pay_mana_cost_auto(
34    game: &mut GameState,
35    pool: &mut ManaPool,
36    player: PlayerId,
37    mana_cost: &ManaCost,
38    current_spell: Option<CardId>,
39    commander_tax: i32,
40    payment_ctx: &ManaPaymentContext,
41    any_color_conversion: bool,
42) -> Option<AutoPayResult> {
43    pay_mana_cost_auto_with_chooser(
44        game,
45        pool,
46        player,
47        mana_cost,
48        current_spell,
49        commander_tax,
50        payment_ctx,
51        any_color_conversion,
52        None,
53    )
54}
55
56/// Same as [`pay_mana_cost_auto`] but accepts an optional sacrifice chooser
57/// callback for parity with Java's `choosePermanentsToSacrifice` RNG path.
58pub fn pay_mana_cost_auto_with_chooser(
59    game: &mut GameState,
60    pool: &mut ManaPool,
61    player: PlayerId,
62    mana_cost: &ManaCost,
63    current_spell: Option<CardId>,
64    commander_tax: i32,
65    payment_ctx: &ManaPaymentContext,
66    any_color_conversion: bool,
67    sacrifice_chooser: Option<SacrificeChooser<'_>>,
68) -> Option<AutoPayResult> {
69    let tapped = match sacrifice_chooser {
70        Some(chooser) => {
71            let mut tapped =
72                auto_tap_lands_with_chooser(game, pool, player, mana_cost, current_spell, chooser);
73            if commander_tax > 0 {
74                tapped.extend(auto_tap_lands_with_chooser(
75                    game,
76                    pool,
77                    player,
78                    &ManaCost::generic(commander_tax),
79                    current_spell,
80                    chooser,
81                ));
82            }
83            tapped
84        }
85        None => {
86            let mut choices = auto_tap_lands_trace(game, pool, player, mana_cost, current_spell);
87            if commander_tax > 0 {
88                choices.extend(auto_tap_lands_trace(
89                    game,
90                    pool,
91                    player,
92                    &ManaCost::generic(commander_tax),
93                    current_spell,
94                ));
95            }
96            choices.into_iter().map(|choice| choice.card_id).collect()
97        }
98    };
99    let choices = tapped
100        .iter()
101        .map(|&card_id| AutoTapChoice {
102            card_id,
103            mana_ability_index: None,
104            chosen_atom: 0,
105            needs_express_choice: false,
106        })
107        .collect();
108
109    let payment = pool.try_pay_for_spell_converted_with_phyrexian_life_result(
110        mana_cost,
111        payment_ctx,
112        any_color_conversion,
113        game.player(player).life,
114    )?;
115    if commander_tax > 0 && !pool.try_pay_extra_generic(commander_tax) {
116        return None;
117    }
118    Some(AutoPayResult {
119        tapped,
120        choices,
121        life_paid: payment.life_paid,
122        colors_spent: payment.colors_spent,
123        paying_mana: payment.paying_mana,
124        cancelled: false,
125    })
126}
127
128/// Same as [`pay_mana_cost_auto`] but accepts the unified callback for both
129/// sacrifice chooser and confirm payment (Treasure Token self-sacrifice).
130pub fn pay_mana_cost_auto_with_callback(
131    game: &mut GameState,
132    pool: &mut ManaPool,
133    player: PlayerId,
134    mana_cost: &ManaCost,
135    current_spell: Option<CardId>,
136    commander_tax: i32,
137    payment_ctx: &ManaPaymentContext,
138    any_color_conversion: bool,
139    callback: ManaPayCallbackFn<'_>,
140) -> Option<AutoPayResult> {
141    pay_mana_cost_auto_with_callback_and_reserved_sacrifices(
142        game,
143        pool,
144        player,
145        mana_cost,
146        current_spell,
147        commander_tax,
148        payment_ctx,
149        any_color_conversion,
150        &[],
151        callback,
152    )
153}
154
155/// Same as [`pay_mana_cost_auto_with_callback`] but takes a list of permanents
156/// already reserved by an additional-cost sacrifice on the current spell. The
157/// auto-payer skips those permanents when picking mana abilities, so the spell's
158/// `Sac<1/X>` cost and its mana payment can never double-book the same card.
159///
160/// Without this, the spell auto-payer (`cast_spell.rs:pay_mana_cost_session`)
161/// would freely pick the very permanent the player already chose to sacrifice
162/// for the additional cost, then silently drop the additional sacrifice at
163/// pay-time. That's the seed-62 Eviscerator's Insight bug.
164#[allow(clippy::too_many_arguments)]
165pub fn pay_mana_cost_auto_with_callback_and_reserved_sacrifices(
166    game: &mut GameState,
167    pool: &mut ManaPool,
168    player: PlayerId,
169    mana_cost: &ManaCost,
170    current_spell: Option<CardId>,
171    commander_tax: i32,
172    payment_ctx: &ManaPaymentContext,
173    any_color_conversion: bool,
174    reserved_sacrifices: &[CardId],
175    callback: ManaPayCallbackFn<'_>,
176) -> Option<AutoPayResult> {
177    let mut trace =
178        super::computer_util_mana::auto_tap_lands_pay_incremental_with_callbacks_reserved_and_ctx(
179            game,
180            pool,
181            player,
182            mana_cost,
183            current_spell,
184            reserved_sacrifices,
185            callback,
186            payment_ctx,
187            any_color_conversion,
188        );
189    if commander_tax > 0 && trace.paid {
190        let tapped_tax =
191            super::computer_util_mana::auto_tap_lands_pay_incremental_with_callbacks_reserved_and_ctx(
192                game,
193                pool,
194                player,
195                &ManaCost::generic(commander_tax),
196                current_spell,
197                reserved_sacrifices,
198                callback,
199                payment_ctx,
200                any_color_conversion,
201            );
202        trace.choices.extend(tapped_tax.choices);
203        trace.payment.colors_spent |= tapped_tax.payment.colors_spent;
204        trace
205            .payment
206            .paying_mana
207            .extend(tapped_tax.payment.paying_mana);
208        trace.paid = trace.paid && tapped_tax.paid;
209    }
210    let choices = trace.choices;
211    let tapped: Vec<CardId> = choices.iter().map(|choice| choice.card_id).collect();
212    let life_paid = trace.payment.life_paid;
213
214    if !trace.paid {
215        // Java parity: keep the partial taps in the returned trace so the
216        // agent emits `[TapLand …, Cancel]` and consumes the same RNG.
217        return Some(AutoPayResult {
218            tapped,
219            choices,
220            life_paid: 0,
221            colors_spent: 0,
222            paying_mana: Vec::new(),
223            cancelled: true,
224        });
225    }
226    Some(AutoPayResult {
227        tapped,
228        choices,
229        life_paid,
230        colors_spent: trace.payment.colors_spent,
231        paying_mana: trace.payment.paying_mana,
232        cancelled: false,
233    })
234}