manabrew_agent_interface/
auto_pay.rs1use 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}