Friday, November 19, 2010

Python Texas Hold’em Simulator

Update

I finally got around to looking at this again, and I decided to re-write the program. I also posted the new version to GitHub. You can find it at https://github.com/jfilliben/poker-sim. Feel free to disregard the below info, except perhaps to learn a lot of things you shouldn't do with Python ;)


Original

In addition to my practical Python script for Cisco switch configuration verification, I wrote a purely impractical Texas Hold’em Simulator.  I enjoy the game, and it occurred to me that a poker simulator would be a fun way to explore Monte Carlo simulations.  The following program takes as an input a count of simulations to perform and two player hands, plus five community cards.  For any community card specified with “Xx”, it will randomly select a card and play out the hand.  At it’s simplest form, you can run it with one iteration and a fully-specified set of cards, and it will tell you which hand wins:
C:>python.exe pokersim.py 1 Ac7d 8sKd 2s3s4s4c4d
Total Hands: 1
Hand1: 1 Hand2: 0 Ties: 0
Hand1: 100.0% Hand2: 0.0% Ties: 0.0%

Or you can leave a couple cards ‘blank’ and see what random turn and river cards will provide:
C:>python.exe pokersim.py 1000 Ac7d 8sKd 2s3sXxXxXx
Total Hands: 1000
Hand1: 612 Hand2: 385 Ties: 3
Hand1: 61.2% Hand2: 38.5% Ties: 0.3%
(note, this program uses a Monte Carlo simulation, so there is a measure of randomness to the results.  If you run the same test multiple times, you will almost always get slightly different results.  And of course, the more iterations you choose to do, the more accurate the results)
Here is the script.  Again, any comments would be appreciated.  If there is a more efficient way to accomplish portions of this program, I’d love to hear of them.  And if you spot any errors, please let me know.  As a disclaimer, there is no guarantee that the results of this script are accurate.  I don’t use this for profit in any way, it was purely a thought exercise for me.
Run it with no options (or “——help”) to get a brief description of the options.
----------
pokersim.py:
#
# pokersim.py - Runs a Monte Carlo simulation of two Texas Hold'em hands
#               with user-specified (or random) community cards
#
# Work to be done:
# Add exhaustive search?
# Compare speed of copy.deepcopy and hand_copy()
# Add input checking for user input
#
import sys
import random
def hand_copy(cards):
#
# Replace copy.deepcopy with specific copy function to speed things up
# (presumably, but not tested)
#
    results = []
    for i, v in enumerate(cards):
        results.append(cards[i])
    return results
def legal_hand(cards):
#
# Returns 1 if hand is legal
# Returns 0 if hand is illegal (two of same card)
#
    for i, v in enumerate(cards):
        if cards.count(v) > 1: return 0
        elif cards.count([-1, -1]): return 0
    return 1
def valid_card(card):
#
# Returns 1 if card is a valid card in text format (rank in (A-2),
#  suit in (c, d, h, s) or wildcard (Xx)
# Returns 0 if card is invalid
#
    if card[0] in ("X", "x", "A", "a", "K", "k", "Q", "q", "J", "j"\
    , "T", "t", "9", "8", "7", "6", "5", "4", "3", "2"):
        if card[1] in ("x", "X", "c", "C", "d", "D", "h", "H", "s", "S"):
            return 1
    else: return 0
def readable_hand(cards):
#
# Returns a readable version of a set of cards
#
    string = ""
    for i, v in enumerate(cards):
        if v[0] == 0: string += "2"
        elif v[0] == 1: string += "3"
        elif v[0] == 2: string += "4"
        elif v[0] == 3: string += "5"
        elif v[0] == 4: string += "6"
        elif v[0] == 5: string += "7"
        elif v[0] == 6: string += "8"
        elif v[0] == 7: string += "9"
        elif v[0] == 8: string += "T"
        elif v[0] == 9: string += "J"
        elif v[0] == 10: string += "Q"
        elif v[0] == 11: string += "K"
        elif v[0] == 12: string += "A"
        elif v[0] == -1: string += "X"
        if v[1] == 0: string += "c"
        elif v[1] == 1: string += "d"
        elif v[1] == 2: string += "h"
        elif v[1] == 3: string += "s"
        elif v[1] == -1: string += "x"
    return string
