1use crate::error::Error;
8use serde_json::Value;
9use std::str::FromStr;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct Card {
21 pub rank: char,
22 pub suit: char,
23}
24
25const VALID_RANKS: &[char] = &[
26 '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K', 'A',
27];
28const VALID_SUITS: &[char] = &['h', 'd', 'c', 's'];
29
30impl Card {
31 pub fn new(rank: char, suit: char) -> Result<Self, Error> {
32 if !VALID_RANKS.contains(&rank) {
33 return Err(Error::Protocol(format!("invalid card rank: {rank:?}")));
34 }
35 if !VALID_SUITS.contains(&suit) {
36 return Err(Error::Protocol(format!("invalid card suit: {suit:?}")));
37 }
38 Ok(Self { rank, suit })
39 }
40}
41
42impl FromStr for Card {
43 type Err = Error;
44
45 fn from_str(s: &str) -> Result<Self, Self::Err> {
46 let mut chars = s.chars();
47 let rank = chars
48 .next()
49 .ok_or_else(|| Error::Protocol(format!("empty card string: {s:?}")))?;
50 let suit = chars
51 .next()
52 .ok_or_else(|| Error::Protocol(format!("card string too short: {s:?}")))?;
53 if chars.next().is_some() {
54 return Err(Error::Protocol(format!(
55 "card string too long: {s:?} (expected 2 chars)"
56 )));
57 }
58 Card::new(rank, suit)
59 }
60}
61
62impl std::fmt::Display for Card {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 write!(f, "{}{}", self.rank, self.suit)
65 }
66}
67
68pub fn parse_card(s: &str) -> Result<Card, Error> {
72 s.parse()
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82pub enum ActionKind {
83 Fold,
84 Check,
85 Call,
86 Raise,
87 AllIn,
88}
89
90impl ActionKind {
91 pub fn as_str(&self) -> &'static str {
92 match self {
93 ActionKind::Fold => "fold",
94 ActionKind::Check => "check",
95 ActionKind::Call => "call",
96 ActionKind::Raise => "raise",
97 ActionKind::AllIn => "all_in",
98 }
99 }
100
101 pub fn parse(s: &str) -> Option<Self> {
102 match s {
103 "fold" => Some(ActionKind::Fold),
104 "check" => Some(ActionKind::Check),
105 "call" => Some(ActionKind::Call),
106 "raise" => Some(ActionKind::Raise),
107 "all_in" => Some(ActionKind::AllIn),
108 _ => None,
109 }
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum Action {
122 Fold,
123 Check,
124 Call,
125 Raise(u64),
126 AllIn,
127}
128
129impl Action {
130 pub fn kind(&self) -> ActionKind {
131 match self {
132 Action::Fold => ActionKind::Fold,
133 Action::Check => ActionKind::Check,
134 Action::Call => ActionKind::Call,
135 Action::Raise(_) => ActionKind::Raise,
136 Action::AllIn => ActionKind::AllIn,
137 }
138 }
139
140 pub fn to_wire(&self) -> (&'static str, Value) {
144 let action = self.kind().as_str();
145 let params = match self {
146 Action::Raise(amount) => serde_json::json!({ "amount": *amount }),
147 _ => serde_json::json!({}),
148 };
149 (action, params)
150 }
151}
152
153#[derive(Debug, Clone)]
161pub struct ActionHistoryEntry {
162 pub seat: i64,
163 pub action: String,
164 pub amount: Option<i64>,
165 pub is_timeout: Option<bool>,
166}
167
168#[derive(Debug, Clone)]
175pub struct GameState {
176 pub hand_number: i64,
177 pub phase: String,
178 pub hole_cards: Vec<Card>,
179 pub board: Vec<Card>,
180 pub pot: i64,
181 pub your_stack: i64,
182 pub opponent_stacks: Vec<i64>,
183 pub your_seat: i64,
184 pub dealer_seat: i64,
185 pub to_call: i64,
186 pub min_raise: i64,
187 pub max_raise: i64,
188 pub valid_actions: Vec<String>,
189 pub action_history: Vec<ActionHistoryEntry>,
190 pub round_id: String,
191 pub request_id: String,
192}
193
194pub fn parse_game_state(message: &Value) -> GameState {
200 let state = message.get("state").cloned().unwrap_or(Value::Null);
201 let state_obj = state.as_object();
202
203 let hole_strs = state_obj
204 .and_then(|o| o.get("your_hole_cards"))
205 .and_then(|v| v.as_array());
206 let board_strs = state_obj
207 .and_then(|o| o.get("board"))
208 .and_then(|v| v.as_array());
209
210 let valid_actions = message
211 .get("valid_actions")
212 .and_then(|v| v.as_array())
213 .or_else(|| {
214 state_obj
215 .and_then(|o| o.get("valid_actions"))
216 .and_then(|v| v.as_array())
217 })
218 .map(|arr| {
219 arr.iter()
220 .filter_map(|v| v.as_str().map(String::from))
221 .collect()
222 })
223 .unwrap_or_default();
224
225 let action_history = state_obj
226 .and_then(|o| o.get("action_history"))
227 .and_then(|v| v.as_array())
228 .map(|arr| arr.iter().map(parse_history_entry).collect())
229 .unwrap_or_default();
230
231 GameState {
232 hand_number: state_obj
233 .and_then(|o| o.get("hand_number"))
234 .and_then(Value::as_i64)
235 .unwrap_or(0),
236 phase: state_obj
237 .and_then(|o| o.get("phase"))
238 .and_then(|v| v.as_str())
239 .unwrap_or("preflop")
240 .to_string(),
241 hole_cards: hole_strs
242 .map(|arr| {
243 arr.iter()
244 .filter_map(|v| v.as_str())
245 .filter_map(|s| s.parse::<Card>().ok())
246 .collect()
247 })
248 .unwrap_or_default(),
249 board: board_strs
250 .map(|arr| {
251 arr.iter()
252 .filter_map(|v| v.as_str())
253 .filter_map(|s| s.parse::<Card>().ok())
254 .collect()
255 })
256 .unwrap_or_default(),
257 pot: state_obj
258 .and_then(|o| o.get("pot"))
259 .and_then(Value::as_i64)
260 .unwrap_or(0),
261 your_stack: state_obj
262 .and_then(|o| o.get("your_stack"))
263 .and_then(Value::as_i64)
264 .unwrap_or(0),
265 opponent_stacks: state_obj
266 .and_then(|o| o.get("opponent_stacks"))
267 .and_then(|v| v.as_array())
268 .map(|arr| arr.iter().filter_map(Value::as_i64).collect())
269 .unwrap_or_default(),
270 your_seat: state_obj
271 .and_then(|o| o.get("your_seat"))
272 .and_then(Value::as_i64)
273 .unwrap_or(0),
274 dealer_seat: state_obj
275 .and_then(|o| o.get("dealer_seat"))
276 .and_then(Value::as_i64)
277 .unwrap_or(0),
278 to_call: state_obj
279 .and_then(|o| o.get("to_call"))
280 .and_then(Value::as_i64)
281 .unwrap_or(0),
282 min_raise: state_obj
283 .and_then(|o| o.get("min_raise"))
284 .and_then(Value::as_i64)
285 .unwrap_or(0),
286 max_raise: state_obj
287 .and_then(|o| o.get("max_raise"))
288 .and_then(Value::as_i64)
289 .unwrap_or(0),
290 valid_actions,
291 action_history,
292 round_id: message
293 .get("round_id")
294 .and_then(|v| v.as_str())
295 .unwrap_or("")
296 .to_string(),
297 request_id: message
298 .get("request_id")
299 .and_then(|v| v.as_str())
300 .unwrap_or("")
301 .to_string(),
302 }
303}
304
305fn parse_history_entry(raw: &Value) -> ActionHistoryEntry {
306 ActionHistoryEntry {
307 seat: raw.get("seat").and_then(Value::as_i64).unwrap_or(0),
308 action: raw
309 .get("action")
310 .and_then(|v| v.as_str())
311 .unwrap_or("")
312 .to_string(),
313 amount: raw.get("amount").and_then(Value::as_i64),
314 is_timeout: raw.get("is_timeout").and_then(Value::as_bool),
315 }
316}