Building a Memory Game with Python and Pygame

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Setup
  4. Creating the Game Window
  5. Creating the Card Objects
  6. Displaying the Cards
  7. Adding Interactivity
  8. Implementing the Game Logic
  9. Adding Sound Effects
  10. Conclusion

Introduction

In this tutorial, we will be building a memory game using Python and Pygame. A memory game is a popular game that tests your memory by requiring you to remember the locations of matching pairs of cards. By the end of this tutorial, you will have a fully functional memory game that you can play and customize.

Prerequisites

To follow this tutorial, you should have a basic understanding of Python programming language concepts. Familiarity with object-oriented programming (OOP) will be beneficial but not essential. Additionally, you will need to have Pygame installed on your system. Pygame is a popular library for game development in Python. If you haven’t installed Pygame yet, follow the setup instructions in the next section.

Setup

To install Pygame, open your terminal or command prompt and run the following command: pip install pygame Once Pygame is installed, let’s proceed to build our memory game step by step.

Creating the Game Window

First, let’s import the necessary libraries and set up the Pygame window. Create a new Python file and add the following code: ```python import pygame

pygame.init()

# Set up the window
window_width = 800
window_height = 600
window = pygame.display.set_mode((window_width, window_height))
pygame.display.set_caption("Memory Game")

# Game loop
running = True
while running:
    # Handle events
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Update game state

    # Render
    pygame.display.update()

# Quit the game
pygame.quit()
``` Let's go through the code step by step. First, we import the `pygame` module and initialize it using `pygame.init()`. This will initialize all the pygame modules. Next, we set up the game window by defining its width and height. We create the window using `pygame.display.set_mode()` and set the window's caption using `pygame.display.set_caption()`.

Next, we enter the game loop, which allows our game to continuously run until we choose to exit. Inside the game loop, we handle events using pygame.event.get(). In the current implementation, we handle only the QUIT event, which occurs when we click the close button on the window. When this event is detected, we set running to False to exit the game loop.

Finally, outside the game loop, we call pygame.quit() to quit the game properly.

Run the script, and you should see an empty game window titled “Memory Game.”

Creating the Card Objects

To create a memory game, we need a set of cards that the player can flip to find matching pairs. Each card should have a hidden state and be associated with a specific image. Let’s create a Card class to represent these card objects. python class Card: def __init__(self, image_path): self.image = pygame.image.load(image_path) self.hidden = True In this code snippet, we define the Card class with an __init__() method. In the __init__() method, we load the card’s image using pygame.image.load() and assign it to the image attribute. We also set the initial hidden state as True.

To use the Card class, update your code as follows: ```python import pygame

pygame.init()

# Set up the window
window_width = 800
window_height = 600
window = pygame.display.set_mode((window_width, window_height))
pygame.display.set_caption("Memory Game")

# Create card objects
card_images = ["card1.png", "card2.png", "card3.png", "card4.png"]
cards = [Card(image_path) for image_path in card_images]

# Game loop
running = True
while running:
    # Handle events
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Update game state

    # Render
    pygame.display.update()

# Quit the game
pygame.quit()
``` In this updated code, we create a list `card_images` containing the paths to the images for our cards. We then create a list `cards` using a list comprehension, where each element is an instance of the `Card` class initialized with the respective image path.

Displaying the Cards