def hand_to_numeric(cards):
#
# Converts alphanumeric hand to numeric values for easier comparisons
# Also sorts cards based on rank
#
    result = []
    for i, v in enumerate(cards):
        currentcard = [0, 0]
        if cards[i][0] == "2": currentcard[0] = 0
        elif cards[i][0] == "3": currentcard[0] = 1
        elif cards[i][0] == "4": currentcard[0] = 2
        elif cards[i][0] == "5": currentcard[0] = 3
        elif cards[i][0] == "6": currentcard[0] = 4
        elif cards[i][0] == "7": currentcard[0] = 5
        elif cards[i][0] == "8": currentcard[0] = 6
        elif cards[i][0] == "9": currentcard[0] = 7
        elif cards[i][0] in ("t","T"): currentcard[0] = 8
        elif cards[i][0] in ("j","J"): currentcard[0] = 9
        elif cards[i][0] in ("q","Q"): currentcard[0] = 10
        elif cards[i][0] in ("k","K"): currentcard[0] = 11
        elif cards[i][0] in ("a","A"): currentcard[0] = 12
        elif cards[i][0] in ("x","X"): currentcard[0] = -1
        if cards[i][1] in ("c","C"): currentcard[1] = 0
        elif cards[i][1] in ("d","D"): currentcard[1] = 1
        elif cards[i][1] in ("h","H"): currentcard[1] = 2
        elif cards[i][1] in ("s","S"): currentcard[1] = 3
        elif cards[i][1] in ("x","X"): currentcard[1] = -1
        result.append(currentcard)
    result.sort()
    result.reverse()
    return result
def check_flush(hand):
# Return 0 if not true
# Return 1 if true
#
# Initialization
#
    hand_suit = []
    hand_suit.append(hand[0][1])
    hand_suit.append(hand[1][1])
    hand_suit.append(hand[2][1])
    hand_suit.append(hand[3][1])
    hand_suit.append(hand[4][1])
    for i in range(0, 4):
        if hand_suit.count(i) == 5: return 1
    return 0
def check_straight(hand):
# Return 0 if not true
# Return 1 if true
    if hand[0][0] == (hand[1][0] + 1) == (hand[2][0] + 2) == (hand[3][0] + 3)\
    == (hand[4][0] + 4): return 1
    elif (hand[0][0] == 12) and (hand[1][0] == 3) and (hand[2][0] == 2)\
    and (hand[3][0] == 1) and (hand[4][0] == 0): return 1
    return 0
def check_straightflush(hand):
# Return 0 if not true
# Return 1 if true
    if check_flush(hand) and check_straight(hand): return 1
    return 0
def check_fourofakind(hand):
# Return 0 if not true
# Return 1 if true
# Also returns rank of four of a kind card and rank of fifth card
# (garbage value if no four of a kind)
    hand_rank = []
    hand_rank.append(hand[0][0])
    hand_rank.append(hand[1][0])
    hand_rank.append(hand[2][0])
    hand_rank.append(hand[3][0])
    hand_rank.append(hand[4][0])
    for value in range (0, 13):
        if hand_rank.count(value) == 4:
            for n in range (0, 13):
                if hand_rank.count(n) == 1: return 1, value, n
    return 0, 13, 13
def check_fullhouse(hand):
# Return 0 if not true
# Return 1 if true
# Also returns rank of three of a kind card and two of a kind card
# (garbage values if no full house)
    hand_rank = []
    hand_rank.append(hand[0][0])
    hand_rank.append(hand[1][0])
    hand_rank.append(hand[2][0])
    hand_rank.append(hand[3][0])
    hand_rank.append(hand[4][0])
    for value in range(0, 13):
        if hand_rank.count(value) == 3:
            for n in range(0, 13):
                if hand_rank.count(n) == 2: return 1, value, n
    return 0, 13, 13
