poker 0.7.0

A crate for speedy poker hand evaluation
Documentation
use constants::*;
use rustc_hash::{FxBuildHasher, FxHashMap};

use crate::{
    constants::{INT_RANKS, PRIMES},
    evaluate::{
        eval::Eval,
        poker_type::FiveCard,
        utils::{self, BitSequence},
    },
};

/// Stores information about looking up poker hands.
///
/// There are two hash tables, one for hands where the cards are suited (flushes
/// and straight flushes) and one for hands where the cards are not suited (the
/// rest of the poker hands). Both tables are indexed by a hand's prime product.
///
/// For example, the worst possible hand is 23457 (unsuited). The prime product
/// of these ranks is 2 * 3 * 5 * 7 * 13 = 2730. The evaluation implementation
/// first checks to make sure the hand is not suited, then indexes into the
/// unsuited lookup to find that `unsuited_lookup\[2730\]` is equal
/// to `Meta::HighCard { hand_rank: HandRank(7462), high_rank: Rank::Seven }`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LookupTable {
    pub flush_lookup: FxHashMap<i32, Eval<FiveCard>>,
    pub unsuited_lookup: FxHashMap<i32, Eval<FiveCard>>,
}

impl LookupTable {
    pub fn new() -> Self {
        let mut table = Self {
            flush_lookup: FxHashMap::with_capacity_and_hasher(6175, FxBuildHasher),
            unsuited_lookup: FxHashMap::with_capacity_and_hasher(1287, FxBuildHasher),
        };
        table.flushes_straights_high_cards();
        table.multiples();
        table
    }

    /// Calculate the metadata for flushes, straights, high cards, and straight
    /// flushes.
    fn flushes_straights_high_cards(&mut self) {
        const NOT_STRAIGHTS: [i16; 1277] = constants::not_straights();

        // Now we have `STRAIGHTS` (our constant) and `not_straights` (const-
        // calculated). Using these, we can consider both sets as if the ranks
        // they encode are suited
        //   - `STRAIGHT` hands suited become straight flushes
        //   - `not_straight` hands become flushes
        // We can also consider them unsuited:
        //   - `STRAIGHT` hands are just straights
        //   - `not_straight` hands are high-card hands (pairs, etc. not possible)

        // Let's first work with the `STRAIGHTS`.

        // If suited, we start with the best hand, a royal flush. This corresponds to a
        // value of 1
        let mut rank_suited = 1;

        // If unsuited, we start with the best possible straight, which is one worse
        // (1+) the worst (max) flush
        let mut rank_unsuited = WORST_FLUSH + 1;

        // These are recycled and hold the rank of the highest card of the hand and the
        // prime product
        let mut high_rank: i16;
        let mut prime_product: i32;

        // Straight flushes and straights
        for straight in STRAIGHTS {
            // We get the prime product using the bits
            prime_product = utils::prime_product_from_rank_bits(straight);

            // We also obtain the highest rank from the bits
            high_rank = utils::high_rank_from_rank_bits_five(straight);

            // Into the flush table we map the prime product to a straight flush
            // the has our current `rank_suited` value and the highest card's rank
            self.flush_lookup
                .insert(prime_product, Eval::new(rank_suited, high_rank));

            // Into the unsuited table, we map the same prime product to a straight
            // with our current `rank_unsuited` value and the highest card's rank
            self.unsuited_lookup
                .insert(prime_product, Eval::new(rank_unsuited, high_rank));

            // We increment our values as in the next loop we consider the next-worse hand.
            rank_suited = rank_suited.wrapping_add(1);
            rank_unsuited = rank_unsuited.wrapping_add(1);
        }

        // Now, we work with our `not_straights`.

        // If suited, we have a flush, which is starts just worse than the worst full
        // house
        rank_suited = WORST_FULL_HOUSE.wrapping_add(1);

        // If unsuited, we start just worse than the worst pair
        rank_unsuited = WORST_PAIR.wrapping_add(1);

        // Flushes and high cards
        // We reverse `not_straights` before looping because we generated the worst
        // hands first, but we want to start mapping from the best hands
        for bits in NOT_STRAIGHTS.into_iter().rev() {
            // Get the prime product from the bits
            prime_product = utils::prime_product_from_rank_bits(bits);

            // Get the highest card's rank
            high_rank = utils::high_rank_from_rank_bits_five(bits);

            // In the flush table, map the prime product to a flush
            self.flush_lookup
                .insert(prime_product, Eval::new(rank_suited, high_rank));

            // In the unsuited table, map it to a high card hand
            self.unsuited_lookup
                .insert(prime_product, Eval::new(rank_unsuited, high_rank));

            // Increment our values to consider the next worst hand
            rank_suited = rank_suited.wrapping_add(1);
            rank_unsuited = rank_unsuited.wrapping_add(1);
        }
    }

