🏠 Home page > 🐍 Pygame Zero tutorials
A tutorial for Python and Pygame Zero 1.2
There are seven types of pieces. Each piece contains four blocks.
Pieces fall from the top of the playing area. The player can move the pieces left and right and rotate them. When a piece comes to rest, the next piece falls.
The type of the next piece that will fall is shown above the playing area.
When an unbroken row of blocks is formed, the row disappears and all the blocks above it move down one row.
The game ends when a piece has come to rest and the next piece would immediately overlap a previously fallen block.
Left arrow | Move left |
Right arrow | Move right |
z | Rotate counterclockwise |
x | Rotate clockwise |
c | Drop |
A grid stores the inert blocks which have already fallen.
The state of a block can either be empty or filled with a block of a certain color.
The string ' ' (a space) represents an empty block, and the strings 'i', 'j', 'l', 'o', 's', 't' and 'z' represent blocks of different colors.
All the different types of pieces are stored with their rotated variations.
The currently falling piece is stored as a number representing which type of piece it is, a number representing which rotation variation it is at, and numbers representing its X and Y position in the playing area.
A new piece is created at the top of the screen, unless it would overlap an inert block, in which case the game is over.
The player can move the piece left and right, unless this new position would overlap an inert block or be outside the playing area.
After an amount of time has passed, the piece moves down, unless this new position would overlap an inert block or be outside the playing area, in which case it has come to rest.
When one of the rotate buttons is pressed, the piece changes its rotation variation, unless this variation would overlap an inert block or be outside the playing area.
When the drop button is pressed, the piece moves down until the next position would overlap an inert block or be outside the playing area, at which point it has come to rest.
When a piece comes to rest, the blocks of the piece are added to the inert blocks, and the next piece is created.
A sequence of one of each of the seven pieces in a random order is created, and the next piece is taken from this sequence. Once all of the pieces have been taken, a new random sequence is created.
A square is drawn for each block in the playing area.
def draw(): screen.fill((255, 255, 255)) for y in range(18): for x in range(10): block_size = 20 block_draw_size = block_size - 1 screen.draw.filled_rect( Rect( x * block_size, y * block_size, block_draw_size, block_draw_size ), color=(222, 222, 222) )
The grid for the inert blocks is created and every block is set to ' ' (a string containing the space character), representing an empty block.
The width and height of the grid in blocks is reused from drawing the blocks, so they are made into variables.
grid_x_count = 10 grid_y_count = 18 inert = [] for y in range(grid_y_count): inert.append([]) for x in range(grid_x_count): inert[y].append(' ') def draw(): screen.fill((255, 255, 255)) for y in range(grid_y_count): for x in range(grid_x_count): # etc.
When blocks are drawn, the color is set based on what type the block is.
To test this, some blocks in the inert grid are set to different types.
# etc. # Temporary inert[17][0] = 'i' inert[16][1] = 'j' inert[15][2] = 'l' inert[14][3] = 'o' inert[13][4] = 's' inert[12][5] = 't' inert[11][6] = 'z' def draw(): screen.fill((255, 255, 255)) for y in range(grid_y_count): for x in range(grid_x_count): colors = { ' ': (222, 222, 222), 'i': (120, 195, 239), 'j': (236, 231, 108), 'l': (124, 218, 193), 'o': (234, 177, 121), 's': (211, 136, 236), 't': (248, 147, 196), 'z': (169, 221, 118), } block = inert[y][x] color = colors[block] block_size = 20 block_draw_size = block_size - 1 screen.draw.filled_rect( Rect( x * block_size, y * block_size, block_draw_size, block_draw_size ), color=color )
Each rotation of a piece structure is stored as a 4 by 4 grid of strings.
[ [' ', ' ', ' ', ' '], ['i', 'i', 'i', 'i'], [' ', ' ', ' ', ' '], [' ', ' ', ' ', ' '], ]
Each piece structure is stored as a list of piece rotations.
[ [ [' ', ' ', ' ', ' '], ['i', 'i', 'i', 'i'], [' ', ' ', ' ', ' '], [' ', ' ', ' ', ' '], ], [ [' ', 'i', ' ', ' '], [' ', 'i', ' ', ' '], [' ', 'i', ' ', ' '], [' ', 'i', ' ', ' '], ], ]
All of piece structures are stored in a list.
piece_structures = [ [ [ [' ', ' ', ' ', ' '], ['i', 'i', 'i', 'i'], [' ', ' ', ' ', ' '], [' ', ' ', ' ', ' '], ], [ [' ', 'i', ' ', ' '], [' ', 'i', ' ', ' '], [' ', 'i', ' ', ' '], [' ', 'i', ' ', ' '], ], ], [ [ [' ', ' ', ' ', ' '], [' ', 'o', 'o', ' '], [' ', 'o', 'o', ' '], [' ', ' ', ' ', ' '], ], ], [ [ [' ', ' ', ' ', ' '], ['j', 'j', 'j', ' '], [' ', ' ', 'j', ' '], [' ', ' ', ' ', ' '], ], [ [' ', 'j', ' ', ' '], [' ', 'j', ' ', ' '], ['j', 'j', ' ', ' '], [' ', ' ', ' ', ' '], ], [ ['j', ' ', ' ', ' '], ['j', 'j', 'j', ' '], [' ', ' ', ' ', ' '], [' ', ' ', ' ', ' '], ], [ [' ', 'j', 'j', ' '], [' ', 'j', ' ', ' '], [' ', 'j', ' ', ' '], [' ', ' ', ' ', ' '], ], ], [ [ [' ', ' ', ' ', ' '], ['l', 'l', 'l', ' '], ['l', ' ', ' ', ' '], [' ', ' ', ' ', ' '], ], [ [' ', 'l', ' ', ' '], [' ', 'l', ' ', ' '], [' ', 'l', 'l', ' '], [' ', ' ', ' ', ' '], ], [ [' ', ' ', 'l', ' '], ['l', 'l', 'l', ' '], [' ', ' ', ' ', ' '], [' ', ' ', ' ', ' '], ], [ ['l', 'l', ' ', ' '], [' ', 'l', ' ', ' '], [' ', 'l', ' ', ' '], [' ', ' ', ' ', ' '], ], ], [ [ [' ', ' ', ' ', ' '], ['t', 't', 't', ' '], [' ', 't', ' ', ' '], [' ', ' ', ' ', ' '], ], [ [' ', 't', ' ', ' '], [' ', 't', 't', ' '], [' ', 't', ' ', ' '], [' ', ' ', ' ', ' '], ], [ [' ', 't', ' ', ' '], ['t', 't', 't', ' '], [' ', ' ', ' ', ' '], [' ', ' ', ' ', ' '], ], [ [' ', 't', ' ', ' '], ['t', 't', ' ', ' '], [' ', 't', ' ', ' '], [' ', ' ', ' ', ' '], ], ], [ [ [' ', ' ', ' ', ' '], [' ', 's', 's', ' '], ['s', 's', ' ', ' '], [' ', ' ', ' ', ' '], ], [ ['s', ' ', ' ', ' '], ['s', 's', ' ', ' '], [' ', 's', ' ', ' '], [' ', ' ', ' ', ' '], ], ], [ [ [' ', ' ', ' ', ' '], ['z', 'z', ' ', ' '], [' ', 'z', 'z', ' '], [' ', ' ', ' ', ' '], ], [ [' ', 'z', ' ', ' '], ['z', 'z', ' ', ' '], ['z', ' ', ' ', ' '], [' ', ' ', ' ', ' '], ], ], ]
The currently falling piece is represented by a number indicating which type it is (which will be used to index the list of piece structures), and a number indicating which rotation it is at (which will be used to index the list of rotations).
# etc. piece_type = 0 piece_rotation = 0
The piece is drawn by looping through its structure, and, unless the block is empty, drawing a square with a color determined by the block type.
def draw(): # etc. for y in range(4): for x in range(4): block = piece_structures[piece_type][piece_rotation][y][x] if block != ' ': colors = { ' ': (222, 222, 222), 'i': (120, 195, 239), 'j': (236, 231, 108), 'l': (124, 218, 193), 'o': (234, 177, 121), 's': (211, 136, 236), 't': (248, 147, 196), 'z': (169, 221, 118), } color = colors[block] block_size = 20 block_draw_size = block_size - 1 screen.draw.filled_rect( Rect( x * block_size, y * block_size, block_draw_size, block_draw_size ), color=color )
The code for drawing an inert block and drawing a block of the falling piece is similar, so a function is made.
def draw(): screen.fill((255, 255, 255)) def draw_block(block, x, y): colors = { ' ': (222, 222, 222), 'i': (120, 195, 239), 'j': (236, 231, 108), 'l': (124, 218, 193), 'o': (234, 177, 121), 's': (211, 136, 236), 't': (248, 147, 196), 'z': (169, 221, 118), } color = colors[block] block_size = 20 block_draw_size = block_size - 1 screen.draw.filled_rect( Rect( x * block_size, y * block_size, block_draw_size, block_draw_size ), color=color ) for y in range(grid_y_count): for x in range(grid_x_count): draw_block(inert[y][x], x, y) for y in range(4): for x in range(4): block = piece_structures[piece_type][piece_rotation][y][x] if block != ' ': draw_block(block, x, y)
When the x key is pressed, the piece's rotation number is increased by 1, rotating the piece clockwise.
If the rotation number is greater than the number of rotation positions minus 1, the rotation number is set to 0 (i.e. the first rotation position).
Likewise, when the z key is pressed, the piece rotation number is decreased by 1, rotating the piece counterclockwise.
If the rotation number is less than 0, the rotation number is set to the number of rotation positions minus 1 (i.e. the last rotation position).
def on_key_down(key): global piece_rotation if key == keys.X: piece_rotation += 1 if piece_rotation > len(piece_structures[piece_type]) - 1: piece_rotation = 0 elif key == keys.Z: piece_rotation -= 1 if piece_rotation < 0: piece_rotation = len(piece_structures[piece_type]) - 1
For testing purposes, the up and down arrows cycle through the piece types.
def on_key_down(key): global piece_rotation global piece_type # etc. # Temporary elif key == keys.DOWN: piece_type += 1 if piece_type > len(piece_structures) - 1: piece_type = 0 piece_rotation = 0 # Temporary elif key == keys.UP: piece_type -= 1 if piece_type < 0: piece_type = len(piece_structures) - 1 piece_rotation = 0
The position of the piece in the playing area is stored, and the piece is drawn at that position.
# etc. piece_x = 3 piece_y = 0 def draw(): # etc. for y in range(4): for x in range(4): block = piece_structures[piece_type][piece_rotation][y][x] if block != ' ': draw_block(block, x + piece_x, y + piece_y)
The left and right arrows subtract or add 1 to the piece's X position.
def on_key_down(key): # etc. global piece_x # etc. elif key == keys.LEFT: piece_x -= 1 elif key == keys.RIGHT: piece_x += 1 # etc.
Pieces will fall every 0.5 seconds.
A timer variable starts at 0 and increases by dt each frame.
When the timer is at or above 0.5 it is reset to 0.
For now, 'tick' is printed every time the piece will fall.
# etc. timer = 0 def update(dt): global timer timer += dt if timer >= 0.5: timer = 0 # Temporary print('tick')
The timer is used to increase the piece's Y position every 0.5 seconds.
def update(dt): global timer global piece_y timer += dt if timer >= 0.5: timer = 0 piece_y += 1
To prevent the piece from moving off the left or right of the screen when it is moved or rotated, each of its blocks are checked to see if they are within the playing area before the piece is moved or rotated.
Because this checking will be done in multiple places, it will be written as a function. This function is given the position and rotation to check, and returns True or False depending on whether the piece can move or rotate.
To begin with, this function will always return True, so moving and rotating is still always possible.
The code is changed from immediately setting positions/rotations, to creating variables for the changed values, and if the checking function returns True, the actual position/rotation is set to the changed values.
def can_piece_move(test_x, test_y, test_rotation): return True def update(dt): global timer global piece_y timer += dt if timer >= 0.5: timer = 0 test_y = piece_y + 1 if can_piece_move(piece_x, test_y, piece_rotation): piece_y = test_y def on_key_down(key): global piece_rotation global piece_type global piece_x if key == keys.X: test_rotation = piece_rotation + 1 if test_rotation > len(piece_structures[piece_type]) - 1: test_rotation = 0 if can_piece_move(piece_x, piece_y, test_rotation): piece_rotation = test_rotation elif key == keys.Z: test_rotation = piece_rotation - 1 if test_rotation < 0: test_rotation = len(piece_structures[piece_type]) - 1 if can_piece_move(piece_x, piece_y, test_rotation): piece_rotation = test_rotation elif key == keys.LEFT: test_x = piece_x - 1 if can_piece_move(test_x, piece_y, piece_rotation): piece_x = test_x elif key == keys.RIGHT: test_x = piece_x + 1 if can_piece_move(test_x, piece_y, piece_rotation): piece_x = test_x
If any block is not empty and its X position is less than 0 (i.e. off the left of the playing area), then the function returns False.
def can_piece_move(test_x, test_y, test_rotation): for y in range(4): for x in range(4): if ( piece_structures[piece_type][test_rotation][y][x] != ' ' and (test_x + x) < 0 ): return False return True
The number of blocks each piece has on the X and Y axes are reused from drawing the pieces, so variables are made for them.
piece_x_count = 4 piece_y_count = 4 def can_piece_move(test_x, test_y, test_rotation): for y in range(piece_y_count): for x in range(piece_x_count): if ( piece_structures[piece_type][test_rotation][y][x] != ' ' and (test_x + x) < 0 ): return False return True def draw(): # etc. for y in range(piece_y_count): for x in range(piece_x_count): block = piece_structures[piece_type][piece_rotation][y][x] if block != ' ': draw_block(block, x + piece_x, y + piece_y)
If any block's X position is greater than or equal to the width of the playing area (i.e. off the right of the playing area), then the function also returns False.
def can_piece_move(test_x, test_y, test_rotation): for y in range(piece_y_count): for x in range(piece_x_count): if ( piece_structures[piece_type][test_rotation][y][x] != ' ' and ( (test_x + x) < 0 or (test_x + x) >= grid_x_count ) ): return False return True
If any block's Y position is greater than or equal to the height of the playing area (i.e off the bottom of the playing area), then the function also returns False.
def can_piece_move(test_x, test_y, test_rotation): for y in range(piece_y_count): for x in range(piece_x_count): if ( piece_structures[piece_type][test_rotation][y][x] != ' ' and ( (test_x + x) < 0 or (test_x + x) >= grid_x_count or (test_y + y) >= grid_y_count ) ): return False return True
If there is an inert block at any block's position, then the function also returns False.
To test this, an inert block is manually set.
def can_piece_move(test_x, test_y, test_rotation): for y in range(piece_y_count): for x in range(piece_x_count): if ( piece_structures[piece_type][test_rotation][y][x] != ' ' and ( (test_x + x) < 0 or (test_x + x) >= grid_x_count or (test_y + y) >= grid_y_count or inert[test_y + y][test_x + x] != ' ' ) ): return False return True # Temporary inert[7][4] = 'z'
The calculated block positions to test are reused, so they are stored in variables.
def can_piece_move(test_x, test_y, test_rotation): for y in range(piece_y_count): for x in range(piece_x_count): test_block_x = test_x + x test_block_y = test_y + y if ( piece_structures[piece_type][test_rotation][y][x] != ' ' and ( test_block_x < 0 or test_block_x >= grid_x_count or test_block_y >= grid_y_count or inert[test_block_y][test_block_x] != ' ' ) ): return False return True
When the c key is pressed, the piece's Y position is increased by 1 while that position is movable.
def on_key_down(key): # etc. elif key == keys.C: while can_piece_move(piece_x, piece_y + 1, piece_rotation): piece_y += 1
If the timer ticks and the piece can't move down, the piece is reset to its initial position and rotation, and (for now) its initial type.
def update(dt): global timer global piece_y global piece_x global piece_type global piece_rotation timer += dt if timer >= 0.5: timer = 0 test_y = piece_y + 1 if can_piece_move(piece_x, test_y, piece_rotation): piece_y = test_y else: piece_x = 3 piece_y = 0 piece_type = 0 piece_rotation = 0
The piece is set to its initial state in two places, so a function is made.
def new_piece(): global piece_x global piece_y global piece_type global piece_rotation piece_x = 3 piece_y = 0 piece_type = 0 piece_rotation = 0 new_piece() def update(dt): global timer global piece_y # Removed: global piece_x # Removed: global piece_type # Removed: global piece_rotation timer += dt if timer >= 0.5: timer = 0 test_y = piece_y + 1 if can_piece_move(piece_x, test_y, piece_rotation): piece_y = test_y else: new_piece()
The sequence of next pieces is stored as a list containing the numbers representing piece types in a random order.
A list is created from a range from 0 to one less than the length of piece_structures, and is then shuffled.
To test this, a new sequence is created when the s key is pressed, and the sequence is printed.
The random module is imported so that random.shuffle can be used.
import random def new_sequence(): global sequence sequence = list(range(len(piece_structures))) random.shuffle(sequence) new_sequence() def on_key_down(key): # etc. # Temporary elif key == keys.S: new_sequence() print(sequence)
[3, 2, 4, 1, 0, 5, 6]
When a new piece is created, it removes the last item from the sequence and uses it for the piece type.
When the sequence is empty, a new sequence is created.
def new_piece(): global piece_x global piece_y global piece_type global piece_rotation piece_x = 3 piece_y = 0 piece_type = sequence.pop() if len(sequence) == 0: new_sequence() piece_rotation = 0
When a piece has come to rest, the piece's blocks are added to the inert blocks.
The piece's blocks are looped through, and if a block isn't empty, then the inert block at this position is set to the type of the piece's block.
def update(dt): global timer global piece_y timer += dt if timer >= 0.5: timer = 0 test_y = piece_y + 1 if can_piece_move(piece_x, test_y, piece_rotation): piece_y = test_y else: # Add piece to inert for y in range(piece_y_count): for x in range(piece_x_count): block = piece_structures[piece_type][piece_rotation][y][x] if block != ' ': inert[piece_y + y][piece_x + x] = block new_piece()
When a piece is dropped, the timer is set immediately to the limit so that adding the piece to the inert pieces and creating the new piece happen immediately instead of waiting for the timer.
The timer limit is reused, so it is made into a variable.
timer_limit = 0.5 def update(dt): # etc. if timer >= timer_limit: # etc. def on_key_down(key): # etc. global timer # etc. elif key == keys.C: while can_piece_move(piece_x, piece_y + 1, piece_rotation): piece_y += 1 timer = timer_limit
Each row of the inert blocks is looped through, and if none of the columns of the row contain an empty block, then the row is complete.
For now, the complete row numbers are printed out.
def update(dt): global timer global piece_y timer += dt if timer >= timer_limit: timer = 0 test_y = piece_y + 1 if can_piece_move(piece_x, test_y, piece_rotation): piece_y = test_y else: # Add piece to inert for y in range(piece_y_count): for x in range(piece_x_count): block = piece_structures[piece_type][piece_rotation][y][x] if block != ' ': inert[piece_y + y][piece_x + x] = block # Find complete rows for y in range(grid_y_count): complete = True for x in range(grid_x_count): if inert[y][x] == ' ': complete = False break if complete: # Temporary print('Complete row: ' + str(y)) new_piece()
If the row is complete, the rows from the complete row to the row second from the top are looped through.
Each block in the row is looped through and set to the value of the block above it. Because there is nothing above the top row it doesn't need to be looped through.
The top row is then set to all empty blocks.
def update(dt): # etc. # Find complete rows for y in range(grid_y_count): complete = True for x in range(grid_x_count): if inert[y][x] == ' ': complete = False break if complete: for ry in range(y, 1, -1): for rx in range(grid_x_count): inert[ry][rx] = inert[ry - 1][rx] for rx in range(grid_x_count): inert[0][rx] = ' ' # etc.
If a newly created piece is in an unmovable position, then the game is over.
A function is made which sets the initial state of the game.
This function is called before the game begins and when the game is over.
piece_structures = [ # etc. ] piece_x_count = 4 piece_y_count = 4 grid_x_count = 10 grid_y_count = 18 timer_limit = 0.5 def new_sequence(): # etc. def new_piece(): # etc. def reset(): global inert global timer inert = [] for y in range(grid_y_count): inert.append([]) for x in range(grid_x_count): inert[y].append(' ') timer = 0 new_sequence() new_piece() reset() def update(dt): # etc. new_piece() if not can_piece_move(piece_x, piece_y, piece_rotation): reset()
The playing area is drawn 2 blocks from the left of the screen and 5 blocks from the top of the screen.
def draw(): # etc. offset_x = 2 offset_y = 5 for y in range(grid_y_count): for x in range(grid_x_count): draw_block(inert[y][x], x + offset_x, y + offset_y) for y in range(piece_y_count): for x in range(piece_x_count): block = piece_structures[piece_type][piece_rotation][y][x] if block != ' ': draw_block( block, x + piece_x + offset_x, y + piece_y + offset_y )
The last piece of the sequence (i.e. the next piece to fall) is drawn at its first rotation position. It is offset 5 blocks from the left and 1 block from the top.
def draw(): screen.fill((255, 255, 255)) def draw_block(block, x, y): colors = { ' ': (222, 222, 222), 'i': (120, 195, 239), 'j': (236, 231, 108), 'l': (124, 218, 193), 'o': (234, 177, 121), 's': (211, 136, 236), 't': (248, 147, 196), 'z': (169, 221, 118), 'preview': (190, 190, 190), } # etc. for y in range(piece_y_count): for x in range(piece_x_count): block = piece_structures[sequence[-1]][0][y][x] if block != ' ': draw_block('preview', x + 5, y + 1)