Skip to main content

manabrew_agent_interface/
auto_pay.rs

1///
2/// DO NOT USE THIS MODULE FOR PARITY HANDLER
3/// AUTO PAY, USE THE PARITY AUTOPAY ENGINE INSTEAD
4///
5use std::collections::HashMap;
6
7use crate::game_view_dto::GameViewDto;
8use crate::prompt::*;
9
10fn parse_mana_tokens(mana_cost: &str) -> Vec<String> {
11    mana_cost
12        .match_indices('{')
13        .filter_map(|(start, _)| {
14            mana_cost[start + 1..]
15                .find('}')
16                .map(|end| mana_cost[start + 1..start + 1 + end].to_string())
17        })
18        .collect()
19}
20
21fn color_letter(color: ManaColor) -> &'static str {
22    match color {
23        ManaColor::White => "W",
24        ManaColor::Blue => "U",
25        ManaColor::Black => "B",
26        ManaColor::Red => "R",
27        ManaColor::Green => "G",
28        ManaColor::Colorless => "C",
29    }
30}
31
32fn mana_matches_color(mana: &Mana, letter: &str) -> bool {
33    color_letter(mana.color) == letter
34}
35
36fn action_produces_color(action: &PaymentAction, letter: &str) -> bool {
37    match &action.kind {
38        PaymentActionKind::ActivateManaAbility(info) => info
39            .produced_mana
40            .as_ref()
41            .map(|mana| mana.iter().any(|m| mana_matches_color(m, letter)))
42            .unwrap_or(false),
43        _ => false,
44    }
45}
46
47fn can_pay_mana_cost(pool: &HashMap<String, i32>, mana_cost: &str, player_life: i32) -> bool {
48    let mut available = pool.clone();
49    let mut generic = 0i32;
50    let mut hybrids: Vec<(String, String)> = Vec::new();
51    let mut phyrexian_life_needed = 0i32;
52
53    for token in parse_mana_tokens(mana_cost) {
54        if let Ok(n) = token.parse::<i32>() {
55            generic += n;
56            continue;
57        }
58        if token == "X" {
59            continue;
60        }
61        if token.contains('/') {
62            let mut parts = token.split('/');
63            if let (Some(a), Some(b)) = (parts.next(), parts.next()) {
64                if b == "P" {
65                    let count = available.entry(a.to_string()).or_insert(0);
66                    if *count > 0 {
67                        *count -= 1;
68                    } else {
69                        phyrexian_life_needed += 2;
70                    }
71                    continue;
72                }
73                hybrids.push((a.to_string(), b.to_string()));
74                continue;
75            }
76        }
77        let count = available.entry(token.clone()).or_insert(0);
78        if *count <= 0 {
79            return false;
80        }
81        *count -= 1;
82    }
83
84    for (a, b) in hybrids {
85        let a_count = *available.get(&a).unwrap_or(&0);
86        let b_count = *available.get(&b).unwrap_or(&0);
87        if a_count > 0 {
88            if let Some(count) = available.get_mut(&a) {
89                *count -= 1;
90            }
91        } else if b_count > 0 {
92            if let Some(count) = available.get_mut(&b) {
93                *count -= 1;
94            }
95        } else {
96            return false;
97        }
98    }
99
100    if phyrexian_life_needed > player_life {
101        return false;
102    }
103    let remaining_total: i32 = available.values().copied().sum();
104    remaining_total >= generic
105}
106
107pub fn choose_pay_mana_cost_action(
108    game_view: &GameViewDto,
109    mana_cost: &str,
110    actions: &[PaymentAction],
111) -> Option<PromptOutput> {
112    let player = game_view
113        .players
114        .iter()
115        .find(|p| p.id == game_view.priority_player_id)
116        .cloned();
117    let player_life = player.as_ref().map(|p| p.life).unwrap_or_default();
118    let player_pool: HashMap<String, i32> = player
119        .map(|p| p.mana_pool)
120        .unwrap_or_default()
121        .into_iter()
122        .map(|(color, amount)| (color_letter(color).to_string(), amount as i32))
123        .collect();
124    let mut needed_colors: Vec<String> = parse_mana_tokens(mana_cost)
125        .into_iter()
126        .filter_map(|token| {
127            if token.len() == 1 && token != "X" {
128                return Some(token);
129            }
130            if let Some(color) = token.strip_suffix("/P") {
131                return Some(color.to_string());
132            }
133            None
134        })
135        .collect();
136    for (color, amount) in &player_pool {
137        for _ in 0..(*amount).max(0) {
138            if let Some(pos) = needed_colors.iter().position(|needed| needed == color) {
139                needed_colors.remove(pos);
140            }
141        }
142    }
143
144    for needed in &needed_colors {
145        if let Some(action) = actions
146            .iter()
147            .find(|action| action_produces_color(action, needed))
148        {
149            return Some(PromptOutput::PayManaCost(PayManaCostOutput::Act {
150                action_id: action.id.clone(),
151            }));
152        }
153    }
154
155    if can_pay_mana_cost(&player_pool, mana_cost, player_life) {
156        return Some(PromptOutput::PayManaCost(PayManaCostOutput::Pay {
157            auto: true,
158        }));
159    }
160
161    actions
162        .iter()
163        .find(|action| matches!(action.kind, PaymentActionKind::ActivateManaAbility(_)))
164        .map(|action| {
165            PromptOutput::PayManaCost(PayManaCostOutput::Act {
166                action_id: action.id.clone(),
167            })
168        })
169}