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|
///
}}}