aya_poker/
lib.rs

1//! AyaPoker is a Rust library for fast poker hand evaluation based on the
2//! [OMPEval C++ hand evaluator](https://github.com/zekyll/OMPEval), with
3//! support for most popular poker variants.
4//!
5//! # Features
6//!
7//! - Can be used to rank hands from standard poker, ace-to-five lowball,
8//!   deuce-to-seven lowball, six-or-better (short-deck), Omaha, Omaha Hi/Lo,
9//!   Badugi or Baduci.
10//! - Can evaluate hands with 0 to 7 cards, with the missing cards counting as
11//!   the worst possible kickers, allowing for use in stud poker games.
12//! - Uses compile-time generated perfect hash function lookup tables for
13//!   excellent runtime performance and fast initialization.
14//! - Has extensive suite of tests to ensure correct implementation of the hand
15//!   ranking rules for each variant.
16//!
17//! # Flags
18//!
19//! This crate has the following Cargo features:
20//!
21//! - `std`: By default, `aya_poker` is a `!#[no_std]` crate, but can be
22//!   compiled with the `std` feature in order to allow the initialization of
23//!   `Deck`s with system-generated random seeds.
24//! - `colored`: Use [`colored`](https://crates.io/crates/colored) to display
25//!   cards and hands in color.
26//! - `colored-4color`: Same as `colored`, but using a four-color deck.
27//!
28//! # Example
29//!
30//! ```
31//! use std::cmp::Ordering;
32//!
33//! use aya_poker::{base::*, deck::Deck, poker_rank};
34//!
35//! const SEED: u64 = 42;
36//! const SAMPLE_SIZE: usize = 100_000;
37//!
38//! fn main() -> Result<(), ParseError> {
39//!     // We can initialize cards by specifying the rank and suit,
40//!     // and then use collect to combine the cards into a Hand
41//!     let player = [
42//!         Card::new(Rank::King, Suit::Hearts),
43//!         Card::new(Rank::King, Suit::Clubs),
44//!     ]
45//!     .iter()
46//!     .collect();
47//!
48//!     // Or, first parse the cards from strings
49//!     let opponent = ["Js".parse::<Card>(), "Qs".parse::<Card>()]
50//!         .iter()
51//!         .copied()
52//!         .collect::<Result<Hand, ParseError>>()?;
53//!
54//!     // Or, parse a space-separated string of cards as a Hand
55//!     let board = "Ah 5s Ts".parse()?;
56//!
57//!     let equity = equity_calculator(&player, &opponent, &board);
58//!     println!(
59//!         "{} has {:.1}% equity on {} against {}.",
60//!         player,
61//!         100.0 * equity,
62//!         board,
63//!         opponent
64//!     );
65//!
66//!     Ok(())
67//! }
68//!
69//! fn equity_calculator(
70//!     player: &Hand,
71//!     opponent: &Hand,
72//!     board: &Hand,
73//! ) -> f64 {
74//!     // To simulate board run-outs, we begin by preparing a deck
75//!     // that doesn't contain the already dealt-out cards
76//!     let available_cards = CARDS
77//!         .iter()
78//!         .filter(|c| !player.contains(c))
79//!         .filter(|c| !opponent.contains(c))
80//!         .filter(|c| !board.contains(c));
81//!     let mut deck = Deck::with_seed(available_cards, SEED);
82//!
83//!     let mut pots_won = 0.0;
84//!     for _ in 0..SAMPLE_SIZE {
85//!         // Then, for each run we draw cards to complete the board
86//!         deck.reset();
87//!         let missing = 5 - board.len();
88//!         let complete_board = board
89//!             .iter()
90//!             .chain(deck.deal(missing).unwrap().iter())
91//!             .collect::<Hand>();
92//!
93//!         // Evaluate the player's hand given the completed board
94//!         let mut player_hand = *player;
95//!         player_hand.extend(complete_board.iter());
96//!         let player_rank = poker_rank(&player_hand);
97//!
98//!         // Evaluate the opponent's hand
99//!         let mut opponent_hand = *opponent;
100//!         opponent_hand.extend(complete_board.iter());
101//!         let opponent_rank = poker_rank(&opponent_hand);
102//!
103//!         // And record the player's share of the pot for the run
104//!         match player_rank.cmp(&opponent_rank) {
105//!             Ordering::Greater => pots_won += 1.0,
106//!             Ordering::Less => {}
107//!             Ordering::Equal => pots_won += 0.5,
108//!         };
109//!     }
110//!
111//!     pots_won / SAMPLE_SIZE as f64
112//! }
113//! ```
114
115#![cfg_attr(not(any(std, test)), no_std)]
116
117use quickdiv::DivisorU64;
118
119mod ace_five;
120mod baduci;
121mod badugi;
122mod deuce_seven;
123mod display;
124mod omaha;
125mod short_deck;
126mod standard;
127
128/// Basic types for playing card games.
129pub mod base {
130    pub use aya_base::{Card, Hand, ParseError, Rank, Suit, CARDS};
131}
132
133/// Deck types optimized for fast shuffling suitable for use in simulators.
134pub mod deck {
135    pub use aya_base::{Deck, FullDeck, ShortDeck};
136}
137
138pub use ace_five::{ace_five_rank, AceFiveHandRank};
139pub use baduci::{baduci_rank, BaduciHandRank};
140pub use badugi::{badugi_rank, BadugiHandRank};
141pub use deuce_seven::{deuce_seven_rank, DeuceSevenHandRank};
142pub use omaha::{omaha_lo_rank, omaha_rank};
143pub use short_deck::{short_deck_rank, ShortDeckHandRank};
144pub use standard::{poker_rank, PokerHandRank};
145
146struct MiniPhf {
147    buckets_len: DivisorU64,
148    len: DivisorU64,
149    values: &'static [u16],
150    pilots: &'static [u32],
151}
152
153impl MiniPhf {
154    pub const fn new(values: &'static [u16], pilots: &'static [u32]) -> MiniPhf {
155        let buckets_len = DivisorU64::new(pilots.len() as u64);
156        let len = DivisorU64::new(values.len() as u64);
157        MiniPhf {
158            buckets_len,
159            len,
160            values,
161            pilots,
162        }
163    }
164
165    #[inline]
166    pub fn get(&self, key: u64) -> u16 {
167        let pilot = self.pilots[(key % self.buckets_len) as usize] as u64;
168        let idx = ((key ^ pilot) % self.len) as usize;
169        self.values[idx]
170    }
171}
172
173/// A poker hand-ranking category, i.e. a straight, a flush, etc.
174///
175/// Note we do not implement [`PartialOrd`] since we use the same ranking
176/// categories for both regular and lowball poker variants.
177#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)]
178pub enum PokerRankCategory {
179    /// A hand without a valid ranking, for example a 9-high in a 8 or better
180    /// lowball game.
181    Ineligible,
182    /// A valid hand that does not fall into any of the other categories.
183    HighCard,
184    /// Two cards of one rank, and three cards of three other ranks.
185    Pair,
186    /// Two cards of one rank, two cards of another rank and a fifth card of
187    /// a different, third rank.
188    TwoPair,
189    /// Three cards of the same rank, and two cards of two other ranks.
190    ThreeOfAKind,
191    /// Five cards of sequential rank, with at least two different suits.
192    Straight,
193    /// Five cards of the same suit, but without sequential rank.
194    Flush,
195    /// Three cards of one rank and two cards of another rank.
196    FullHouse,
197    /// Four cards of the same rank and one card of another rank.
198    FourOfAKind,
199    /// Five cards of sequential rank, all of the same suit, excluding an
200    /// ace-high sequence.
201    StraightFlush,
202    /// The sequence A-K-Q-J-T all of the same suit, i.e. an ace-high
203    /// straight flush.
204    RoyalFlush,
205}
206
207impl core::fmt::Display for PokerRankCategory {
208    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
209        match self {
210            PokerRankCategory::Ineligible => write!(f, "Ineligible"),
211            PokerRankCategory::HighCard => write!(f, "High Card"),
212            PokerRankCategory::Pair => write!(f, "Pair"),
213            PokerRankCategory::TwoPair => write!(f, "Two Pair"),
214            PokerRankCategory::ThreeOfAKind => write!(f, "Three of a Kind"),
215            PokerRankCategory::Straight => write!(f, "Straight"),
216            PokerRankCategory::Flush => write!(f, "Flush"),
217            PokerRankCategory::FullHouse => write!(f, "Full House"),
218            PokerRankCategory::FourOfAKind => write!(f, "Four of a Kind"),
219            PokerRankCategory::StraightFlush => write!(f, "Straight Flush"),
220            PokerRankCategory::RoyalFlush => write!(f, "Royal Flush"),
221        }
222    }
223}
224
225/// A Badugi/Baduci hand-ranking category corresponding to the size
226/// of the made hand.
227#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)]
228pub enum BadugiRankCategory {
229    /// A single card.
230    OneCard,
231    /// Two cards with different suits and ranks.
232    TwoCards,
233    /// Three cards with three distinct ranks and suits.
234    ThreeCards,
235    /// Four cards with four distinct ranks and suits.
236    FourCards,
237}
238
239impl core::fmt::Display for BadugiRankCategory {
240    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
241        match self {
242            BadugiRankCategory::OneCard => write!(f, "One Card"),
243            BadugiRankCategory::TwoCards => write!(f, "Two Cards"),
244            BadugiRankCategory::ThreeCards => write!(f, "Three Cards"),
245            BadugiRankCategory::FourCards => write!(f, "Four Cards"),
246        }
247    }
248}
249
250fn insert_cards<'a>(hand: &base::Hand, dest: &'a mut [base::Card]) -> &'a [base::Card] {
251    let n = hand.len();
252    for (i, &card) in hand.iter().enumerate() {
253        dest[i] = card;
254    }
255    &dest[..n]
256}