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