How To Create Memory Game in Python Using Tkinter
In this Python Tutorial we will see How to Create a Memory Game (card-matching game) with smooth animations, multiple difficulty levels, and a dark theme interface.
The Game Features:
- Multiple Difficulty Levels: 4x4 (16 cards, 8 pairs), 4x5 (20 cards, 10 pairs), and 5x6 grid sizes (30 cards, 15 pairs).
- Card Matching: Automatic pair detection.
- Move counter and timer: The game tracks moves, elapsed time, and provides detailed performance statistics.
What We Are Gonna Use In This Project:
- Python Programming Language.- VS Editor.
Project Source Code:
- Create frame for cards.
def create_game_grid(self):
# Create frame to hold all cards
self.cards_frame = tk.Frame(self.game_frame, bg=self.colors['bg'])
# Center the grid in the game area
self.cards_frame.pack(expand=True)
# Initialize cards list and get symbol pairs
self.cards = [] # Empty list to store card widgets
# Get grid dimensions for current difficulty
rows, cols = self.difficulty_levels[self.current_difficulty]["grid"]
# Double the symbols list for pairs
symbols = self.difficulty_levels[self.current_difficulty]["symbols"] * 2
# Randomly shuffle symbols to place them on the grid
random.shuffle(symbols)
# Store shuffled symbols for later reference
self.symbols = symbols
# Create cards in grid
for i in range(rows): # Loop through each row
for j in range(cols): # Loop through each column in current row
card_idx = i*cols+j # Calculate linear index from row and column
# Create card canvas
# Create canvas with fixed size And Set background And remove border
card = tk.Canvas(self.cards_frame, width=80, height=100,
bg=self.colors['card_bg'], highlightthickness=0)
# Position card in grid with spacing
card.grid(row=i, column=j, padx=5, pady=5)
# Bind click event
card.bind("<Button-1>", lambda e, idx=card_idx: self.on_card_click(idx))
# Create card back (visible initially)
# Create card shape And Add colored border
card.create_rectangle(5, 5, 75, 95, fill=self.colors['card_bg'],
outline=self.colors['card_fg'], width=2)
# Add question mark and Set text color
card.create_text(40, 50, text="?", font=("Helvetica", 24, "bold"),
fill=self.colors['card_fg'])
# Create card front (hidden initially)
# Create card shape
# Add colored border
# Hide initially and add tag for later reference
card.create_rectangle(5, 5, 75, 95, fill=self.colors['card_fg'],
outline=self.colors['card_bg'], width=2,
state='hidden', tags=('front',))
# Add symbol text
# Use bold font for symbol
# Set text color
# Hide initially and add tag
card.create_text(40, 50, text=self.symbols[card_idx],
font=("Helvetica", 24, "bold"),
fill=self.colors['card_bg'],
state='hidden', tags=('symbol',))
self.cards.append(card) # Add card to list for later reference
- Handles click events on cards.
def on_card_click(self, idx):
# Start timer on first click
if self.start_time is None: # If this is the first click of the game
self.start_time = time.time() # Record start time
self.update_time() # Start timer updates
# Ignore invalid clicks
if idx in self.revealed or idx in self.matched_cards or len(self.revealed) == 2:
# Skip if card is already revealed, matched, or 2 cards are already flipped
return
# Reveal the clicked card
self.reveal_card(idx) # Show the card face
self.revealed.append(idx) # Add to list of revealed cards
# If two cards are revealed, check for a match
if len(self.revealed) == 2: # When two cards have been flipped
self.moves += 1 # Increment move counter
self.moves_label.config(text=f"Moves: {self.moves}") # Update moves display
self.master.after(500, self.check_match) # schedule match check after a delay
- Reveal and Hide Cards
def reveal_card(self, idx):
card = self.cards[idx] # Get the card canvas widget
card.itemconfig('front', state='normal') # Show the card front
card.itemconfig('symbol', state='normal') # Show the symbol
def hide_card(self, idx):
card = self.cards[idx] # Get the card canvas widget
card.itemconfig('front', state='hidden') # Hide the card front
card.itemconfig('symbol', state='hidden') # Hide the symbol
- Method to check for a match between flipped cards.
def check_match(self):
idx1, idx2 = self.revealed # Get indices of the two revealed cards
if self.symbols[idx1] == self.symbols[idx2]: # If symbols match
# Match found
self.matched_pairs += 1 # Increment matched pairs counter
self.matched_cards.extend([idx1, idx2]) # Add both cards to matched list
# Highlight matched cards
for idx in [idx1, idx2]: # For each matched card
card = self.cards[idx] # Get card widget
# Change to green color to indicate match
card.itemconfig('front', fill="#8bc34a")
# Check if game is over
if self.matched_pairs == len(self.symbols) // 2: # If all pairs are found
# Schedule game over screen after delay
self.master.after(500, self.game_over)
else: # If symbols don't match
# No match
self.hide_card(idx1) # Flip first card back
self.hide_card(idx2) # Flip second card back
self.revealed.clear() # Clear the revealed cards list for next move
- Start a New Game.
def new_game(self):
# Reset game state
self.game_solved = False # Reset game solved flag
self.revealed.clear() # Clear revealed cards list
self.matched_cards.clear() # Clear matched cards list
self.matched_pairs = 0 # Reset matched pairs counter
self.moves = 0 # Reset moves counter
self.start_time = None # Reset start time
# Reset labels
self.moves_label.config(text="Moves: 0") # Reset moves display
self.time_label.config(text="Time: 0:00") # Reset time display
# Recreate the game grid
self.cards_frame.destroy() # Remove old card grid
self.create_game_grid() # Create new shuffled grid
- Method to display the congratulations dialog when the game is won.
def game_over(self):
self.game_solved = True # Mark game as completed
elapsed_time = int(time.time() - self.start_time) # Calculate total game time
minutes, seconds = divmod(elapsed_time, 60) # Convert to minutes and seconds
# Create game over window
game_over_window = tk.Toplevel(self.master) # Create new popup window
game_over_window.title("Game Over") # Set window title
game_over_window.geometry("350x350") # Set window size
game_over_window.configure(bg=self.colors['gameover_bg']) # Set background
game_over_window.grab_set() # Make window modal
game_over_window.transient(self.master) # Set as transient to main window
# Center the game over window on screen
x = self.master.winfo_x() + (self.master.winfo_width() // 2) - (350 // 2)
y = self.master.winfo_y() + (self.master.winfo_height() // 2) - (350 // 2)
game_over_window.geometry(f"+{x}+{y}")
# Create congratulations label
tk.Label(game_over_window, text="Congratulations!",
font=("Helvetica", 28, "bold"),
bg=self.colors['gameover_bg'], fg=self.colors['text']).pack(pady=(20, 30))
# Create stats frame
stats_frame = tk.Frame(game_over_window, bg=self.colors['gameover_bg'])
stats_frame.pack(pady=(0, 30)) # Add padding below frame
# Display stats
# Show difficulty level
self.create_stat_label(stats_frame, "Difficulty", self.current_difficulty)
# Show total moves
self.create_stat_label(stats_frame, "Moves", str(self.moves))
# Show total time
self.create_stat_label(stats_frame, "Time", f"{minutes}:{seconds:02d}")
# Play again button
play_again_button = tk.Button(game_over_window, text="Play Again",
font=self.custom_font,
bg=self.colors['button_bg'], fg=self.colors['button_fg'],
relief=tk.FLAT,
command=lambda: [game_over_window.destroy(), self.new_game()])
play_again_button.pack(pady=20) # Add padding around button
# Add hover effect to play again button
play_again_button.bind("<Enter>",
lambda e: e.widget.config(bg="#5dbb5e")) # Lighten on hover
play_again_button.bind("<Leave>",
lambda e: e.widget.config(bg=self.colors['button_bg'])) # Restore on leave
The Final Result:
if you want the full source code click on the download button below
disclaimer: you will get the source code, and to make it work in your machine is your responsibility and to debug any error/exception is your responsibility this project is for the students who want to see an example and read the code not to get and run.
Download Projects Source Code