megacolorboy

Back

Published on March 30th, 2019

Algorithms

Poker Hand Analyser in Python

An algorithm that parses a five-card poker hand and determines it's rank.

3 minutes read

I've never played Poker and don't think I ever will because I'm not a fan of gambling and placing bets. However, I ran into an interesting problem on Project Euler that led me to write a poker hand analyser to determine the rank of each hand.

Before writing this article, I didn't know anything about cards or Poker, I had to do some research on Wikipedia about it. So, forgive me if there's any information that's not accurate in the article.

Poker Hands

From what I had understood, a hand is a set of five cards and each card has a rank, which is in the order shown below:

    
    Cards are valued in the order of lowest to highest (Left to Right):
    2, 3, 4, 5, 6, 7, 8, 9, 10, Jack, Queen, King, Ace
    

Based on the card pattern formed in each hand, the ranking category is determined and it's ranked within it's category based on the ranks of it's cards.

Hand Ranking Categories

In Poker, there are about 10 ranking categories from lowest to highest:

Before diving into the code snippets, I wrote a library named poker_lib.py which contains all the methods used in the code snippets.

To make things simple, I created a class named Card that has two attributes, face and suit, with namedtuple() as it's datatype.

High Card

This hand contains no pairs and it doesn't fall into any other category.

    
    def high_card(hand):
        # collect all faces from each card
        allfaces = [f for f,s in hand]

        #sort the faces and show the highest card
        return "high_card", sorted(allfaces, key=lambda f: allfaces.index(f), reverse=True)[0]
    

One Pair

This hand contains two cards of one rank and three cards of three other ranks.

    
    def one_pair(hand):
        allfaces = [f for f,s in hand]
        allftypes = set(allfaces)

        # collect pairs
        pairs = [f for f in allftypes if allfaces.count(f) == 2]

        # if there's more than one pair
        if len(pairs) != 1:
            return False

        allftypes.remove(pairs[0])
        return 'one-pair', pairs + sorted(allftypes, key=lambda f: face.index(f), reverse=True)
    

Two Pairs

This hand contains two cards of one rank, two cards of a second rank and one card of a third rank.

    
    def two_pair(hand):
        allfaces = [f for f,s in hand]
        allftypes = set(allfaces)

        # collect pairs
        pairs = [f for f in allftypes if allfaces.count(f) == 2]

        # if there are more than two pairs
        if len(pairs) != 2:
            return False

        p1, p2 = pairs
        # get the difference using sets
        other_cards = [(allftypes - set(pairs)).pop()]
        return 'two-pair', pairs + other_cards if(face.index(p1) > face.index(p2)) else pairs[::-1] + other_cards
    

Three of a Kind

This hand, also known as trips or a set, contains three cards of one rank and two cards of two other ranks.

    
    def three_of_a_kind(hand):
        allfaces = [f for f,s in hand]

        uniqueRanks = set(allfaces)

        if len(uniqueRanks) != 3:
            return False

        for f in uniqueRanks:
            if allfaces.count(f) == 3:
                uniqueRanks.remove(f)
                return "three-of-a-kind", f

        return False;
    

Straight

This hand contains five cards arranged in a sequential order but not all of them have same suits.

    
    def straight(hand):
        ordered = sorted(hand, key=lambda card: (faces.index(card.face), card.suit))
        if ''.join(card.face for card in ordered) in ''.join(face):
            return 'straight', ordered[-1].face
        return False;
    

Flush

This hand contains five cards of the same suit and not necessarily arranged in sequential order.

    
    def flush(hand):
        allfaces = [f for f,s in hand]

        first_card = hand[0]
        other_cards = hand[1:]

        if all(first_card.suit == card.suit for card in other_cards):
            return 'flush', sorted(allfaces, key=lambda f: face.index(f), reverse=True)

        return False
    

Full House

This hand, also known as full boat or a boat, contains three cards of one rank and two cards of another rank.

    
    def full_house(hand):
        allfaces = [f for f,s in hand]

        rankFrequency = pe_lib.character_frequency(allfaces)

        # if there are 2 types of ranks and there's a card with 1 pair and 3 of a kind
        if len(rankFrequency) == 2 and (rankFrequency.values()[0] == 2 and rankFrequency.values()[1] == 3):
            return 'full-house'

        return False
    

Four of a Kind

This hand, also known as quads, contains four cards of one rank and one card of another rank.

    
    def four_of_a_kind(hand):
        allfaces = [f for f,s in hand]

        # create a unique set of ranks
        uniqueRanks = set(allfaces)

        # if there are more than 2 ranks, it's not four of a kind
        if len(uniqueRanks) != 2:
            return False

        for f in uniqueRanks:
            # if there are 4 faces, it is four of a kind
            if allfaces.count(f) == 4:
                uniqueRanks.remove(f)
                return "four-of-a-kind", f

        return False
    

Straight Flush

This hand contains five cards arranged in a sequential order with all cards having the same suit.

    
    def straight_flush(hand):
        # sort the cards based on the face rank of each card
        ordered = sorted(hand, key=lambda card: (faces.index(card.face), card.suit))

        first_card = ordered[0]
        other_cards = ordered[1:]

        # check if all are of the same suit
        if all(first_card.suit == card.suit for card in other_cards):
            # check if they are in sequential order
            # compare the ordered faces substring with the face list (which is converted to string)
            if ''.join(card.face for card in ordered) in ''.join(face):
                return 'straight-flush', ordered[-1].face
        return False
    

Royal Flush

This hand contains the royal ranks in sequential order in the same suit.

    
    def royal_flush(hand):
        royalface = "TJQKA"
        # sort the cards based on the face rank of each card
        ordered = sorted(hand, key=lambda card: (faces.index(card.face), card.suit))

        first_card = ordered[0]
        other_cards = ordered[1:]

        # check if all are of the same suit
        if all(first_card.suit == card.suit for card in other_cards):
            # check if they are in sequential order
            # compare the ordered faces substring with the face list (which is converted to string)
            if ''.join(card.face for card in ordered) in royalface:
                return 'royal-flush', ordered[-1].face
        return False
    

Conclusion

It was a fun project to work on and I learnt new styles of array and string manipulation techniques using Python.

Inspired by this, I'm planning to create an interactive version of this project using Javascript and talk about it on another article.

The code for this program can be found in my GitHub repository.

Hope you liked reading this article!

Stay tuned for more!