🧠 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:
- When the game starts, all cards are hidden with a card-back image.
- When the player clicks on a card, it flips to reveal the symbol.
- If three flipped cards match, they remain open and marked as “matched”.
- If they don’t match, they flip back after a short delay.
- 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)
