Level 2 Python: Texas Hold Em

Level 2 Python projects are projects meant for those who have a grasp on the basics of Python already. If you are familiar with most of the Super Simple Python projects and can do each of them in under 30 minutes, you’re ready to go. The goal of the Level 2 Python projects is to get you familiar with using well known Python libraries, building more logically complex applications, and learning API development.

In this tutorial, we’ll be building a simple version of Texas Hold Em. This is an exercise for building more logically complex applications. Not going to lie, I thought this would be a beginner level Python project until I started working on it and I was like oh, this is harder than I thought it would be. In fact, Texas Hold Em is such a complex game, that we will leave the full implementation as a Level 3 Python tutorial. Before we get into the code make sure you have a solid understanding of the basics of Python classes. I’ve attached the source code for this project at the end.

Creating the Card Deck

The first thing we need to do is create a deck of cards. There are 13 values and 4 suits in a standard deck of 52 cards. The values range from 2 to 14 with 11, 12, 13, and 14 being Jack, Queen, King, and Ace respectively. The suits are clubs, diamonds, hearts, and spades. For purposes of displaying the cards in a standard manner, let’s also create a dictionary that translates the numbers to the face cards and vice versa. Each card can be represented as a value and a suit, you can do this with a class or a tuple in Python. I have chosen to do it with a class for added clarity.

import random
# 11 = J, 12 = Q, 13 = K, 14 = A
card_values = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
suits = ["clubs", "diamonds", "hearts", "spades"]
 
face_cards = {
    "J": 11,
    "Q": 12,
    "K": 13,
    "A": 14,
    11: "J",
    12: "Q",
    13: "K",
    14: "A"
}
class Card:
    def __init__(self, value, suit):
        self.value = value
        self.suit = suit

Once we’ve set up our `Card` class and the lists of possible values and suits, we can generate our deck of cards. Let’s create a `generate_cards` function which will initialize an empty list of cards and populate it. We’ll do this by looping through all the values and suits and initializing a card object for each possible combination. If the value is in the `face_cards` dictionary we created earlier, we’ll save it with the appropriate face card representation (J, Q, K, A). Our `generate_cards` function will then return that list of cards. To use it, we’ll simply set a variable, `cards` equal to the list of cards returned by the function.

def generate_cards():
    cards = []
    for value in card_values:
        for suit in suits:
            if value in face_cards:
                _card = Card(face_cards[value], suit)
            else:
                _card = Card(value, suit)
            cards.append(_card)
    return cards
 
cards = generate_cards()

Dealing the Cards

Now that we have a deck of cards stored in our `cards` variable it’s time to deal the cards. We’ll start by making a function to deal a card. Our `deal_card` function will intake a list, `cards`. Unlike real life, we won’t be dealing the top card, we’ll be picking a random card from the deck to deal. We’ll be using the `randint` function from the `random` library we imported earlier. We’ll pick a random index and pop the card. Then we’ll return the card and the edited card deck.

Now You See Me – Pick a Card
def deal_card(cards):
    i = random.randint(0, len(cards)-1)
    card = cards[i]
    cards.pop(i)
    return card, cards

Now that we’ve created a way to deal a card, let’s create a `deal` function to deal hands to the players’. This function will take two parameters, the cards and the number of opponents. For this example, we’ll set the default of the `cards` parameter to the `cards` variable we created earlier and set the default number of opponents to 2.

We’ll start off this function by dealing the opponents’ hands. In real Texas Hold Em, they deal one card at a time, but this is just a simple example. Note that I use the `_` placeholder variable in the for loop, that’s because we never need to access the index. After we deal the opponents’ hands, we deal our hand. Finally, we’ll return two variables, our hand, and the list of our opponents’ hands.

def deal(cards = cards, num_opp = 2):
    opp_hands = []
    for _ in range(num_opp):
        card1, cards = deal_card(cards)
        card2, cards = deal_card(cards)
        opp_hands.append([card1, card2])
    card1, cards = deal_card(cards)
    card2, cards = deal_card(cards)
    your_hand = [card1, card2]
    return your_hand, opp_hands
 
your_hand, opp_hands = deal()
print([(card.value, card.suit) for card in your_hand])

Let’s print here out here to see what your hand should look like at this point.

Python Texas Hold Em - Dealing Your Hand
Python Texas Hold Em – Dealing Your Hand

