First a disclaimer: I am a medical professional who plays with both Python and poker as a hobby. I have no formal training in neither of those and I am not aware of what is in the curriculum of a computer science class. The computer that I use is a desktop i7-4790 3.6 Ghz with 16 GB of RAM along with Jupyter Notebooks.
My goal was to code the equivalent of pokerstrategy.com Equilab or https://www.cardschat.com/poker-odds-calculator.php for me. I would stick to Texas Holdem only.
In order to to that I needed to code an evaluator for any 5 card combination.I did that and it does the job perfectly, considers every card in a hand and produces a tuple as an output, for example:
('2h', '3c', '4s', '6s', 'Jh'): (0, 11, 6, 4, 3, 2)
High-card hand, kickers J, 6, 4, 3, 2
('7c', 'Ad', 'Kd', 'Kh', 'Tc'): (1, 13, 14, 10, 7)
One pair, pair of kings, kickers A, T, 7
('2c', '3c', '4c', '5c', 'Ac'): (8, 5)
Straight flush, 5 high
So it differentiates between A 9 8 7 3 and A 9 8 7 5 flush or high-hand. I checked with theoretical number of royal flushes, quads, full houses etc. over all 2 598 960 card ccombinations and the frequencies check out (https://www.quora.com/How-many-possible-hands-are-there-in-a-five-card-poker-game)
Now I tried to evaluate every possible 5-card combination out of those 2.6 millions and it took disappointing 51 seconds.
I kind of expected that thinking my 5-card evaluator can't be the champion of algorithm competitions and surely there is a better way to do it (I can post it here if it is of relevance), but I thought never mind. Once all 5-card combinations are evaluated I will save them in a dictionary and next time I will load the dictionary and when I have any 5-card combination I will simply look up the result.
Another disappointment. 10 000 000 (10 million) board searches takes approx. 23-24 seconds.This is a a part that I do not understand!!!I have basically a database that has 2.6 mil. rows x 2 columns and the search is SO PROHIBITIVELY slow. How do then billion record databases get anything accomplished?My whole dictionary when saved to a file takes 88 Mb - is that a HUGE database?
And finally I made a full hand vs. hand evaluator that in pseudo-code does this:
Given 2 hands, for example AhAs vs. 6d6h
list all boards that can be dealt with those 2 "dead" cards, which is 1 712 304 boards
list all 21 combinations of hand1 with board 1,
search the ranked_hands dictionary with those 21 combinations and return the best possible outcome (21 combinations because in texas holdem you can use one, two or no cards from your hand with any of the 5 community cards on board)
do the same with hand2 and board1
compare best outcome of hand1 with best outcome of hand2
count if the outcome favours hand1, hand2 or if it is a tie
go to next board
This algorithm does approximately 71 million dictionary lookups - each of 1.7 million boards x 42 (every hand's 21 combinations twice).
Now, THIS IS A DISASTER. Approximately 80 seconds per hand vs. hand matchup. With those speeds there is nothing I can begin. So, any input would be appreciated as to how I can make this better?
Is it me and my lack of proper computer science and algorithm knowledge?
Is it Python? Is it Jupyter Notebooks inside of Chrome?
Any other suggestions?
Code as requested:
import collections
import random
import itertools
import timeit
import time
ranks = ['2','3','4','5','6','7','8','9','T','J','Q','K','A']
names ="Deuces Threes Fours Fives Sixes Sevens Eights Nines Tens Jacks Queens Kings Aces"
cardnames = names.split()
cardnames
suitsall = "hearts spades diamonds clubs"
suitnames = suitsall.split()
suitnames
suits = ['h','s','d','c']
cards = []
# Create all cards from suits and ranks
for suit in suits:
for rank in ranks:
cards.append(rank + suit)
# Create all possible flops by chosing 3 cards out of a deck
flops = list(itertools.combinations(cards, 3))
# Create all possible boards by chosing 5 cards out of a deck
boards = list(itertools.combinations(cards, 5))
# Create all possible starting hands
startingHands = list(itertools.combinations(cards, 2))
# Function dict_hand_rank ranks every board and returns a tuple (board) (value)
def hand_rank_dict(hand):
suits = []
ranks_alphabetical = []
ranks_numerical = []
ranks_histogram = []
kickers = []
kickers_text = []
isFlush = False
isStraight = False
isStraightFlush = False
handrankValue = 0
straightHeight = -1
straightName = "No straight"
handName = "none yet"
for card in hand:
suits.append(card[1])
ranks_alphabetical.append(card[0])
# create ranks_histogram where from A 2 ... J Q K A every card has the corresponding number of occurencies, A double counted
ranks_histogram.append(str(ranks_alphabetical.count('A')))
for rank in ranks:
ranks_histogram.append(str(ranks_alphabetical.count(rank)))
joined_histogram = ''.join(ranks_histogram)
# create ranks numerical instead of T, J, Q, K A
for card in hand:
ranks_numerical.append(ranks.index(card[0])+2)
# create kickers
kickers = sorted([x for x in ranks_numerical if ranks_numerical.count(x) <2], reverse = True)
# check if a hand is a straight
if '11111' in joined_histogram:
isStraight = True
straightHeight = joined_histogram.find('11111') + 5
straightName = cardnames[straightHeight - 2]
handName = "Straight"
handrankValue = (4,) + (straightHeight,)
# check if a hand is a flush
if all(x == suits[0] for x in suits):
isFlush = True
handName = "Flush " + cardnames[kickers[0] - 2] + " " + cardnames[kickers[1] - 2] \
+ " " + cardnames[kickers[2] - 2] + " " + cardnames[kickers[3] - 2] + " " + cardnames[kickers[4] - 2]
handrankValue = (5,) + tuple(kickers)
# check if a hand is a straight and a flush
if isFlush & isStraight:
isStraightFlush = True
handName = "Straight Flush"
handrankValue = (8,) + (straightHeight,)
# check if a hand is four of a kind
if '4' in joined_histogram:
fourofakindcard = (joined_histogram[1:].find('4') + 2)
handName = "Four of a Kind " + cardnames[fourofakindcard -2] + " " + cardnames[kickers[0] - 2] + " kicker"
handrankValue = (7,) + ((joined_histogram[1:].find('4') + 2),) + tuple(kickers)
# check if a hand is a full house
if ('3' in joined_histogram) & ('2' in joined_histogram):
handName = "Full house"
handrankValue = (6,) + ((joined_histogram[1:].find('3') + 2),) + ((joined_histogram[1:].find('2') + 2),) + tuple(kickers)
# check if a hand is three of a kind
if ('3' in joined_histogram) & (len(kickers) == 2):
threeofakindcard = (joined_histogram[1:].find('3') + 2)
handName = "Three of a Kind " + cardnames[threeofakindcard -2] + " " + cardnames[kickers[0] - 2] + \
" " + cardnames[kickers[1] - 2]
handrankValue = (3,) + ((joined_histogram[1:].find('3') + 2),) + tuple(kickers)
# check if a hand is two pairs
if ('2' in joined_histogram) & (len(kickers) == 1):
lowerpair = (joined_histogram[1:].find('2') + 2)
higherpair = (joined_histogram[lowerpair:].find('2') + 1 + lowerpair)
handName = "Two pair " + cardnames[higherpair -2] + " and " + cardnames[lowerpair - 2] + " " + \
cardnames[kickers[0] - 2] + " kicker"
handrankValue = (2,) + (higherpair, lowerpair) + tuple(kickers)
# check if a hand is one pair
if ('2' in joined_histogram) & (len(kickers) == 3):
lowerpair = (joined_histogram[1:].find('2') + 2)
handName = "One pair " + cardnames[lowerpair - 2] + " kickers " + cardnames[kickers[0] - 2] \
+ " " + cardnames[kickers[1] - 2] + " " + cardnames[kickers[2] - 2]
handrankValue = (1,) + (lowerpair,) + tuple(kickers)
# evaluate high card hand
if (len(ranks_numerical) == len(set(ranks_numerical))) & (isStraight == False) & (isFlush == False):
handName = "High card " + cardnames[kickers[0] - 2] + " " + cardnames[kickers[1] - 2] \
+ " " + cardnames[kickers[2] - 2] + " " + cardnames[kickers[3] - 2] + " " + cardnames[kickers[4] - 2]
handrankValue = (0,) + tuple(kickers)
return {tuple(sorted(hand)) : handrankValue}
ranked_hands_dict = {}
t0 = time.time()
for board in boards:
ranked_hands_dict.update(hand_rank_dict(board))
t1 = time.time()
total = t1-t0
# print(total)
# Function that given board and 2 cards gives back tuple of the best possible hand by searching through ranked_hands_dict keys
def find_the_best_hand(board, card1, card2):
seven_card_hand = board + (card1,) + (card2,)
evaluated_all_possible_hands = []
if (card1 in board) or (card2 in board):
return "Illegal board"
else:
all_possible_hands = list(itertools.combinations(seven_card_hand, 5))
for hand in all_possible_hands:
evaluated_all_possible_hands.append(ranked_hands_dict[tuple(sorted(hand))])
return max(evaluated_all_possible_hands)
# Function that returns a list of possible boards given the dead cards
def create_allowed_boards(cards):
list_of_allowed_boards = []
for board in boards:
if not any(karta in cards for karta in board):
list_of_allowed_boards.append(board)
return list_of_allowed_boards
hand1 = ['2h','7d']
hand2 = ['Ad','Ah']
# HAND vs. HAND EVALUATOR
t0 = time.time()
one = 0
two = 0
tie = 0
deadcards= hand1 + hand2
list_of_possible_boards = create_allowed_boards(deadcards)
for board in list_of_possible_boards:
hand1rank = find_the_best_hand(board, hand1[0], hand1[1])
hand2rank = find_the_best_hand(board, hand2[0], hand2[1])
if hand1rank > hand2rank:
one = one + 1
if hand1rank < hand2rank:
two = two + 1
if hand1rank == hand2rank:
tie = tie + 1
onepercent = (one/len(list_of_possible_boards))*100
twopercent = (two/len(list_of_possible_boards))*100
tiepercent = (tie/len(list_of_possible_boards))*100
print(onepercent, twopercent, tiepercent)
t1 = time.time()
total = t1-t0
print(total)
Maybe one print(total) to many but was originally in Jupyter Notebook