1use super::*;
2use rbp_cards::*;
3use rbp_core::*;
4
5#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Ord, PartialOrd)]
11pub enum Size {
12 SPR(Chips, Chips),
13 BBs(Chips),
14}
15
16impl Size {
17 pub fn into_chips(self, pot: Chips) -> Chips {
19 match self {
20 Self::SPR(n, d) => (pot as Utility * n as Utility / d as Utility) as Chips,
21 Self::BBs(n) => n * rbp_core::B_BLIND,
22 }
23 }
24 pub fn from_chips(
27 chips: Chips,
28 pot: Chips,
29 opening: bool,
30 street: Street,
31 depth: usize,
32 ) -> Self {
33 let raises = Self::raises(street, depth);
34 if opening {
35 Self::nearest_bb(chips, raises)
36 } else {
37 Self::nearest_pot(chips, pot, raises)
38 }
39 }
40 fn nearest_bb(chips: Chips, raises: &[Self]) -> Self {
41 let target = chips / rbp_core::B_BLIND;
42 raises
43 .iter()
44 .filter_map(|s| match s {
45 Self::BBs(n) => Some((*n, *s)),
46 Self::SPR(..) => None,
47 })
48 .min_by_key(|(n, _)| (target as i64 - *n as i64).abs())
49 .map(|(_, s)| s)
50 .unwrap_or(Self::BBs(2))
51 }
52 fn nearest_pot(chips: Chips, pot: Chips, raises: &[Self]) -> Self {
53 let target = chips as Utility / pot as Utility;
54 raises
55 .iter()
56 .filter_map(|s| match s {
57 Self::SPR(n, d) => Some((*n as Probability / *d as Probability, *s)),
58 Self::BBs(_) => None,
59 })
60 .min_by(|(a, _), (b, _)| (target - a).abs().partial_cmp(&(target - b).abs()).unwrap())
61 .map(|(_, s)| s)
62 .unwrap_or(Self::SPR(1, 1))
63 }
64 pub fn odds(self) -> Odds {
67 match self {
68 Self::SPR(n, d) => Odds::new(n, d),
69 Self::BBs(n) => Odds::new(n, 1),
70 }
71 }
72 pub fn as_spr(self) -> Self {
75 match self {
76 Self::BBs(n) => Self::SPR(n, 1),
77 spr => spr,
78 }
79 }
80 pub fn raises(street: Street, depth: usize) -> &'static [Self] {
83 if depth > rbp_core::MAX_RAISE_REPEATS {
84 return &[];
85 }
86 match (street, depth) {
87 (Street::Pref, 0) => &Self::PREF_0,
88 (Street::Pref, 1) => &Self::PREF_1,
89 (Street::Pref, _) => &Self::PREF_N,
90 (Street::Flop, 0) => &Self::FLOP_0,
91 (Street::Flop, 1) => &Self::FLOP_1,
92 (Street::Flop, _) => &Self::FLOP_N,
93 (Street::Turn, 0) => &Self::TURN_0,
94 (Street::Turn, _) => &Self::TURN_N,
95 (Street::Rive, 0) => &Self::RIVE_0,
96 (Street::Rive, 1) => &Self::RIVE_1,
97 (Street::Rive, _) => &Self::RIVE_N,
98 }
99 }
100}
101
102const BLINDS_GRID: [Chips; 4] = [2, 3, 4, 8];
104const SPR_GRID: [Size; 6] = [
106 Size::SPR(1, 3), Size::SPR(1, 2), Size::SPR(2, 3), Size::SPR(1, 1), Size::SPR(3, 2), Size::SPR(2, 1), ];
113
114#[rustfmt::skip]
115impl Size {
116 const PREF_0: [Self; 4] = [Self::BBs(2), Self::BBs(3), Self::BBs(4), Self::BBs(8)]; const PREF_1: [Self; 3] = [Self::SPR(1, 1), Self::SPR(3, 2), Self::SPR(2, 1)]; const PREF_N: [Self; 2] = [Self::SPR(1, 1), Self::SPR(2, 1)]; const FLOP_0: [Self; 4] = [Self::SPR(1, 3), Self::SPR(1, 2), Self::SPR(1, 1), Self::SPR(2, 1)]; const FLOP_1: [Self; 3] = [Self::SPR(2, 3), Self::SPR(1, 1), Self::SPR(3, 2)]; const FLOP_N: [Self; 2] = [Self::SPR(1, 1), Self::SPR(3, 2)]; const TURN_0: [Self; 4] = [Self::SPR(1, 3), Self::SPR(2, 3), Self::SPR(1, 1), Self::SPR(2, 1)]; const TURN_N: [Self; 2] = [Self::SPR(1, 1), Self::SPR(3, 2)]; const RIVE_0: [Self; 4] = [Self::SPR(1, 3), Self::SPR(1, 2), Self::SPR(1, 1), Self::SPR(2, 1)]; const RIVE_1: [Self; 3] = [Self::SPR(2, 3), Self::SPR(1, 1), Self::SPR(2, 1)]; const RIVE_N: [Self; 1] = [Self::SPR(1, 1)]; }
128
129impl From<Odds> for Size {
130 fn from(odds: Odds) -> Self {
131 Self::SPR(odds.numer(), odds.denom())
132 }
133}
134
135impl From<Size> for u8 {
138 fn from(size: Size) -> Self {
139 match size {
140 Size::BBs(n) => {
141 6 + BLINDS_GRID
142 .iter()
143 .position(|&b| b == n)
144 .expect("invalid blinds value") as u8
145 }
146 Size::SPR(..) => {
147 10 + SPR_GRID
148 .iter()
149 .position(|&s| s == size)
150 .expect("invalid SPR value") as u8
151 }
152 }
153 }
154}
155impl From<u8> for Size {
156 fn from(value: u8) -> Self {
157 match value {
158 6..=9 => Self::BBs(BLINDS_GRID[value as usize - 6]),
159 10..=15 => SPR_GRID[value as usize - 10],
160 _ => panic!("invalid size encoding: {}", value),
161 }
162 }
163}
164impl From<Size> for u64 {
166 fn from(size: Size) -> Self {
167 match size {
168 Size::BBs(n) => (1 << 19) | ((n as u64) << 3),
169 Size::SPR(n, d) => ((n as u64) << 3) | ((d as u64) << 11),
170 }
171 }
172}
173impl From<u64> for Size {
174 fn from(value: u64) -> Self {
175 if value & (1 << 19) != 0 {
176 Self::BBs(((value >> 3) & 0xFF) as Chips)
177 } else {
178 Self::SPR(
179 ((value >> 3) & 0xFF) as Chips,
180 ((value >> 11) & 0xFF) as Chips,
181 )
182 }
183 }
184}
185impl std::fmt::Display for Size {
186 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
187 match self {
188 Self::SPR(n, d) => write!(f, "{}:{}", n, d),
189 Self::BBs(n) => write!(f, "{}bb", n),
190 }
191 }
192}
193impl TryFrom<&str> for Size {
194 type Error = anyhow::Error;
195 fn try_from(s: &str) -> Result<Self, Self::Error> {
196 if let Some(bb) = s.strip_suffix("bb") {
197 return bb
198 .parse::<Chips>()
199 .map(Self::BBs)
200 .map_err(|e| anyhow::anyhow!("invalid bb format: {}", e));
201 }
202 if let Some((n, d)) = s.split_once(':') {
203 let n = n
204 .parse::<Chips>()
205 .map_err(|e| anyhow::anyhow!("invalid SPR numerator: {}", e))?;
206 let d = d
207 .parse::<Chips>()
208 .map_err(|e| anyhow::anyhow!("invalid SPR denominator: {}", e))?;
209 return Ok(Self::SPR(n, d));
210 }
211 Err(anyhow::anyhow!("invalid size format: {}", s))
212 }
213}
214impl Arbitrary for Size {
215 fn random() -> Self {
216 use rand::prelude::IndexedRandom;
217 let ref mut rng = rand::rng();
218 let all_sizes: Vec<Self> = BLINDS_GRID
219 .iter()
220 .map(|&n| Self::BBs(n))
221 .chain(SPR_GRID.iter().copied())
222 .collect();
223 *all_sizes.choose(rng).expect("sizes empty")
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use rbp_core::MAX_RAISE_REPEATS;
231 #[test]
234 fn raises_grid_counts() {
235 assert_eq!(Size::raises(Street::Pref, 0).len(), 4); assert_eq!(Size::raises(Street::Pref, 1).len(), 3); assert_eq!(Size::raises(Street::Pref, 2).len(), 2); assert_eq!(Size::raises(Street::Pref, 3).len(), 2); assert_eq!(Size::raises(Street::Flop, 0).len(), 4); assert_eq!(Size::raises(Street::Flop, 1).len(), 3); assert_eq!(Size::raises(Street::Flop, 2).len(), 2); assert_eq!(Size::raises(Street::Turn, 0).len(), 4); assert_eq!(Size::raises(Street::Turn, 1).len(), 2); assert_eq!(Size::raises(Street::Rive, 0).len(), 4); assert_eq!(Size::raises(Street::Rive, 1).len(), 3); assert_eq!(Size::raises(Street::Pref, MAX_RAISE_REPEATS + 1).len(), 0);
252 }
253 #[test]
255 fn preflop_opening_uses_bbs() {
256 for size in Size::raises(Street::Pref, 0) {
257 assert!(
258 matches!(size, Size::BBs(_)),
259 "preflop depth=0 should use BBs, got {:?}",
260 size
261 );
262 }
263 }
264 #[test]
266 fn postflop_uses_spr() {
267 for street in [Street::Flop, Street::Turn, Street::Rive] {
268 for depth in 0..=MAX_RAISE_REPEATS {
269 for size in Size::raises(street, depth) {
270 assert!(
271 matches!(size, Size::SPR(..)),
272 "{:?} depth={} should use SPR, got {:?}",
273 street,
274 depth,
275 size
276 );
277 }
278 }
279 }
280 for depth in 1..=MAX_RAISE_REPEATS {
281 for size in Size::raises(Street::Pref, depth) {
282 assert!(
283 matches!(size, Size::SPR(..)),
284 "preflop depth={} should use SPR, got {:?}",
285 depth,
286 size
287 );
288 }
289 }
290 }
291 #[test]
293 fn all_raises_are_encodable() {
294 for street in [Street::Pref, Street::Flop, Street::Turn, Street::Rive] {
295 for depth in 0..=MAX_RAISE_REPEATS {
296 for &size in Size::raises(street, depth) {
297 let encoded = u8::from(size);
298 let decoded = Size::from(encoded);
299 assert_eq!(size, decoded, "roundtrip failed for {:?}", size);
300 }
301 }
302 }
303 }
304 #[test]
306 fn bijective_u8() {
307 for &n in &BLINDS_GRID {
308 let size = Size::BBs(n);
309 assert_eq!(size, Size::from(u8::from(size)));
310 }
311 for &size in &SPR_GRID {
312 assert_eq!(size, Size::from(u8::from(size)));
313 }
314 }
315 #[test]
317 fn bijective_u64() {
318 for &n in &BLINDS_GRID {
319 let size = Size::BBs(n);
320 assert_eq!(size, Size::from(u64::from(size)));
321 }
322 for &size in &SPR_GRID {
323 assert_eq!(size, Size::from(u64::from(size)));
324 }
325 }
326 #[test]
328 fn into_chips_bbs() {
329 let pot = 100; assert_eq!(Size::BBs(2).into_chips(pot), 2 * rbp_core::B_BLIND);
331 assert_eq!(Size::BBs(3).into_chips(pot), 3 * rbp_core::B_BLIND);
332 assert_eq!(Size::BBs(8).into_chips(pot), 8 * rbp_core::B_BLIND);
333 }
334 #[test]
336 fn into_chips_spr() {
337 let pot = 100;
338 assert_eq!(Size::SPR(1, 2).into_chips(pot), 50); assert_eq!(Size::SPR(1, 1).into_chips(pot), 100); assert_eq!(Size::SPR(2, 1).into_chips(pot), 200); }
342 #[test]
344 fn from_chips_snaps_to_nearest() {
345 let pot = 100;
346 let size = Size::from_chips(5, pot, true, Street::Pref, 0);
348 assert!(matches!(size, Size::BBs(2) | Size::BBs(3)));
349 let size = Size::from_chips(75, pot, false, Street::Flop, 0);
351 assert!(matches!(size, Size::SPR(..)));
352 }
353 #[test]
355 fn spr_grid_is_complete() {
356 let mut all_spr = std::collections::HashSet::new();
357 for street in [Street::Pref, Street::Flop, Street::Turn, Street::Rive] {
358 for depth in 0..=MAX_RAISE_REPEATS {
359 for &size in Size::raises(street, depth) {
360 if matches!(size, Size::SPR(..)) {
361 all_spr.insert(size);
362 }
363 }
364 }
365 }
366 for size in all_spr {
367 assert!(SPR_GRID.contains(&size), "SPR_GRID missing {:?}", size);
368 }
369 }
370 #[test]
372 fn blinds_grid_is_complete() {
373 let mut all_bbs = std::collections::HashSet::new();
374 for street in [Street::Pref, Street::Flop, Street::Turn, Street::Rive] {
375 for depth in 0..=MAX_RAISE_REPEATS {
376 for size in Size::raises(street, depth) {
377 if let Size::BBs(n) = size {
378 all_bbs.insert(*n);
379 }
380 }
381 }
382 }
383 for n in all_bbs {
384 assert!(BLINDS_GRID.contains(&n), "BLINDS_GRID missing {}", n);
385 }
386 }
387 #[test]
389 fn display_format() {
390 assert_eq!(format!("{}", Size::BBs(3)), "3bb");
391 assert_eq!(format!("{}", Size::SPR(1, 2)), "1:2");
392 assert_eq!(format!("{}", Size::SPR(1, 1)), "1:1");
393 assert_eq!(format!("{}", Size::SPR(3, 2)), "3:2");
394 assert_eq!(format!("{}", Size::SPR(2, 1)), "2:1");
395 assert_eq!(format!("{}", Size::SPR(1, 3)), "1:3");
396 assert_eq!(format!("{}", Size::SPR(2, 3)), "2:3");
397 }
398 #[test]
400 fn string_roundtrip() {
401 for &n in &BLINDS_GRID {
402 let size = Size::BBs(n);
403 let s = size.to_string();
404 assert_eq!(Size::try_from(s.as_str()).unwrap(), size);
405 }
406 for &size in &SPR_GRID {
407 let s = size.to_string();
408 assert_eq!(Size::try_from(s.as_str()).unwrap(), size);
409 }
410 }
411}