Now that we have our card objects, let’s display them on the game window. We’ll lay out the cards in a grid-like fashion. ```python import pygame

pygame.init()

# Set up the window
window_width = 800
window_height = 600
window = pygame.display.set_mode((window_width, window_height))
pygame.display.set_caption("Memory Game")

# Create card objects
card_images = ["card1.png", "card2.png", "card3.png", "card4.png"]
cards = [Card(image_path) for image_path in card_images]

# Set up the card grid
grid_width = 2
grid_height = 2
card_width = 200
card_height = 200
padding_x = (window_width - (card_width * grid_width)) // (grid_width + 1)
padding_y = (window_height - (card_height * grid_height)) // (grid_height + 1)

# Game loop
running = True
while running:
    # Handle events
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Render
    window.fill((255, 255, 255))  # Fill the window with white color

    # Display cards
    for i, card in enumerate(cards):
        row = i // grid_width
        col = i % grid_width
        x = (padding_x * (col + 1)) + (card_width * col)
        y = (padding_y * (row + 1)) + (card_height * row)
        
        if card.hidden:
            pygame.draw.rect(window, (0, 0, 0), (x, y, card_width, card_height))
        else:
            window.blit(card.image, (x, y))

    pygame.display.update()

# Quit the game
pygame.quit()
``` In this updated code, we define the card grid's dimensions (`grid_width` and `grid_height`) and the size of each card (`card_width` and `card_height`). We also define the padding between cards (`padding_x` and `padding_y`). All these values are calculated based on the window's dimensions and the desired layout.

Inside the game loop, we fill the window with a white color using window.fill() to clear the previous frame. We then loop through each card object and calculate its position using the padding_x, padding_y, card_width, and card_height values. If a card is hidden, we draw a black rectangle using pygame.draw.rect(). Otherwise, we display the card’s image using window.blit().

Run the script, and you should see the cards displayed in a grid-like layout.

Adding Interactivity

Now that we have the cards displayed, let’s make them interactive. Specifically, we want to allow the player to flip the cards by clicking on them. When a card is flipped, it should reveal its image. ```python import pygame

pygame.init()

# Set up the window
window_width = 800
window_height = 600
window = pygame.display.set_mode((window_width, window_height))
pygame.display.set_caption("Memory Game")

# Create card objects
card_images = ["card1.png", "card2.png", "card3.png", "card4.png"]
cards = [Card(image_path) for image_path in card_images]

# Set up the card grid
grid_width = 2
grid_height = 2
card_width = 200
card_height = 200
padding_x = (window_width - (card_width * grid_width)) // (grid_width + 1)
padding_y = (window_height - (card_height * grid_height)) // (grid_height + 1)

# Game loop
running = True
while running:
    # Handle events
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_x, mouse_y = pygame.mouse.get_pos()
            for i, card in enumerate(cards):
                row = i // grid_width
                col = i % grid_width
                x = (padding_x * (col + 1)) + (card_width * col)
                y = (padding_y * (row + 1)) + (card_height * row)
                
                if x <= mouse_x <= x + card_width and y <= mouse_y <= y + card_height:
                    card.hidden = not card.hidden

    # Render
    window.fill((255, 255, 255))  # Fill the window with white color

    # Display cards
    for i, card in enumerate(cards):
        row = i // grid_width
        col = i % grid_width
        x = (padding_x * (col + 1)) + (card_width * col)
        y = (padding_y * (row + 1)) + (card_height * row)
        
        if card.hidden:
            pygame.draw.rect(window, (0, 0, 0), (x, y, card_width, card_height))
        else:
            window.blit(card.image, (x, y))

    pygame.display.update()

# Quit the game
pygame.quit()
``` In this updated code, we handle the `MOUSEBUTTONDOWN` event using `pygame.MOUSEBUTTONDOWN`. We get the mouse's position when the event occurs using `pygame.mouse.get_pos()`. We then loop through each card and check if the mouse's position falls within the card's boundaries. If it does, we toggle the card's `hidden` attribute.

Run the script, and you should be able to flip the cards by clicking on them.

Implementing the Game Logic

