Recently one of the problems that I had to solve on one of my interviews was the sortable poker hands problem , and because I received very good feedback and my solution was described as “uniquely structured to solve the problem in a human way” I thought to share this solution.

## TL;DR

You can find the source code for this solution in this repository github.com/gabihodoroaga/sortable-poker-hands.

## Prerequisites

• python3
• enough time to have a little fun

## The problem

The problem was taken from this Kata and I will describe it in detail below.

Create a poker hand that has a constructor that accepts a string containing 5 cards:

``````hand = PokerHand("KS 2H 5C JD TD")
``````

The characteristics of the string of cards are:

• A space is used as card separator
• Each card consists of two characters
• The first character is the value of the card, valid characters are:
`2, 3, 4, 5, 6, 7, 8, 9, T(en), J(ack), Q(ueen), K(ing), A(ce)`
• The second character represents the suit, valid characters are:
`S(pades), H(earts), D(iamonds), C(lubs)`

The poker hands must be sortable by rank, the highest rank first.

Apply the Texas Hold’em rules for ranking the cards. There is no ranking for the suits. An ace can either rank high or rank low in a straight or straight flush. Example of a straight with a low ace: `"5C 4D 3C 2S AS"`

## The solution

When we, humans, play poker we draw cards one by one and after each draw we evaluate the rank of the hand to see if we have a pair, or a full house, or something else.

This is best explained by an example:

• Draw the first card: 9 of spades (9S), our hand rank is now High card.
• Draw the second card: 9 of hearts (9H), now we have a Pair now and we hope for a three of a kind.
• Draw the third card: 9 of clubs (9C), now we have a Three of a kind, very well, but we hope for the 4th nine.
• Draw the fourth card: 9 of diamond (9D), best card that you can draw, now we have Four of a kind.
• Draw the fifth card: 6 of hearts (6H), it really does not matter that much for this hand.

With this hand you have very good chances to become rich.

The algorithm that we will develop next will follow the same logic.

First let’s create some helper classes

