logo

Ziffur

Code-Along: Use Basic Python to Simulate a Card Game

Code-Along: Use Basic Python to Simulate a Card Game

3 August 2021

In this article we will write a Python script to simulate a simple card game and collect some statistics about it. By doing this, we hope to provide an overview over some basic programming concepts (variables, data types, loops, and functions) and how they come together in Python to make a useful script.

The Card Game

The game we will be simulating is very simple. In Iceland it is known as "Langavitleysa", which translates to "long nonsense" (if you recognize the rules and know the game by another name, please shoot us an email using the address in the footer!).

The game is played by two players using a deck of 52 playing cards and proceeds in turns. Each player starts with half of the deck, face down. Each turn, both players draw the top card from their stack and present it. The player with the higher card takes both cards and places them at the bottom of their stack. This procedure is repeated until one player runs out of cards.

Simple, right? There's only one special case: If both cards are of equal value (same number, suits don't matter), each player draws three more cards from the top of their stack and places them face-down on the table. Then they present the next card from the stack. Whoever draws the higher card takes all ten cards on the table and adds them to the bottom of their stack. If the cards are equal again, repeat this procedure until a winner is decided.

The Code

Since we're going to be simulating a game, we will start by programming the game's logic. First, we'll create a list to hold our deck of cards.

A Deck of Cards

Since aces are high, we'll use the numbers 2–14 to represent the card values 2–10 followed by jack, queen, king, and ace. Since suits don't matter, we'll add each card four times to reach a total of 52.

cards = []
for i in range(13):
    cards.append(i + 2)
# cards = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

deck = []
for i in range(4):
    for card in cards:
        deck.append(card)

Let's analyze how this code works.

  • First we initialize a variable named cards as an empty list.
  • We then create a loop through the numbers 0–12.
  • In every iteration of the loop, we add 2 to the current number i and append the result to the end of the list before moving on to the next iteration. append is a built-in function available to any list and adds the given item to the end of the list.

We now have a list of the numbers 2–14. In order to make a deck, we need to add these numbers 4 times to simulate the 4 suits:

  • We initialize the deck variable as an empty list.
  • We create a loop with 4 iterations, one for each suit (♠ ♥ ♦ ♣)
  • In each iteration, we create an inner loop to loop through all 13 cards again and append them to the deck one by one.

We now have a full deck of 52 cards. The code we used to get there works fine but it's a little more complicated than it needs to be. In order to produce the same deck of 52 cards, we could also write:

cards = [i + 2 for i in range(13)]
deck = cards * 4

Here, we use a list comprehension which is a shorthand syntax and allows us to populate a list using a for loop with very little extra code. In order to repeat the list of cards 4 times, Python allows us to "multiply" the list using the * operator.

Shuffling and Dealing

Now that we have our deck, we'll need to shuffle the cards and then deal half to each player. Lucky for us, shuffling a list is easy using the random library which should come included with your Python installation. We can include modules such as the random library in our code by using the import keyword:

from random import shuffle

shuffle(deck)

This will randomly rearrange the order of the cards in the list we called deck. We can now proceed to deal the cards. Since the order is already random and each player gets half of the deck, we can simply split the list in the middle:

middle = len(deck) // 2
player1 = deck[:middle]
player2 = deck[middle:]

There's a few tricks hidden in there. Let's go through them one at a time:

  • The len function takes a list and returns its length, so len(deck) will return the number 52.
  • We divide the number 52 in half to get the index of the middle of the deck. An index is a term used to describe a position in a list and can be used to read the corresponding item. For example, deck[0] will return the first item in deck.
  • We divide using the // operator to make sure the result is an integer (a whole number). If the result would be a decimal, it is rounded down instead.
  • Similar to how deck[0] selects the first item in a list, we can use deck[a:b] to get a range of items. If we omit a we get all items from the beginning to the index b (non-inclusive). If we omit b, we get everything from the index a (inclusive) to the end of the list.

Now we have two players, each represented by their stack of 26 cards. They can now start playing!

Playing the Game

The game proceeds in turns, each turn having the following steps:

  • Each player draws the top card from their stack and presents it.
  • The player with the higher card takes both cards and adds them to their deck.
  • If the cards are equal, each player adds their next 3 cards to the pot and continues.

Let's start with the main logic and leave out the tie for now. Remember that player1 and player2 are lists containing each player's card stack.

card1 = player1.pop(0)
card2 = player2.pop(0)

pot = [card1, card2]

if card1 > card2:
    # Player 1 wins the pot
    for card in pot:
        player1.append(card)

if card1 < card2:
    # Player 2 wins the pot
    for card in pot:
        player2.append(card)

Similar to append, the pop function is built-in and available to any list. It removes an item from a list and returns it. By default, it removes and returns the last item. By giving it the index 0 as a parameter, we can make it remove and return the first item instead. This simulates drawing a card from the stack.

Once the cards are drawn, we put them in a pot and compare them to determine which player wins the pot. We use if statements to determine what happens in each case. After a winner is determined, we use the append function to add each card in the pot to the end of the list, representing the bottom of that player's stack.

Example: Player 1 draws an 11 (jack) while player 2 draws a 14 (ace). Player 2 wins the turn and puts both cards in the pot (11 and 14) on the bottom of their stack.