Now that our cards are interactive, let’s implement the game logic to check for matching pairs. We’ll keep track of the flipped cards and compare their images to determine if they match. If they do, we’ll keep them face up; otherwise, we’ll flip them back. ```python import pygame import time

pygame.init()

# Set up the window
window_width = 800
window_height = 600
window = pygame.display.set_mode((window_width, window_height))
pygame.display.set_caption("Memory Game")

# Create card objects
card_images = ["card1.png", "card2.png", "card3.png", "card4.png"]
cards = [Card(image_path) for image_path in card_images]

# Set up the card grid
grid_width = 2
grid_height = 2
card_width = 200
card_height = 200
padding_x = (window_width - (card_width * grid_width)) // (grid_width + 1)
padding_y = (window_height - (card_height * grid_height)) // (grid_height + 1)

# Game variables
flipped_cards = []
matched_pairs = 0

# Game loop
running = True
while running:
    # Handle events
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_x, mouse_y = pygame.mouse.get_pos()
            for i, card in enumerate(cards):
                row = i // grid_width
                col = i % grid_width
                x = (padding_x * (col + 1)) + (card_width * col)
                y = (padding_y * (row + 1)) + (card_height * row)
                
                if x <= mouse_x <= x + card_width and y <= mouse_y <= y + card_height:
                    if card.hidden and len(flipped_cards) < 2:
                        card.hidden = False
                        flipped_cards.append(card)
                        
                        if len(flipped_cards) == 2:
                            if flipped_cards[0].image == flipped_cards[1].image:
                                matched_pairs += 1
                                flipped_cards.clear()
                            else:
                                # Wait for a short duration before flipping the unmatched cards back
                                time.sleep(1)
                                for flipped_card in flipped_cards:
                                    flipped_card.hidden = True
                                flipped_cards.clear()

    # Render
    window.fill((255, 255, 255))  # Fill the window with white color

    # Display cards
    for i, card in enumerate(cards):
        row = i // grid_width
        col = i % grid_width
        x = (padding_x * (col + 1)) + (card_width * col)
        y = (padding_y * (row + 1)) + (card_height * row)
        
        if card.hidden:
            pygame.draw.rect(window, (0, 0, 0), (x, y, card_width, card_height))
        else:
            window.blit(card.image, (x, y))

    pygame.display.update()

    # Check if all pairs are matched
    if matched_pairs == len(cards) // 2:
        running = False

# Game over
print("Congratulations! You won!")

# Quit the game
pygame.quit()
``` In this updated code, we introduce two new variables: `flipped_cards` (to keep track of the cards that are currently face up) and `matched_pairs` (to keep track of the number of matched pairs).

Inside the MOUSEBUTTONDOWN event, we check if a card is hidden and if the number of currently flipped cards is less than 2. If both conditions are met, we flip the card by setting its hidden attribute to False and add it to the flipped_cards list.

When two cards are flipped, we compare their images. If they match, we increment matched_pairs and clear the flipped_cards list. If they don’t match, we briefly wait for one second using time.sleep() and then flip the unmatched cards by setting their hidden attribute back to True. We also clear the flipped_cards list.

After rendering the cards and updating the display, we check if all pairs are matched by comparing matched_pairs to half the number of cards. If they are, we exit the game loop and display a “Congratulations! You won!” message.

Run the script, and you should be able to play the memory game. When all pairs are matched, the game will end, and the winning message will be displayed.

Adding Sound Effects

To make our game more engaging, let’s add sound effects for flipping matched and unmatched cards. We’ll use the pygame.mixer.Sound class to load and play the sound effects. ```python import pygame import time

pygame.init()

# Set up the window
window_width = 800
window_height = 600
window = pygame.display.set_mode((window_width, window_height))
pygame.display.set_caption("Memory Game")

# Create card objects
card_images = ["card1.png", "card2.png", "card3.png", "card4.png"]
cards = [Card(image_path) for image_path in card_images]

# Set up the card grid
grid_width = 2
grid_height = 2
card_width = 200
card_height = 200
padding_x = (window_width - (card_width * grid_width)) // (grid_width + 1)
padding_y = (window_height - (card_height * grid_height)) // (grid_height + 1)

# Load sound effects
flip_sound = pygame.mixer.Sound("flip.wav")
match_sound = pygame.mixer.Sound("match.wav")
unmatch_sound = pygame.mixer.Sound("unmatch.wav")

# Game variables
flipped_cards = []
matched_pairs = 0

# Game loop
running = True
while running:
    # Handle events
    for event in pygame.event.get():
        if event.type