Tuesday, July 21, 2009

Python for Simulation: Poker

Here is a poker simulation I developed in introducing Python to my Bioinformatics class. The cards are simply strings of the form 'SP4' or 'DI11'. Evaluation of a hand of 5 cards proceeds by testing first for pairs or higher (using a dictionary keyed by the card ranks), then a test for straights and finally flush.

Here is are the results from 1E6 trials:

1000000 trials:
('12HT 9HT 10HT 11HT 13HT', 'straight flush')
(' 9HT 12HT 11HT 13HT 10HT', 'straight flush')
(' 8HT 6HT 7HT 10HT 9HT', 'straight flush')
('12DI 11DI 1DI 13DI 10DI', 'straight flush')
(' 4DI 5DI 6DI 8DI 7DI', 'straight flush')
(' 5DI 6DI 3DI 2DI 4DI', 'straight flush')
(' 9HT 6HT 8HT 7HT 10HT', 'straight flush')
(' 7HT 10HT 11HT 8HT 9HT', 'straight flush')
(' 4CL 3CL 2CL 6CL 5CL', 'straight flush')
('10DI 13DI 11DI 12DI 1DI', 'straight flush')
(' 8SP 10SP 11SP 12SP 9SP', 'straight flush')
(' 5DI 4DI 6DI 7DI 8DI', 'straight flush')
('10HT 9HT 8HT 11HT 12HT', 'straight flush')
(' 6CL 5CL 2CL 3CL 4CL', 'straight flush')

501903 -
422728 two
46969 two twos
21266 three
3527 straight
1936 - flush
1431 full house
226 four
14 straight flush


The total number of possible hands is:



52 "choose" 5 
= 52! / (47! x 5!)
= 52*51*50*49*48 / 5*4*3*2
= 311875200 / 120
= 2,598,960


A simulation of 1E7 hands should be enought to test the statistics. For the run of 1E6 above, we saw 14 straight flushes where the expected number is 8*4 / 2.6 = 12. (Note: I didn't bother to explicitly mark a royal flush---two are seen in the output, also about as expected).

Python scripts are so simple to write, and this is seductive but dangerous. It is a temptation to put as little effort into testing as you did programming. I showed the demo 6 months ago, but only this morning found a bug in my code to test for straights!

Here is the whole listing:

import random,sys
N = [str(n) for n in range(1,14)]
suits = ['SP','HT','CL','DI']
Deck = [n+s for n in N for s in suits]

def count(H):
D = dict()
L = [s[:-2] for s in H]
for n in L:
try: D[n] += 1
except KeyError: D[n] = 1
return D

def straight(H):
# depends on no pairs or higher
L = [int(s[:-2]) for s in H]
L.sort()
# ace high
if L[0] == 1:
if L == [1,10,11,12,13]:
return True
else: # we need else
if L[-1] - L[0] == 4: return True
return False

def flush(H):
L = [s[-2:] for s in H]
return len(list(set(L))) == 1

def evaluate(H):
D = count(H)
v = D.values()
n = len(D.values())
if n == 5: s = '-'
if n == 4: s = 'two'
if n == 3:
if 3 in v: s = 'three'
else: s = 'two twos'
if n == 2:
if 4 in v: s = 'four'
else: s = 'full house'

# don't test if has >= pair
if s == '-' and straight(H):
s = 'straight'
if flush(H):
s += ' flush'
return s

def oneHand():
random.shuffle(Deck)
H = Deck[:5]
pL = [s.rjust(4) for s in H]
return ' '.join(pL),evaluate(H)

def test(N=100):
for i in range(N):
print oneHand()
sys.exit()
#test()

def simulate(N=int(1E6)):
print N, 'trials:'
D = dict()
for i in range(N):
t = oneHand()
s = t[1]
try:
D[s] += 1
except KeyError:
D[s] = 1
if 'flush' in s:
if 'straight' in s:
print t
print
def f(k): return D[k]
for k in reversed(sorted(D.keys(),key=f)):
print str(D[k]).rjust(len(str(N))),k

simulate()