Flop, Turn, and River

Other than the player hands that are dealt, Texas Hold Em also has a “table”. The table is dealt as the Flop, Turn, and River. The Flop is the first three cards, the Turn is the fourth, and the River is the fifth. There is usually betting involved between each deal, but we’ll leave that to the advanced course (Level 3). For this we’ll make two functions, `flop`, and `table_deal`. We’ll use `table_deal` for both the turn and river because they’re both the same deal. Could we use the `deal_card` function and simply not deal with the returned deck of `cards`? Yes, but in this tutorial, we won’t. This is just an example of another possible design choice.

Let’s start with the `flop` function. The `flop` function will take `cards` as a parameter and use `deal_card` to deal three cards and then return a list of those cards. The `table_deal` function will take `cards` as a parameter and call `deal_card` to deal a card and return only the card dealt. 

We’ll initialize the table using the `flop()` function and print out the cards on the table. Then we’ll call the `table_deal` function two times to deal the Turn and River.

def flop(cards=cards):
    card1, cards = deal_card(cards)
    card2, cards = deal_card(cards)
    card3, cards = deal_card(cards)
    return [card1, card2, card3]
 
def table_deal(cards=cards):
    card, cards = deal_card(cards)
    return card
 
table = flop()
print(f"Cards on the table: {[(card.value, card.suit) for card in table]}")
table.append(table_deal())
print(f"Cards after turn: {[(card.value, card.suit) for card in table]}")
table.append(table_deal())
print(f"Cards after river: {[(card.value, card.suit) for card in table]}")

At this point if we print run our program, we should see something like the image below.

Python Texas HoldEm Code – Hand + Table

Evaluating the Hands

In real Texas Hold Em, you’ll need to compare your hand to the cards on the table and create your highest hand of 5. However, that’s out of the scope of a Level 2 Python project. We’ll cover that in the Level 3 version. Let’s create a function called `evaluate`. The `evaluate` function will take a players’ hand and the cards on the table as its parameters, combine the hands, and evaluate what the highest hand is in that combined set of cards.

To do the evaluation, we’ll need to know the counts of each value and the counts of each suit. We’ll initialize those to empty dictionaries. We’ll also keep a set of values, initialized to an empty set. Why have values separate from the counts of the values? Because `counts` will be used for calculating pairs/triples and `vals` will be used to see if we have a straight. Can we just use a `counts` and later check its indices? Yes, but once again, design choice. It’s just easier to loop through the list in order with `vals`.

We will populate the sets and dictionaries we created earlier by iterating through the cards in the `total_hand`. If the card’s value is in `face_cards` we’ll convert back from the letter representation (J, Q, K, A) into its numerical value. One of the nice things about Python dictionaries is that they can contain indices of any type or combination of types.

def evaluate(hand, table):
    total_hand = hand + table
    # count values and suit
    counts = {}
    suits = {}
    vals = set()
    # loop through all the cards
    for card in total_hand:
        if card.value in face_cards:
            card_value = face_cards[card.value]
        else:
            card_value = card.value
        vals.add(card_value)
        if card_value in counts:
            counts[card_value] += 1
        else:
            counts[card_value] = 1
        if card.suit in suits:
            suits[card.suit] += 1
        else:
            suits[card.suit] = 1

Sort Counts of Values, Suits, and actual Values

Continuing our function, we’ll sort our `counts`, our `suits`, and `vals`. We do this in reverse order and by the number of occurrences of the value or the suit. Once we’ve gotten everything sorted out, we’ll check for straights and flushes. This is because the order of hands in Poker is high, pair, 2 pair, 3 kind, straight, flush, full house, quads, and straight flush.

    # sort counts and suits
    sorted_counts = sorted(counts.items(), key=lambda item:(item[1], item[0]), reverse=True)
    sorted_suits = sorted(suits.items(), key=lambda item:(item[1], item[0]), reverse=True)
 
    # check if vals contains a straight
    run = [sorted(list(vals))[0]]
    lastval = sorted(list(vals))[0]
    is_straight = False
    for val in sorted(list(vals)):
        if val - lastval == 1:
            run.append(val)
        else:
            run = [val]
        lastval = val
        if len(run) == 5:
            is_straight = True
            break
    # check if sorted_suits contains a flush
    is_flush = False
    if sorted_suits[0][1] == 5:
        is_flush = True

