1use super::*;
2use rbp_cards::Street;
3use rbp_core::*;
4use std::hash::Hash;
5
6#[derive(Debug, Clone, Copy, Hash, Ord, PartialOrd, PartialEq, Eq)]
19pub enum Edge {
20 Draw,
21 Fold,
22 Check,
23 Call,
24 Open(Chips),
25 Raise(Odds),
26 Shove,
27}
28
29
30impl Edge {
31 pub fn is_shove(&self) -> bool {
33 matches!(self, Edge::Shove)
34 }
35 pub fn is_raise(&self) -> bool {
37 matches!(self, Edge::Raise(_) | Edge::Open(_))
38 }
39 pub fn is_folded(&self) -> bool {
41 matches!(self, Edge::Fold)
42 }
43 pub fn is_chance(&self) -> bool {
45 matches!(self, Edge::Draw)
46 }
47 pub fn is_aggro(&self) -> bool {
49 self.is_raise() || self.is_shove()
50 }
51 pub fn is_choice(&self) -> bool {
53 !self.is_chance()
54 }
55}
56
57impl Edge {
58 pub fn regret(&self) -> (Utility, Utility) {
62 match self {
63 Edge::Open(_) => (Utility::default(), BIAS_RAISE),
64 Edge::Raise(_) => (Utility::default(), BIAS_RAISE),
65 Edge::Check => (Utility::default(), BIAS_OTHER),
66 Edge::Shove => (Utility::default(), BIAS_RAISE),
67 Edge::Call => (Utility::default(), BIAS_OTHER),
68 Edge::Fold => (Utility::default(), BIAS_FOLDS),
69 Edge::Draw => panic!("chance edges have no learned regret"),
70 }
71 }
72 pub fn policy(&self) -> (Probability, Probability) {
74 (Probability::default(), Probability::default())
75 }
76}
77
78impl From<Action> for Edge {
79 fn from(action: Action) -> Self {
80 match action {
81 Action::Fold => Edge::Fold,
82 Action::Check => Edge::Check,
83 Action::Call(_) => Edge::Call,
84 Action::Draw(_) => Edge::Draw,
85 Action::Shove(_) => Edge::Shove,
86 Action::Raise(_) => panic!("raise must be converted via Game::edgify"),
87 Action::Blind(_) => panic!("blinds are not in any MCCFR trees"),
88 }
89 }
90}
91
92impl From<Odds> for Edge {
93 fn from(odds: Odds) -> Self {
94 Edge::Raise(odds)
95 }
96}
97
98const OPENS_GRID: [Chips; 4] = [2, 3, 4, 8];
100const RAISE_GRID: [Odds; 6] = [
102 Odds::new(1, 3), Odds::new(1, 2), Odds::new(2, 3), Odds::new(1, 1), Odds::new(3, 2), Odds::new(2, 1), ];
109
110impl Edge {
111 pub fn raises(street: Street, depth: usize) -> Vec<Self> {
114 if depth > MAX_RAISE_REPEATS {
115 return vec![];
116 }
117 match (street, depth) {
118 (Street::Pref, 0) => OPENS_GRID.iter().map(|&n| Edge::Open(n)).collect(),
120 (Street::Pref, 1) => vec![
122 Edge::Raise(Odds::new(1, 1)),
123 Edge::Raise(Odds::new(3, 2)),
124 Edge::Raise(Odds::new(2, 1)),
125 ],
126 (Street::Pref, _) => vec![Edge::Raise(Odds::new(1, 1)), Edge::Raise(Odds::new(2, 1))],
127 (Street::Flop, 0) => vec![
129 Edge::Raise(Odds::new(1, 3)),
130 Edge::Raise(Odds::new(1, 2)),
131 Edge::Raise(Odds::new(1, 1)),
132 Edge::Raise(Odds::new(2, 1)),
133 ],
134 (Street::Flop, 1) => vec![
135 Edge::Raise(Odds::new(2, 3)),
136 Edge::Raise(Odds::new(1, 1)),
137 Edge::Raise(Odds::new(3, 2)),
138 ],
139 (Street::Flop, _) => vec![Edge::Raise(Odds::new(1, 1)), Edge::Raise(Odds::new(3, 2))],
140 (Street::Turn, 0) => vec![
142 Edge::Raise(Odds::new(1, 3)),
143 Edge::Raise(Odds::new(2, 3)),
144 Edge::Raise(Odds::new(1, 1)),
145 Edge::Raise(Odds::new(2, 1)),
146 ],
147 (Street::Turn, _) => vec![Edge::Raise(Odds::new(1, 1)), Edge::Raise(Odds::new(3, 2))],
148 (Street::Rive, 0) => vec![
150 Edge::Raise(Odds::new(1, 3)),
151 Edge::Raise(Odds::new(1, 2)),
152 Edge::Raise(Odds::new(1, 1)),
153 Edge::Raise(Odds::new(2, 1)),
154 ],
155 (Street::Rive, 1) => vec![
156 Edge::Raise(Odds::new(2, 3)),
157 Edge::Raise(Odds::new(1, 1)),
158 Edge::Raise(Odds::new(2, 1)),
159 ],
160 (Street::Rive, _) => vec![Edge::Raise(Odds::new(1, 1))],
161 }
162 }
163 pub fn into_chips(self, pot: Chips) -> Chips {
165 match self {
166 Edge::Open(n) => n * B_BLIND,
167 Edge::Raise(odds) => (pot as Utility * Probability::from(odds)) as Chips,
168 _ => 0,
169 }
170 }
171}
172
173impl From<Edge> for u8 {
176 fn from(edge: Edge) -> Self {
177 match edge {
178 Edge::Draw => 1,
179 Edge::Fold => 2,
180 Edge::Check => 3,
181 Edge::Call => 4,
182 Edge::Shove => 5,
183 Edge::Open(n) => {
184 6 + OPENS_GRID
185 .iter()
186 .position(|&b| b == n)
187 .expect("invalid open size") as u8
188 }
189 Edge::Raise(odds) => {
190 10 + RAISE_GRID
191 .iter()
192 .position(|&o| o == odds)
193 .expect("invalid raise odds") as u8
194 }
195 }
196 }
197}
198impl From<u8> for Edge {
199 fn from(value: u8) -> Self {
200 match value {
201 1 => Edge::Draw,
202 2 => Edge::Fold,
203 3 => Edge::Check,
204 4 => Edge::Call,
205 5 => Edge::Shove,
206 6..=9 => Edge::Open(OPENS_GRID[value as usize - 6]),
207 10..=15 => Edge::Raise(RAISE_GRID[value as usize - 10]),
208 _ => unreachable!("invalid edge encoding: {}", value),
209 }
210 }
211}
212
213impl From<u64> for Edge {
217 fn from(value: u64) -> Self {
218 match value & 0b111 {
219 0 => Self::Draw,
220 1 => Self::Fold,
221 2 => Self::Check,
222 3 => Self::Call,
223 4 => {
224 if value & (1 << 19) != 0 {
226 Self::Open(((value >> 3) & 0xFF) as Chips)
228 } else {
229 Self::Raise(Odds::new(
231 ((value >> 3) & 0xFF) as Chips,
232 ((value >> 11) & 0xFF) as Chips,
233 ))
234 }
235 }
236 5 => Self::Shove,
237 6 => Self::Open(((value >> 3) & 0xFF) as Chips),
238 _ => unreachable!("invalid edge encoding"),
239 }
240 }
241}
242impl From<Edge> for u64 {
243 fn from(edge: Edge) -> Self {
244 match edge {
245 Edge::Draw => 0,
246 Edge::Fold => 1,
247 Edge::Check => 2,
248 Edge::Call => 3,
249 Edge::Raise(odds) => 4 | ((odds.numer() as u64) << 3) | ((odds.denom() as u64) << 11),
250 Edge::Shove => 5,
251 Edge::Open(n) => 6 | ((n as u64) << 3),
252 }
253 }
254}
255
256impl TryFrom<&str> for Edge {
257 type Error = anyhow::Error;
258 fn try_from(s: &str) -> Result<Self, Self::Error> {
259 match s {
260 "?" => Ok(Edge::Draw),
261 "F" => Ok(Edge::Fold),
262 "*" => Ok(Edge::Call),
263 "O" => Ok(Edge::Check),
264 "!" => Ok(Edge::Shove),
265 s if s.ends_with("bb") => {
266 let n = s
267 .strip_suffix("bb")
268 .and_then(|x| x.parse::<Chips>().ok())
269 .ok_or_else(|| anyhow::anyhow!("invalid bb format"))?;
270 Ok(Edge::Open(n))
271 }
272 s if s.contains(':') => {
273 let (n, d) = s
274 .split_once(':')
275 .ok_or_else(|| anyhow::anyhow!("invalid ratio format"))?;
276 let n = n.parse::<Chips>()?;
277 let d = d.parse::<Chips>()?;
278 Ok(Edge::Raise(Odds::new(n, d)))
279 }
280 _ => Err(anyhow::anyhow!("invalid edge format: {}", s)),
281 }
282 }
283}
284
285impl std::fmt::Display for Edge {
286 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287 match self {
288 Edge::Draw => write!(f, "?"),
289 Edge::Fold => write!(f, "F"),
290 Edge::Call => write!(f, "*"),
291 Edge::Check => write!(f, "O"),
292 Edge::Shove => write!(f, "!"),
293 Edge::Open(n) => write!(f, "{}bb", n),
294 Edge::Raise(odds) => write!(f, "{}:{}", odds.numer(), odds.denom()),
295 }
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use rbp_cards::Street;
303 #[test]
304 fn bijective_u8() {
305 let edges = vec![Edge::Draw, Edge::Fold, Edge::Check, Edge::Call, Edge::Shove];
306 let opens = OPENS_GRID.iter().map(|&n| Edge::Open(n));
307 let raises = RAISE_GRID.iter().map(|&o| Edge::Raise(o));
308 for edge in edges.into_iter().chain(opens).chain(raises) {
309 assert_eq!(
310 edge,
311 Edge::from(u8::from(edge)),
312 "u8 roundtrip failed for {:?}",
313 edge
314 );
315 }
316 }
317 #[test]
318 fn bijective_u64() {
319 let edges = vec![Edge::Draw, Edge::Fold, Edge::Check, Edge::Call, Edge::Shove];
320 let opens = OPENS_GRID.iter().map(|&n| Edge::Open(n));
321 let raises = RAISE_GRID.iter().map(|&o| Edge::Raise(o));
322 for edge in edges.into_iter().chain(opens).chain(raises) {
323 assert_eq!(
324 edge,
325 Edge::from(u64::from(edge)),
326 "u64 roundtrip failed for {:?}",
327 edge
328 );
329 }
330 }
331 #[test]
332 fn string_roundtrip() {
333 let edges = vec![
334 Edge::Draw,
335 Edge::Fold,
336 Edge::Check,
337 Edge::Call,
338 Edge::Shove,
339 Edge::Open(2),
340 Edge::Open(3),
341 Edge::Open(8),
342 Edge::Raise(Odds::new(1, 2)),
343 Edge::Raise(Odds::new(1, 1)),
344 Edge::Raise(Odds::new(3, 2)),
345 Edge::Raise(Odds::new(2, 1)),
346 ];
347 for edge in edges {
348 let s = edge.to_string();
349 let parsed = Edge::try_from(s.as_str()).unwrap();
350 assert_eq!(edge, parsed, "string roundtrip failed for {:?}", edge);
351 }
352 }
353 #[test]
354 fn backwards_compat_u64_bbs() {
355 let old_bbs_8 = 4u64 | (1 << 19) | (8 << 3);
357 assert_eq!(Edge::from(old_bbs_8), Edge::Open(8));
358 let old_bbs_2 = 4u64 | (1 << 19) | (2 << 3);
359 assert_eq!(Edge::from(old_bbs_2), Edge::Open(2));
360 }
361 #[test]
362 fn raises_preflop_depth0_returns_opens() {
363 let edges = Edge::raises(Street::Pref, 0);
364 assert!(edges.iter().all(|e| matches!(e, Edge::Open(_))));
365 assert_eq!(edges.len(), 4);
366 }
367 #[test]
368 fn raises_postflop_returns_raises() {
369 for street in [Street::Flop, Street::Turn, Street::Rive] {
370 for depth in 0..=2 {
371 let edges = Edge::raises(street, depth);
372 assert!(edges.iter().all(|e| matches!(e, Edge::Raise(_))));
373 }
374 }
375 }
376}
377
378impl Arbitrary for Edge {
379 fn random() -> Self {
380 use rand::prelude::IndexedRandom;
381 match rand::random_range(0..7) {
382 0 => Self::Draw,
383 1 => Self::Fold,
384 2 => Self::Check,
385 3 => Self::Call,
386 4 => Self::Shove,
387 5 => Self::Open(*OPENS_GRID.choose(&mut rand::rng()).unwrap()),
388 6 => Self::Raise(*RAISE_GRID.choose(&mut rand::rng()).unwrap()),
389 _ => unreachable!(),
390 }
391 }
392}
393