def check_threeofakind(hand):
# Return 0 if not true
# Return 1 if true
# Also returns rank of three of a kind card and remaining two cards
# (garbage values if no three of a kind)
    hand_rank = []
    hand_rank.append(hand[0][0])
    hand_rank.append(hand[1][0])
    hand_rank.append(hand[2][0])
    hand_rank.append(hand[3][0])
    hand_rank.append(hand[4][0])
    for value in range(0, 13):
        if hand_rank.count(value) == 3:
            for n in range(0, 13):
                if hand_rank.count(n) == 1:
                    for m in range(n+1, 13):
                        if hand_rank.count(m) == 1: return 1, value, [m, n]
    return 0, 13, [13, 13]
def check_twopair(hand):
# Return 0 if not true
# Return 1 if true
# Also returns ranks of paired cards and remaining card
# (garbage values if no two pair)
    value = 0
    hand_rank = []
    hand_rank.append(hand[0][0])
    hand_rank.append(hand[1][0])
    hand_rank.append(hand[2][0])
    hand_rank.append(hand[3][0])
    hand_rank.append(hand[4][0])
    for value in range(0, 13):
        if hand_rank.count(value) == 2:
            for n in range(value+1, 13):
                if hand_rank.count(n) == 2:
                    for m in range(0, 13):
                        if hand_rank.count(m) == 1: return 1, [n, value], m
    return 0, [13, 13], 13
def check_onepair(hand):
# Return 0 if not true
# Return 1 if true
# Also returns ranks of paired cards and remaining three cards
# (garbage values if no pair)
    hand_rank = []
    hand_rank.append(hand[0][0])
    hand_rank.append(hand[1][0])
    hand_rank.append(hand[2][0])
    hand_rank.append(hand[3][0])
    hand_rank.append(hand[4][0])
    for value in range(0, 13):
        if hand_rank.count(value) == 2:
            for n in range (0, 13):
                if hand_rank.count(n) == 1:
                    for m in range(n+1, 13):
                        if hand_rank.count(m) == 1:
                            for o in range (m+1, 13):
                                if hand_rank.count(o) == 1: return 1, value, [o, m, n]
    return 0, 13, [13, 13, 13]
def highest_card(hand1, hand2):
# Return 0 if hand1 is higher
# Return 1 if hand2 is higher
# Return 2 if equal
#
# Initialization
#
    hand1_rank = []
    hand1_rank.append(hand1[0][0])
    hand1_rank.append(hand1[1][0])
    hand1_rank.append(hand1[2][0])
    hand1_rank.append(hand1[3][0])
    hand1_rank.append(hand1[4][0])
    hand2_rank = []
    hand2_rank.append(hand2[0][0])
    hand2_rank.append(hand2[1][0])
    hand2_rank.append(hand2[2][0])
    hand2_rank.append(hand2[3][0])
    hand2_rank.append(hand2[4][0])
#
# Compare
#
    if hand1_rank > hand2_rank: return 0
    elif hand1_rank < hand2_rank: return 1
    return 2
def highest_card_straight(hand1, hand2):
# Return 0 if hand1 is higher
# Return 1 if hand2 is higher
# Return 2 if equal
#
# Compare second card first (to account for Ace low straights)
# if equal, we could have Ace low straight, so compare first card. 
# If first card is Ace, that is the lower straight
#
    if hand1[1][0] > hand2[1][0]: return 0
    elif hand1[1][0] < hand2[1][0]: return 1
    elif hand1[0][0] > hand2[0][0]: return 1
    elif hand1[0][0] < hand2[0][0]: return 0
    return 2
def compare_hands(hand1, hand2):
#
# Compare two hands
# Return 0 if hand1 is better
# Return 1 if hand2 is better
# Return 2 if equal
#
#
# Initialization
#
    result1 = []
    result2 = []
