Juggling between coding languages? Let ourCode Converter help. Your one-stop solution for language conversion. Start now!
Slide puzzles, also known as 15 puzzles or sliding tile puzzles, have been a favorite pastime for generations. The challenge lies in rearranging a set of numbered tiles or images into a specific order by sliding them into an empty space.
By following this step-by-step guide, you will learn how to build a slide puzzle game from scratch, allowing you to exercise your problem-solving skills and create an engaging and interactive gaming experience.Pygame's intuitive interface and powerful functionality make it an ideal choice for developing such games, and Python's simplicity adds to the ease of implementation.
Throughout this article, we will cover various aspects of the game development process, including:
Whether you're a beginner seeking an introduction to game development or an experienced Python programmer looking to expand your skill set, this article will provide valuable insights and practical knowledge. Let's dive in and embark on this exciting journey of creating a Slide Puzzle game with Pygame!
In this section, we will create the main class that handles the game's main loop and events. We will also initialize the puzzle grid, setting it up with the desired configuration, whether it's a solved state or a randomized arrangement. Before we start, here is the game flow of our slide puzzle:

The first thing we need to do is to make sure we have Python installed. You can download Python from theofficial website.
Once Python is installed, we need to install the Pygame library:
$ pip install pygameNow that we have Python and Pygame installed, we can create the directory for our game. Create a new folder and name itSlide-Puzzle.
Inside our main directory, we need to create several Python files:main.py,game.py,frame.py,cell.py, andpiece.py.
Finally, create a folder namedpuzz-pieces inside the Slide-Puzzle directory. This folder will contain the images for our puzzle pieces. Paste inside thepuzz-pieces directory your puzzle pieces pictures. If you don't have piece pictures, you can access my piece pictureshere.
The structure of our game should look like this:

