Master Object-Oriented Programming by Creating a Card Game

What will we cover?

The best way to learn Object-Oriented Programming is by creating something object-oriented. In this tutorial we will create a simple card game.

Step 1: What is Object-Oriented Programming?

At is core, Object-Oriented Programming helps you with structuring your program to resemble reality.

That is, you declare objects with parameters and methods (functions) you need on them.

The best way is to learn it by creating an easy example you can relate to.

Consider the following.

This diagram represents three objects we want to model. The first is a Card, second a Deck, and finally a Hand.

There are many things to notice, but fist that Hand is actually a sub-class of Deck. What does that mean? Don’t worry, we’ll get there.

Step 2: Implement the Card class

What does it all mean?

Well, first of all, there are many ways to represent a class and the above is just one possible option. But the if we look at Card, we have two groups to look at. First, suit and rank. Second, __str__() and __lt__(other).

The suit and rank are instance variables, while __str__() and __lt__(other) are class methods.

Instance variables are variables only available to a specific object instance. Hence, different instances of the same class can have different values.

Class methods are methods you can call on an object instance.

The function __str__() is a special method, which will give the string representation of the object instance. This is how the object will be represented if printed.

The function __lt__(other) is also a special method, which returns whether the object and another object other is greater. Hence, it returns a truths statement.

One way to implement is as follows.

class Card:
    suits = ['\u2666', '\u2665', '\u2663', '\u2660']
    ranks = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]
    
    def __init__(self, suit, rank):
        self.suit = suit
        self.rank = rank
        
    def __str__(self):
        return f"{Card.ranks[self.rank]}{Card.suits[self.suit]}"
    
    def __lt__(self, other):
        if self.rank == other.rank:
            return self.suit < other.suit
        else:
            return self.rank < other.rank

Notice we also have class variables suits and ranks (with s). They are used to give a representation in the __str__() method.

Class variables are available and the same across all objects.

Also, notice the __init__(self, suit, rank), which is a method which is called at creation of the object, and it assigns variables to the instance variables (the ones with self)

Step 3: Implement the Deck class

A Deck should represent a pile of card.

Here we want it to create a new shuffled deck of cards when you create a new instance of the Deck object.

That can be accomplished as follows.

import random

class Deck:
    def __init__(self):
        self.deck = []
        for suit in range(4):
            for rank in range(13):
                self.deck.append(Card(suit, rank))
        self.shuffle()
        
    def __len__(self):
        return len(self.deck)
    
    def add_card(self, card):
        self.deck.append(card)
        
    def pop_card(self):
        return self.deck.pop()
    
    def shuffle(self):
        random.shuffle(self.deck)

Notice that __len__() method is also a special, and returns the length of the object. This is handy, if you want to use len(…) on an object instance of Deck.

The rest of the methods are simple and straightforward.

Step 4: Implement the Hand class

The hand class is a sub-class of Deck. How does that make sense?

Well, it will share the same instance variable and methods with some additional ones.

Think about it, a Hand is like a Deck of card, as it is a collection of cards.

How to implement that.

class Hand(Deck):
    def __init__(self, label):
        self.deck = []
        self.label = label
        self.win_count = 0
        
    def __str__(self):
        return self.label + ': ' + ' '.join([str(card) for card in self.deck])
    
    def get_label(self):
        return self.label
    
    def get_win_count(self):
        return self.win_count
    
    def round_winner(self):
        self.win_count = self.win_count + 1

Notice that we overwrite the __init__(…) method, as we do not want to create a full deck of cards. Here we start with empty hands.

Step 5: A simple game

  • Create a Deck of cards.
  • Create 4 players (P1, P2, P3, P4)
  • Divided all cards to 4 players.
  • Assume you are P1, print the hand of P1.
  • The game has 13 rounds:
    • Each player plays 1 card.
    • The player with highest card wins.
    • Update the score for the winning hand.
    • Print cards played in round and the winner (with winning card).
  • After the 13 rounds – print score for all players (P1, P2, P3, P4).

How to do that?

deck = Deck()

hands = []
for i in range(1, 5):
    hands.append(Hand(f'P{i}'))
    
while len(deck) > 0:
    for hand in hands:
        hand.add_card(deck.pop_card())
        
print(hands[0])

for i in range(13):
    input()
    played_cards = []
    for hand in hands:
        played_cards.append(hand.pop_card())
    
    winner_card = max(played_cards)
    winner_hand = hands[played_cards.index(winner_card)]
    winner_hand.round_winner()
    
    print(f"R{i}: " + ' '.join([str(card) for card in played_cards]) + f' Winner: {winner_hand.get_label()} {str(winner_card)}')
    
for hand in hands:
    print(f"Score for {hand.get_label()}: {hand.get_win_count()}")

Amazing, right?

Want to learn more?

If this is something you like and you want to get started with Python, then this is part of a 8 hours FREE video course with full explanations, projects on each levels, and guided solutions.

The course is structured with the following resources to improve your learning experience.

  • 17 video lessons teaching you everything you need to know to get started with Python.
  • 34 Jupyter Notebooks with lesson code and projects.
  • A FREE 70+ pages eBook with all the learnings from the lessons.

See the full FREE course page here.

Leave a Reply