use crate::core::card::Card;
use super::eval;
use super::{CardBitSet, FlatHand, Hand};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Copy)]
#[repr(transparent)]
pub struct Rank(u16);
const CATEGORY_SHIFT: u32 = 12;
impl Rank {
#[inline]
pub(crate) const fn from_score(score: u16) -> Self {
Self::from_raw(score)
}
pub const MAX: Rank = CoreRank::StraightFlush.maximum();
pub const FIVE_CARD_MIN: Rank = CoreRank::HighCard.minimum();
#[inline]
pub const fn from_raw(score: u16) -> Self {
Rank(score)
}
#[inline]
pub const fn to_raw(self) -> u16 {
self.0
}
pub const HIGH_CARD_MIN: Rank = Rank(1 << CATEGORY_SHIFT);
pub const ONE_PAIR_MIN: Rank = Rank(2 << CATEGORY_SHIFT);
pub const TWO_PAIR_MIN: Rank = Rank(3 << CATEGORY_SHIFT);
pub const THREE_OF_A_KIND_MIN: Rank = Rank(4 << CATEGORY_SHIFT);
pub const STRAIGHT_MIN: Rank = Rank(5 << CATEGORY_SHIFT);
pub const FLUSH_MIN: Rank = Rank(6 << CATEGORY_SHIFT);
pub const FULL_HOUSE_MIN: Rank = Rank(7 << CATEGORY_SHIFT);
pub const FOUR_OF_A_KIND_MIN: Rank = Rank(8 << CATEGORY_SHIFT);
pub const STRAIGHT_FLUSH_MIN: Rank = Rank(9 << CATEGORY_SHIFT);
#[inline]
pub const fn category(self) -> CoreRank {
debug_assert!(
self.0 >> CATEGORY_SHIFT >= 1 && self.0 >> CATEGORY_SHIFT <= 9,
"Rank category bits outside 1..=9: not a hand the evaluator can produce"
);
match self.0 >> CATEGORY_SHIFT {
1 => CoreRank::HighCard,
2 => CoreRank::OnePair,
3 => CoreRank::TwoPair,
4 => CoreRank::ThreeOfAKind,
5 => CoreRank::Straight,
6 => CoreRank::Flush,
7 => CoreRank::FullHouse,
8 => CoreRank::FourOfAKind,
_ => CoreRank::StraightFlush,
}
}
#[inline]
pub const fn value_bits(self) -> u16 {
self.0 & ((1 << CATEGORY_SHIFT) - 1)
}
}
impl From<Rank> for u16 {
fn from(rank: Rank) -> Self {
rank.0
}
}
impl std::fmt::Debug for Rank {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}({})", self.category(), self.value_bits())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Copy)]
pub enum CoreRank {
HighCard,
OnePair,
TwoPair,
ThreeOfAKind,
Straight,
Flush,
FullHouse,
FourOfAKind,
StraightFlush,
}
impl CoreRank {
pub const fn minimum(self) -> Rank {
let (category, subrank) = match self {
Self::HighCard => (1, 47),
Self::OnePair => (2, 8),
Self::TwoPair => (3, 2),
Self::ThreeOfAKind => (4, 4),
Self::Straight => (5, 1),
Self::Flush => (6, 1),
Self::FullHouse => (7, 1),
Self::FourOfAKind => (8, 2),
Self::StraightFlush => (9, 1),
};
Rank((category << CATEGORY_SHIFT) | subrank)
}
pub const fn maximum(self) -> Rank {
let (category, subrank) = match self {
Self::HighCard => (1, 2370),
Self::OnePair => (2, 3887),
Self::TwoPair => (3, 936),
Self::ThreeOfAKind => (4, 1027),
Self::Straight => (5, 10),
Self::Flush => (6, 1277),
Self::FullHouse => (7, 156),
Self::FourOfAKind => (8, 169),
Self::StraightFlush => (9, 10),
};
Rank((category << CATEGORY_SHIFT) | subrank)
}
}
impl std::fmt::Display for CoreRank {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::HighCard => write!(f, "High Card"),
Self::OnePair => write!(f, "One Pair"),
Self::TwoPair => write!(f, "Two Pair"),
Self::ThreeOfAKind => write!(f, "Three of a Kind"),
Self::Straight => write!(f, "Straight"),
Self::Flush => write!(f, "Flush"),
Self::FullHouse => write!(f, "Full House"),
Self::FourOfAKind => write!(f, "Four of a Kind"),
Self::StraightFlush => write!(f, "Straight Flush"),
}
}
}
impl From<Rank> for CoreRank {
fn from(rank: Rank) -> Self {
rank.category()
}
}
pub trait Rankable {
fn rank(&self) -> Rank;
}
#[derive(Clone, Copy)]
pub struct SevenCardAccum {
key: u64,
mask: u64,
}
impl Default for SevenCardAccum {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl SevenCardAccum {
#[inline]
pub fn new() -> Self {
Self {
key: eval::DEFAULT_KEY,
mask: 0,
}
}
#[inline]
pub fn add(&mut self, c: Card) {
eval::add_card(&mut self.key, &mut self.mask, u8::from(c));
}
#[inline]
pub fn rank(&self) -> Rank {
debug_assert!(
self.mask.count_ones() <= 7,
"hand evaluator supports at most 7 cards, got {}",
self.mask.count_ones()
);
Rank::from_score(eval::evaluate_key(self.key, self.mask))
}
}
impl std::ops::AddAssign<Card> for SevenCardAccum {
#[inline]
fn add_assign(&mut self, c: Card) {
self.add(c);
}
}
impl std::ops::Add<Card> for SevenCardAccum {
type Output = SevenCardAccum;
#[inline]
fn add(mut self, c: Card) -> SevenCardAccum {
SevenCardAccum::add(&mut self, c);
self
}
}
const _: () = assert!(std::mem::size_of::<SevenCardAccum>() == 16);
fn rank_cards<I: Iterator<Item = Card>>(cards: I) -> Rank {
let mut acc = SevenCardAccum::new();
for c in cards {
acc.add(c);
}
acc.rank()
}
impl Rankable for FlatHand {
fn rank(&self) -> Rank {
rank_cards(self.iter().copied())
}
}
impl Rankable for Vec<Card> {
fn rank(&self) -> Rank {
rank_cards(self.iter().copied())
}
}
impl Rankable for [Card] {
fn rank(&self) -> Rank {
rank_cards(self.iter().copied())
}
}
impl Rankable for &[Card] {
fn rank(&self) -> Rank {
rank_cards(self.iter().copied())
}
}
impl Rankable for Hand {
fn rank(&self) -> Rank {
rank_cards(self.iter())
}
}
impl Rankable for CardBitSet {
fn rank(&self) -> Rank {
rank_cards(self.into_iter())
}
}
#[cfg(test)]
pub(crate) mod oracle {
include!("rank_oracle.rs");
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Card;
use crate::core::card::*;
fn bits_of(cards: &[Card]) -> u64 {
cards.iter().fold(0u64, |a, c| a | (1u64 << u8::from(*c)))
}
fn new_rank(cards: &[Card]) -> Rank {
let mut acc = SevenCardAccum::new();
for c in cards {
acc.add(*c);
}
acc.rank()
}
fn old_core(packed: u32) -> CoreRank {
match oracle::category(packed) {
0 => CoreRank::HighCard,
1 => CoreRank::OnePair,
2 => CoreRank::TwoPair,
3 => CoreRank::ThreeOfAKind,
4 => CoreRank::Straight,
5 => CoreRank::Flush,
6 => CoreRank::FullHouse,
7 => CoreRank::FourOfAKind,
_ => CoreRank::StraightFlush,
}
}
#[test]
fn category_agrees_exhaustive_five() {
let cards: Vec<Card> = (0u8..52).map(Card::from).collect();
let n = cards.len();
for a in 0..n {
for b in (a + 1)..n {
for c in (b + 1)..n {
for d in (c + 1)..n {
for e in (d + 1)..n {
let hand = [cards[a], cards[b], cards[c], cards[d], cards[e]];
let newr = new_rank(&hand);
let oldp = oracle::rank_u64(bits_of(&hand));
assert_eq!(
newr.category(),
old_core(oldp),
"category mismatch {hand:?}"
);
}
}
}
}
}
}
#[test]
fn order_isomorphism_exhaustive_five() {
use std::collections::HashMap;
let cards: Vec<Card> = (0u8..52).map(Card::from).collect();
let n = cards.len();
let mut map: HashMap<u32, u16> = HashMap::new();
for a in 0..n {
for b in (a + 1)..n {
for c in (b + 1)..n {
for d in (c + 1)..n {
for e in (d + 1)..n {
let hand = [cards[a], cards[b], cards[c], cards[d], cards[e]];
let oldp = oracle::rank_u64(bits_of(&hand));
let news = new_rank(&hand).0;
if let Some(prev) = map.insert(oldp, news) {
assert_eq!(prev, news, "old rank maps to two new scores");
}
}
}
}
}
}
let mut pairs: Vec<(u32, u16)> = map.into_iter().collect();
pairs.sort_unstable_by_key(|p| p.0);
for w in pairs.windows(2) {
assert!(w[0].1 < w[1].1, "ordering not preserved: {w:?}");
}
}
#[test]
fn differential_random_seven() {
use rand::SeedableRng;
use rand::rngs::StdRng;
use rand::seq::SliceRandom;
let mut cards: Vec<Card> = (0u8..52).map(Card::from).collect();
let mut rng = StdRng::seed_from_u64(0xC0FFEE);
let mut samples: Vec<(u32, u16)> = Vec::new();
for _ in 0..200_000 {
cards.shuffle(&mut rng);
let hand = &cards[..7];
let oldp = oracle::rank_u64(bits_of(hand));
let news = new_rank(hand);
assert_eq!(
news.category(),
old_core(oldp),
"category mismatch {hand:?}"
);
samples.push((oldp, news.0));
}
for i in 0..samples.len().min(2000) {
for j in (i + 1)..samples.len().min(2000) {
let (oa, na) = samples[i];
let (ob, nb) = samples[j];
assert_eq!(oa.cmp(&ob), na.cmp(&nb), "pairwise order disagreement");
}
}
}
#[test]
fn differential_partial_hands() {
let cards: Vec<Card> = (0u8..52).map(Card::from).collect();
for count in [2usize, 3, 4, 6] {
for start in 0..(52 - count) {
let hand = &cards[start..start + count];
let oldp = oracle::rank_u64(bits_of(hand));
let news = new_rank(hand);
assert_eq!(
news.category(),
old_core(oldp),
"category mismatch {hand:?}"
);
}
}
}
#[test]
fn category_ordering_holds() {
assert!(Rank::HIGH_CARD_MIN < Rank::ONE_PAIR_MIN);
assert!(Rank::ONE_PAIR_MIN < Rank::TWO_PAIR_MIN);
assert!(Rank::TWO_PAIR_MIN < Rank::THREE_OF_A_KIND_MIN);
assert!(Rank::THREE_OF_A_KIND_MIN < Rank::STRAIGHT_MIN);
assert!(Rank::STRAIGHT_MIN < Rank::FLUSH_MIN);
assert!(Rank::FLUSH_MIN < Rank::FULL_HOUSE_MIN);
assert!(Rank::FULL_HOUSE_MIN < Rank::FOUR_OF_A_KIND_MIN);
assert!(Rank::FOUR_OF_A_KIND_MIN < Rank::STRAIGHT_FLUSH_MIN);
}
#[test]
fn seven_card_accum_size() {
assert_eq!(std::mem::size_of::<SevenCardAccum>(), 16);
}
#[test]
fn known_hands_have_expected_categories() {
let sf = FlatHand::new_from_str("AdKdQdJdTd").unwrap();
assert_eq!(sf.rank().category(), CoreRank::StraightFlush);
let quads = FlatHand::new_from_str("AsAhAdAcKs").unwrap();
assert_eq!(quads.rank().category(), CoreRank::FourOfAKind);
let wheel = FlatHand::new_from_str("Ad2c3s4h5d").unwrap();
assert_eq!(wheel.rank().category(), CoreRank::Straight);
}
#[test]
fn rank_max_is_royal_flush() {
let royal = FlatHand::new_from_str("AsKsQsJsTs").unwrap().rank();
assert_eq!(Rank::MAX, royal);
assert_eq!(Rank::MAX.category(), CoreRank::StraightFlush);
}
#[test]
fn rank_min_is_worst_five_card_hand() {
let worst = FlatHand::new_from_str("7c5s4h3d2c").unwrap().rank();
assert_eq!(Rank::FIVE_CARD_MIN, worst);
assert_eq!(Rank::FIVE_CARD_MIN.category(), CoreRank::HighCard);
}
#[test]
fn five_card_min_is_not_a_universal_floor() {
let cards: Vec<Card> = (0u8..52).map(Card::from).collect();
let n = cards.len();
let mut below_five_card_min = 0;
for a in 0..n {
for b in (a + 1)..n {
for c in (b + 1)..n {
for d in (c + 1)..n {
for len in 2..=4 {
let hand = [cards[a], cards[b], cards[c], cards[d]];
let r = hand[..len].rank();
assert!(r >= Rank::HIGH_CARD_MIN, "{hand:?}[..{len}] below floor");
if r < Rank::FIVE_CARD_MIN {
below_five_card_min += 1;
}
}
}
}
}
}
assert!(below_five_card_min > 0);
}
#[test]
fn core_rank_bounds_match_real_five_card_extremes() {
use std::collections::HashMap;
let cards: Vec<Card> = (0u8..52).map(Card::from).collect();
let n = cards.len();
let mut mins: HashMap<CoreRank, Rank> = HashMap::new();
let mut maxs: HashMap<CoreRank, Rank> = HashMap::new();
for a in 0..n {
for b in (a + 1)..n {
for c in (b + 1)..n {
for d in (c + 1)..n {
for e in (d + 1)..n {
let r = [cards[a], cards[b], cards[c], cards[d], cards[e]][..].rank();
let cat = r.category();
mins.entry(cat)
.and_modify(|m| *m = (*m).min(r))
.or_insert(r);
maxs.entry(cat)
.and_modify(|m| *m = (*m).max(r))
.or_insert(r);
}
}
}
}
}
for cat in [
CoreRank::HighCard,
CoreRank::OnePair,
CoreRank::TwoPair,
CoreRank::ThreeOfAKind,
CoreRank::Straight,
CoreRank::Flush,
CoreRank::FullHouse,
CoreRank::FourOfAKind,
CoreRank::StraightFlush,
] {
assert_eq!(cat.minimum(), mins[&cat], "minimum() wrong for {cat:?}");
assert_eq!(cat.maximum(), maxs[&cat], "maximum() wrong for {cat:?}");
assert!(cat.minimum() <= cat.maximum());
}
assert_eq!(Rank::FIVE_CARD_MIN, CoreRank::HighCard.minimum());
assert_eq!(Rank::MAX, CoreRank::StraightFlush.maximum());
}
#[test]
fn core_rank_bands_are_disjoint_and_ordered() {
let ordered = [
CoreRank::HighCard,
CoreRank::OnePair,
CoreRank::TwoPair,
CoreRank::ThreeOfAKind,
CoreRank::Straight,
CoreRank::Flush,
CoreRank::FullHouse,
CoreRank::FourOfAKind,
CoreRank::StraightFlush,
];
for w in ordered.windows(2) {
assert!(
w[0].maximum() < w[1].minimum(),
"{:?} overlaps {:?}",
w[0],
w[1]
);
}
}
#[test]
fn from_raw_to_raw_round_trips() {
let royal = FlatHand::new_from_str("AsKsQsJsTs").unwrap().rank();
assert_eq!(Rank::from_raw(royal.to_raw()), royal);
assert_eq!(Rank::MAX.to_raw(), 36874);
assert_eq!(u16::from(Rank::MAX), 36874);
}
#[test]
#[should_panic(expected = "outside 1..=9")]
#[cfg(debug_assertions)]
fn category_of_a_hand_built_score_is_checked_in_debug() {
let _ = Rank::from_raw(0).category();
}
#[test]
fn issue_291_royal_flush_constant_usecase() {
const ROYAL_FLUSH_RANK: Rank = Rank::MAX;
let royal = FlatHand::new_from_str("AsKsQsJsTs").unwrap().rank();
let steel_wheel = FlatHand::new_from_str("5s4s3s2sAs").unwrap().rank();
assert_eq!(royal, ROYAL_FLUSH_RANK);
assert_eq!(steel_wheel.category(), CoreRank::StraightFlush);
assert_ne!(steel_wheel, ROYAL_FLUSH_RANK);
assert!(steel_wheel < ROYAL_FLUSH_RANK);
}
#[test]
fn test_core_rank_from_categories() {
assert_eq!(CoreRank::HighCard, Rank::HIGH_CARD_MIN.category());
assert_eq!(CoreRank::OnePair, Rank::ONE_PAIR_MIN.category());
assert_eq!(CoreRank::TwoPair, Rank::TWO_PAIR_MIN.category());
assert_eq!(CoreRank::ThreeOfAKind, Rank::THREE_OF_A_KIND_MIN.category());
assert_eq!(CoreRank::Straight, Rank::STRAIGHT_MIN.category());
assert_eq!(CoreRank::Flush, Rank::FLUSH_MIN.category());
assert_eq!(CoreRank::FullHouse, Rank::FULL_HOUSE_MIN.category());
assert_eq!(CoreRank::FourOfAKind, Rank::FOUR_OF_A_KIND_MIN.category());
assert_eq!(CoreRank::StraightFlush, Rank::STRAIGHT_FLUSH_MIN.category());
}
#[test]
fn test_core_rank_into() {
let r: CoreRank = Rank::FLUSH_MIN.into();
assert_eq!(r, CoreRank::Flush);
}
#[test]
fn test_core_rank_ordering() {
assert!(CoreRank::HighCard < CoreRank::OnePair);
assert!(CoreRank::OnePair < CoreRank::TwoPair);
assert!(CoreRank::TwoPair < CoreRank::ThreeOfAKind);
assert!(CoreRank::ThreeOfAKind < CoreRank::Straight);
assert!(CoreRank::Straight < CoreRank::Flush);
assert!(CoreRank::Flush < CoreRank::FullHouse);
assert!(CoreRank::FullHouse < CoreRank::FourOfAKind);
assert!(CoreRank::FourOfAKind < CoreRank::StraightFlush);
}
#[test]
fn test_core_rank_display() {
assert_eq!(CoreRank::HighCard.to_string(), "High Card");
assert_eq!(CoreRank::OnePair.to_string(), "One Pair");
assert_eq!(CoreRank::TwoPair.to_string(), "Two Pair");
assert_eq!(CoreRank::ThreeOfAKind.to_string(), "Three of a Kind");
assert_eq!(CoreRank::Straight.to_string(), "Straight");
assert_eq!(CoreRank::Flush.to_string(), "Flush");
assert_eq!(CoreRank::FullHouse.to_string(), "Full House");
assert_eq!(CoreRank::FourOfAKind.to_string(), "Four of a Kind");
assert_eq!(CoreRank::StraightFlush.to_string(), "Straight Flush");
}
#[test]
fn test_rank_ordering_within_same_type() {
let pair_aces = FlatHand::new_from_str("AsAhKdQcJs").unwrap();
let pair_kings = FlatHand::new_from_str("KsKhAdQcJs").unwrap();
assert!(pair_aces.rank() > pair_kings.rank());
let two_pair_ak = FlatHand::new_from_str("AsAhKdKcJs").unwrap();
let two_pair_aq = FlatHand::new_from_str("AsAhQdQcKs").unwrap();
assert!(two_pair_ak.rank() > two_pair_aq.rank());
let trips_aces = FlatHand::new_from_str("AsAhAdKcJs").unwrap();
let trips_kings = FlatHand::new_from_str("KsKhKdAcJs").unwrap();
assert!(trips_aces.rank() > trips_kings.rank());
}
#[test]
fn test_rankable_vec_and_slice() {
let cards: Vec<Card> = vec![
Card::new(Value::Ace, Suit::Spade),
Card::new(Value::King, Suit::Spade),
Card::new(Value::Queen, Suit::Spade),
Card::new(Value::Jack, Suit::Spade),
Card::new(Value::Ten, Suit::Spade),
];
assert_eq!(cards.rank().category(), CoreRank::StraightFlush);
let slice: &[Card] = &cards;
assert_eq!(slice.rank().category(), CoreRank::StraightFlush);
assert_eq!(cards[..].rank().category(), CoreRank::StraightFlush);
}
#[test]
fn test_wheel_straight_detection() {
let wheel = FlatHand::new_from_str("Ad2c3s4h5d").unwrap();
assert_eq!(wheel.rank().category(), CoreRank::Straight);
let six_high = FlatHand::new_from_str("2c3s4h5d6c").unwrap();
assert!(wheel.rank() < six_high.rank());
let not_wheel = FlatHand::new_from_str("Ad2c3s4h6d").unwrap();
assert_eq!(not_wheel.rank().category(), CoreRank::HighCard);
let almost_wheel = FlatHand::new_from_str("Ad2c3s4h6c").unwrap();
assert_eq!(almost_wheel.rank().category(), CoreRank::HighCard);
}
#[test]
fn test_seven_card_categories() {
let cards: Vec<Card> = vec![
Card::new(Value::Ace, Suit::Spade),
Card::new(Value::King, Suit::Spade),
Card::new(Value::Queen, Suit::Spade),
Card::new(Value::Jack, Suit::Spade),
Card::new(Value::Ten, Suit::Spade),
Card::new(Value::Nine, Suit::Spade),
Card::new(Value::Eight, Suit::Spade),
];
assert_eq!(cards.rank().category(), CoreRank::StraightFlush);
}
#[test]
fn seven_card_accum_matches_rank_best_of() {
for s in [
"Ad8h9cTc5c2s7d", "AdAc9d8cTs2h3s", "AdAc9d8cTs8s3s", "AdAcAs8cTs2h3s", "2c3s4h5s6d8cKh", "Ad8d9dTd5d2h3s", "AdAc9d9c9s2h3s", "AdAcAsAh8cTs2h", "AdKdQdJdTd9d8d", ] {
let hand = FlatHand::new_from_str(s).unwrap();
let mut acc = SevenCardAccum::new();
for c in hand.iter() {
acc.add(*c);
}
assert_eq!(acc.rank(), hand.rank(), "mismatch for {s}");
}
}
#[test]
fn seven_card_accum_order_independent() {
let hand = FlatHand::new_from_str("2s2h2d2c8d8sKd").unwrap();
let cards: Vec<Card> = hand.iter().copied().collect();
let mut forward = SevenCardAccum::new();
for c in &cards {
forward.add(*c);
}
let mut backward = SevenCardAccum::new();
for c in cards.iter().rev() {
backward.add(*c);
}
assert_eq!(forward.rank(), backward.rank());
assert_eq!(forward.rank(), hand.rank());
}
#[test]
fn seven_card_accum_add_operators() {
let base = FlatHand::new_from_str("AsAhKs7c2d").unwrap();
let mut via_assign = SevenCardAccum::new();
for c in base.iter() {
via_assign += *c;
}
let via_add = base.iter().fold(SevenCardAccum::new(), |acc, c| acc + *c);
assert_eq!(via_assign.rank(), via_add.rank());
assert_eq!(via_assign.rank(), base.rank());
}
#[test]
fn seven_card_accum_copy_reuse_equals_from_scratch() {
let base_hand = FlatHand::new_from_str("AsAhKs7c2d").unwrap();
let mut base = SevenCardAccum::new();
for c in base_hand.iter() {
base.add(*c);
}
let mut quads = base; quads.add(Card::new(Value::Ace, Suit::Club));
quads.add(Card::new(Value::Ace, Suit::Diamond));
let quads_scratch = FlatHand::new_from_str("AsAhKs7c2dAcAd").unwrap();
assert_eq!(quads.rank(), quads_scratch.rank());
let mut pair = base; pair.add(Card::new(Value::Queen, Suit::Heart));
pair.add(Card::new(Value::Jack, Suit::Diamond));
let pair_scratch = FlatHand::new_from_str("AsAhKs7c2dQhJd").unwrap();
assert_eq!(pair.rank(), pair_scratch.rank());
assert!(quads.rank() > pair.rank());
}
}