#
# Check for straight flush
#
    if check_straightflush(hand1):
        if check_straightflush(hand2):
            return(highest_card_straight(hand1, hand2))
        else: return 0
    elif check_straightflush(hand2): return 1
#
# Check for four of a kind
#
    result1 = check_fourofakind(hand1)
    result2 = check_fourofakind(hand2)
    if result1[0] == 1:
        if result2[0] == 1:
            if result1[1] > result2[1]: return 0
            elif result1[1] < result2[1]: return 1
            elif result1[2] > result2[2]: return 0
            elif result1[2] < result2[2]: return 1
            else: return 2
        else: return 0
    elif result2[0] == 1: return 1
#
# Check for full house
#
    result1 = check_fullhouse(hand1)
    result2 = check_fullhouse(hand2)
    if result1[0] == 1:
        if result2[0] == 1:
            if result1[1] > result2[1]: return 0
            elif result1[1] < result2[1]: return 1
            elif result1[2] > result2[2]: return 0
            elif result1[2] < result2[2]: return 1
            else: return 2
        else: return 0
    elif result2[0] == 1: return 1
#
# Check for flush
#
    if check_flush(hand1):
        if check_flush(hand2):
            return(highest_card(hand1, hand2))
        else: return 0
    elif check_flush(hand2): return 1
#
# Check for straight
#
    if check_straight(hand1):
        if check_straight(hand2):
            temp = highest_card_straight(hand1, hand2)
            return temp
        else: return 0
    elif check_straight(hand2): return 1
#
# Check for three of a kind
#
    result1 = check_threeofakind(hand1)
    result2 = check_threeofakind(hand2)
    if result1[0] == 1:
        if result2[0] == 1:
            if result1[1] > result2[1]: return 0
            elif result1[1] < result2[1]: return 1
            elif result1[2] > result2[2]: return 0
            elif result1[2] < result2[2]: return 1
            else: return 2
        else: return 0
    elif result2[0] == 1: return 1
#
# Check for two pair
#
    result1 = check_twopair(hand1)
    result2 = check_twopair(hand2)
    if result1[0] == 1:
        if result2[0] == 1:
            if result1[1] > result2[1]: return 0
            elif result1[1] < result2[1]: return 1
            elif result1[2] > result2[2]: return 0
            elif result1[2] < result2[2]: return 1
            else: return 2
        else: return 0
    elif result2[0] == 1: return 1
#
# Check for one pair
#
    result1 = check_onepair(hand1)
    result2 = check_onepair(hand2)
    if result1[0] == 1:
        if result2[0] == 1:
            if result1[1] > result2[1]: return 0
            elif result1[1] < result2[1]: return 1
            elif result1[2] > result2[2]: return 0
            elif result1[2] < result2[2]: return 1
            else: return 2
        else: return 0
    elif result2[0] == 1: return 1
    return (highest_card(hand1, hand2))
def bestfive(hand, community):
#
# Takes hand and community cards in numeric form and returns best five cards
#
    currentbest = hand_copy(community)
    currentbest.sort()
    currentbest.reverse()
    m = 0
#
# Compare current best to five cards including only one player card
#
    for m in range (0, 2):
        for n in range (0, 5):
            comparehand = hand_copy(community)
            comparehand[n] = hand[m]
            comparehand.sort()
            comparehand.reverse()
            if compare_hands(currentbest, comparehand) == 1:
                currentbest = hand_copy(comparehand)
#
# Compare current best to five cards including both player cards
#
    for m in range (0, 5):
        for n in range (m+1, 5):
            comparehand = hand_copy(community)
            comparehand[m] = hand[0]
            comparehand[n] = hand[1]
            comparehand.sort()
            comparehand.reverse()
            if compare_hands(currentbest, comparehand) == 1:
                currentbest = hand_copy(comparehand)
    return currentbest