The main class serves as the backbone of the slide puzzle game, handling the game's main loop and events. It provides the structure for initializing the game, capturing user input, and displaying the game graphics:
# /* main.pyimport pygamefrom frame import Framepygame.init()pygame.font.init()class Puzzle: def __init__(self, screen): self.screen = screen self.running = True self.FPS = pygame.time.Clock() self.is_arranged = False self.font = pygame.font.SysFont("Courier New", 33) self.background_color = (255, 174, 66) self.message_color = (17, 53, 165) def _draw(self, frame): frame.draw(self.screen) pygame.display.update() def _instruction(self): instructions = self.font.render('Use Arrow Keys to Move', True, self.message_color) screen.blit(instructions,(5,460)) def main(self, frame_size): self.screen.fill("white") frame = Frame(frame_size) self._instruction() while self.running: for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False self._draw(frame) self.FPS.tick(30) pygame.quit()if __name__ == "__main__": window_size = (450, 500) screen = pygame.display.set_mode(window_size) pygame.display.set_caption("Slide Puzzle") game = Puzzle(screen) game.main(window_size[0])Let's start coding by importing the necessary modules, such aspygame and theFrame class from a module namedframe. It then initializes the Pygame modules and the font module usingpygame.init() andpygame.font.init().
Next, the code defines thePuzzle class, which serves as the main class for the slide puzzle game. In the__init__() method, various attributes are initialized, including the game screen, running status, clock for controlling the frame rate, arrangement status, font for rendering text, and colors for the background and messages.
The_draw() method takes aframe object as an argument and draws the frame on the game screen using the draw method of the frame object. It then updates the display usingpygame.display.update().
The_instruction() method renders the instruction message using the specified font and color and displays it on the screen at the given position.
Themain() method is the entry point for the game. It takes the size of the frame as an argument and sets up the game loop. Inside the loop, it handles events usingpygame.event.get() and checks for thepygame.QUIT event to exit the game. It then calls the_draw() method to draw the frame on the screen and usesself.FPS.tick(30) to limit the frame rate to 30 frames per second.
Once the game loop ends, thepygame.quit() function is called to properly close the Pygame modules and release resources.
Finally, the code creates thewindow_size object and the game screen (screen) usingpygame.display.set_mode(). It sets the window caption usingpygame.display.set_caption() and creates an instance of thePuzzle class (game). Themain() method of thePuzzle class is then called with the width of the window size to start the game.
This code sets up the basic structure for the slide puzzle game using Pygame. It initializes the necessary components, sets up the game loop, and handles basic event handling and screen updates. Further functionality can be added by implementing methods for user input, sliding logic, and puzzle completion checking.
During the puzzle initialization, we set up the necessary attributes and configurations for the game. This includes defining the game screen, managing the game's running state, setting the frame rate, and preparing the puzzle's initial arrangement.
Let's first create thePiece class which encapsulates the functionality of an individual puzzle piece:
# /* piece.pyimport pygameclass Piece: def __init__(self, piece_size, p_id): self.piece_size = piece_size self.p_id = p_id if self.p_id != 8: img_path = f'puzz-pieces/{self.p_id}.jpg' self.img = pygame.image.load(img_path) self.img = pygame.transform.scale(self.img, self.piece_size) else: self.img = NoneWithin the__init__() method, the constructor of thePiece class, two arguments are passed:piece_size andp_id.
Thepiece_size argument represents the size of the piece, andp_id represents the identifier or value of the piece.
Next, an if statement checks ifp_id is not equal to 8, which indicates that the piece is not an empty space, the code proceeds to load the corresponding image file. To load the image, the code constructs the image path dynamically using thep_id value. For example, ifp_id is 3, it will look for an image file named3.jpg in the "puzz-pieces" directory. The image is loaded usingpygame.image.load() and then scaled to the specifiedpiece_size usingpygame.transform.scale(). The resulting image is stored in theself.img attribute. Ifp_id is equal to 8, representing the empty space, theself.img attribute is set toNone to indicate that there is no image associated with the empty space piece.
Next is theCell class which defines the attributes and functionality of an individual cell in the puzzle grid:
# /* cell.pyimport pygameclass Cell: def __init__(self, row, col, cell_size, c_id): self.row = row self.col = col self.cell_size = cell_size self.width = self.cell_size[0] self.height = self.cell_size[1] self.abs_x = row * self.width self.abs_y = col * self.height self.c_id = c_id self.rect = pygame.Rect( self.abs_x, self.abs_y, self.width, self.height ) self.occupying_piece = None def draw(self, display): pygame.draw.rect(display, (0,0,0), self.rect) if self.occupying_piece != None and self.occupying_piece.p_id != 8: centering_rect = self.occupying_piece.img.get_rect() centering_rect.center = self.rect.center display.blit(self.occupying_piece.img, centering_rect.topleft)Within the__init__() method, several attributes are initialized. These attributes includerow andcol, which represent the row and column indices of the cell in the puzzle grid,cell_size, which represents the size of the cell, andc_id, which represents the identifier or value associated with the cell.
Thewidth andheight attributes are assigned based on the values ofcell_size, representing the dimensions of the cell. Theabs_x andabs_y attributes are calculated by multiplying the row and column indices with the width and height of the cell, respectively. These attributes represent the absolute position of the cell on the game screen.
Therect attribute is created usingpygame.Rect() with the parameters representing the position (self.abs_x andself.abs_y) and size (self.width andself.height) of the cell. It defines a rectangular area on the game screen that represents the cell. Theoccupying_piece attribute is initially set toNone, indicating that the cell is initially empty and not occupied by any puzzle piece.
Thedraw() method takes adisplay object as an argument and is responsible for rendering the cell on the game screen. It usespygame.draw.rect() to draw a filled rectangle with the specified dimensions and position (self.rect). Additionally, if the cell is occupied by a puzzle piece (notNone) and the occupying piece is not the empty space (represented byp_id of 8), the piece's image is drawn onto the cell usingdisplay.blit().
Now let's create theFrame class that is responsible for managing the puzzle, including generating and arranging the puzzle cells and pieces:
# /* frame.pyimport pygameimport randomfrom cell import Cellfrom piece import Piececlass Frame:def __init__(self, frame_size):self.grid_size = 3self.cell_width = frame_size // self.grid_sizeself.cell_height = frame_size // self.grid_sizeself.cell_size = (self.cell_width, self.cell_height)self.grid = self._generate_cell()self.pieces = self._generate_piece()self._setup()self.randomize_puzzle()def _generate_cell(self):cells = []c_id = 0for col in range(self.grid_size):new_row = []for row in range(self.grid_size):new_row.append(Cell(row, col, self.cell_size, c_id))c_id += 1cells.append(new_row)return cellsdef _generate_piece(self):puzzle_pieces = []p_id = 0for col in range(self.grid_size):for row in range(self.grid_size):puzzle_pieces.append(Piece(self.cell_size, p_id))p_id += 1return puzzle_piecesdef _setup(self):for row in self.grid:for cell in row:tile_piece = self.pieces[-1]cell.occupying_piece = tile_pieceself.pieces.remove(tile_piece)def randomize_puzzle(self):moves = [(0, 1),(0, -1),(1, 0),(-1, 0)]for i in range(30):shuffle_move = random.choice(moves)for row in self.grid:for cell in row:tile_x = self.grid.index(row) + shuffle_move[0]tile_y = row.index(cell) + shuffle_move[1]if tile_x >= 0 and tile_x <= 2 and tile_y >= 0 and tile_y <= 2:new_cell = self.grid[tile_x][tile_y]if new_cell.occupying_piece.img == None:c = (cell, new_cell)try:c[0].occupying_piece, c[1].occupying_piece = c[1].occupying_piece, c[0].occupying_pieceexcept:return Falseelse:continuedef draw(self, display):for cell in self.grid:cell.draw(display)The__init__() method initializes attributes includinggrid_size, representing the size of the puzzle grid, andcell_width andcell_height, representing the width and height of each cell in the grid. Thecell_size attribute is set as a tuple containingcell_width andcell_height.
The_generate_cell() method is responsible for generating the puzzle cells. It initializes an empty list calledcells and assigns an incrementalc_id value for each cell. The method iterates over the rows and columns of the grid and creates aCell object for each position, passing therow,column,cell_size, andc_id as arguments. The generated cells are appended to thecells list and returned.
The_generate_piece() method is responsible for generating the puzzle pieces. It initializes an empty list calledpuzzle_pieces and assigns an incrementalp_id value for each piece. The method iterates over the rows and columns of the grid and creates aPiece object for each position, passing thecell_size andp_id as arguments. The generated pieces are appended to thepuzzle_pieces list and returned.
The_setup() method is responsible for assigning puzzle pieces to each cell in the grid. It iterates over each row in the grid and then iterates over each cell within that row. It assigns the last puzzle piece from the list of pieces to theoccupying_piece attribute of the cell, ensuring that each cell has a unique puzzle piece. After assigning the piece, it removes it from the list of available pieces to avoid duplicate assignments.
The randomize_puzzle() method shuffles the puzzle pieces by performing a series of random moves. It uses a list of possible moves, defined as(x, y) coordinate offsets, such as(0, 1) for moving right,(0, -1) for moving left,(1, 0) for moving down, and(-1, 0) for moving up. The method iterates a fixed number of times (30 in this case) to perform the shuffling. Within each iteration, it selects a random move from the list of moves. Then, it iterates over each row in the grid, and within each row, iterates over each cell. It calculates the new coordinates of the target cell based on the current cell's position and the randomly selected move. If the target cell is within the grid boundaries and the occupying piece of the target cell is empty (represented by aNone image), it performs a swap between the occupying pieces of the current cell and the target cell.
Thedraw() method takes adisplay object as an argument and is responsible for rendering the puzzle frame on the game screen. It iterates over each cell in the grid and calls thedraw() method of theCell class to render the cell.
We can check our progress by opening the terminal and runningpython main.py inside our game directory:

