openpql_prelude/rating/
hand_type.rs

1use super::{Display, FromStr, HandRating, N_HANDTYPE, ParseError, cmp};
2
3/// Represents the categorical type of a poker hand.
4///
5/// This enum classifies poker hands into their standard categories, ordered from
6/// weakest to strongest. It is used to quickly identify what type of hand a player
7/// has without considering the specific ranks involved.
8///
9/// # Ordering
10/// The variants are ordered from weakest (`HighCard`) to strongest (`StraightFlush`),
11/// matching standard poker hand rankings.
12#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, Display)]
13pub enum HandType {
14    /// No matching cards (default/weakest hand type)
15    #[default]
16    #[display("HIGH_CARD")]
17    HighCard,
18    /// Two cards of the same rank
19    #[display("PAIR")]
20    Pair,
21    /// Two different pairs
22    #[display("TWO_PAIR")]
23    TwoPair,
24    /// Three cards of the same rank
25    #[display("TRIPS")]
26    Trips,
27    /// Five cards in sequential rank
28    #[display("STRAIGHT")]
29    Straight,
30    /// Five cards of the same suit
31    #[display("FLUSH")]
32    Flush,
33    /// Three of a kind plus a pair
34    #[display("FULL_HOUSE")]
35    FullHouse,
36    /// Four cards of the same rank
37    #[display("QUADS")]
38    Quads,
39    /// Five cards in sequential rank, all of the same suit
40    #[display("STRAIGHT_FLUSH")]
41    StraightFlush,
42}
43
44type Idx = u8;
45
46impl HandType {
47    pub const MAX: Self = Self::StraightFlush;
48    pub const MIN: Self = Self::HighCard;
49
50    pub const ARR_ALL: [Self; N_HANDTYPE] = [
51        Self::HighCard,
52        Self::Pair,
53        Self::TwoPair,
54        Self::Trips,
55        Self::Straight,
56        Self::Flush,
57        Self::FullHouse,
58        Self::Quads,
59        Self::StraightFlush,
60    ];
61
62    const fn to_idx<const SD: bool>(self) -> Idx {
63        match self {
64            Self::HighCard => 0,
65            Self::Pair => 1,
66            Self::TwoPair => 2,
67            Self::Trips => 3,
68            Self::Straight => 4,
69            Self::Flush => 5 + (SD as Idx) * 2, // shortdeck: 7
70            Self::FullHouse => 6,
71            Self::Quads => 8,
72            Self::StraightFlush => 9,
73        }
74    }
75
76    pub fn compare<const SD: bool>(self, other: Self) -> cmp::Ordering {
77        self.to_idx::<SD>().cmp(&other.to_idx::<SD>())
78    }
79}
80
81const MASK_KIND: u16 = 0b1110_0000_0000_0000;
82const MASK_LO: u16 = 0b0000_0000_1111_1111;
83const N_FLUSH_SET_BITS: u32 = 7;
84
85impl From<HandRating> for HandType {
86    /// Extracts the categorical hand type from a `HandRanking`.
87    ///
88    /// This implementation decodes the bit-packed `HandRanking` to determine
89    /// the hand type. It uses bit masking to identify the hand category from
90    /// the upper bits and additional bit checks to distinguish between hands
91    /// that share the same mask (e.g., Flush vs `FullHouse`, Quads vs `StraightFlush`).
92    fn from(hand_ranking: HandRating) -> Self {
93        match hand_ranking.0 & MASK_KIND {
94            HandRating::MASK_QUADS => {
95                if hand_ranking.0 & MASK_LO == 0 {
96                    Self::StraightFlush
97                } else {
98                    Self::Quads
99                }
100            }
101            HandRating::MASK_FULLHOUSE | HandRating::MASK_FLUSH => {
102                if hand_ranking.0.count_ones() == N_FLUSH_SET_BITS {
103                    Self::Flush
104                } else {
105                    Self::FullHouse
106                }
107            }
108            HandRating::MASK_STRAIGHT => Self::Straight,
109            HandRating::MASK_TRIPS => Self::Trips,
110            HandRating::MASK_TWOPAIR => Self::TwoPair,
111            HandRating::MASK_PAIR => Self::Pair,
112            _ => Self::HighCard,
113        }
114    }
115}
116
117impl FromStr for HandType {
118    type Err = ParseError;
119
120    fn from_str(s: &str) -> Result<Self, Self::Err> {
121        match s.to_ascii_lowercase().trim() {
122            "highcard" => Ok(Self::HighCard),
123            "pair" => Ok(Self::Pair),
124            "twopair" => Ok(Self::TwoPair),
125            "trips" => Ok(Self::Trips),
126            "straight" => Ok(Self::Straight),
127            "flush" => Ok(Self::Flush),
128            "fullhouse" => Ok(Self::FullHouse),
129            "quads" => Ok(Self::Quads),
130            "straightflush" => Ok(Self::StraightFlush),
131            _ => Err(ParseError::InvalidHandType(s.into())),
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_ord_holdem() {
142        let mut sorted = HandType::ARR_ALL.to_vec();
143        sorted.sort_unstable_by(|l, r| l.compare::<false>(*r));
144
145        assert_eq!(sorted, HandType::ARR_ALL);
146    }
147
148    #[test]
149    fn test_ord_shortdeck() {
150        let mut sorted = HandType::ARR_ALL.to_vec();
151        sorted.sort_unstable_by(|l, r| l.compare::<true>(*r));
152
153        assert_eq!(
154            sorted,
155            [
156                HandType::HighCard,
157                HandType::Pair,
158                HandType::TwoPair,
159                HandType::Trips,
160                HandType::Straight,
161                HandType::FullHouse,
162                HandType::Flush, // <--- flush is stronger
163                HandType::Quads,
164                HandType::StraightFlush,
165            ]
166        );
167    }
168
169    #[test]
170    fn test_from_str() {
171        fn assert_str(s: &str, expected: HandType) {
172            assert_eq!(s.parse(), Ok(expected));
173        }
174
175        assert_str("highcard     ", HandType::HighCard);
176        assert_str("pair         ", HandType::Pair);
177        assert_str("twopair      ", HandType::TwoPair);
178        assert_str("trips        ", HandType::Trips);
179        assert_str("straight     ", HandType::Straight);
180        assert_str("fullhouse    ", HandType::FullHouse);
181        assert_str("flush        ", HandType::Flush);
182        assert_str("quads        ", HandType::Quads);
183        assert_str("straightflush", HandType::StraightFlush);
184
185        assert_eq!(
186            "invalid".parse::<HandType>(),
187            Err(ParseError::InvalidHandType("invalid".to_string()))
188        );
189    }
190}