Python Memory Game Tutorial: Build a Fun Matching Game with Pygame


🧠 Create a Hard Level Memory Game Using Python (Pygame Triplets Version)

If you’ve been exploring Python game development and want to take your skills to the next level, this project is perfect for you! In this tutorial, we’ll create a Hard Level Memory Game in Python where players match not pairs — but triplets of identical cards. Using the pygame library, we’ll build a fun, interactive, and visually engaging game that challenges both your memory and your logic.


🎯 Project Overview

This project is built with Python and Pygame. It uses a 6x6 grid of cards, where each symbol appears three times. The player’s task is to flip three matching cards to make a perfect triplet. The game continues until all cards are matched — and once you finish, it displays an “EPIC WIN!” message with the total number of turns you took.

  • ✔️ Language: Python (Pygame library)
  • ✔️ Difficulty: Hard (Triplet Matching)
  • ✔️ Grid Size: 6 x 6 (36 cards)
  • ✔️ Features: Flip animations, random shuffle, restart option

⚙️ Before Running the Game

To ensure the game runs smoothly, make sure your project folder is properly organized:

  • 📁 Keep your Python file inside a folder named game
  • 📁 Inside the game folder, create another folder named assets
  • 🖼️ Place all the images inside the assets folder
  • ✅ Make sure image names match exactly with the names used in your Python code

For example, your assets folder should include image files like: burger.png, cherry.png, hotdog.png, card_back.png, etc.


🌐 Car Game Animation


import pygame
import random
import time
import os
import sys

# --- 1. Initialize Pygame ---
pygame.init()

# --- 2. Game Configuration for Hard Level ---
SCREEN_WIDTH = 950
SCREEN_HEIGHT = 750
CARD_SIZE = 80
CARD_MARGIN = 10
BOARD_COLS = 6
BOARD_ROWS = 6
TOTAL_CARDS = BOARD_ROWS * BOARD_COLS
MATCH_COUNT = 3 # Triplets Match!

# Center the board
BOARD_WIDTH = BOARD_COLS * (CARD_SIZE + CARD_MARGIN) - CARD_MARGIN
BOARD_HEIGHT = BOARD_ROWS * (CARD_SIZE + CARD_MARGIN) - CARD_MARGIN
BOARD_START_X = (SCREEN_WIDTH - BOARD_WIDTH) // 2
BOARD_START_Y = (SCREEN_HEIGHT - BOARD_HEIGHT) // 2

SCREEN = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption(f"Hard Memory Game: {BOARD_ROWS}x{BOARD_COLS} Triplets")

# Colors
BACKGROUND_COLOR = (25, 25, 112) # Midnight Blue
WHITE = (255, 255, 255)
FLIPPED_BORDER_COLOR = (255, 255, 0) 

# --- PATH SETUP ---
script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) 
ASSETS_DIR = os.path.join(script_dir, 'assets') 

# --- 3. Load Images (Global Assets) ---
image_names = [
    'burger', 'cherry', 'hotdog', 'chilli', 'skewer_blue', 'sausage', 
    'fire', 'apple', 'skewer_pink', 'ketchup', 'egg', 'pancake'
]
POSSIBLE_EXTENSIONS = ['.png', '.jpeg', '.jpg']

card_images = {}
card_back_img = None
font = pygame.font.Font(None, 74) # Font for messages

# --- Image Loading Function ---
def load_assets():
    """Loads all game images, checking for multiple extensions."""
    global card_back_img, card_images
    
    # Check if images are already loaded (to avoid reloading on restart)
    if card_images:
        return

    try:
        # Load all 12 symbol images
        for name in image_names:
            found_path = None
            for ext in POSSIBLE_EXTENSIONS:
                potential_path = os.path.join(ASSETS_DIR, f'{name}{ext}')
                if os.path.exists(potential_path):
                    found_path = potential_path
                    break
            
            if found_path:
                img = pygame.image.load(found_path).convert_alpha()
                card_images[name] = pygame.transform.scale(img, (CARD_SIZE, CARD_SIZE))
            else:
                raise FileNotFoundError(f"Symbol image missing for: {name}")

        # Load card back image
        card_back_path = None
        for ext in POSSIBLE_EXTENSIONS:
            potential_path = os.path.join(ASSETS_DIR, f'card_back{ext}')
            if os.path.exists(potential_path):
                card_back_path = potential_path
                break
        
        if card_back_path:
            card_back_img = pygame.image.load(card_back_path).convert_alpha() 
            card_back_img = pygame.transform.scale(card_back_img, (CARD_SIZE, CARD_SIZE))
        else:
             raise FileNotFoundError("Card back image 'card_back' is missing.")

    except (pygame.error, FileNotFoundError) as e:
        print(f"\nFATAL ASSET ERROR: {e}")
        print(f"Ensure all 12 symbol images and 'card_back' are in the 'assets' folder.")
        pygame.quit()
        sys.exit()

