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 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}