``````class PokerCard:
values = {'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9,'T':10,'J':12,'Q':13,'K':14,'A':15}
suites = ['S','H','D','C']

def __init__(self, card):
# no card validation for better performance
self.value = card
self.suite = card

def __lt__(self, card):
return self.values[self.value] < self.values[card.value]
``````

The next class will be the base for all other classes

``````class Hand:
def __repr__(self):
return self.__class__.__name__
``````

We will define a class for each of the possible hand ranks, and each class is responsible to decide the rank when a new card is drawn by implementing the Draw method, and also we need to implement the __lt__ function in order to be able to decide which hand has a higher value when the rank is the same.

First possible rank is the High card

``````class HighCard(Hand):
def __init__(self):
self.rank = 1
self.cards = []

def __lt__(self, other):
myCards = self.allCards()
cardCards = other.allCards()

for i in range(0, 5):
if myCards[i] < cardCards[i]:
return True
if myCards[i] > cardCards[i]:
return False
return False

@lru_cache(None)
def allCards(self):
return sorted(self.cards, reverse=True)

def Draw(self, card):
pc = PokerCard(card)
# check for pair
if next((c for c in self.cards if c.value == pc.value), None) != None:
self.cards.append(pc)
return Pair(self.cards, pc)

self.cards.append(pc)

if len(self.cards) == 5:
## check for flush
isFlush = next((c for c in self.cards[:-1] if c.suite != pc.suite), None) == None
## check for straight
cards = sorted(self.cards)
valueList = list(PokerCard.values)
firstCardIdx = valueList.index(cards.value)
isStraight = True
for i in range(0, 5):
if cards[i].value != valueList[firstCardIdx + i]:
isStraight = False
break

# check for low straight
if (cards.value == '2' and
cards.value == '3' and
cards.value == '4' and
cards.value == '5' and
cards.value == 'A'):
isStraight = True

if isFlush and isStraight:
return StraightFlush(self.cards)
if isFlush:
return Flush(self.cards)
if isStraight:
return Straight(self.cards)

return self
``````

If we have a High card and we draw another card we have the following options:

• draw another card with the same value which means we have a pair
• have a straight, a flush, or a straight flush, and this is possible only if we have 5 cards

Also, when we have two high cards, we need to know which one has a lower value according to the rules. Check the __lt__ implementation of the class

Next we have the Pair rank

``````class Pair(Hand):

def __init__(self, cards, pair):
self.rank = 2
self.cards = cards
self.pair = pair

def __lt__(self, card):
if self.pair < card.pair:
return True
if self.pair > card.pair:
return False

myOtherCards =  self.otherCards()
cardOtherCards = card.otherCards()
for i in range(3):
if myOtherCards[i] < cardOtherCards[i]:
return True
if myOtherCards[i] > cardOtherCards[i]:
return False
return False

@lru_cache(None)
def otherCards(self):
return sorted([c for c in self.cards if c.value != self.pair.value], reverse=True)

def Draw(self, card):
pc = PokerCard(card)

# check for 3 of a kind
if self.pair.value == pc.value:
self.cards.append(pc)
return ThreeOfKind(self.cards, pc)

# check for second pair
pair2 = next((c for c in self.cards if c.value != self.pair.value and c.value == pc.value), None)
if (pair2 != None):
self.cards.append(pc)
return TwoPairs(self.cards, self.pair, pc)

self.cards.append(pc)
return self
``````

When we have a pair, the next possible options when we draw another card are:

• three of a kind
• two pairs

Next is two pairs rank

``````class TwoPairs(Hand):

def __init__(self, cards, pair1, pair2):
self.rank = 3
self.cards = cards
self.pair1 = pair1
self.pair2 = pair2

def __lt__(self, card):
myPairs = self.pairs()
cardPairs = card.pairs()
for i in range(2):
if myPairs[i] < cardPairs[i]:
return True
if myPairs[i] > cardPairs[i]:
return False

return self.otherCard() < card.otherCard()

@lru_cache(None)
def otherCard(self):
return next((c for c in self.cards if c.value != self.pair1.value and c.value != self.pair2.value), None)

def pairs(self):
return sorted([self.pair1, self.pair2], reverse=True)

def Draw(self, card):
pc = PokerCard(card)
# check for full house
pair3 = next((c for c in self.cards if c.value == pc.value), None)
if pair3 != None:
self.cards.append(pc)
return FullHouse(self.cards,
pc, self.pair2 if pc.value == self.pair1.value else self.pair1)

self.cards.append(pc)
return self
``````

From two pairs you can only go to a full house or to remain as two pairs, which is not that bad after all.

Next is three of a kind rank

``````class ThreeOfKind(Hand):

def __init__(self, cards, pair):
self.rank = 4
self.cards = cards
self.pair = pair

def __lt__(self, card):
if self.pair < card.pair:
return True
if self.pair > card.pair:
return False

myOtherCards =  self.otherCards()
cardOtherCards = card.otherCards()
for i in range(3):
if myOtherCards[i] < cardOtherCards[i]:
return True
if myOtherCards[i] > cardOtherCards[i]:
return False
return False

@lru_cache(None)
def otherCards(self):
return sorted([c for c in self.cards if c.value != self.pair.value], reverse=True)

def Draw(self, card):
pc = PokerCard(card)

# Check for FourOfKind
if self.pair.value == pc.value:
self.cards.append(card)
return FourOfKind(self.cards, pc)

# Check for FullHouse
if len(self.cards) == 4:
pair = next((c for c in self.cards
if c.value != self.pair.value and c.value == pc.value), None)
if pair != None:
self.cards.append(pc)
return FullHouse(self.cards, self.pair, pc)

self.cards.append(pc)
return self
``````

From three of a kind the next options are:

• full house
• four of a kind

Next is four of a kind

``````class FourOfKind(Hand):

def __init__(self, cards, pair):
self.rank = 8
self.cards = cards
self.pair = pair

def __lt__(self, card):
if self.pair < card.pair:
return True
if self.pair > card.pair:
return False
return self.otherCard() < card.otherCard()

@lru_cache(None)
def otherCard(self):
return next((c for c in self.cards if c.value != self.pair.value), None)

def Draw(self, card):
pc = PokerCard(card)
self.cards.append(card)
return self
``````

From four of a kind we can’t go anywhere, we are done but we still need to implement the function to compare 2 four of a kind.

Next is full house rank

``````class FullHouse(Hand):

def __init__(self, cards, three, two):
self.rank = 7
self.cards = cards
self.three = three
self.two = two

def __lt__(self, card):
if self.three < card.three:
return True
if self.three > card.three:
return False
if self.two < card.two:
return True

return False

def Draw(self, card):
raise ValueError("This should not be possible")
``````

I know that by now this is becoming boring, but these are the rules of the game and we have to implement all the possible ranks.

Next is straight rank

``````class Straight(Hand):

def __init__(self, cards):
self.rank = 5
self.cards = cards

def __lt__(self, card):
return self.allCards() < card.allCards()

@lru_cache(None)
def allCards(self):
sortedCards = sorted(self.cards, reverse=True)
if sortedCards.value == "A" and sortedCards.value == '2':
sortedCards.append(sortedCards.pop(0))
return sortedCards

return sortedCards

def Draw(self, card):
raise ValueError("This should not be possible")
``````

and flush rank

``````class Flush(Hand):

def __init__(self, cards):
self.rank = 6
self.cards = cards

def __lt__(self, card):
myCards = self.allCards()
cardCards = card.allCards()

for i in range(0, 5):
if myCards[i] < cardCards[i]:
return True
if myCards[i] > cardCards[i]:
return False
return False

@lru_cache(None)
def allCards(self):
return sorted(self.cards,reverse=True)

def Draw(self, card):
raise ValueError("This should not be possible")
``````

and the straight flush rank

``````class StraightFlush(Hand):

def __init__(self, cards):
self.rank = 9
self.cards = cards

def __lt__(self, card):
return self.allCards() < card.allCards()

@lru_cache(None)
def allCards(self):
sortedCards = sorted(self.cards, reverse=True)
if sortedCards.value == "A" and sortedCards.value == '2':
sortedCards.append(sortedCards.pop(0))
return sortedCards

return sortedCards

def Draw(self, card):
raise ValueError("This should not be possible")
``````

In order to implement the __lt__ operator I used helper functions like allCards() or otherCards() and if you add some caching capabilities to this functions you will get huge improvement in performance. You can read all about caching in python here.

Now, let’s put all together and create our final class

``````
class PokerHand(object):

def __repr__(self):  return self.hand

def __init__(self, hand):
self.value = None
self.hand = hand
self.calcValue()
def __lt__(self, hand):
if self.value.rank == hand.value.rank:
return self.value > hand.value
return self.value.rank > hand.value.rank

def calcValue(self):
r = HighCard()
for card in self.hand.split():
r = r.Draw(card.upper())
self.value = r
``````

The __lt__ function is implemented here in the reverse order to comply with the requirements of the problem.

Now we can play. Let’s test if a pair is lower than a four of a kind.

``````print(PokerHand("AS AH 9C 8D 2S") > PokerHand("9S 9H 9C 9D JS")) # reverse
``````

You can test the complete solution using this Kata. There are 25.000 random tests. Check out the timing.

## Conclusion

Sometimes the best solution to a problem can be found by mimic how we do it in the real world.

You can find all the full source code, and tests of course, here at github.com/gabihodoroaga/sortable-poker-hands.

1733 Words

2021-02-17 20:22 +0000