How To Create Image Puzzle Game in Python Tkinter
In this Python Tutorial we will see How to Create a picture puzzle game using Python And Tkinter.
This project demonstrates how to build a complete image puzzle game where players can upload their own pictures and solve them as puzzles with customizable difficulty levels.
The game features a modern dark theme, smooth piece-swapping animations, move counting, and a built-in timer.
The Game Features:
- Upload your own images: Players can upload any image (PNG, JPG, JPEG, GIF, or BMP) and instantly transform it into a playable puzzle. The game automatically resizes and crops images to fit perfectly within the puzzle grid.
- Multiple difficulty levels: With grid sizes ranging from 4x4 (16 pieces) to 8x8 (64 pieces), players can choose their preferred challenge level.
- Move counter and timer: The application tracks moves and elapsed time, adding a competitive element to the experience.
- Smooth piece-swapping animations: The piece-swapping feature includes a modern animations with easing functions, making the gameplay feel cool.
- Winning detection and celebration: Displays final statistics in a congratulations window.
How the Game Works:
When you start the game, you'll see a window split into two parts:
- A sidebar with controls and game stats.
- The main puzzle grid area.
You can upload any image, and the game will automatically:
- Resize it to fit the game window.
- Split it into equal pieces based on your chosen difficulty.
- Shuffle the pieces for you to solve.
Project Source Code:
- Opens file chooser and loads selected image to start puzzle.
def load_and_start_puzzle(self):
# Open file dialog to select an image
file_path = filedialog.askopenfilename(
filetypes=[("Image files", "*.png *.jpg *.jpeg *.gif *.bmp")])
# Check if user selected a file
if file_path:
# Load the selected image and start the game
self.original_image = Image.open(file_path)
self.reset_game() # Reset and start the game with the new image
- Reset game state variables.
def reset_game(self):
self.moves = 0 # Reset move counter
self.elapsed_time = 0 # Reset timer
self.moves_label.config(text="Moves: 0") # Update moves display
self.timer_label.config(text="Time: 00:00") # Update timer display
self.stop_timer() # Stop any existing timer
# Clear previous game elements
self.canvas.delete("all") # Clear the canvas
self.label_list.clear() # Clear the list of puzzle pieces
self.correct_order.clear() # Clear the correct order list
# Resize the uploaded image to fit the puzzle area
resized_image = self.original_image.resize((self.image_size, self.image_size),
Image.LANCZOS)
# Slice the image into puzzle pieces
self.sliced_images = self.slice_image(resized_image)
self.correct_order = self.sliced_images.copy() # Store the correct order
random.shuffle(self.sliced_images) # Shuffle pieces to start the game
# Calculate the size of each puzzle piece
slice_width = self.image_size // self.grid_size
slice_height = self.image_size // self.grid_size
# Place sliced images on the canvas
for i, img in enumerate(self.sliced_images):
row = i // self.grid_size # Calculate row position
col = i % self.grid_size # Calculate column position
x = col * slice_width # Calculate x-coordinate
y = row * slice_height # Calculate y-coordinate
# Create the image on the canvas and store its reference
image_item = self.canvas.create_image(x, y, image=img, anchor="nw")
self.label_list.append(image_item) # Add to the list of pieces
# Bind click event to each puzzle piece
self.canvas.tag_bind(image_item, "<Button-1>",
lambda event, item=image_item: self.on_click(item))
# Start the game timer
self.start_timer()
- Slice the image into a grid of smaller images.
def slice_image(self, image):
slices = [] # List to store sliced images
width, height = image.size # Get image dimensions
# Calculate dimensions of each slice
slice_width = width // self.grid_size
slice_height = height // self.grid_size
# Create slices by cropping the original image
for row in range(self.grid_size):
for col in range(self.grid_size):
# Calculate the coordinates for cropping
left = col * slice_width
upper = row * slice_height
right = left + slice_width
lower = upper + slice_height
# Crop the image and convert to PhotoImage for tkinter display
slice_img = image.crop((left, upper, right, lower))
photo_img = ImageTk.PhotoImage(slice_img)
slices.append(photo_img) # Add to slices list
return slices
- Handles click events on puzzle pieces.
def on_click(self, item):
# Handle click events on puzzle pieces
if self.first_selected is None:
# First piece selected
self.first_selected = item # Store the selected piece
self.draw_selection_outline(item) # Draw outline around the selected piece
elif self.second_selected is None and item != self.first_selected:
# Second piece selected (and different from first)
self.second_selected = item # Store the second piece
self.canvas.delete("selection_outline") # Clear existing outlines
self.draw_selection_outline(item) # Draw outline around second piece
# Start the swap animation after a short delay
self.root.after(100, self.swap_slices_with_animation)
- Draw a highlight outline around the selected puzzle piece.
def draw_selection_outline(self, item):
coords = self.canvas.coords(item) # Get coordinates of the piece
if coords:
x, y = coords[0], coords[1] # Extract x and y coordinates
# Calculate width and height of the piece
width = self.image_size // self.grid_size
height = self.image_size // self.grid_size
# Create a rectangle outline around the selected piece
self.selection_outline = self.canvas.create_rectangle(
x, y, x + width, y + height,
outline="#f9c74f", width=3, tags="selection_outline")
- Animates the swapping of two puzzle pieces.
def swap_slices_with_animation(self):
# Animate the swapping of two selected puzzle pieces
if self.first_selected and self.second_selected:
# Get current positions of both pieces
start_pos1 = self.canvas.coords(self.first_selected)
start_pos2 = self.canvas.coords(self.second_selected)
# Set target positions (swap them)
end_pos1 = start_pos2
end_pos2 = start_pos1
# Start the animation for both pieces
self.animate_swap(self.first_selected, start_pos1, end_pos1, 0)
self.animate_swap(self.second_selected, start_pos2, end_pos2, 0)
# Increment move counter and update display
self.moves += 1
self.moves_label.config(text=f"Moves: {self.moves}")
# Finalize the swap after the animation completes
self.root.after(500, self.finalize_swap)
- Piece Swapping Animations.
def animate_swap(self, item, start_pos, end_pos, step):
# Perform a single step of the swap animation
if step <= 10: # Animation has 10 steps
# Calculate progress with easing function for smooth animation
progress = self.ease_in_out_quad(step / 10)
# Calculate new position
new_x = start_pos[0] + (end_pos[0] - start_pos[0]) * progress
new_y = start_pos[1] + (end_pos[1] - start_pos[1]) * progress
# Move the piece to the new position
self.canvas.coords(item, new_x, new_y)
# Schedule the next animation step
self.root.after(50, lambda:
self.animate_swap(item, start_pos, end_pos, step + 1))
- Completes the swap operation after animation.
def finalize_swap(self):
# Complete the swap operation after animation
# Find indices of selected pieces in the label list
idx1 = self.label_list.index(self.first_selected)
idx2 = self.label_list.index(self.second_selected)
# Delete the selection outline
if hasattr(self, 'selection_outline'):
self.canvas.delete(self.selection_outline)
# Swap the images in the label list
self.label_list[idx1], self.label_list[idx2] = self.label_list[idx2],
self.label_list[idx1]
# Reset selection variables
self.first_selected = None
self.second_selected = None
# Check if the puzzle is solved
self.check_if_solved()
- Easing function for smooth animation.
@staticmethod
def ease_in_out_quad(t):
# Easing function for smooth animation
# Makes animation start and end slowly, with faster movement in the middle
return 2 * t * t if t < 0.5 else 1 - pow(-2 * t + 2, 2) / 2
- Check if puzzle is solved.
def check_if_solved(self):
# Check if the puzzle is solved
# Get current order of images from canvas
current_order = [self.canvas.itemcget(item, 'image')
for item in self.label_list]
# Get correct order as strings for comparison
correct_order = [str(img) for img in self.correct_order]
# Compare current order with correct order
if current_order == correct_order:
self.stop_timer() # Stop the timer
# Show congratulations message after a short delay
self.root.after(100, self.show_congratulations)
- Shows congratulation dialog when puzzle is solved.
def show_congratulations(self):
# Display a congratulations window when the puzzle is solved
congrats_window = tk.Toplevel(self.root) # Create new window
congrats_window.title("Congratulations!") # Set window title
congrats_window.geometry("400x350") # Set window size
congrats_window.configure(bg="#313244") # Set background color
# Add "Puzzle Solved!" title
tk.Label(congrats_window, text="Puzzle Solved!",
font=("Montserrat", 28, "bold"),
bg="#313244", fg="#cba6f7").pack(pady=(20, 10))
# Display the number of moves and time taken
tk.Label(congrats_window, text=f"Moves: {self.moves}",
font=("Montserrat", 20), bg="#313244", fg="#b4befe").pack(pady=5)
tk.Label(congrats_window, text=f"Time: {self.format_time(self.elapsed_time)}",
font=("Montserrat", 20), bg="#313244", fg="#b4befe").pack(pady=5)
# Display a congratulatory message
message = "Great job! You've successfully completed the puzzle."
tk.Label(congrats_window, text=message, font=("Montserrat", 16),
bg="#313244", fg="#a6e3a1", wraplength=350).pack(pady=(10, 20))
# Create a close button for the congratulations window
close_button = tk.Button(
congrats_window, text="Close", font=("Montserrat", 16, "bold"),
bg="#f9c74f", fg="#1e1e2e", activebackground="#f3a712",
activeforeground="#1e1e2e", bd=0, command=congrats_window.destroy,
width=20, height=2)
close_button.pack(pady=10)
- Shuffle the pieces.
def shuffle_grid(self):
# Shuffle the puzzle pieces
if hasattr(self, 'sliced_images'): # Check if puzzle is loaded
random.shuffle(self.sliced_images) # Shuffle the pieces
# Update the images on the canvas
for i, img in enumerate(self.sliced_images):
self.canvas.itemconfig(self.label_list[i], image=img)
# Reset game state
self.moves = 0 # Reset moves counter
self.elapsed_time = 0 # Reset timer
self.moves_label.config(text="Moves: 0") # Update moves display
self.timer_label.config(text="Time: 00:00") # Update timer display
self.stop_timer() # Stop the timer before starting again
self.start_timer() # Start the timer again
- Change the game difficulty
def change_difficulty(self, *args):
# Change the grid size based on selected difficulty
self.grid_size = int(self.difficulty_var.get()[0]) # Get first character as number
if hasattr(self, 'original_image'): # Check if an image is loaded
self.reset_game() # Reset the game with the new grid size
The Final Result:
if you want the 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