PH Evaluator

repository·develop·Indexed 19 days ago

https://github.com/henryrlee/pokerhandevaluator

A high-performance poker hand evaluation library based on a Perfect Hash algorithm. It efficiently evaluates 5, 6, and 7-card hands, as well as Omaha variants (PLO4, PLO5, PLO6), without brute-force combination checking. Available as a C/C++ library and a Python package (version 0.6.1), it is designed for high throughput and minimal memory overhead.

Tokens
10K
Snippets
36
Records
53
Agent score
63%

What's inside phevaluator

  1. Overview of PH Evaluator

    develop
    PH Evaluator is a high-performance poker hand evaluator that uses a Perfect Hash Algorithm. Unlike brute-force methods that iterate through all possible card combinations (e.g., checking all 21 combinations of 7 cards to find the best 5-card hand), PH Evaluator uses a pre-computed hash table to determine hand strength. This approach is highly efficient, requiring very few CPU cycles and minimal memory (approximately 100KB for 7-card evaluation). It is designed to handle hands with more than 5 cards, including Texas Hold'em and various Omaha poker formats.
  2. Overview of PokerHandEvaluator

    develop
    PH Evaluator is an efficient poker hand evaluator that utilizes a perfect hash algorithm. Instead of traversing all possible combinations, it performs a lookup in a pre-computed hash table. This approach is highly optimized, requiring very few CPU cycles and minimal memory (approximately 100kb for 7-card evaluation). The algorithm can be adapted to evaluate Omaha and Pot Limit Omaha hands with slight modifications.
  3. Overview of the phevaluator Python package

    develop
    The phevaluator package provides a thin Python interface for the Poker Hand Evaluator. It wraps the high-performance C implementation used by the C/C++ library, allowing Python users to leverage the same perfect-hash algorithm for poker hand evaluation. This ensures that Python users benefit from the same computational efficiency and algorithmic accuracy as the C/C++ versions.
  4. Use the C/C++ Poker Hand Evaluator library

    develop

    The C/C++ implementation of the Poker Hand Evaluator is the reference implementation of the perfect-hash algorithm used to evaluate poker hands. It supports evaluating 5-card, 6-card, and 7-card hands, as well as Omaha poker hands (PLO4, PLO5, and PLO6).

    To use the library, you will need to follow the building, usage, and card ID documentation located in the cpp directory of the repository.

  5. Hash an n-bit binary with k bits set (HashNBinaryKSum)

    develop

    A basic evaluation algorithm for hands with 5 to 9 cards. It maps an $n$-bit binary representation (where exactly $k$ bits are set to 1) to its unique position in a lexicographical ordering. This position serves as a perfect hash function for a hash table.

    For a 7-card poker hand, the hand is represented as a 52-bit binary. The function runs in at most $n$ cycles (e.g., 52 cycles for a full deck).

    int hash_binary(unsigned char q[], int len, int k)
    {
      int sum = 0;
      int i;
    
      for (i=0; i<len; i++)
      {
        if (q[i])
        {
          if (len-i-1 >= k)
            sum += choose[len-i-1][k];
    
          k--;
    
          if (k == 0)
            break;
        }
      }
    
      return ++sum;
    }
  6. Represent cards using integer Card IDs

    develop

    In the C/C++ implementation, cards are represented as integers. The integer is constructed using the rank and the suit, where the two least significant bits represent the suit (0-3) and the remaining bits represent the rank (0-12).

    To calculate a Card ID, use the formula: rank * 4 + suit

    Rank Mapping

    NameValue
    deuce0
    trey1
    four2
    five3
    six4
    seven5
    eight6
    nine7
    ten8
    jack9
    queen10
    king11
    ace12

    Suit Mapping

    NameValue
    club0
    diamond1
    heart2
    spade3

    Note: If you choose to swap the suit values, you must ensure they are uniquely mapped to 0, 1, 2, and 3, and you must update the suitMap in include/phevaluator/card.h to maintain consistency.

    // Example: Creating an Ace of Spades
    // Ace = 12, Spade = 3
    int ace_of_spades = 12 * 4 + 3; // Result: 51
  7. Understand the Card ID representation

    develop

    The library represents poker cards using a single integer. The integer is constructed using the rank and the suit, where the two least significant bits represent the suit (0-3) and the remaining bits represent the rank (0-12).

    To calculate a Card ID manually, use the formula: rank * 4 + suit

    Rank Mapping

    NameValue
    deuce0
    trey1
    four2
    five3
    six4
    seven5
    eight6
    nine7
    ten8
    jack9
    queen10
    king11
    ace12

    Suit Mapping

    NameValue
    club0
    diamond1
    heart2
    spade3
    # Example: Calculating the ID for an Ace of Spades
    # Ace = 12, Spade = 3
    card_id = 12 * 4 + 3  # Result: 51
  8. Hash a restricted quinary (HashNQuinaryKSum)

    develop

    For hands where suits do not matter, the hand is represented as a 13-bit quinary (base 5) number. Each digit corresponds to the count of cards of a specific rank. The sum of all digits in the quinary must equal $k$ (the total number of cards).

    This algorithm uses dynamic programming to find the lexicographical position of the quinary, serving as a perfect hash function. It uses a 3D DP array dp[l][n][k] where:

    • l: The most significant bit of the excluding endpoint.
    • n: The number of trailing zero bits.
    • k: The remaining number of cards to be distributed.

    The resulting hash table contains 49,205 entries for a 7-card hand, and the hash function computes in at most 13 cycles.

    int hash_quinary(unsigned char q[], int len, int k)
    {
      int sum = 0;
      int i;
      for (i=0; i<len; i++) {
        sum += dp[q[i]][len-i-1][k];
    
        k -= q[i];
    
        if (k <= 0)
          break;
      }
    
      return ++sum;
    }
  9. Evaluate flushes separately to optimize performance

    develop

    To handle the complexity of the Flush category (including Straight Flushes), the algorithm splits evaluation into two branches:

    1. Flush Branch: If a hand contains at least 5 cards of the same suit, it is guaranteed to be a Flush or Straight Flush (in a 7-card hand). The suit is ignored for the other branch, and the specific suit's binary is passed to a flush evaluator. Since a 13-bit binary (for ranks) has only $2^{13} = 8192$ possibilities, a direct lookup table is used instead of a complex hash function.
    2. Non-Flush Branch: If no flush is detected, the suits are ignored. The hand is represented as a 13-bit quinary (base 5) number, where each digit represents the count of a specific rank (0-4).

    In a 7-card hand, once a flush is identified, the evaluation can stop immediately and return the result.

  10. Choose the correct library variant

    develop

    The project provides several library variants depending on your hand evaluation needs and memory constraints:

    • libpheval.a: Full library. Includes 5, 6, and 7-card evaluators and rank description methods. Note: Rank description methods increase memory usage by ~356k.
    • libpheval5.a, libpheval6.a, libpheval7.a: Memory-optimized versions for 5, 6, or 7-card hands respectively. These do not include rank description methods to save memory.
    • libphevalplo4.a, libphevalplo5.a, libphevalplo6.a: Optimized for Pot Limit Omaha (PLO) hands (4, 5, or 6 cards). These do include rank description methods.

    Note on PLO6: Building the PLO6 library requires significant memory. You can disable it via CMake:

    cmake -DBUILD_PLO6=OFF .. ; make
  11. Understand the Card ID integer representation

    develop

    Cards can be represented as integers using the formula rank * 4 + suit.

    Bit Structure

    • The two least significant bits represent the suit (0-3).
    • The remaining bits represent the rank (0-12).

    Mappings

    Ranks (0-12): deuce=0, trey=1, four=2, five=3, six=4, seven=5, eight=6, nine=7, ten=8, jack=9, queen=10, king=11, ace=12

    Suits (0-3): club=0, diamond=1, heart=2, spade=3

    Card ID Table

    RankCDHS
    20123
    34567
    4891011
    512131415
    616171819
    720212223
    824252627
    928293031
    T32333435
    J36373839
    Q40414243
    K44454647
    A48495051
  12. Understand the Dynamic Programming Hash Algorithm

    develop

    The project describes a high-performance hashing approach for poker hand evaluation using a Dynamic Programming (DP) table. The goal is to solve the HashNBinaryKSum problem: finding the lexicographical position of an $n$-bit binary string (where $n=52$ for a deck of cards) that has exactly $k$ bits set to 1.

    How it works

    1. Block Splitting: The 52-bit hand ID is split into smaller blocks (e.g., four 13-bit blocks or 16-bit blocks).
    2. Precomputation: A DP table is precomputed to store the lexicographical rank of bit patterns within those blocks. For 16-bit blocks, the table size is $2^{16} imes 4 imes 8$.
    3. Fast Hashing: The final hash is computed by summing the precomputed values from the DP table for each block, adjusting the remaining bit count ($k$) as each block is processed.

    Performance Note

    While this algorithm minimizes CPU cycles (requiring only a few summations and decrements), its real-world performance is often limited by memory access latency because the DP table size typically exceeds a standard memory page size (64KB).

    // Conceptual logic for the fast hash function
    int fast_hash(unsigned long long handid, int k)
    {
      int hash = 0;
      unsigned short * a = (unsigned short *)&handid;
    
      hash += dp_fast[a[3]][3][k];
      k -= bitcount[a[3]];
    
      hash += dp_fast[a[2]][2][k];
      k -= bitcount[a[2]];
    
      hash += dp_fast[a[1]][1][k];
      k -= bitcount[a[1]];
    
      hash += dp_fast[a[0]][0][k];
    
      return hash;
    }