Find Highest Hand

Alright let’s get into the annoying logic of this program. Earlier I listed the winning hands in order → high, pair, 2 pair, 3 kind, straight, flush, full house, quads, and straight flush. So, we’re going to check for each of these in backwards order to optimize run time. If we see a straight flush, we’ll return a straight flush automatically. You can set the values of the straight flush as well, but it’s so rare to see (odds are 649,739 to 1) that if you see a straight flush, your chance of winning is almost 100%. After the straight flush, we’ll check for quads based on the highest count of a card value in `counts`. Then we go down the list, a full house is a triple and a double. We’ve already checked for flushes and straights for the straight flush, here we just check them separately for a flush and a straight respectively. After straight, it’s a triple, a two pair, a pair, and then the high card. All of these are easily checked using the `counts` dictionary.

    # check for straight flush
    if is_straight:
        if is_flush:
            return "Straight Flush!"
    if sorted_counts[0][1] == 4:
        return f"Quad {face_cards.get(sorted_counts[0][0]) if sorted_counts[0][0] in face_cards else sorted_counts[0][0]}s!"
    if sorted_counts[0][1] == 3:
        if sorted_counts[1][1] == 2:
            return f"Full house {face_cards.get(sorted_counts[0][0]) if sorted_counts[0][0] in face_cards else sorted_counts[0][0]}s over {face_cards.get(sorted_counts[1][0]) if sorted_counts[1][0] in face_cards else sorted_counts[1][0]}s!"
    if is_flush:
        return f"Flush in {face_cards.get(sorted_counts[0][0]) if sorted_counts[0][0] in face_cards else sorted_counts[0][0]}!"
    if is_straight:
        return f"Straight! {run}"
    # check for groups
       
    if sorted_counts[0][1] == 3:
        return f"Triple {face_cards.get(sorted_counts[0][0]) if sorted_counts[0][0] in face_cards else sorted_counts[0][0]}s!"
    if sorted_counts[0][1] == 2:
        if sorted_counts[1][1] == 2:
            return f"Two pair {face_cards.get(sorted_counts[0][0]) if sorted_counts[0][0] in face_cards else sorted_counts[0][0]} and {face_cards.get(sorted_counts[1][0]) if sorted_counts[1][0] in face_cards else sorted_counts[1][0]}!"
        else:
            return f"Pair of {face_cards.get(sorted_counts[0][0]) if sorted_counts[0][0] in face_cards else sorted_counts[0][0]}!"
    if sorted_counts[0][1] == 1:
        return f"High Card {face_cards.get(sorted_counts[0][0]) if sorted_counts[0][0] in face_cards else sorted_counts[0][0]}!"

Finalized Function

I just want to add what the finalized function for the Level 2 Python version of evaluating a hand looks like.

def evaluate(hand, table):
    total_hand = hand + table
    # count values and suit
    counts = {}
    suits = {}
    vals = set()
    # loop through all the cards
    for card in total_hand:
        if card.value in face_cards:
            card_value = face_cards[card.value]
        else:
            card_value = card.value
        vals.add(card_value)
        if card_value in counts:
            counts[card_value] += 1
        else:
            counts[card_value] = 1
        if card.suit in suits:
            suits[card.suit] += 1
        else:
            suits[card.suit] = 1
    # sort counts and suits
    sorted_counts = sorted(counts.items(), key=lambda item:(item[1], item[0]), reverse=True)
    sorted_suits = sorted(suits.items(), key=lambda item:(item[1], item[0]), reverse=True)
 
    # check if vals contains a straight
    run = [sorted(list(vals))[0]]
    lastval = sorted(list(vals))[0]
    is_straight = False
    for val in sorted(list(vals)):
        if val - lastval == 1:
            run.append(val)
        else:
            run = [val]
        lastval = val
        if len(run) == 5:
            is_straight = True
            break
   
    # check if sorted_suits contains a flush
    is_flush = False
    if sorted_suits[0][1] == 5:
        is_flush = True
    # check for straight flush
    if is_straight:
        if is_flush:
            return "Straight Flush!"
    if sorted_counts[0][1] == 4:
        return f"Quad {face_cards.get(sorted_counts[0][0]) if sorted_counts[0][0] in face_cards else sorted_counts[0][0]}s!"
    if sorted_counts[0][1] == 3:
        if sorted_counts[1][1] == 2:
            return f"Full house {face_cards.get(sorted_counts[0][0]) if sorted_counts[0][0] in face_cards else sorted_counts[0][0]}s over {face_cards.get(sorted_counts[1][0]) if sorted_counts[1][0] in face_cards else sorted_counts[1][0]}s!"
    if is_flush:
        return f"Flush in {face_cards.get(sorted_counts[0][0]) if sorted_counts[0][0] in face_cards else sorted_counts[0][0]}!"
    if is_straight:
        return f"Straight! {run}"
    # check for groups
       
    if sorted_counts[0][1] == 3:
        return f"Triple {face_cards.get(sorted_counts[0][0]) if sorted_counts[0][0] in face_cards else sorted_counts[0][0]}s!"
    if sorted_counts[0][1] == 2:
        if sorted_counts[1][1] == 2:
            return f"Two pair {face_cards.get(sorted_counts[0][0]) if sorted_counts[0][0] in face_cards else sorted_counts[0][0]} and {face_cards.get(sorted_counts[1][0]) if sorted_counts[1][0] in face_cards else sorted_counts[1][0]}!"
        else:
            return f"Pair of {face_cards.get(sorted_counts[0][0]) if sorted_counts[0][0] in face_cards else sorted_counts[0][0]}!"
    if sorted_counts[0][1] == 1:
        return f"High Card {face_cards.get(sorted_counts[0][0]) if sorted_counts[0][0] in face_cards else sorted_counts[0][0]}!"