# --- 4. Game Board Data and State Variables ---
board = []
card_state = []
flipped_coords = []
match_delay_start_time = 0 
game_over = False
matches_found = 0
total_matches_needed = TOTAL_CARDS // MATCH_COUNT
turns = 0

# --- Core Game Setup Function ---
def reset_game():
    """Shuffles the board and resets all game state variables."""
    global board, card_state, flipped_coords, match_delay_start_time
    global game_over, matches_found, turns
    
    # 1. Reset counters and state
    flipped_coords = []
    match_delay_start_time = 0
    game_over = False
    matches_found = 0
    turns = 0
    
    # 2. Shuffle and create new board
    symbols_for_board = image_names * MATCH_COUNT
    random.shuffle(symbols_for_board)

    board = [symbols_for_board[i:i + BOARD_COLS] for i in range(0, len(symbols_for_board), BOARD_COLS)]
    
    # 3. Reset card state to covered (0)
    card_state = [[0 for _ in range(BOARD_COLS)] for _ in range(BOARD_ROWS)]
    print("\n--- Game Reset. New Board Shuffled. Good luck! ---")


# --- 5. Helper Functions ---
def get_card_rect(row, col):
    """Returns the pygame Rect for a given card position."""
    x = BOARD_START_X + col * (CARD_SIZE + CARD_MARGIN)
    y = BOARD_START_Y + row * (CARD_SIZE + CARD_MARGIN)
    return pygame.Rect(x, y, CARD_SIZE, CARD_SIZE)

def draw_board():
    """Draws all cards based on their state."""
    SCREEN.fill(BACKGROUND_COLOR)

    for r in range(BOARD_ROWS):
        for c in range(BOARD_COLS):
            rect = get_card_rect(r, c)
            
            pygame.draw.rect(SCREEN, (50, 50, 150), rect)

            # Show image if flipped (1) or matched (2)
            if card_state[r][c] in (1, 2):
                symbol = board[r][c]
                SCREEN.blit(card_images[symbol], rect.topleft)
            # Otherwise, show the card back
            else:
                SCREEN.blit(card_back_img, rect.topleft)
            
            # Draw borders
            if card_state[r][c] == 2:
                 pygame.draw.rect(SCREEN, WHITE, rect, 3)
            elif card_state[r][c] == 1:
                 pygame.draw.rect(SCREEN, FLIPPED_BORDER_COLOR, rect, 3)
            else:
                 pygame.draw.rect(SCREEN, (100, 100, 200), rect, 2)

    # Display turn counter
    turn_text = font.render(f"Turns: {turns}", True, WHITE)
    SCREEN.blit(turn_text, (20, SCREEN_HEIGHT - 60))

    pygame.display.flip()

# --- Initial Setup ---
load_assets() # Load images once
reset_game() # Setup the first board

# --- 6. Main Game Loop ---
running = True
clock = pygame.time.Clock() # To control the game speed

