openpql_prelude/card/
board.rs

1use super::{Card, Card64, Flop, Hash, fmt};
2
3#[macro_export]
4macro_rules! board {
5    ($s:expr) => {
6        $crate::Board::from(cards!($s).as_slice())
7    };
8}
9
10/// Board representation for poker games.
11///
12/// Represents community cards (flop, turn, river) with a macro for convenient creation.
13#[derive(Copy, Clone, derive_more::Debug, PartialEq, Eq, Hash, Default)]
14#[debug("Board<{}>", self)]
15pub struct Board {
16    pub flop: Option<Flop>,
17    pub turn: Option<Card>,
18    pub river: Option<Card>,
19}
20
21impl Board {
22    /// Index of the turn card in a board array
23    pub const IDX_TURN: usize = 3;
24    /// Index of the river card in a board array
25    pub const IDX_RIVER: usize = 4;
26    /// Number of board cards preflop
27    pub const N_PREFLOP: usize = 0;
28    /// Number of board cards in a flop
29    pub const N_FLOP: usize = 3;
30    /// Number of board cards in a flop + turn
31    pub const N_TURN: usize = 4;
32    /// Number of board cards in a flop + turn + river
33    pub const N_RIVER: usize = 5;
34
35    /// Creates a board from a slice of cards.
36    ///
37    /// Expects cards in order: flop (3 cards), turn, river.
38    /// Cards beyond the first 5 are ignored.
39    pub fn from_slice(cards: &[Card]) -> Self {
40        let flop = if cards.len() >= Self::N_FLOP {
41            Some(Flop::from_slice(&cards[0..Self::N_FLOP]))
42        } else {
43            None
44        };
45        let turn = cards.get(Self::IDX_TURN).copied();
46        let river = cards.get(Self::IDX_RIVER).copied();
47
48        Self { flop, turn, river }
49    }
50
51    /// Returns `true` if the board has no cards (no flop).
52    #[must_use]
53    #[inline]
54    pub const fn is_empty(&self) -> bool {
55        self.flop.is_none()
56    }
57
58    /// Returns the number of cards on the board (0, 3, 4, or 5).
59    pub fn len(&self) -> usize {
60        match self.flop {
61            Some(_) => {
62                Self::N_FLOP
63                    + self.turn.iter().count()
64                    + self.river.iter().count()
65            }
66            None => 0,
67        }
68    }
69
70    /// Returns an iterator over all cards on the board.
71    pub fn iter(&self) -> impl Iterator<Item = Card> + '_ {
72        self.flop
73            .iter()
74            .flat_map(|flop| flop.iter().copied())
75            .chain(self.turn)
76            .chain(self.river)
77    }
78
79    /// Returns `true` if the board contains the specified card.
80    pub const fn contains_card(&self, card: Card) -> bool {
81        #[inline]
82        const fn inner_eq(op: Option<Card>, rhs: Card) -> bool {
83            match op {
84                Some(lhs) => lhs.eq(rhs),
85                None => false,
86            }
87        }
88
89        if let Some(flop) = self.flop
90            && flop.contains_card(card)
91        {
92            return true;
93        }
94
95        inner_eq(self.turn, card) || inner_eq(self.river, card)
96    }
97
98    pub(crate) const fn to_c64_flop(self) -> Card64 {
99        match self.flop {
100            Some(flop) => flop.to_c64(),
101            None => Card64::EMPTY,
102        }
103    }
104
105    pub(crate) const fn to_c64_turn(self) -> Card64 {
106        match self.turn {
107            Some(turn) => turn.to_c64(),
108            None => Card64::EMPTY,
109        }
110    }
111
112    pub(crate) const fn to_c64_river(self) -> Card64 {
113        match self.river {
114            Some(river) => river.to_c64(),
115            None => Card64::EMPTY,
116        }
117    }
118}
119
120impl From<&[Card]> for Board {
121    fn from(xs: &[Card]) -> Self {
122        Self::from_slice(xs)
123    }
124}
125
126impl fmt::Display for Board {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        self.iter().try_for_each(|card| write!(f, "{card}"))
129    }
130}
131
132impl From<Board> for Card64 {
133    fn from(board: Board) -> Self {
134        board.iter().collect()
135    }
136}
137
138#[cfg(any(test, feature = "quickcheck"))]
139impl quickcheck::Arbitrary for Board {
140    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
141        let cards = crate::CardN::<{ Self::N_RIVER }>::arbitrary(g);
142
143        Self::from_slice(cards.as_ref())
144    }
145}
146
147#[cfg(test)]
148#[cfg_attr(coverage_nightly, coverage(off))]
149mod tests {
150    use super::*;
151    use crate::*;
152
153    #[quickcheck]
154    fn test_from_slice(cs: CardN<5>) {
155        assert_eq!(
156            Board::from([cs[0], cs[1], cs[2], cs[3], cs[4]].as_slice()),
157            Board::from([cs[2], cs[1], cs[0], cs[3], cs[4]].as_slice()),
158        );
159        assert_ne!(
160            Board::from([cs[0], cs[1], cs[2], cs[3], cs[4]].as_slice()),
161            Board::from([cs[0], cs[1], cs[2], cs[4], cs[3]].as_slice()),
162        );
163    }
164
165    #[test]
166    fn test_empty() {
167        assert!(Board::default().is_empty());
168    }
169
170    #[test]
171    fn test_board_iter_and_len() {
172        let board = board!("Qd As Kh Jc");
173
174        assert_eq!(board.iter().collect::<Vec<_>>(), cards!("Qd Kh As Jc"));
175        assert_eq!(board.len(), 4);
176        assert_eq!(Board::default().len(), 0);
177    }
178
179    #[quickcheck]
180    fn test_board_contains_card(board: Board, card: Card) {
181        assert_eq!(board.iter().any(|x| x == card), board.contains_card(card));
182        assert!(!Board::default().contains_card(card));
183    }
184
185    #[test]
186    fn test_display() {
187        let cards = cards!["AsKhQd3s2s"];
188        let board = Board::from_slice(&cards);
189
190        assert_eq!(format!("{board}"), "QdKhAs3s2s");
191        assert_eq!(format!("{board:?}"), "Board<QdKhAs3s2s>");
192    }
193
194    #[test]
195    fn test_to_c64() {
196        let cards = cards!("AsKhQdJcTs");
197
198        for i in 3..=5 {
199            let board = Board::from_slice(&cards[0..i]);
200            let card64 = Card64::from(board);
201
202            for j in 0..i {
203                assert!(card64.contains_card(cards[j]));
204            }
205
206            assert_eq!(card64.count() as usize, i);
207        }
208    }
209}