Home page > Pygame Zero tutorials

Blocks

A tutorial for Python and Pygame Zero 1.2

Download blocks.py

Rules

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.

Controls

Left arrowMove left
Right arrowMove right
zRotate counterclockwise
xRotate clockwise
cDrop

Overview

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.

Coding

Drawing the grid of blocks

A square is drawn for each block in the playing area.

Full code at this point

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)
            )

Storing inert blocks

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.

Full code at this point

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.

Setting block color

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.

Full code at this point

# 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
            )

Storing the piece structures

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', ' ', ' ', ' '],
            [' ', ' ', ' ', ' '],
        ],
    ],
]

Storing the current piece

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

Drawing the piece

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.

Full code at this point

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
                )

Simplifying code

The code for drawing an inert block and drawing a block of the falling piece is similar, so a function is made.

Full code at this point

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)

Rotation

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).

Full code at this point

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

Testing pieces

For testing purposes, the up and down arrows cycle through the piece types.

Full code at this point

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

Setting piece position

The position of the piece in the playing area is stored, and the piece is drawn at that position.

Full code at this point

# 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)

Moving the piece

The left and right arrows subtract or add 1 to the piece's X position.

Full code at this point

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.

Timer

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.

Full code at this point

# etc.

timer = 0

def update(dt):
    global timer

    timer += dt
    if timer >= 0.5:
        timer = 0
        # Temporary
        print('tick')

Falling

The timer is used to increase the piece's Y position every 0.5 seconds.

Full code at this point

def update(dt):
    global timer
    global piece_y

    timer += dt
    if timer >= 0.5:
        timer = 0
        piece_y += 1

Confining movement

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.

Full code at this point

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

Checking left of playing area

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.

Full code at this point

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

Simplifying code

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.

Full code at this point

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)

Checking right of playing area

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.

Full code at this point

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

Checking bottom of playing area

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.

Full code at this point

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

Checking inert

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.

Full code at this point

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'

Simplifying code

The calculated block positions to test are reused, so they are stored in variables.

Full code at this point

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

Drop

When the c key is pressed, the piece's Y position is increased by 1 while that position is movable.

Full code at this point

def on_key_down(key):
    # etc.

    elif key == keys.C:
        while can_piece_move(piece_x, piece_y + 1, piece_rotation):
            piece_y += 1

Resetting piece

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.

Full code at this point

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

Simplifying code

The piece is set to its initial state in two places, so a function is made.

Full code at this point

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()

Creating the sequence of next pieces

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.

Full code at this point

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]

New piece from sequence

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.

Full code at this point

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

Add to inert

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.

Full code at this point

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()

New piece immediately after drop

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.

Full code at this point

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

Finding complete rows

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.

Full code at this point

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()

Removing complete rows

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.

Full code at this point

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.

Game over

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.

Full code at this point

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()

Offsetting the playing area

The playing area is drawn 2 blocks from the left of the screen and 5 blocks from the top of the screen.

Full code at this point

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
                )

Drawing the upcoming piece

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.

Full code at this point

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)