This section focuses on implementing the functionality to capture arrow moves from the player, checking the validity of moves, executing the moves, and incorporating a puzzle checker to determine the completion of the puzzle. By enabling player interaction and ensuring the puzzle logic, this part adds an engaging and challenging experience to the game.
Let's create another class insidegame.py: theGame class that represents the core structure of the game and provides essential methods for controlling the gameplay:
# /* game.pyimport pygameclass Game: def __init__(self): self.font = pygame.font.SysFont("Courier New", 35) self.background_color = (255, 174, 66) self.message_color = (17, 53, 165) def arrow_key_clicked(self, click): try: if click.key == pygame.K_LEFT or click.key == pygame.K_RIGHT or click.key == pygame.K_UP or click.key == pygame.K_DOWN: return(True) except: return(False)The__init__() method serves as the constructor and is responsible for initializing certain attributes of theGame object. It assigns values forbackground_color andmessage_color, which represent the background color and message color used in the game interface, respectively.
Thearrow_key_clicked() method takes an input parameterclick, representing a keyboard event captured by the game and attempts to check if theclick event corresponds to an arrow key press. By comparing theclick.key value with the arrow key constants provided by the pygame module, namelypygame.K_LEFT,pygame.K_RIGHT,pygame.K_UP, andpygame.K_DOWN, it determines if the pressed key is an arrow key. If it is, the method returnsTrue to indicate that an arrow key has been clicked. If there is an exception during the comparison, such as a click not having a key attribute or an unexpected value, the except block is executed, and the method returnsFalse to indicate that the click event does not correspond to an arrow key press.
Let's modify the code frommain() method inPuzzle class, we will make it capture keys pressed and apply ourarrow_key_clicked() method to check if the key pressed was an arrow key:
# /* main.pyclass Puzzle: ... def main(self, frame_size): ... game = Game() while self.running: for event in pygame.event.get(): ... if event.type == pygame.KEYDOWN: if not self.is_arranged: if game.arrow_key_clicked(event): frame.handle_click(event)Within themain() method, after initializing the game screen and other necessary components, aGame object is created with the linegame = Game(). This object is used to interact with the game logic and functionalities.
In the while loop, the code captures events usingpygame.event.get() and iterates over them. For each event, it checks if the event type ispygame.KEYDOWN, which indicates a key press event. If the puzzle is not yet arranged (self.is_arranged isFalse), it further checks if the arrow key pressed by the player is detected using thegame.arrow_key_clicked(event) call. If an arrow key is clicked, it proceeds to handle the click by calling thehandle_click() method of theframe object.
Inside theFrame class fromframe.py, let's also addhandle_click() method which we'll use in handling arrow key clicks but for now, let's use it to print the arrow key clicked.
def handle_click(self, click): print(click)By running the game and clicking the arrow keys, it will print them in the terminal. In the picture below, I clicked the up arrow key:

