Skip to main content

rbp_gameplay/
odds.rs

1use rbp_core::*;
2
3/// Pot-relative bet sizing as a fraction.
4///
5/// Represents raise sizes as `numerator/denominator` of the pot. For example,
6/// `Odds::new(1, 2)` means a half-pot bet, `Odds::new(2, 1)` means a 2x pot overbet.
7///
8/// See [`Size`] for the full sizing abstraction that handles both pot-relative
9/// and BB-relative interpretations.
10#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Ord, PartialOrd)]
11pub struct Odds(Chips, Chips);
12
13impl Odds {
14    /// Creates new odds from numerator and denominator.
15    pub const fn new(n: Chips, d: Chips) -> Self {
16        Self(n, d)
17    }
18    /// Numerator (pot multiplier).
19    pub fn numer(&self) -> Chips {
20        self.0
21    }
22    /// Denominator (pot divisor).
23    pub fn denom(&self) -> Chips {
24        self.1
25    }
26    /// Reduces fraction to lowest terms.
27    fn gcd(a: Chips, b: Chips) -> (Chips, Chips) {
28        let (mut x, mut y) = (a, b);
29        while y != 0 {
30            (x, y) = (y, x % y);
31        }
32        (a / x, b / x)
33    }
34    /// Formats as "N:N" ratio for display.
35    pub fn ratio(&self) -> String {
36        format!("{}:{}", self.0, self.1)
37    }
38    /// Full grid for random sampling.
39    pub const GRID: [Self; 10] = [
40        Self(1, 4), // 0.25 pot
41        Self(1, 3), // 0.33 pot
42        Self(1, 2), // 0.50 pot
43        Self(2, 3), // 0.66 pot
44        Self(3, 4), // 0.75 pot
45        Self(1, 1), // 1.00 pot
46        Self(5, 4), // 1.25 pot
47        Self(3, 2), // 1.50 pot
48        Self(2, 1), // 2x pot
49        Self(3, 1), // 3x pot
50    ];
51}
52
53impl From<Odds> for Probability {
54    fn from(odds: Odds) -> Self {
55        odds.0 as Probability / odds.1 as Probability
56    }
57}
58
59impl From<(Chips, Chips)> for Odds {
60    fn from((a, b): (Chips, Chips)) -> Self {
61        let (a, b) = Self::gcd(a, b);
62        Self(a, b)
63    }
64}
65
66/// For +N format, odds are 1/N (pot fraction)
67/// For -N format, odds are N/1 (overbet or BB multiple)
68impl TryFrom<&str> for Odds {
69    type Error = anyhow::Error;
70    fn try_from(s: &str) -> Result<Self, Self::Error> {
71        match (s.strip_prefix('+'), s.strip_prefix('-')) {
72            (Some(x), _) => Ok(Self::new(1, x.parse()?)),
73            (_, Some(x)) => Ok(Self::new(x.parse()?, 1)),
74            _ => Err(anyhow::anyhow!("odds string missing + or -")),
75        }
76    }
77}
78
79impl std::fmt::Display for Odds {
80    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
81        let p = Probability::from(*self);
82        if p > 1.0 {
83            write!(f, "-{}", p.round() as Chips)
84        } else {
85            write!(f, "+{}", (1.0 / p).round() as Chips)
86        }
87    }
88}
89
90impl Arbitrary for Odds {
91    fn random() -> Self {
92        use rand::prelude::IndexedRandom;
93        let ref mut rng = rand::rng();
94        Self::GRID.choose(rng).copied().expect("GRID is empty")
95    }
96}