openpql_prelude/rating/
hand_rating.rs

1use super::{
2    HandRatingView, HandType, IdxThreeRanks, IdxTwoRanks, Rank16, RatingInner,
3    fmt,
4};
5
6/// Hand Ranking
7/// # Overview
8/// this is used for holdem omaha and shortdeck
9/// there are 10 hand types:
10/// * `RoyalFlush`
11/// * `StraightFlush`
12/// * `Quads`
13/// * `FullHouse`
14/// * `Flush`
15/// * `Straight`
16/// * `Trips`
17/// * `TwoPair`
18/// * `Pair`
19/// * `HighCard`
20///
21/// # Rank representation:
22/// * Rank as index value
23///   * 0b0000 is Deuce and 0b1100 is Ace
24/// * Combination of two ranks
25///   * nCr(13, 2) is 78 so the index of combination can be fitted in 7 bits
26/// * Combination of three ranks
27///   * nCr(13, 3) is 286 so the index of combination can be fitted in 9 bits
28/// * Combination of five ranks
29///   * we just use the 13 bit flags
30///
31/// # Memory Layout:
32/// ```text
33/// u16
34///
35/// `RoyalFlush`/`StraightFlush`
36/// [15, 0]:   1110ssss 00000000 // s: rank of highest card
37///
38/// `Quads`:
39/// [15, 0]:   11100000 qqqqkkkk // q: rank of quads; k: rank of kicker
40///
41/// `FullHouse`:
42/// [15, 0]:   11011111 ttttpppp // q: rank of trips; k: rank of pair
43/// [15, 0]:   10111111 ttttpppp // shortdeck
44///
45/// `Flush`:
46/// [15, 0]:   101rrrrr rrrrrrrr // r: set bit of 5 cards and zeros of the rest
47/// [15, 0]:   111rrrrr rrrrrrrr // shortdeck
48///
49/// `Straight`:
50/// [15, 0]:   10000000 0000ssss // s: rank of highest card
51///
52/// `Trips`:
53/// [15, 0]:   0110tttt 0kkkkkkk // t: rank of trips; k: index of combination
54///
55/// `TwoPair`:
56/// [15, 0]:   01000ppp ppppkkkk // p: index of combination; k: rank of kicker
57///
58/// `Pair`:
59/// [15, 0]:   001ppppk kkkkkkkk // p: rank of pair; k: index of combination
60///
61/// `HighCard`:
62/// [15, 0]:   000rrrrr rrrrrrrr // r: bit flags of 5 cards
63/// ```
64#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
65pub struct HandRating(pub(crate) RatingInner);
66
67/// returns highest rank index
68/// input must have 1 or more ranks
69#[allow(clippy::cast_possible_truncation)]
70#[must_use]
71#[inline]
72const fn rank_idx(ranks: Rank16) -> RatingInner {
73    TOTAL_LEADING_ZEROS - ranks.0.leading_zeros() as u16
74}
75
76#[inline]
77const fn rev_rank_idx(idx: RatingInner) -> Rank16 {
78    const MASK_RANK_IDX: RatingInner = 0b1111;
79    Rank16(1 << (idx & MASK_RANK_IDX))
80}
81
82#[must_use]
83#[inline]
84const fn comb2(ranks: Rank16) -> RatingInner {
85    IdxTwoRanks::from_r16(ranks).0 as RatingInner
86}
87
88#[inline]
89fn rev_comb2(i: RatingInner) -> Rank16 {
90    IdxTwoRanks(IdxTwoRanks::MASK_USED & i.to_le_bytes()[0]).to_r16()
91}
92
93#[must_use]
94#[inline]
95const fn comb3(ranks: Rank16) -> RatingInner {
96    IdxThreeRanks::from_r16(ranks).0 as RatingInner
97}
98
99#[inline]
100fn rev_comb3(i: RatingInner) -> Rank16 {
101    IdxThreeRanks(IdxThreeRanks::MASK_USED & i).to_r16()
102}
103
104const TOTAL_LEADING_ZEROS: RatingInner = 15;
105const OFFSET_RANK_IDX: usize = 4;
106const OFFSET_COMB3: usize = 9;
107const OFFSET_HI: usize = 8;
108const MASK_FULLHOUSE_PADDING: RatingInner = 0b0001_1111_0000_0000;
109
110impl HandRating {
111    pub(crate) const MASK_STRAIGHTFLUSH: RatingInner = 0b1110_0000_0000_0000;
112    pub(crate) const MASK_QUADS: RatingInner = 0b1110_0000_0000_0000;
113    pub(crate) const MASK_FULLHOUSE: RatingInner = 0b1100_0000_0000_0000;
114    pub(crate) const MASK_FLUSH: RatingInner = 0b1010_0000_0000_0000;
115    pub(crate) const MASK_STRAIGHT: RatingInner = 0b1000_0000_0000_0000;
116    pub(crate) const MASK_TRIPS: RatingInner = 0b0110_0000_0000_0000;
117    pub(crate) const MASK_TWOPAIR: RatingInner = 0b0100_0000_0000_0000;
118    pub(crate) const MASK_PAIR: RatingInner = 0b0010_0000_0000_0000;
119    pub(crate) const MASK_HIGHCARD: RatingInner = 0b0000_0000_0000_0000;
120
121    pub(crate) const MASK_FULLHOUSE_SD: RatingInner = Self::MASK_FLUSH;
122    pub(crate) const MASK_FLUSH_SD: RatingInner = Self::MASK_FULLHOUSE;
123
124    pub(crate) const fn new_highcard(ranks: Rank16) -> Self {
125        Self(Self::MASK_HIGHCARD | ranks.0)
126    }
127
128    pub(crate) const fn parse_highcard(self) -> Rank16 {
129        Rank16(Rank16::ALL.0 & self.0)
130    }
131
132    pub(crate) const fn new_pair(pair: Rank16, kicker: Rank16) -> Self {
133        Self(Self::MASK_PAIR | rank_idx(pair) << OFFSET_COMB3 | comb3(kicker))
134    }
135
136    pub(crate) fn parse_pair(self) -> (Rank16, Rank16) {
137        (
138            rev_rank_idx((Self::MASK_PAIR ^ self.0) >> OFFSET_COMB3),
139            rev_comb3(self.0),
140        )
141    }
142
143    pub(crate) const fn new_twopair(pairs: Rank16, kicker: Rank16) -> Self {
144        Self(
145            Self::MASK_TWOPAIR
146                | comb2(pairs) << OFFSET_RANK_IDX
147                | rank_idx(kicker),
148        )
149    }
150
151    pub(crate) fn parse_twopair(self) -> (Rank16, Rank16) {
152        (rev_comb2(self.0 >> OFFSET_RANK_IDX), rev_rank_idx(self.0))
153    }
154
155    pub(crate) const fn new_trips(trips: Rank16, kicker: Rank16) -> Self {
156        Self(Self::MASK_TRIPS | rank_idx(trips) << OFFSET_HI | comb2(kicker))
157    }
158
159    pub(crate) fn parse_trips(self) -> (Rank16, Rank16) {
160        (rev_rank_idx(self.0 >> OFFSET_HI), rev_comb2(self.0))
161    }
162
163    pub(crate) const fn new_straight(ranks: Rank16) -> Self {
164        Self(Self::MASK_STRAIGHT | rank_idx(ranks))
165    }
166
167    pub(crate) const fn parse_straight(self) -> Rank16 {
168        rev_rank_idx(self.0)
169    }
170
171    pub(crate) const fn new_flush(ranks: Rank16) -> Self {
172        Self(Self::MASK_FLUSH | ranks.0)
173    }
174
175    pub(crate) const fn new_flush_sd(ranks: Rank16) -> Self {
176        Self(Self::MASK_FLUSH_SD | ranks.0)
177    }
178
179    pub(crate) const fn parse_flush(self) -> Rank16 {
180        self.parse_highcard()
181    }
182
183    pub(crate) const fn new_fullhouse(trips: Rank16, pairs: Rank16) -> Self {
184        Self(
185            Self::MASK_FULLHOUSE
186                | MASK_FULLHOUSE_PADDING
187                | rank_idx(trips) << OFFSET_RANK_IDX
188                | rank_idx(pairs),
189        )
190    }
191
192    pub(crate) const fn new_fullhouse_sd(trips: Rank16, pairs: Rank16) -> Self {
193        Self(
194            Self::MASK_FULLHOUSE_SD
195                | MASK_FULLHOUSE_PADDING
196                | rank_idx(trips) << OFFSET_RANK_IDX
197                | rank_idx(pairs),
198        )
199    }
200
201    pub(crate) const fn parse_fullhouse(self) -> (Rank16, Rank16) {
202        (
203            rev_rank_idx(self.0 >> OFFSET_RANK_IDX),
204            rev_rank_idx(self.0),
205        )
206    }
207
208    pub(crate) const fn new_quad(quad: Rank16, kicker: Rank16) -> Self {
209        Self(
210            Self::MASK_QUADS
211                | rank_idx(quad) << OFFSET_RANK_IDX
212                | rank_idx(kicker),
213        )
214    }
215
216    pub(crate) const fn parse_quad(self) -> (Rank16, Rank16) {
217        self.parse_fullhouse()
218    }
219
220    pub(crate) const fn new_straightflush(ranks: Rank16) -> Self {
221        Self(Self::MASK_STRAIGHTFLUSH | rank_idx(ranks) << OFFSET_HI)
222    }
223
224    pub(crate) const fn parse_straightflush(self) -> Rank16 {
225        rev_rank_idx(self.0 >> OFFSET_HI)
226    }
227}
228
229impl fmt::Display for HandRating {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        let view = HandRatingView::from(*self);
232        let ht = view.hand_type;
233
234        match ht {
235            HandType::HighCard
236            | HandType::Straight
237            | HandType::Flush
238            | HandType::StraightFlush => write!(f, "{ht}({})", view.high),
239
240            HandType::Pair
241            | HandType::TwoPair
242            | HandType::Trips
243            | HandType::FullHouse
244            | HandType::Quads => write!(f, "{ht}({}, {})", view.high, view.low),
245        }
246    }
247}
248
249impl fmt::Debug for HandRating {
250    #![cfg_attr(coverage_nightly, coverage(off))]
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        <Self as fmt::Display>::fmt(self, f)
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use HandType::*;
259
260    use super::*;
261    use crate::*;
262
263    fn assert_str(text: &str, ht: HandType, hi: &str, lo: &str) {
264        assert_eq!(text, mk_rating(ht, hi, lo).to_string());
265    }
266
267    #[test]
268    fn test_display() {
269        assert_str("STRAIGHT_FLUSH(5)", StraightFlush, "5", "");
270        assert_str("QUADS(A, K)", Quads, "A", "K");
271        assert_str("FULL_HOUSE(T, A)", FullHouse, "T", "A");
272        assert_str("FLUSH(6789J)", Flush, "J6789", "");
273        assert_str("STRAIGHT(9)", Straight, "9", "");
274        assert_str("TRIPS(T, KA)", Trips, "T", "AK");
275        assert_str("TWO_PAIR(6T, K)", TwoPair, "T6", "K");
276        assert_str("PAIR(J, 89K)", Pair, "J", "K98");
277        assert_str("HIGH_CARD(89JQK)", HighCard, "KQJ98", "");
278    }
279
280    #[test]
281    fn test_default() {
282        assert_eq!(HandRating::default().0, RatingInner::MIN);
283    }
284
285    #[quickcheck]
286    fn test_bijection(ranks: Distinct<5, Rank>) {
287        let r5 = Rank16::from(&ranks[..]);
288
289        let hi = Rank16::from(ranks[0]);
290        let lo = Rank16::from(ranks[1]);
291        let r2 = Rank16::from(&ranks[..2]);
292        let r3 = Rank16::from(&ranks[2..]);
293        let tl = Rank16::from(ranks[4]);
294
295        assert_eq!(r5, HandRating::new_highcard(r5).parse_highcard());
296        assert_eq!((hi, r3), HandRating::new_pair(hi, r3).parse_pair());
297        assert_eq!((r2, tl), HandRating::new_twopair(r2, tl).parse_twopair());
298        assert_eq!((tl, r2), HandRating::new_trips(tl, r2).parse_trips());
299        assert_eq!(lo, HandRating::new_straight(lo).parse_straight());
300        assert_eq!(r5, HandRating::new_flush(r5).parse_flush());
301        assert_eq!(r5, HandRating::new_flush_sd(r5).parse_flush());
302        assert_eq!(
303            (hi, lo),
304            HandRating::new_fullhouse(hi, lo).parse_fullhouse()
305        );
306        assert_eq!(
307            (hi, lo),
308            HandRating::new_fullhouse_sd(hi, lo).parse_fullhouse()
309        );
310        assert_eq!((hi, lo), HandRating::new_quad(hi, lo).parse_quad());
311        assert_eq!(hi, HandRating::new_straightflush(hi).parse_straightflush());
312    }
313}