    /// Calculate metadata for all hands where at least one rank is repeated.
    fn multiples(&mut self) {
        // We want backwards ranks so we can consider the best/highest card ranks first

        // Reusable product holder
        let mut product;

        // Four of a kind
        // Given a four of a kind hand, we know one rank is repeated four times, and one
        // extra card, the kicker, is left, which can be one of the other eleven
        // card ranks. We start our rank at just worse than the worst straight
        // flush
        let mut rank = WORST_STRAIGHT_FLUSH + 1;

        // First, select our rank that there will be 4x
        for quad in INT_RANKS.rev() {
            // Then filter out our 4x rank so we can consider each possible kicker
            let kickers = INT_RANKS.rev().filter(|&kicker| kicker != quad);
            for k in kickers {
                // Get the prime product by hand, using `pow` if/when the card is present more
                // than once
                product = PRIMES[quad as usize].wrapping_pow(4) // 4x the quad card
                    .wrapping_mul(PRIMES[k as usize]); // 1x the kicker

                // Map the product to the appropriate hand
                self.unsuited_lookup
                    .insert(product, Eval::new(rank, 1i16.wrapping_shl(quad as u32)));
                rank = rank.wrapping_add(1);
            }
        }

        // Full house
        // We have one three of a kind (trips) and one (pair)
        rank = WORST_FOUR_OF_A_KIND + 1;
        // We select our trips rank...
        for trips in INT_RANKS.rev() {
            // ...and select our pair rank
            let pair_ranks = INT_RANKS.rev().filter(|&pr| pr != trips);
            for pr in pair_ranks {
                // And we calculate the prime product using power of 3 for the 3x rank and power
                // of 2 for the 2x rank
                product = PRIMES[trips as usize].wrapping_pow(3) // 3x trips
                    .wrapping_mul(PRIMES[pr as usize].wrapping_pow(2)); // 2x pair
                let mut rank_flags = 1i16.wrapping_shl(trips as u32) | 1i16.wrapping_shl(pr as u32);
                if pr > trips {
                    rank_flags |= 0b1000_0000_0000_0000u16 as i16;
                }
                self.unsuited_lookup
                    .insert(product, Eval::new(rank, rank_flags));
                rank = rank.wrapping_add(1);
            }
        }

        // Three of a kind
        // One 3x rank and two unique kickers
        rank = WORST_STRAIGHT + 1;
        for trips in INT_RANKS.rev() {
            let kickers = INT_RANKS
                .rev()
                .filter(|&kicker| kicker != trips)
                .collect::<Vec<_>>();
            // We want every combination of two kickers
            let generator = utils::const_combos::<_, 2>(&kickers);
            for [c1, c2] in generator {
                // Calculate our prime product with power of 3 for the trips and simply
                // multiply in the two kickers' primes
                product = PRIMES[trips as usize].wrapping_pow(3) // 3x trips
                    .wrapping_mul(PRIMES[c1 as usize]) // 1x first kicker
                    .wrapping_mul(PRIMES[c2 as usize]); // 1x second kicker

                self.unsuited_lookup
                    .insert(product, Eval::new(rank, 1i16.wrapping_shl(trips as u32)));
                rank = rank.wrapping_add(1);
            }
        }

        // Two pair
        // Two unique 2x cards and one unique kicker
        rank = WORST_THREE_OF_A_KIND + 1;

        // We want want every combination of two card ranks together to consider as our
        // two pair ranks
        let bw = INT_RANKS.rev().collect::<Vec<_>>();
        let two_pairs_combos = utils::const_combos::<_, 2>(&bw);
        for [pair1, pair2] in two_pairs_combos {
            // Our kickers are any rank that isn't equal to one of our two pair ranks
            let kickers = INT_RANKS
                .rev()
                .filter(|&kicker| kicker != pair1 && kicker != pair2);

            for kicker in kickers {
                // Product is power of two for our two pair ranks, multiplied by kicker
                product = PRIMES[pair1 as usize].wrapping_pow(2) // 2x first pair
                    .wrapping_mul(PRIMES[pair2 as usize].wrapping_pow(2)) // 2x second pair
                    .wrapping_mul(PRIMES[kicker as usize]); // 1x kicker
                self.unsuited_lookup.insert(
                    product,
                    Eval::new(
                        rank,
                        1i16.wrapping_shl(pair1 as u32) | 1i16.wrapping_shl(pair2 as u32),
                    ),
                );
                rank = rank.wrapping_add(1);
            }
        }

        // Pair
        // 1 pair rank and three unique kickers
        rank = WORST_TWO_PAIR + 1;
        for pair_rank in INT_RANKS.rev() {
            let kickers = INT_RANKS
                .rev()
                .filter(|&kicker| kicker != pair_rank)
                .collect::<Vec<_>>();

            // We want every combination of three unique ranks that aren't equal to our pair
            // rank
            let kicker_combos = utils::const_combos::<_, 3>(&kickers);
            for kicker_combo in kicker_combos {
                let k1 = kicker_combo[0] as usize;
                let k2 = kicker_combo[1] as usize;
                let k3 = kicker_combo[2] as usize;

                // Our product is the pair rank's prime to the power of two times the kickers'
                // primes
                product = PRIMES[pair_rank as usize].wrapping_pow(2) // 2x pair
                    .wrapping_mul(PRIMES[k1]) // 1x first kicker
                    .wrapping_mul(PRIMES[k2]) // 1x second kicker
                    .wrapping_mul(PRIMES[k3]); // 1x third kicker
                self.unsuited_lookup.insert(
                    product,
                    Eval::new(rank, 1i16.wrapping_shl(pair_rank as u32)),
                );
                rank = rank.wrapping_add(1);
            }
        }

        // And we're done! Phew!
    }
}