#
# Main Program Body
#
#
# Initialization
#
hand1 = []
handnum1 = []
best_hand1 = []
hand2 = []
handnum2 = []
best_hand2 = []
community = []
communitytemp = []
totals = [0,0,0]
iterations = 0
#
# Process command-line arguments
#
if (len(sys.argv) == 1) or (sys.argv[1] in ("-h", "--help")):
        sys.exit("\n\
First input is number of iterations to run the Monte Carlo simulation\n\
Input cards in format [RANK][SUIT], as in Ace Clubs + Four Diamonds = Ac4d)\n\
Input should be two cards for player 1, two cards for player 2 and five community cards\n\
Wildcards should be written as Xx (capital X for rank, lower-case x for suit)\n\
Wildcards should be placed at the end of the community hand\n\n\
--help: This message\n")
else:
    iterations = int(sys.argv[1])
    if iterations < 1: iterations = 1
    if valid_card(sys.argv[2][0:2]): hand1.append(sys.argv[2][0:2])
    else: sys.exit("Player 1 Card 1 Invalid")
    if valid_card(sys.argv[2][2:4]): hand1.append(sys.argv[2][2:4])
    else: sys.exit("Player 1 Card 2 Invalid")
    if valid_card(sys.argv[3][0:2]): hand2.append(sys.argv[3][0:2])
    else: sys.exit("Player 2 Card 1 Invalid")
    if valid_card(sys.argv[3][2:4]): hand2.append(sys.argv[3][2:4])
    else: sys.exit("Player 2 Card 2 Invalid")
    if valid_card(sys.argv[4][0:2]): community.append(sys.argv[4][0:2])
    else: sys.exit("Community Card 1 Invalid")
    if valid_card(sys.argv[4][2:4]): community.append(sys.argv[4][2:4])
    else: sys.exit("Community Card 2 Invalid")
    if valid_card(sys.argv[4][4:6]): community.append(sys.argv[4][4:6])
    else: sys.exit("Community Card 3 Invalid")
    if valid_card(sys.argv[4][6:8]): community.append(sys.argv[4][6:8])
    else: sys.exit("Community Card 4 Invalid")
    if valid_card(sys.argv[4][8:10]): community.append(sys.argv[4][8:10])
    else: sys.exit("Community Card 5 Invalid")
handnum1 = hand_to_numeric(hand1)
handnum2 = hand_to_numeric(hand2)
#
#
# Monte Carlo Simulation
#
#
for n in range (0, iterations):
    communitytemp = hand_to_numeric(community)
    while not legal_hand(handnum1 + handnum2 + communitytemp):
        for i, v in enumerate(community):
            if community[i][0] in ("X", "x"):
                communitytemp[i] = [random.randrange(0,13), random.randrange(0,4)]
    best_hand1 = bestfive(handnum1, communitytemp)
    best_hand2 = bestfive(handnum2, communitytemp)
    totals[compare_hands(best_hand1, best_hand2)] += 1
print "\nTotal Hands: " + str(totals[0]+totals[1]+totals[2])
print "Hand1: " + str(totals[0]) + " Hand2: " + str(totals[1]) + " Ties: " + str(totals[2])
print "Hand1: " + str(round((100*(totals[0])/((totals[0]+totals[1]+totals[2])+0.0)), 2))\
+ "% Hand2: " + str(round(((100*totals[1])/((totals[0]+totals[1]+totals[2])+0.0)), 2))\
+ "% Ties: " + str(round(((100*totals[2])/((totals[0]+totals[1]+totals[2])+0.0)), 2)) + "%"

2 comments:

BillD said...

I am trying to make a poker game in Python and looking at yours I feel like almost all of the long elif chains could be shortened by using dictionaries. Am I wrong?

Jeremy Filliben said...

Bill,

When I was a boy, languages didn't have dictionaries... But that isn't a good excuse for not using them :)

I agree 100%, re-writing readable_hand() and hand_to_numeric() using dictionaries would make a lot of sense. I would have to give some thought to handling invalid input, but it seems like a better method than my elif chains.

Thanks for the suggestion, and if/when you complete your project, please let me know.

Jeremy