This code covers a basic round but doesn't do anything in case of a tie. In a tie, remember, each player adds the next 3 cards from the top of their stack to the pot before presenting a card again (which also joins the pot). To resolve this, we'll update our turn code like so:

card1 = player1.pop(0)
card2 = player2.pop(0)

pot = [card1, card2]

while card1 == card2:
    # Tie, add 3 more cards to the pot from each player
    for i in range(3):
        card = player1.pop(0)
        pot.append(card)
        card = player2.pop(0)
        pot.append(card)
    # Draw again
    card1 = player1.pop(0)
    pot.append(card1)
    card2 = player2.pop(0)
    pot.append(card2)

if card1 > card2:
    # Player 1 wins the pot
    for card in pot:
        player1.append(card)

if card1 < card2:
    # Player 2 wins the pot
    for card in pot:
        player2.append(card)

We keep all the code for a standard turn but add an extra while loop to cover the case of a tie. A while loop is an excellent tool here for two reasons: First, in the case of one tie (or no tie), it works exactly like another if statement. Second, if we encounter many ties in a row, the while loop will run as often as it needs to until the tie is resolved.

Inside the while loop, we do similar things to what we did before. Once a draw is encountered, we use a for loop to simulate each player drawing 3 cards and adding them to the pot. Then we draw a new card for each player reassign the card1 and card2 variables. This allows the condition of the while loop to be resolved (unless a second tie occurs, in which case the loop simply runs again).

Simulating Multiple Turns

Now that we have the players set up and we've defined the logic necessary to play a turn, all we need to do is repeat turns until the game ends. To do this, we first create functions for the code we wrote above. This will make the rest of our logic much easier to build and work with:

# longnonsense.py
from random import shuffle

def shuffleAndDeal():
    # Create a deck of cards and shuffle
    cards = [i + 2 for i in range(13)]
    deck = cards * 4
    shuffle(deck)

    # Deal half of the deck to each player
    middle = len(deck) // 2
    player1 = deck[:middle]
    player2 = deck[middle:]

    # Return the two players' stacks of cards
    return (player1, player2)

def playTurn(player1, player2):
    # Each player draws a card
    card1 = player1.pop(0)
    card2 = player2.pop(0)

    pot = [card1, card2]

    while card1 == card2:
        # Tie, add 3 more cards to the pot from each player
        for i in range(3):
            card = player1.pop(0)
            pot.append(card)
            card = player2.pop(0)
            pot.append(card)
        # Draw again
        card1 = player1.pop(0)
        pot.append(card1)
        card2 = player2.pop(0)
        pot.append(card2)


    if card1 > card2:
        # Player 1 wins the pot
        for card in pot:
            player1.append(card)

    if card1 < card2:
        # Player 2 wins the pot
        for card in pot:
            player2.append(card)

# Prepare the game by creating a deck and dealing it out
player1, player2 = shuffleAndDeal()

# Play a turn, repeat until one player runs out of cards
while len(player1) > 0 and len(player2):
    playTurn(player1, player2)

# Check which player has run out of cards to announce the winner
if len(player2) == 0:
    print('Game over, player 1 wins!')
if len(player1) == 0:
    print('Game over, player 2 wins!')

Having created functions (shuffleAndDeal and playTurn) for the functionality we already had, the rest of the script is easy to build. We no longer need to worry about the implementation details of the functions, all we need to know is their names, their parameters, and their return values:

  • We use the shuffleAndDeal function to create a deck and deal cards to two players.
  • Using a while loop, we simulate one turn at a time until the game ends due to a player running out of cards.
  • After the while loop ends, we use if statements to determine and announce the winner.

Notice that the shuffleAndDeal function returns two values, one for each player's stack of cards. We accomplish this using the (player1, player2) syntax, which creates a tuple. Tuple is a built-in data type in Python we won't cover in detail since it's uncommon in other popular programming languages.

We can run this script from a terminal by using the python command and providing the source file by name, like so:

$ python longnonsense.py
Game over, player 2 wins!

Errors

Running the script a few times reveals two cases where it can fail:

  1. The game continues forever, causing the terminal to hang and no output to be printed. We can stop the program by using ctrl+C to send a KeyboardInterrupt signal to the process.
  2. While resolving a tie, one player runs out of cards. This causes the script to fail with the error message below.
Traceback (most recent call last):
  File "longnonsense.py", line 53, in <module>
    playTurn(player1, player2)
  File "longnonsense.py", line 33, in playTurn
    card1 = player1.pop(0)
IndexError: pop from empty list

Both of these errors happen because the rules of the game don't plan around them. If you play the game in real life, you probably won't run into these problems unless you play thousands of turns per second. Python has some methods built in which allow us to predict errors like these and create new logic to work around them. These methods are out of the scope of this series but we may cover them in future articles.

Conclusion

In this article, we combined lots of basic concepts from previous articles in the series to create a script for a real-world scenario. We learned how variables, data types, control flow, and functions come together to create complete scripts. Using these basic concepts, we can build very useful pieces of software.

If you're a total beginner, hopefully this series helps you gain some valuable insight into the world of programming. While there are many more things to learn for various software systems, understanding these basics and knowing how to apply them is the foundation to writing software in the real-world.

Tags

Computer Science
Programming