A Poker Equity Calculator

Background:

An equity calculator is a useful tool in playing and analyzing hands in poker (in this case, we use the variant "Texas Holdem"). By the term equity, we refer the percentage of the pot a player will win (either outright or by splitting through a tie) on average if the situation were to occur an infinite number of times. There are a few existing equity calculators, notably PokerStove (freeware for Windows which also has a lot of nice features) and the website http://twodimes.net/poker.

The aforementioned calculators compute equities statistically, analyzing a significant sample of trials. The original intent of this project was to compute the equities by using combinatorics, however it was decided to be more practical to use a statistical method. This project also aimed to add features not available on the twodimes.net website, such as computing equity against a range of hands (for example, what is the equity of QQ vs an opponent who can have AA, KK or AK?) rather than against a specific hand.

 

Source code:

{{{id=66| r""" A poker equity calculator. Copyright (C) 2011 Chris Johnson EXAMPLES # ------------------ AsKs vs QcQh preflop: big_slick = 'as ks' pocket_qs = 'qc qh' hands = [big_slick, pocket_qs] board = '' # preflop display(calc_equity(hands, board)) # ------------------ KcKd vs 4h5h vs Ad7d on a 4d 5d 7c flop: kings = 'kc kd' two_pr = '4h 5h' fl_drw = 'ad 7d' hands = [kings, two_pr, fl_drw] board = '4d 5d 7c' display(calc_equity(hands, board)) """ import random # This function sorts an array of cards by rank, highest first. def sort_cards(cards): temp_cards = [] for card in cards: if card[0] == 't': temp_cards.append('z' + card) elif card[0] == 'j': temp_cards.append('zz' + card) elif card[0] == 'q': temp_cards.append('zzz' + card) elif card[0] == 'k': temp_cards.append('zzzz' + card) elif card[0] == 'a': temp_cards.append('zzzzz' + card) else: temp_cards.append(card) temp_cards = sorted(temp_cards) temp_cards.reverse() cards = [] for card in temp_cards: cards.append(card.replace('z', '')) return cards # Returns the numeric rank value; useful when comparing cards # when a Q is higher than a J, but lower than a K, etc. def rank(card): r = card[0] ranks = '23456789tjqka' return ranks.index(r) # Returns suit of a single card (heart, club, etc.). def suit(card): return card[1] # This function evaluates the strength of a hand, returning its 'rough' # value (straight, flush, etc.) and also the specific hand as an array. def eval_hand(cards): # cards = cards.split[' '] cards = sort_cards(cards) # check for royal if (('ah' in cards) & ('kh' in cards) & ('qh' in cards) & ('jh' in cards) & ('th' in cards)): return [9, 'ah kh qh jh th'.split(' ')] if (('as' in cards) & ('ks' in cards) & ('qs' in cards) & ('js' in cards) & ('ts' in cards)): return [9, 'as ks qs js ts'.split(' ')] if (('ac' in cards) & ('kc' in cards) & ('qc' in cards) & ('jc' in cards) & ('tc' in cards)): return [9, 'ac kc qc jc tc'.split(' ')] if (('ad' in cards) & ('kd' in cards) & ('qd' in cards) & ('jd' in cards) & ('td' in cards)): return [9, 'ad kd qd jd td'.split(' ')] # check for straight flush suit_counts = [0, 0, 0, 0] for card in cards: if (suit(card) == 's'): if (suit_counts[0] < 5): suit_counts[0] += 1 if (suit(card) == 'h'): if (suit_counts[1] < 5): suit_counts[1] += 1 if (suit(card) == 'd'): if (suit_counts[2] < 5): suit_counts[2] += 1 if (suit(card) == 'c'): if (suit_counts[3] < 5): suit_counts[3] += 1 if (5 in suit_counts): suits = ['s', 'h', 'd', 'c'] fl_suit = suits[suit_counts.index(5)] s_cards = [] for card in cards: if (suit(card) == fl_suit): s_cards.append(card) for i in (0..len(s_cards) - 5): if rank(s_cards[i]) == 3 & rank(s_cards[i+1]) == 2 & rank(s_cards[i+2]) == 1 & rank(s_cards[i+3]) == 0 & rank(s_cards[0]) == 12: hand = [] hand.append(s_cards[0]) hand.extend(s_cards[i:i+4]) return [8, hand] if rank(s_cards[i]) == (rank(s_cards[i+1]) + 1) & rank(s_cards[i]) == (rank(s_cards[i+2]) + 2) & \ rank(s_cards[i]) == (rank(s_cards[i+3]) + 3) & rank(s_cards[i]) == (rank(s_cards[i+4]) + 4): return [8, s_cards[i:(i+5)]] # check for quads for i in (0..len(cards)-4): if (rank(cards[i]) == rank(cards[i+1])) & (rank(cards[i]) == rank(cards[i+2])) & (rank(cards[i]) == rank(cards[i+3])): hand = [cards[i], cards[i+1], cards[i+2], cards[i+3]] for card in cards: if rank(card) != rank(hand[0]): hand.append(card) return (7, hand) # check for full house hand = [] for i in (0..(len(cards)-3)): if rank(cards[i]) == rank(cards[i+1]) & rank(cards[i]) == rank(cards[i+2]): hand.extend(cards[i:(i+3)]) for j in ((i+3)..(len(cards)-2)): if rank(cards[j]) == rank(cards[j+1]): hand.extend(cards[j:(j+2)]) return [6, hand] # check for flush suit_counts = [0, 0, 0, 0] for card in cards: if suit(card) == 's': suit_counts[0] += 1 if suit(card) == 'h': suit_counts[1] += 1 if suit(card) == 'd': suit_counts[2] += 1 if suit(card) == 'c': suit_counts[3] += 1 if 5 in suit_counts: suits = ['s', 'h', 'd', 'c'] fl_suit = suits[suit_counts.index(5)] hand = [] for card in cards: if suit(card) == fl_suit: hand.append(card) if len(hand) == 5: return [5, hand] # check for straight uniq = [] uniq_ranks = [] for card in cards: if rank(card) not in uniq_ranks: uniq_ranks.append(card[0]) uniq.append(card) if len(uniq) >= 5: for i in (0..len(uniq) - 5): if rank(uniq[i]) == 3 & rank(uniq[i+1]) == 2 & rank(uniq[i+2]) == 1 & rank(uniq[i+3]) == 0 & rank(uniq[0]) == 12: hand = [] hand.append(uniq[0]) hand.extend(uniq[i:i+4]) return [4, hand] if (rank(uniq[i]) == (rank(uniq[i+1]) + 1)) & (rank(uniq[i]) == (rank(uniq[i+2]) + 2)) & \ (rank(uniq[i]) == (rank(uniq[i+3]) + 3)) & (rank(uniq[i]) == (rank(uniq[i+4]) + 4)): return [4, uniq[i:(i+5)]] # check for 3 of a kind for i in (0..len(cards)-3): if rank(cards[i]) == rank(cards[i+1]) & rank(cards[i]) == rank(cards[i+2]): kickers = [] for card in cards: if (len(kickers) < 2) & (rank(card) != rank(cards[i])): kickers.append(card) hand = cards[i:(i+3)] hand.extend(kickers) return [3, hand] # check for 2 pair hand = [] for i in (0..(len(cards) - 2)): if rank(cards[i]) == rank(cards[i+1]): hand.extend(cards[i:i+2]) if len(hand) == 4: for card in cards: if card not in hand: hand.append(card) return [2, hand] # check for 1 pair hand = [] for i in (0..(len(cards) - 2)): if rank(cards[i]) == rank(cards[i+1]): hand.extend(cards[i:i+2]) if len(hand) == 2: for card in cards: if card not in hand: hand.append(card) if len(hand) == 5: return [1, hand] # check for high card return [0, cards[:5]] # Returns the greater of two cards. def better_kicker(kicker1, kicker2): if rank(kicker1) > rank(kicker2): return kicker1 else: return kicker2 # Compares two hands and returns the stronger. def winner(hand1, hand2): if (hand1 == hand2): return hand1 if (hand1[0] != hand2[0]): if (hand1[0] > hand2[0]): return hand1 else: return hand2 # eval kickers if ((hand1[0] == 0) | (hand1[0] == 4) | (hand1[0] == 5) | (hand1[0] == 8)): for i in (0..4): try: kicker1 = hand1[1][i] kicker2 = hand2[1][i] if (kicker1 != kicker2): kicker = better_kicker(kicker1, kicker2) if (kicker == kicker1): return hand1 else: return hand2 except IndexError: print "oops! index error..." print i print hand1 print hand2 return hand1 if (hand1[0] == 1): # first determine if there is a higher pair pair1 = hand1[1][0] pair2 = hand2[1][0] if (pair1 != pair2): pair = better_kicker(pair1, pair2) if (pair == pair1): return hand1 else: return hand2 # if they have same pair, determine better kicker for i in (2..4): try: kicker1 = hand1[1][i] kicker2 = hand2[1][i] except IndexError: print "oops! index error..." print i print hand1 print hand2 if (kicker1 != kicker2): kicker = better_kicker(kicker1, kicker2) if (kicker == kicker1): return hand1 else: return hand2 if (hand1[0] == 2): # determine if there is a higher top pair top_pair1 = hand1[1][0] top_pair2 = hand2[1][0] if (top_pair1 != top_pair2): pair = better_kicker(top_pair1, top_pair2) if (pair == top_pair1): return hand1 else: return hand2 # determine if there is a higher 2nd pair btm_pair1 = hand1[1][6] btm_pair2 = hand2[1][6] if (btm_pair1 != btm_pair2): pair = better_kicker(btm_pair1, btm_pair2) if (pair == btm_pair1): return hand1 else: return hand2 # same two pair, determine better kicker try: kicker1 = hand1[1][4] kicker2 = hand2[1][4] except IndexError: print "oops! index error...middle" print hand1 print hand2 kicker = better_kicker(kicker1, kicker2) if (kicker == kicker1): return hand1 else: return hand2 if (hand1[0] == 3): # determine if there is a higher set set1 = hand1[1][0] set2 = hand2[1][0] if (set1 != set2): toak = better_kicker(set1, set2) if (toak == set1): return hand1 else: return hand2 # determine better kicker for i in (3..4): try: kicker1 = hand1[1][i] kicker2 = hand2[1][i] except IndexError: print "oops! index error..." print i print hand1 print hand2 if (kicker1 != kicker2): kicker = better_kicker(kicker1, kicker2) if (kicker == kicker1): return hand1 else: return hand2 if (hand1[0] == 6): # determine if there is a higher 'filling set' top_fill1 = hand1[1][0] top_fill2 = hand2[1][0] if (top_fill1 != top_fill2): toak = better_kicker(top_fill1, top_fill2) if (toak == top_fill1): return hand1 else: return hand2 # determine higher 'filling pair' btm_fill1 = hand1[1][3] btm_fill2 = hand2[1][3] if (btm_fill1 != btm_fill2): pair = better_kicker(btm_fill1, btm_fill2) if (pair == btm_fill1): return hand1 else: return hand2 if (hand1[0] == 7): # first determine if there is a quad quad1 = hand1[1][0] quad2 = hand2[1][0] if (quad1 != quad2): quad = better_kicker(quad1, quad2) if (quad == quad1): return hand1 else: return hand2 # if they have same quad, determine better kicker try: kicker1 = hand1[1][4] kicker2 = hand2[1][4] except IndexError: print "oops! index error...bottom one" print hand1 print hand2 if (kicker1 != kicker2): kicker = better_kicker(kicker1, kicker2) if (kicker == kicker1): return hand1 else: return hand2 if (hand1[0] == 9): return hand1 # Generates the random cards needed to complete a hand. def sample_trial(hands, board): if (board == ''): # deal five cards board = [] for i in (1..5): board.append(get_random_card(board)) if (len(board) == 8): # deal two cards board = board.split(' ') for i in (1..2): board.append(get_random_card(board)) if (len(board) == 11): # deal one card board = board.split(' ') board.append(get_random_card(board)) return determine_winners(hands, board) # Returns a random card not found in argument array. def get_random_card(exclude_list): while true: card = random.choice('23456789tjqka') + random.choice('hdsc') if card not in exclude_list: return card # Returns an array of two element arrays, each hand w/ their respective score. def determine_winners(hands, board): hand2 = hands[0].split(' ') hand2.extend(board) best_hand = eval_hand(hand2) for hand in hands: handi = hand.split(' ') handi.extend(board) current_hand = eval_hand(handi) best_hand = winner(best_hand, current_hand) winner_count = 0 for hand in hands: handi = hand.split(' ') handi.extend(board) if (eval_hand(handi) == best_hand): winner_count += 1 results = [] for hand in hands: handi = hand.split(' ') handi.extend(board) if (eval_hand(handi) == best_hand): results.append([hand, 1/winner_count]) else: results.append([hand, 0]) return results # Usage of this function can be seen in the examples # at the top of the source code. def calc_equity(hands, board): # returns an array of two-element arrays # e.g. [ ['jc jd', 52.3], ['qh ah', 47.7] ] sample_size = 10000 wins = {} for hand in hands: wins[hand] = 0 for i in (1..sample_size): results = sample_trial(hands, board) for result in results: wins[result[0]] += result[1] equities = [] for hand in hands: equities.append([hand, wins[hand]/sample_size]) return equities # Nicely outputs the results. def display(results): for result in results: print "%(hand)s: %(equity)f" % {'hand':result[0], 'equity':result[1]} /// }}}

Some results and examples:

{{{id=119| hand1 = 'kc kd' hand2 = 'ah as' board = '' hands = [hand1, hand2] display(calc_equity(hands, board)) /// kc kd: 0.265312 ah as: 0.734687 }}}

Sample size: 10,000

kc kd: 0.266000
ah as: 0.734000

Sample size: 20,000

kc kd: 0.261225
ah as: 0.738775

Sample size: 40,000

kc kd: 0.265312
ah as: 0.734687

It's worth noting that twodimes.net gives the following values which indicate my results are slightly off:

http://twodimes.net/h/?z=12837
pokenum -h kc kd - ah as
Holdem Hi: 1712304 enumerated boards
cards win %win lose %lose tie %tie EV
Kc Kd 317694 18.55 1388072 81.06 6538 0.38 0.187
As Ah 1388072 81.06 317694 18.55 6538 0.38 0.813
{{{id=120| hand1 = '8h 7c' hand2 = '6s 5s' board = '8s 7s 2h' hands = [hand1, hand2] display(calc_equity(hands, board)) /// 8h 7c: 0.526900 6s 5s: 0.473100 }}}

This result was computed using a 10k sample, and is close to the values given on twotimes.net:

http://twodimes.net/h/?z=8935886
pokenum -h 8h 7c - 6s 5s -- 8s 7s 2h
Holdem Hi: 990 enumerated boards containing 8s 7s 2h
cards win %win lose %lose tie %tie EV
7c 8h 483 48.79 507 51.21 0 0.00 0.488
6s 5s 507 51.21 483 48.79 0 0.00 0.512
{{{id=129| hand1 = 'ah ac' hand2 = '7s 2s' board = 'as ad kh 5c' hands = [hand1, hand2] display(calc_equity(hands, board)) /// ah ac: 1.000000 7s 2s: 0.000000 }}}

Hand 1 is guaranteed to win this hand with one card to come. If hand 2 was shown to have any equity, there would be a serious flaw in my code.

Comments and conclusion:

Unfortunately, due to time constraints, it was not possible to implement this project using Cython. There are many function calls throughout the code, strings are created and compared often, and obviously tens of thousands of trials are ran when computing the hand equities. However, the performance using just Python and Sage was not as slow as expected.

While the results seem fairly accurate on the whole, the precision could be improved upon. Changing the sample size beyond 10k did not seem to change the equities more than a fraction of a percent, however some calculations were a few percentage off what was given on twodimes.net. One possible bias is the random function.

{{{id=130| /// }}}