Source code for texasholdem.evaluator.lookup_table

"""
The lookup table module keeps the books on all possible hand strengths.
We construct the table once on import and reference it repeatedly.

Number of Distinct Hand Values::

    Straight Flush   10
    Four of a Kind   156      [(13 choose 2) * (2 choose 1)]
    Full Houses      156      [(13 choose 2) * (2 choose 1)]
    Flush            1277     [(13 choose 5) - 10 straight flushes]
    Straight         10
    Three of a Kind  858      [(13 choose 3) * (3 choose 1)]
    Two Pair         858      [(13 choose 3) * (3 choose 2)]
    One Pair         2860     [(13 choose 4) * (4 choose 1)]
    High card      + 1277     [(13 choose 5) - 10 straights]
    -------------------------
    TOTAL            7462

Here we create a lookup table which maps:

    - 5 card hand's unique prime product -> rank in range [1, 7462]

Example:
    - Royal flush (best hand possible) -> 1
    - 7-5-4-3-2 unsuited (worst hand possible) -> 7462

"""

from typing import Dict
import itertools

from texasholdem.card import card
from texasholdem.card.card import Card


[docs] class LookupTable: # pylint: disable=too-few-public-methods """ Attributes: flush_lookup (Dict[int, int]): map from prime-product to rank for suited cards unsuited_lookup (Dict[int, int]): map from prime-product to rank for unsuited cards """ MAX_STRAIGHT_FLUSH = 10 MAX_FOUR_OF_A_KIND = 166 MAX_FULL_HOUSE = 322 MAX_FLUSH = 1599 MAX_STRAIGHT = 1609 MAX_THREE_OF_A_KIND = 2467 MAX_TWO_PAIR = 3325 MAX_PAIR = 6185 MAX_HIGH_CARD = 7462 MAX_TO_RANK_CLASS = { MAX_STRAIGHT_FLUSH: 1, MAX_FOUR_OF_A_KIND: 2, MAX_FULL_HOUSE: 3, MAX_FLUSH: 4, MAX_STRAIGHT: 5, MAX_THREE_OF_A_KIND: 6, MAX_TWO_PAIR: 7, MAX_PAIR: 8, MAX_HIGH_CARD: 9, } RANK_CLASS_TO_STRING = { 1: "Straight Flush", 2: "Four of a Kind", 3: "Full House", 4: "Flush", 5: "Straight", 6: "Three of a Kind", 7: "Two Pair", 8: "Pair", 9: "High card", } def __init__(self): # create dictionaries self.flush_lookup: Dict[int, int] = {} self.unsuited_lookup: Dict[int, int] = {} # create the lookup table in piecewise fashion self._flushes() # this will call straights and high card method, # we reuse some of the bit sequences self._multiples() def _flushes(self): """ Straight flushes and flushes. Lookup is done on 13 bit integer (2^13 > 7462): xxxbbbbb bbbbbbbb => integer hand index """ # straight flushes in rank order straight_flushes = [ 7936, # int('0b1111100000000', 2), # royal flush 3968, # int('0b111110000000', 2), 1984, # int('0b11111000000', 2), 992, # int('0b1111100000', 2), 496, # int('0b111110000', 2), 248, # int('0b11111000', 2), 124, # int('0b1111100', 2), 62, # int('0b111110', 2), 31, # int('0b11111', 2), 4111, # int('0b1000000001111', 2) # 5 high ] # now we'll dynamically generate all the other # flushes (including straight flushes) flushes = [] gen = LookupTable._get_lexographically_next_bit_sequence(int("0b11111", 2)) # 1277 = number of high card # 1277 + len(str_flushes) is number of hands with all card unique rank for _ in range(1277 + len(straight_flushes) - 1): # we also iterate over SFs # pull the next flush pattern from our generator flush = next(gen) # if this flush matches perfectly any # straight flush, do not add it not_sf = True for straight_flush in straight_flushes: # if f XOR sf == 0, then bit pattern # is same, and we should not add if not flush ^ straight_flush: not_sf = False if not_sf: flushes.append(flush) # we started from the lowest straight pattern, now we want to start ranking from # the most powerful hands, so we reverse flushes.reverse() # now add to the lookup map: # start with straight flushes and the rank of 1 # since theyit is the best hand in poker # rank 1 = Royal Flush! rank = 1 for straight_flush in straight_flushes: prime_product = card.prime_product_from_rankbits(straight_flush) self.flush_lookup[prime_product] = rank rank += 1 # we start the counting for flushes on max full house, which # is the worst rank that a full house can have (2,2,2,3,3) rank = LookupTable.MAX_FULL_HOUSE + 1 for flush in flushes: prime_product = card.prime_product_from_rankbits(flush) self.flush_lookup[prime_product] = rank rank += 1 # we can reuse these bit sequences for straights # and high card since they are inherently related # and differ only by context self._straight_and_highcards(straight_flushes, flushes) def _straight_and_highcards(self, straights, highcards): """ Unique five card sets. Straights and highcards. Reuses bit sequences from flush calculations. """ rank = LookupTable.MAX_FLUSH + 1 for straight in straights: prime_product = card.prime_product_from_rankbits(straight) self.unsuited_lookup[prime_product] = rank rank += 1 rank = LookupTable.MAX_PAIR + 1 for high_card in highcards: prime_product = card.prime_product_from_rankbits(high_card) self.unsuited_lookup[prime_product] = rank rank += 1 def _multiples(self): # pylint: disable=too-many-locals """ Pair, Two Pair, Three of a Kind, Full House, and 4 of a Kind. """ backwards_ranks = range(len(Card.INT_RANKS) - 1, -1, -1) # 1) Four of a Kind rank = LookupTable.MAX_STRAIGHT_FLUSH + 1 # for each choice of a set of four rank for i in backwards_ranks: # and for each possible kicker rank kickers = list(backwards_ranks[:]) kickers.remove(i) for k in kickers: product = Card.PRIMES[i] ** 4 * Card.PRIMES[k] self.unsuited_lookup[product] = rank rank += 1 # 2) Full House rank = LookupTable.MAX_FOUR_OF_A_KIND + 1 # for each three of a kind for i in backwards_ranks: # and for each choice of pair rank pair_ranks = list(backwards_ranks[:]) pair_ranks.remove(i) for pair_rank in pair_ranks: product = Card.PRIMES[i] ** 3 * Card.PRIMES[pair_rank] ** 2 self.unsuited_lookup[product] = rank rank += 1 # 3) Three of a Kind rank = LookupTable.MAX_STRAIGHT + 1 # pick three of one rank for b_rank in backwards_ranks: kickers = list(backwards_ranks[:]) kickers.remove(b_rank) for kickers in itertools.combinations(kickers, 2): card1, card2 = kickers product = ( Card.PRIMES[b_rank] ** 3 * Card.PRIMES[card1] * Card.PRIMES[card2] ) self.unsuited_lookup[product] = rank rank += 1 # 4) Two Pair rank = LookupTable.MAX_THREE_OF_A_KIND + 1 for two_pair in itertools.combinations(backwards_ranks, 2): pair1, pair2 = two_pair kickers = list(backwards_ranks[:]) kickers.remove(pair1) kickers.remove(pair2) for kicker in kickers: product = ( Card.PRIMES[pair1] ** 2 * Card.PRIMES[pair2] ** 2 * Card.PRIMES[kicker] ) self.unsuited_lookup[product] = rank rank += 1 # 5) Pair rank = LookupTable.MAX_TWO_PAIR + 1 # choose a pair for pairrank in backwards_ranks: kickers = list(backwards_ranks[:]) kickers.remove(pairrank) for kickers in itertools.combinations(kickers, 3): kicker1, kicker2, kicker3 = kickers product = ( Card.PRIMES[pairrank] ** 2 * Card.PRIMES[kicker1] * Card.PRIMES[kicker2] * Card.PRIMES[kicker3] ) self.unsuited_lookup[product] = rank rank += 1 @staticmethod def _get_lexographically_next_bit_sequence(bits): """ Bit hack from here: http://www-graphics.stanford.edu/~seander/bithacks.html#NextBitPermutation Generator even does this in poker order rank so no need to sort when done! """ xbits = (bits | (bits - 1)) + 1 lexo_next = xbits | ((((xbits & -xbits) // (bits & -bits)) >> 1) - 1) yield lexo_next while True: xbits = (lexo_next | (lexo_next - 1)) + 1 lexo_next = xbits | ( (((xbits & -xbits) // (lexo_next & -lexo_next)) >> 1) - 1 ) yield lexo_next
LOOKUP_TABLE = LookupTable() """ The lookup table that is created when imported """