Tip:By changing theprint(click) toprint(click.scancode) will print the scancode numbers only of the arrow keys pressed. If you run it and hit the arrow keys in this order [right, left, down, up], you'll get [79, 80, 81, 82]. The table below shows the direction of the following scancode:
79 = right80 = left81 = down82 = upAnd if you're confused about what's it for, we will use the scancodes to identify the arrow movement.
We're adding another method for theFrame class, the_is_move_valid():
# /* frame.pydef _is_move_valid(self, click):moves = {79: (0, 1),80: (0, -1),81: (1, 0),82: (-1, 0)}for row in self.grid:for cell in row:move = moves[click.scancode]tile_x = self.grid.index(row) + move[0]tile_y = row.index(cell) + move[1]if tile_x >= 0 and tile_x <= 2 and tile_y >= 0 and tile_y <= 2:new_cell = self.grid[tile_x][tile_y]if new_cell.occupying_piece.img == None:return (cell, new_cell)else:continueThe_is_move_valid() method checks if a move is valid in the puzzle game. It takes aclick object as input, which contains information about the key that was pressed. It defines a dictionarymoves that maps key scancodes to move values, where each move value is a tuple representing the change in coordinates(x, y) for a valid move.
The method iterates over each cell in the grid using nested loops. For each cell, it retrieves the move corresponding to the clicked key from themoves dictionary. It then calculates the new coordinates of the cell based on the current row index, column index, and move values. The new coordinates are stored intile_x andtile_y. Next, it checks if the new coordinates (tile_x,tile_y) are within the valid range of 0 to 2, which represents the grid size. If the new coordinates are valid, it retrieves the cell at the new coordinates from the grid. It then checks if theoccupying_piece of the new cell has aNone image, indicating an empty cell. If the new cell is empty, it returns a tuple containing the current cell and the new cell. If the new coordinates are out of range, the method continues to the next iteration of the loop to check the next cell in the grid.
Now let's get back to thehandle_click() method in theFrame class and replace the existing code from that method:
# /* frame.py def handle_click(self, click): c = self._is_move_valid(click) try: # print(c[0].c_id, c[1].c_id) c[0].occupying_piece, c[1].occupying_piece = c[1].occupying_piece, c[0].occupying_piece except: return FalseThehandle_click() method takes aclick event as a parameter. It first calls the_is_move_valid(click) method to check if the move initiated by theclick event is valid within the game's grid.
The_is_move_valid() method returns a tuplec containing the current cell (c[0]) and the new cell (c[1]) involved in the move, orNone if the move is invalid.
A try-except block is used to handle the case where the move is valid and the tuplec is notNone. In this case, the code within the try block is executed.
The line inside thetry blockswaps theoccupying_piece attribute between the current cell (c[0]) and the new cell (c[1]). This effectively moves the puzzle piece from the current cell to the new cell and vice versa. By swapping theoccupying_piece attributes, the positions of the puzzle pieces are updated according to the player's valid move. If any exception occurs during the execution of the code within the try block, it means that the move was invalid or some error occurred. In such cases, thehandle_click() method returnsFalse, indicating that the move was not successful.
In this section, we focus on implementing the functionality to check the game's state and determine if the puzzle has been successfully solved.
And for the last part of coding, we're two more methods in theGame class, theis_game_over() andmessage() methods.
# /* game.pydef is_game_over(self, frame):for row in frame.grid:for cell in row:piece_id = cell.occupying_piece.p_idif cell.c_id == piece_id:is_arranged = Trueelse:is_arranged = Falsebreakreturn is_arrangeddef message(self, screen):screen.fill(self.background_color, (5, 460, 440, 35))instructions = self.font.render('You Win!!', True, self.message_color)screen.blit(instructions,(125,460))Theis_game_over(self, frame) method in theGame class checks whether the puzzle has been successfully solved by iterating over each cell in the frame's grid. It compares thec_id (cell ID) of each cell with thep_id (piece ID) of the piece occupying that cell. If all cell IDs match their corresponding piece IDs, the puzzle is considered arranged, and the method returnsTrue. Otherwise, it returnsFalse.
Themessage(self, screen) method displays a victory message "You Win!!" on the screen upon successful puzzle completion, using the predefined font and color. It fills a screen section with a specified background color and places the message at set coordinates.
To apply the following code above, let's modify the main class (Puzzle class) once again:
# /* main.pyclass Puzzle: ... def main(self, frame_size): ... while self.running: if game.is_game_over(frame): self.is_arranged = True game.message()Themain(self, frame_size) method in thePuzzle class checks game completion usingis_game_over(frame) from theGame class. If true, it confirms the correct puzzle arrangement by setting theis_arranged attribute toTrue, blocking further key presses in the solved state. It then calls themessage() method to display a victory message, enabling the game to recognize and respond appropriately to a solved puzzle.
Let's run themain.py in the terminal and try the game. Here are some of the game snapshots:



In conclusion, we have explored the process of creating a slide puzzle game using the Pygame library in Python. We started by setting up the game, initializing the puzzle, and controlling the puzzle's movement.
We implemented features such as capturing arrow key moves, checking move validity, and executing moves. Additionally, we added functionality to check the game's state and display a victory message when the puzzle is successfully solved.
By following these steps and understanding the underlying logic, you can now hopefully create your own interactive and challenging slide puzzle game using Pygame.
Check the complete codehere.
Here are some related games built with Python:
Happy coding ♥
Take the stress out of learning Python. Meet ourPython Code Assistant – your new coding buddy. Give it a whirl!
View Full Code Explain My CodeGot a coding query or need some guidance before you comment? Check out thisPython Code Assistant for expert advice and handy tips. It's like having a coding tutor right in your fingertips!