“Playing” the Game

Now that we’ve finished up all the coding, let’s take a look at running the game. We’ll just have the game automatically play for us by dealing the cards and then calculating the winning hand. This function will take three parameters – your hand, the opponents’ hands, and the cards on the table. It will evaluate all of the hands and print out the highest possible poker hand from each one.

"""
takes your hand, the opponents hands, and the cards on the table
determines the values
returns a winner with all values shown
"""
def determine(hand, opp_hands, table):
    print(f"Your highest poker hand: {evaluate(hand, table)}")
    for opp in opp_hands:
        print(f"Opponent hand: {opp[0].value} {opp[0].suit}, {opp[1].value} {opp[1].suit}")
        print(f"Your opponents highest poker hand: {evaluate(opp, table)}")
 
determine(your_hand, opp_hands, table)

When we run this, we should get an output like the image below. In the image below the first opponent with the pair of 3’s has won. In the Level 2 Python version, we’re not going to implement a way to calculate the winner’s hand.

python poker game output
Python Poker Game Output

An Exercise for the Reader

As I stated above, there are some things that I have left for the Level 3 Python version of Texas Hold Em. The reason I’ve left these out of the Level 2 Python version is because they add quite some complexity to the program. For example, the real way to calculate a hand requires you to sort through 10 different possible hands (5 choose 3 is 10) and pick the highest one. There’s quite a few things I’ve left out on purpose that you can try to figure out or wait for me to drop the Level 3 article to find out 🙂

Thanks for reading! Here’s the Source Code.

Further Reading

Learn More

To learn more, feel free to reach out to me @yujian_tang on Twitter, connect with me on LinkedIn, and join our Discord. Remember to follow the blog to stay updated with cool Python projects and ways to level up your Software and Python skills! If you liked this article, please Tweet it, share it on LinkedIn, or tell your friends!

I run this site to help you and others like you find cool projects and practice software skills. If this is helpful for you and you enjoy your ad free site, please help fund this site by donating below! If you can’t donate right now, please think of us next time.

Yujian Tang
Yujian Tang

I started my professional software career interning for IBM in high school after winning ACSL two years in a row. I got into AI/ML in college where I published a first author paper to IEEE Big Data. After college I worked on the AutoML infrastructure at Amazon before leaving to work in startups. I believe I create the highest quality software content so that’s what I’m doing now. Drop a comment to let me know!

One-Time
Monthly
Yearly

Make a one-time donation

Make a monthly donation

Make a yearly donation

Choose an amount

$5.00
$15.00
$100.00
$5.00
$15.00
$100.00
$5.00
$15.00
$100.00

Or enter a custom amount

$

Your contribution is appreciated.

Your contribution is appreciated.

Your contribution is appreciated.

DonateDonate monthlyDonate yearly

Leave a Reply

%d bloggers like this: