import itertools from deuces3.card import Card from deuces3.deck import Deck """ 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] Examples: * Royal flush (best hand possible) => 1 * 7-5-4-3-2 unsuited (worst hand possible) => 7462 """ class LookupTable(object): 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): """ Calculates lookup tables """ # create dictionaries self.flush_lookup = {} self.unsuited_lookup = {} # create the lookup table in piecewise fashion self.flushes() # this will call straights and high cards 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 = self.get_lexographically_next_bit_sequence(int('0b11111', 2)) # 1277 = number of high cards # 1277 + len(str_flushes) is number of hands with all cards unique rank for i in range(1277 + len(straight_flushes) - 1): # we also iterate over SFs # pull the next flush pattern from our generator f = next(gen) # if this flush matches perfectly any # straight flush, do not add it notSF = True for sf in straight_flushes: # if f XOR sf == 0, then bit pattern # is same, and we should not add if not f ^ sf: notSF = False if notSF: flushes.append(f) # 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 sf in straight_flushes: prime_product = Card.prime_product_from_rankbits(sf) 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 f in flushes: prime_product = Card.prime_product_from_rankbits(f) self.flush_lookup[prime_product] = rank rank += 1 # we can reuse these bit sequences for straights # and high cards 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 s in straights: prime_product = Card.prime_product_from_rankbits(s) self.unsuited_lookup[prime_product] = rank rank += 1 rank = LookupTable.MAX_PAIR + 1 for h in highcards: prime_product = Card.prime_product_from_rankbits(h) self.unsuited_lookup[prime_product] = rank rank += 1 def multiples(self): """ 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 pairranks = list(backwards_ranks[:]) pairranks.remove(i) for pr in pairranks: product = Card.PRIMES[i]**3 * Card.PRIMES[pr]**2 self.unsuited_lookup[product] = rank rank += 1 # 3) Three of a Kind rank = LookupTable.MAX_STRAIGHT + 1 # pick three of one rank for r in backwards_ranks: kickers = list(backwards_ranks[:]) kickers.remove(r) gen = itertools.combinations(kickers, 2) for kickers in gen: c1, c2 = kickers product = Card.PRIMES[r]**3 * Card.PRIMES[c1] * Card.PRIMES[c2] self.unsuited_lookup[product] = rank rank += 1 # 4) Two Pair rank = LookupTable.MAX_THREE_OF_A_KIND + 1 tpgen = itertools.combinations(backwards_ranks, 2) for tp in tpgen: pair1, pair2 = tp 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) kgen = itertools.combinations(kickers, 3) for kickers in kgen: k1, k2, k3 = kickers product = Card.PRIMES[pairrank]**2 * Card.PRIMES[k1] \ * Card.PRIMES[k2] * Card.PRIMES[k3] self.unsuited_lookup[product] = rank rank += 1 def write_table_to_disk(self, table, filepath): """ Writes lookup table to disk """ with open(filepath, 'w') as f: for prime_prod, rank in table.iteritems(): f.write(str(prime_prod) +","+ str(rank) + '\n') def get_lexographically_next_bit_sequence(self, 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! Perfect. """ t = (bits | (bits - 1)) + 1 next = t | ((int((t & -t) / (bits & -bits)) >> 1) - 1) yield next while True: t = (next | (next - 1)) + 1 next = t | ((int((t & -t) / (next & -next)) >> 1) - 1) yield next class Evaluator(object): """ Evaluates hand strengths using a variant of Cactus Kev's algorithm: http://suffe.cool/poker/evaluator.html I make considerable optimizations in terms of speed and memory usage, in fact the lookup table generation can be done in under a second and consequent evaluations are very fast. Won't beat C, but very fast as all calculations are done with bit arithmetic and table lookups. """ def __init__(self): self.table = LookupTable() self.hand_size_map = { 5 : self._five, 6 : self._six, 7 : self._seven } def evaluate(self, cards, board): """ This is the function that the user calls to get a hand rank. Supports empty board, etc very flexible. No input validation because that's cycles! """ all_cards = cards + board return self.hand_size_map[len(all_cards)](all_cards) def _five(self, cards): """ Performs an evalution given cards in integer form, mapping them to a rank in the range [1, 7462], with lower ranks being more powerful. Variant of Cactus Kev's 5 card evaluator, though I saved a lot of memory space using a hash table and condensing some of the calculations. """ # if flush if cards[0] & cards[1] & cards[2] & cards[3] & cards[4] & 0xF000: handOR = (cards[0] | cards[1] | cards[2] | cards[3] | cards[4]) >> 16 prime = Card.prime_product_from_rankbits(handOR) return self.table.flush_lookup[prime] # otherwise else: prime = Card.prime_product_from_hand(cards) return self.table.unsuited_lookup[prime] def _six(self, cards): """ Performs five_card_eval() on all (6 choose 5) = 6 subsets of 5 cards in the set of 6 to determine the best ranking, and returns this ranking. """ minimum = LookupTable.MAX_HIGH_CARD all5cardcombobs = itertools.combinations(cards, 5) for combo in all5cardcombobs: score = self._five(combo) if score < minimum: minimum = score return minimum def _seven(self, cards): """ Performs five_card_eval() on all (7 choose 5) = 21 subsets of 5 cards in the set of 7 to determine the best ranking, and returns this ranking. """ minimum = LookupTable.MAX_HIGH_CARD all5cardcombobs = itertools.combinations(cards, 5) for combo in all5cardcombobs: score = self._five(combo) if score < minimum: minimum = score return minimum def get_rank_class(self, hr): """ Returns the class of hand given the hand hand_rank returned from evaluate. """ if hr >= 0 and hr <= LookupTable.MAX_STRAIGHT_FLUSH: return LookupTable.MAX_TO_RANK_CLASS[LookupTable.MAX_STRAIGHT_FLUSH] elif hr <= LookupTable.MAX_FOUR_OF_A_KIND: return LookupTable.MAX_TO_RANK_CLASS[LookupTable.MAX_FOUR_OF_A_KIND] elif hr <= LookupTable.MAX_FULL_HOUSE: return LookupTable.MAX_TO_RANK_CLASS[LookupTable.MAX_FULL_HOUSE] elif hr <= LookupTable.MAX_FLUSH: return LookupTable.MAX_TO_RANK_CLASS[LookupTable.MAX_FLUSH] elif hr <= LookupTable.MAX_STRAIGHT: return LookupTable.MAX_TO_RANK_CLASS[LookupTable.MAX_STRAIGHT] elif hr <= LookupTable.MAX_THREE_OF_A_KIND: return LookupTable.MAX_TO_RANK_CLASS[LookupTable.MAX_THREE_OF_A_KIND] elif hr <= LookupTable.MAX_TWO_PAIR: return LookupTable.MAX_TO_RANK_CLASS[LookupTable.MAX_TWO_PAIR] elif hr <= LookupTable.MAX_PAIR: return LookupTable.MAX_TO_RANK_CLASS[LookupTable.MAX_PAIR] elif hr <= LookupTable.MAX_HIGH_CARD: return LookupTable.MAX_TO_RANK_CLASS[LookupTable.MAX_HIGH_CARD] else: raise Exception("Inavlid hand rank, cannot return rank class") def class_to_string(self, class_int): """ Converts the integer class hand score into a human-readable string. """ return LookupTable.RANK_CLASS_TO_STRING[class_int] def get_five_card_rank_percentage(self, hand_rank): """ Scales the hand rank score to the [0.0, 1.0] range. """ return float(hand_rank) / float(LookupTable.MAX_HIGH_CARD) def hand_summary(self, board, hands): """ Gives a sumamry of the hand with ranks as time proceeds. Requires that the board is in chronological order for the analysis to make sense. """ assert len(board) == 5, "Invalid board length" for hand in hands: assert len(hand) == 2, "Inavlid hand length" line_length = 10 stages = ["FLOP", "TURN", "RIVER"] for i in range(len(stages)): line = ("=" * line_length + " %s " + "=" * line_length) print(line % stages[i]) best_rank = 7463 # rank one worse than worst hand winners = [] for player, hand in enumerate(hands): # evaluate current board position rank = self.evaluate(hand, board[:(i + 3)]) rank_class = self.get_rank_class(rank) class_string = self.class_to_string(rank_class) percentage = 1.0 - self.get_five_card_rank_percentage(rank) # higher better here print("Player %d hand = %s, percentage rank among all hands = %f" % ( player + 1, class_string, percentage)) # detect winner if rank == best_rank: winners.append(player) best_rank = rank elif rank < best_rank: winners = [player] best_rank = rank # if we're not on the river if i != stages.index("RIVER"): if len(winners) == 1: print("Player %d hand is currently winning.\n" % (winners[0] + 1,)) else: print("Players %s are tied for the lead.\n" % [x + 1 for x in winners]) # otherwise on all other streets else: print ("=" * line_length + " HAND OVER " + "=" * line_length) if len(winners) == 1: print("Player %d is the winner with a %s\n" % (winners[0] + 1, self.class_to_string(self.get_rank_class(self.evaluate(hands[winners[0]], board))))) else: print("Players %s tied for the win with a %s\n" % (winners, self.class_to_string(self.get_rank_class(self.evaluate(hands[winners[0]], board)))))