PH Evaluator
repository·develop·Indexed 19 days ago
https://github.com/henryrlee/pokerhandevaluatorA 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.
What's inside phevaluator
- 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.
Overview of PokerHandEvaluator
developPH 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.Overview of the phevaluator Python package
developThephevaluatorpackage 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.Use the C/C++ Poker Hand Evaluator library
developThe 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
cppdirectory of the repository.Hash an n-bit binary with k bits set (HashNBinaryKSum)
developA 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; }Represent cards using integer Card IDs
developIn 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 + suitRank Mapping
Name Value 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 Suit Mapping
Name Value club 0 diamond 1 heart 2 spade 3 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
suitMapininclude/phevaluator/card.hto maintain consistency.// Example: Creating an Ace of Spades // Ace = 12, Spade = 3 int ace_of_spades = 12 * 4 + 3; // Result: 51Understand the Card ID representation
developThe 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 + suitRank Mapping
Name Value 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 Suit Mapping
Name Value club 0 diamond 1 heart 2 spade 3 # Example: Calculating the ID for an Ace of Spades # Ace = 12, Spade = 3 card_id = 12 * 4 + 3 # Result: 51Hash a restricted quinary (HashNQuinaryKSum)
developFor 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; }Evaluate flushes separately to optimize performance
developTo handle the complexity of the Flush category (including Straight Flushes), the algorithm splits evaluation into two branches:
- 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.
- 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.
Choose the correct library variant
developThe 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 .. ; makeUnderstand the Card ID integer representation
developCards 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=12Suits (0-3):
club=0, diamond=1, heart=2, spade=3Card ID Table
Rank C D H S 2 0 1 2 3 3 4 5 6 7 4 8 9 10 11 5 12 13 14 15 6 16 17 18 19 7 20 21 22 23 8 24 25 26 27 9 28 29 30 31 T 32 33 34 35 J 36 37 38 39 Q 40 41 42 43 K 44 45 46 47 A 48 49 50 51 Understand the Dynamic Programming Hash Algorithm
developThe project describes a high-performance hashing approach for poker hand evaluation using a Dynamic Programming (DP) table. The goal is to solve the
HashNBinaryKSumproblem: 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
- Block Splitting: The 52-bit hand ID is split into smaller blocks (e.g., four 13-bit blocks or 16-bit blocks).
- 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$.
- 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; }