while running:
    current_time = time.time()
    clock.tick(60) # Limit to 60 frames per second

    # --- Match Check Logic (Only runs when 3 cards are flipped) ---
    if len(flipped_coords) == MATCH_COUNT and match_delay_start_time and current_time - match_delay_start_time > 1.5:
        
        symbols_flipped = [board[r][c] for r, c in flipped_coords]
        
        if len(set(symbols_flipped)) == 1: # Triplets Match
            print(f"TRIPLET MATCH found for '{symbols_flipped[0]}'!")
            for r, c in flipped_coords:
                card_state[r][c] = 2 # Mark as matched
            
            matches_found += 1
            if matches_found == total_matches_needed:
                game_over = True
        
        else:
            # Not a match. Flip them back.
            for r, c in flipped_coords:
                card_state[r][c] = 0 # Cover again
        
        flipped_coords = []
        match_delay_start_time = 0

    # --- Event Handling ---
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        
        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: # Left click
            if game_over:
                # ⚠️ NEW LOGIC: Restart game on click after win ⚠️
                reset_game()
            
            else:
                mouse_x, mouse_y = event.pos
                
                for r in range(BOARD_ROWS):
                    for c in range(BOARD_COLS):
                        rect = get_card_rect(r, c)
                        
                        if rect.collidepoint(mouse_x, mouse_y):
                            if card_state[r][c] == 0 and len(flipped_coords) < MATCH_COUNT:
                                if len(flipped_coords) == 0:
                                    turns += 1 # Increment turn only on the first card of a new attempt
                                    
                                card_state[r][c] = 1 # Mark as flipped
                                flipped_coords.append((r, c))

                                if len(flipped_coords) == MATCH_COUNT:
                                    # Three cards are flipped. Start the checking delay.
                                    draw_board() 
                                    match_delay_start_time = current_time 

                            break 
                    else:
                        continue
                    break


    # --- Drawing ---
    draw_board()

    # Game Over message (Drawn over the board)
    if game_over:
        win_text_line1 = font.render("EPIC WIN!", True, (255, 0, 0))
        win_text_line2 = font.render(f"Finished in {turns} Turns!", True, (255, 255, 0))
        restart_text = font.render("Click to Play Again", True, WHITE)
        
        # Center text lines
        r1 = win_text_line1.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 - 80))
        r2 = win_text_line2.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2))
        r3 = restart_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 80))
        
        # Draw a semi-transparent background box
        s = pygame.Surface((450, 280))
        s.set_alpha(150) # Transparency level
        s.fill((0, 0, 0)) # Black color
        SCREEN.blit(s, (SCREEN_WIDTH // 2 - 225, SCREEN_HEIGHT // 2 - 140))
        
        SCREEN.blit(win_text_line1, r1)
        SCREEN.blit(win_text_line2, r2)
        SCREEN.blit(restart_text, r3)
        pygame.display.flip()

# --- 7. Quit Pygame ---
pygame.quit()


🔍 Output Preview


🧩 How the Game Works

The logic behind the Python Memory Game is simple yet effective:

  1. When the game starts, all cards are hidden with a card-back image.
  2. When the player clicks on a card, it flips to reveal the symbol.
  3. If three flipped cards match, they remain open and marked as “matched”.
  4. If they don’t match, they flip back after a short delay.
  5. The game ends when all triplets are matched, showing a win message.

The entire gameplay is powered by event handling in Pygame and uses logic loops for card flipping and state tracking.

🧠 Understanding the Code Structure

The code is divided into clear sections to make it easy to understand:

1️⃣ Game Initialization

Pygame is initialized, and the game window is created with a custom size and title. The background color and board layout are defined here.

2️⃣ Asset Loading

The load_assets() function loads all the images from your assets folder. It checks for multiple file extensions like .png, .jpg, and .jpeg. If an image is missing, it gives an error message and safely exits the game.

3️⃣ Board Setup

The cards are randomly shuffled and arranged in a grid. Each card has a hidden state until flipped. The board is redrawn on every game frame for a smooth experience.

4️⃣ Game Logic

When three cards are flipped, the game checks if they match. If yes, they stay revealed; if not, they flip back after a 1.5-second delay. The player’s turn count increases after every new attempt.

5️⃣ Winning Screen

Once all triplets are found, a semi-transparent popup displays “EPIC WIN!” along with the total number of turns. You can click anywhere to start a new game instantly.


🎮 Why This Project is Awesome

This game is not only fun but also helps beginners practice logic building, loops, arrays, and image handling in Python. It’s a great mini-project to include in your Python portfolio or to showcase your understanding of Pygame and GUI-based development.

  • 🔥 Great for beginners learning Python
  • 🧩 Strengthens logic and conditional skills
  • 🎨 Enhances creativity in GUI development
  • 🚀 Easy to expand (add sounds, timer, or difficulty levels)

🔗