🏠 Home page > 🐍 Pygame Zero tutorials
A tutorial for Python and Pygame Zero 1.2
There is a board with 15 pieces and an empty space. Move the pieces around until they are in sequential order by using the arrow keys to move pieces into the empty space.
Arrow keys | Move piece |
The pieces are stored as a grid of numbers.
The number 16 represents the empty space.
The other numbers are swapped with the empty space when an arrow key is pressed.
At the start of the game, the grid is initially in sorted order, and random moves are made to shuffle it. (If the piece positions were totally random instead, it could result in an unsolvable board.)
After a piece has been moved, the pieces are looped through, and if they all have their initial sorted values, then the game is over.
The pieces are drawn as squares.
For now, a piece is drawn where the empty space should be.
def draw(): screen.fill((0, 0, 0)) for y in range(4): for x in range(4): piece_size = 100 piece_draw_size = piece_size - 1 screen.draw.filled_rect( Rect( x * piece_size, y * piece_size, piece_draw_size, piece_draw_size ), color=(100, 20, 150) )
The piece numbers are drawn on top of the pieces.
A piece number is calculated by adding the Y position (i.e. row number) multiplied by the number of pieces in a row to the X position plus 1.
For example, on the first row, the Y position is 0, so nothing is added to each X position, so the first number on the first row is 1. On the second row, 4 is added to each X position, so the first number on the second row is 5.
def draw(): screen.fill((0, 0, 0)) for y in range(4): for x in range(4): # etc. screen.draw.text( str(y * 4 + x + 1), (x * piece_size, y * piece_size), fontsize=60 )
A grid is created with each piece's number stored at its position on the grid, and this number is drawn.
The number of pieces on the X and Y axes are reused from drawing the pieces, so they are made into variables.
grid_x_count = 4 grid_y_count = 4 grid = [] for y in range(grid_y_count): grid.append([]) for x in range(grid_x_count): grid[y].append(y * grid_x_count + x + 1) def draw(): screen.fill((0, 0, 0)) for y in range(grid_y_count): for x in range(grid_x_count): # etc. screen.draw.text( str(grid[y][x]), (x * piece_size, y * piece_size), fontsize=60 )
The number of pieces on each axis multiplied together gives the total number of pieces (i.e. 4 times 4 means 16 pieces), and a piece is drawn only if it isn't this number.
def draw(): screen.fill((0, 0, 0)) for y in range(grid_y_count): for x in range(grid_x_count): if grid[y][x] == grid_x_count * grid_y_count: continue # etc.
The first step in moving a piece is finding the position of the empty space.
When a key is pressed, the grid is looped through, and if a piece is equal to the number of pieces on each axis multiplied together (i.e. it's the empty space), then, for now, its position is printed.
def on_key_down(key): for y in range(grid_y_count): for x in range(grid_x_count): if grid[y][x] == grid_x_count * grid_y_count: empty_x = x empty_y = y # Temporary print('empty x: ' + str(empty_x) + ', empty y: ' + str(empty_y))
empty x: 3, empty y: 3
If the Y position of the empty space is greater than 0, it means that there is a piece above the empty space, so moving a piece down is possible.
The empty space is changed to the piece number above the space, and the piece above the space is changed to the space number.
For now, any key moves a piece down.
def on_key_down(key): # etc. if empty_y > 0: changed = (grid[empty_y][empty_x], grid[empty_y - 1][empty_x]) grid[empty_y - 1][empty_x], grid[empty_y][empty_x] = changed
If the Y position of the empty space is less than number of rows of the grid, it means that there is a piece below the empty space, so moving the piece up is possible.
The Y position of the piece that the empty space swaps with is made into a variable. When the up key is pressed, it is set to the position below the empty space (i.e. plus 1 on the Y axis).
def on_key_down(key): # etc. new_empty_y = empty_y if key == keys.DOWN: new_empty_y -= 1 elif key == keys.UP: new_empty_y += 1 if 0 <= new_empty_y < grid_y_count: changed = (grid[empty_y][empty_x], grid[new_empty_y][empty_x]) grid[new_empty_y][empty_x], grid[empty_y][empty_x] = changed
The X position of the piece that the empty space swaps with is made into a variable, and it is changed when the left or right arrow is pressed.
def on_key_down(key): # etc. new_empty_y = empty_y new_empty_x = empty_x if key == keys.DOWN: new_empty_y -= 1 elif key == keys.UP: new_empty_y += 1 elif key == keys.RIGHT: new_empty_x -= 1 elif key == keys.LEFT: new_empty_x += 1 if ( 0 <= new_empty_y < grid_y_count and 0 <= new_empty_x < grid_x_count ): changed = (grid[empty_y][empty_x], grid[new_empty_y][new_empty_x]) grid[new_empty_y][new_empty_x], grid[empty_y][empty_x] = changed
At the beginning of the game, a number of random moves are made to shuffle the board.
A random number between 1 and 4 is generated and a move is made in one of the four movement directions based on this number.
The random module is imported so that random.randint can be used.
import random # etc. for move_number in range(1000): for y in range(grid_y_count): for x in range(grid_x_count): if grid[y][x] == grid_x_count * grid_y_count: empty_x = x empty_y = y new_empty_y = empty_y new_empty_x = empty_x roll = random.randint(0, 3) if roll == 0: new_empty_y -= 1 elif roll == 1: new_empty_y += 1 elif roll == 2: new_empty_x -= 1 elif roll == 3: new_empty_x += 1 if ( 0 <= new_empty_y < grid_y_count and 0 <= new_empty_x < grid_x_count ): changed = (grid[empty_y][empty_x], grid[new_empty_y][new_empty_x]) grid[new_empty_y][new_empty_x], grid[empty_y][empty_x] = changed
The only difference between the shuffling code and the keyboard controlled code is how the direction of the move is determined, so a function is made with the direction as a parameter.
def move(direction): for y in range(grid_y_count): for x in range(grid_x_count): if grid[y][x] == grid_x_count * grid_y_count: empty_x = x empty_y = y new_empty_y = empty_y new_empty_x = empty_x if direction == 'down': new_empty_y -= 1 elif direction == 'up': new_empty_y += 1 elif direction == 'right': new_empty_x -= 1 elif direction == 'left': new_empty_x += 1 if ( 0 <= new_empty_y < grid_y_count and 0 <= new_empty_x < grid_x_count ): changed = (grid[empty_y][empty_x], grid[new_empty_y][new_empty_x]) grid[new_empty_y][new_empty_x], grid[empty_y][empty_x] = changed for move_number in range(1000): move(random.choice(('down', 'up', 'right', 'left'))) def on_key_down(key): if key == keys.DOWN: move('down') elif key == keys.UP: move('up') elif key == keys.RIGHT: move('right') elif key == keys.LEFT: move('left')
So that the empty space always starts in the bottom-right corner, the pieces are moved left and up repeatedly. The number of pieces on an axis minus 1 is the maximum number of moves it would take to move the space from one side to the other.
for move_number in range(1000): move(random.choice(('down', 'up', 'right', 'left'))) for move_number in range(grid_x_count - 1): move('left') for move_number in range(grid_y_count - 1): move('up')
A function is made which sets the initial state of the game.
This function is called before the game begins and when the r key is pressed.
import random grid_x_count = 4 grid_y_count = 4 def move(direction): # etc. def reset(): global grid grid = [] for y in range(grid_y_count): grid.append([]) for x in range(grid_x_count): grid[y].append(y * grid_x_count + x + 1) for move_number in range(1000): move(random.choice(('down', 'up', 'right', 'left'))) for move_number in range(grid_x_count - 1): move('left') for move_number in range(grid_y_count - 1): move('up') reset() def on_key_down(key): # etc. elif key == keys.R: reset()
After a move is made, the pieces are looped through, and if none of the pieces are not equal to the number they were initially given (i.e. they are all in their sorted positions), then the game is reset.
def on_key_down(key): # etc. complete = True for y in range(grid_y_count): for x in range(grid_x_count): if grid[y][x] != y * grid_x_count + x + 1: complete = False if complete: reset()
The code for calculating the initial value of a piece is reused, so it is made into a function.
def get_initial_value(x, y): return y * grid_x_count + x + 1 def reset(): global grid grid = [] for y in range(grid_y_count): grid.append([]) for x in range(grid_x_count): grid[y].append(get_initial_value(x, y)) # etc. def on_key_down(key): # etc. for y in range(grid_y_count): for x in range(grid_x_count): if grid[y][x] != get_initial_value(x, y): complete = False # etc.
If the pieces are still in their initial order after shuffling, the shuffling process happens again.
The code for checking if the pieces are in their initial order is reused, so it is made into a function.
def is_complete(): for y in range(grid_y_count): for x in range(grid_x_count): if grid[y][x] != get_initial_value(x, y): return False return True def reset(): global grid grid = [] for y in range(grid_y_count): grid.append([]) for x in range(grid_x_count): grid[y].append(get_initial_value(x, y)) while True: for move_number in range(1000): move(random.choice(('down', 'up', 'right', 'left'))) for move_number in range(grid_x_count - 1): move('left') for move_number in range(grid_y_count - 1): move('up') if not is_complete(): break reset() def on_key_down(key): if key == keys.DOWN: move('down') elif key == keys.UP: move('up') elif key == keys.RIGHT: move('right') elif key == keys.LEFT: move('left') elif key == keys.R: reset() if is_complete(): reset()