impl Default for LookupTable {
    fn default() -> Self { Self::new() }
}

pub mod constants {
    use super::*;

    // These are the worst hand ranks for each of the poker hands

    pub const WORST_STRAIGHT_FLUSH: i16 = 10;
    pub const WORST_FOUR_OF_A_KIND: i16 = 166;
    pub const WORST_FULL_HOUSE: i16 = 322;
    pub const WORST_FLUSH: i16 = 1599;
    pub const WORST_STRAIGHT: i16 = 1609;
    pub const WORST_THREE_OF_A_KIND: i16 = 2467;
    pub const WORST_TWO_PAIR: i16 = 3325;
    pub const WORST_PAIR: i16 = 6185;
    pub const WORST_HIGH_CARD: i16 = 7462;

    /// Statically calculated bit straights
    pub const STRAIGHTS: [i16; 10] = [
        0b1_1111_0000_0000, // 7936 => TJQKA
        0b0_1111_1000_0000, // 3968 => 9TJQK
        0b0_0111_1100_0000, // 1984 => 89TJQ
        0b0_0011_1110_0000, // 992 => 789TJ
        0b0_0001_1111_0000, // 496 => 6789T
        0b0_0000_1111_1000, // 248 => 56789
        0b0_0000_0111_1100, // 124 => 45678
        0b0_0000_0011_1110, // 62 => 34567
        0b0_0000_0001_1111, // 31 => 23456
        0b1_0000_0000_1111, // 4111 => A2345
    ];

    // Bit sequences that are not in the list above
    pub const fn not_straights() -> [i16; 1277] {
        let mut arr = [0; 1277];
        let mut generator = BitSequence::new(0b11111);
        let mut i = 0;
        let mut cur = 0;
        while i < 1286 {
            let bits = generator.get_next();
            let mut not_straight = true;
            let mut j = 0;
            while j < STRAIGHTS.len() {
                let straight = STRAIGHTS[j];
                // If the bits XOR a straight is 0, then it **is** a s traight, so we don't add
                // it to our vector
                if bits ^ straight == 0 {
                    not_straight = false;
                    break;
                }
                j += 1;
            }
            if not_straight {
                arr[cur] = bits;
                cur += 1;
            }
            i += 1;
        }
        arr
    }
}

#[cfg(test)]
mod tests {
    use std::collections::HashSet;

    use super::*;

    #[test]
    fn five_card_table_contains_all_entries() {
        let table = LookupTable::new();
        let mut hand_ranks = HashSet::new();

        let LookupTable {
            flush_lookup,
            unsuited_lookup,
        } = table;

        flush_lookup.values().for_each(|&eval| {
            assert!(hand_ranks.insert(eval.hand_rank()));
        });

        unsuited_lookup.values().for_each(|&eval| {
            assert!(hand_ranks.insert(eval.hand_rank()));
        });

        assert_eq!(hand_ranks.len(), WORST_HIGH_CARD as usize);

        let mut v = hand_ranks.into_iter().collect::<Vec<_>>();
        v.sort();
        let w = (1..=WORST_HIGH_CARD).collect::<Vec<_>>();
        assert_eq!(v, w);
    }
}