How to Create a Tetris Game With Python Pygame Library

Introduction

Tetris is a classic puzzle video game created by Russian software engineer Alexey Pajitnov in 1984. In Tetris, players are tasked with arranging falling blocks called Tetriminos that come in various shapes (composed of four square blocks each) to form complete horizontal lines without any gaps. When a line is completed, it disappears, and any blocks above it will fall down to fill the space.

Welcome! Today, we’re diving into an exciting journey as we craft a graphical-based Tetris game using the powerful Python gaming library, Pygame. Get ready to embark on a hands-on tutorial filled with creativity, challenges, and the thrill of bringing this classic game to life on your screen.

Setting Up Your Environment and Installing Dependencies

To begin creating the Tetris game with Pygame, we’ll set up a virtual environment to manage our project dependencies. Using a virtual environment ensures that our project’s dependencies are isolated from the system-wide Python installation, providing better dependency management and avoiding conflicts between different projects.

Firstly, we’ll create a virtual environment by running the following command in our terminal or command prompt.

Bash
$ python3 -m venv tetris_venv

This command creates a virtual environment named tetris_venv in the current directory. We can activate the virtual environment using platform-specific commands.

  • On Windows
Bash
$ tetris_venv\Scripts\activate
  • On macOS and Linux
Bash
$ source tetris_venv/bin/activate

Once the virtual environment is activated, we can proceed to install the required module, Pygame. Pygame is a set of Python modules designed for writing video games. We’ll install it using pip, Python’s package manager.

Bash
$ pip install pygame

Now that Pygame is installed, we’re prepared to craft the Python script for our Tetris game. Let’s proceed by creating a Python file, such as tetris_game.py, within our project directory.

Initializing Pygame

import pygame
import random

At the outset of the code, two crucial elements are imported: the Pygame library and the random module.

The line import pygame brings in the Pygame library, a powerful toolset extensively used for developing games in Python. Pygame provides a range of functionalities, including creating windows, handling events like keyboard and mouse input, and rendering graphics.

Next, import random imports the random module, a standard Python library that allows for the generation of random numbers and choices. In the context of this Tetris game, the random module is employed to select Tetrominos and their colors randomly, adding variability and unpredictability to the gameplay experience.

Setting Up the Tetris Environment

# Define some colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (128, 128, 128)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
CYAN = (0, 255, 255)
MAGENTA = (255, 0, 255)
YELLOW = (255, 255, 0)

# Define the shapes of the tetrominos
tetrominos = [
    [[1, 1, 1],
     [0, 1, 0]],  # T
    [[0, 2, 2],
     [2, 2, 0]],  # S
    [[3, 3, 0],
     [0, 3, 3]],  # Z
    [[4, 4],
     [4, 4]],  # O
    [[5, 5, 5, 5]],  # I
    [[0, 0, 6],
     [6, 6, 6]],  # J
    [[7, 0, 0],
     [7, 7, 7]],  # L
]

# Define the size of the grid and the size of each cell
GRID_WIDTH = 10
GRID_HEIGHT = 20
CELL_SIZE = 30

# Define the width and height of the screen
SCREEN_WIDTH = GRID_WIDTH * CELL_SIZE
SCREEN_HEIGHT = GRID_HEIGHT * CELL_SIZE

# Initialize Pygame
pygame.init()

# Set up the screen
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Tetris")

# Clock for controlling the frame rate
clock = pygame.time.Clock()

In this segment, several fundamental components of the Tetris game are established.

Firstly, a palette of colors is defined, each represented by RGB (Red, Green, Blue) tuples. These colors will be utilized throughout the game interface to render various elements. For instance, BLACK is defined as (0, 0, 0), while WHITE is (255, 255, 255), providing essential contrasts for visual clarity. Additionally, colors like RED, GREEN, BLUE, CYAN, MAGENTA, and YELLOW add vibrancy to the game’s visual presentation.

Following the color definitions, the shapes of the Tetrominos are outlined. Tetrominos are the fundamental building blocks of Tetris, each composed of four squares. The Tetrominos are represented as nested lists, with each inner list corresponding to a row of the Tetromino shape. Numerical values within these lists denote different block types, visually representing the Tetrominos. For instance, the “T” Tetromino is represented by the list [[1, 1, 1], [0, 1, 0]], while the “S” Tetromino is denoted as [[0, 2, 2], [2, 2, 0]], and so forth.

Further, the dimensions of the game grid are defined, specifying the width and height of the grid as well as the size of each cell within the grid. These parameters determine the overall layout and scale of the game’s playing field.

Pygame is then initialized with pygame.init(), preparing the Pygame framework for use. Subsequently, a game window is set up using pygame.display.set_mode() to create a screen with dimensions determined by the grid size and cell size. Additionally, a window title, “Tetris”, is set using pygame.display.set_caption().

Lastly, a clock object is initialized with pygame.time.Clock() to control the frame rate of the game, ensuring smooth and consistent gameplay. This clock will regulate the rate at which the game’s loop executes, maintaining a consistent speed across different systems.

Creating a New Tetromino

def new_tetromino():
    tetromino = random.choice(tetrominos)
    color = random.choice([RED, GREEN, BLUE, CYAN, MAGENTA, YELLOW])
    x = GRID_WIDTH // 2 - len(tetromino[0]) // 2
    y = 0
    return {"shape": tetromino, "color": color, "x": x, "y": y}

In this function, we define a method to generate a new Tetromino, a crucial element in our Tetris game. First, we randomly select a Tetromino shape from our predefined list of Tetrominos using random.choice(tetrominos). Additionally, we randomly choose a color for the Tetromino from a predefined set of colors, including RED, GREEN, BLUE, CYAN, MAGENTA, and YELLOW.

Next, we determine the initial position of the Tetromino within the game grid. The x-coordinate is calculated by finding the center of the grid (GRID_WIDTH // 2) and adjusting it based on the width of the selected Tetromino shape (len(tetromino[0]) // 2). This ensures that the Tetromino appears centered horizontally within the grid. The y-coordinate is initialized to 0, indicating that the Tetromino starts at the top of the grid.

Finally, we construct and return a dictionary representing the newly created Tetromino. This dictionary contains key-value pairs for the Tetromino’s shape, color, x-coordinate, and y-coordinate. By encapsulating this information in a dictionary, we can easily manage and manipulate Tetrominos within our game environment.

Drawing a Tetromino on the Screen

def draw_tetromino(tetromino, dx=0, dy=0):
    for y, row in enumerate(tetromino["shape"]):
        for x, cell in enumerate(row):
            if cell:
                pygame.draw.rect(screen, tetromino["color"], pygame.Rect((tetromino["x"] + x + dx) * CELL_SIZE, (tetromino["y"] + y + dy) * CELL_SIZE, CELL_SIZE, CELL_SIZE), 0)

In this function, we define the process of rendering a Tetromino onto the game screen. It takes parameters including the Tetromino to be drawn (tetromino), along with optional parameters dx and dy representing any horizontal or vertical offset from the Tetromino’s base position.

We iterate over each row and column of the Tetromino’s shape using nested loops. For each cell in the shape, we check if it contains a block (non-zero value). If so, we draw a rectangle representing the block on the screen using Pygame’s pygame.draw.rect() function.

The position of each block is calculated based on the Tetromino’s coordinates (tetromino["x"], tetromino["y"]) and the current iteration indices (x, y). Any specified offsets (dx, dy) are also taken into account to adjust the position.

Finally, the rectangle is drawn with the specified color (tetromino["color"]) and dimensions (CELL_SIZE), ensuring that each block of the Tetromino is rendered accurately on the screen.

Checking for Collision with the Grid or Other Tetrominos

def is_collision(tetromino, dx=0, dy=0):
    for y, row in enumerate(tetromino["shape"]):
        for x, cell in enumerate(row):
            if cell:
                if tetromino["x"] + x + dx < 0 or tetromino["x"] + x + dx >= GRID_WIDTH or tetromino["y"] + y + dy >= GRID_HEIGHT:
                    return True
                if grid[tetromino["y"] + y + dy][tetromino["x"] + x + dx]:
                    return True
    return False

This function assesses whether a collision occurs between a Tetromino and the game grid or other fallen Tetrominos. It takes the Tetromino to be checked (tetromino) and optional horizontal (dx) and vertical (dy) offsets.

We iterate over each cell of the Tetromino’s shape using nested loops. For each cell containing a block (non-zero value), we assess if it collides with the boundaries of the grid or existing blocks on the grid.

If the cell extends beyond the grid’s boundaries horizontally (tetromino["x"] + x + dx < 0 or tetromino["x"] + x + dx >= GRID_WIDTH) or vertically (tetromino["y"] + y + dy >= GRID_HEIGHT), a collision is detected, and True is returned.

Similarly, if the cell overlaps with an occupied cell on the grid (grid[tetromino["y"] + y + dy][tetromino["x"] + x + dx]), indicating a collision with a fallen Tetromino, True is returned.

If no collisions are detected, the function returns False, indicating that the Tetromino can proceed without obstruction.

Drawing the Game Grid

def draw_grid():
    for y in range(GRID_HEIGHT):
        for x in range(GRID_WIDTH):
            pygame.draw.rect(screen, GRAY, pygame.Rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE), 1)

In this function, we define the process of rendering the game grid onto the screen. We iterate over each cell of the grid, drawing a rectangle to represent the boundaries of each cell.

Using nested loops, we traverse through each row and column of the grid. For each cell, we draw a rectangle using Pygame’s pygame.draw.rect() function. The position and size of each rectangle are calculated based on the cell’s coordinates and the specified cell size (CELL_SIZE).

Each rectangle is drawn with the color GRAY, representing the grid lines, and is outlined with a thickness of 1. This creates a visual grid pattern on the screen, effectively delineating the boundaries of each cell in the game grid.

Overall, this function contributes to the visual structure of the game grid, enhancing the clarity and organization of the Tetris game interface for players.

Rendering the Game State

def draw(score):
    screen.fill(BLACK)
    draw_grid()
    draw_tetromino(current_tetromino)
    for tetromino in fallen_tetrominos:
        draw_tetromino(tetromino)
    font = pygame.font.SysFont(None, 30)
    score_text = font.render("Score: " + str(score), True, WHITE)
    screen.blit(score_text, (10, 10))
    pygame.display.flip()

In this function, we define the process of rendering the current state of the game onto the screen.

First, we fill the entire screen with the color BLACK using screen.fill(BLACK), effectively clearing any previous content.

Next, we draw the game grid using the draw_grid() function, ensuring that the grid lines are displayed on the screen.

We then proceed to draw the current Tetromino (current_tetromino) onto the screen using the draw_tetromino() function. This Tetromino represents the shape currently controlled by the player.

Following that, we iterate over each fallen Tetromino in the list fallen_tetrominos, drawing each one onto the screen using draw_tetromino(). These Tetrominos have already been placed on the grid and are no longer controlled by the player.

Subsequently, we create a font object with a size of 30 using pygame.font.SysFont(None, 30). This font will be used to render the score text.

We then render the player’s score onto the screen by creating a text surface with the current score value appended to the string “Score: “. This is achieved using font.render("Score: " + str(score), True, WHITE), where the score value is converted to a string and concatenated with the “Score: ” string. The text surface is rendered with the color WHITE.

Finally, we blit (i.e., draw) the score text surface onto the screen at the specified position (10, 10), and update the display using pygame.display.flip(), ensuring that all the changes are reflected on the screen.

Overall, this function orchestrates the rendering of the game’s visual elements, including the grid, Tetrominos, and the player’s score, providing a comprehensive representation of the game state to the player.

Updating the Game Grid

def update_grid():
    for tetromino in fallen_tetrominos:
        for y, row in enumerate(tetromino["shape"]):
            for x, cell in enumerate(row):
                if cell:
                    grid[tetromino["y"] + y][tetromino["x"] + x] = 1

In this function, we define the process of updating the game grid to incorporate fallen Tetrominos.

We iterate over each fallen Tetromino in the list fallen_tetrominos. For each Tetromino, we iterate over its shape using nested loops, examining each cell. If a cell in the Tetromino’s shape contains a block (non-zero value), we update the corresponding cell on the grid to indicate that it is occupied.

Specifically, we set the value of the corresponding cell in the grid to 1, indicating that it is now filled with a block from a fallen Tetromino.

This process ensures that the game grid accurately reflects the positions of fallen Tetrominos, allowing for proper collision detection and gameplay mechanics.

Removing Completed Rows

def remove_completed_rows():
    global score
    rows_removed = 0
    y = GRID_HEIGHT - 1
    while y >= 0:
        if all(grid[y]):
            del grid[y]
            grid.insert(0, [0] * GRID_WIDTH)
            rows_removed += 1
        else:
            y -= 1
    score += rows_removed ** 2

we define the process of removing completed rows from the game grid.

Initially, we declare score as a global variable to ensure it can be modified within the function. We also initialize rows_removed to keep track of the number of rows removed.

Next, we start iterating from the bottom of the grid (GRID_HEIGHT - 1) towards the top. For each row (y), we check if all cells in that row are occupied (i.e., contain a block from a fallen Tetromino). If all cells in the row are occupied, we remove the row by deleting it from the grid using del grid[y].

After removing the completed row, we insert a new empty row at the top of the grid to maintain its original height. This is achieved by inserting a list of zeros ([0] * GRID_WIDTH) at the beginning of the grid (grid.insert(0, [0] * GRID_WIDTH)).

We increment the rows_removed variable to keep track of the number of rows removed.

Finally, we update the player’s score by adding a bonus based on the number of rows removed. The bonus is calculated by squaring the number of rows removed (rows_removed ** 2) and adding it to the current score.

Overall, this function efficiently removes completed rows from the game grid, updates the player’s score accordingly, and ensures smooth gameplay progression.

Handling User Input

def handle_input():
    global current_tetromino
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            exit()
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT and not is_collision(current_tetromino, dx=-1):
                current_tetromino["x"] -= 1
            if event.key == pygame.K_RIGHT and not is_collision(current_tetromino, dx=1):
                current_tetromino["x"] += 1
            if event.key == pygame.K_DOWN and not is_collision(current_tetromino, dy=1):
                current_tetromino["y"] += 1
            if event.key == pygame.K_UP:
                rotated_tetromino = {"shape": [[current_tetromino["shape"][j][i] for j in range(len(current_tetromino["shape"]))] for i in range(len(current_tetromino["shape"][0]))], "color": current_tetromino["color"], "x": current_tetromino["x"], "y": current_tetromino["y"]}
                if not is_collision(rotated_tetromino):
                    current_tetromino["shape"] = rotated_tetromino["shape"]

In this function, we define the process of handling user input to control the movement and rotation of Tetrominos within the game.

We begin by declaring current_tetromino as a global variable, ensuring it can be accessed and modified within the function.

We then iterate through each event in the event queue using pygame.event.get(). The event queue contains user input events such as key presses and mouse clicks.

For each event, we check its type. If the event type is pygame.QUIT, indicating that the user has attempted to close the game window, we gracefully exit the game by calling pygame.quit() to quit the Pygame module and exit() to terminate the program.

If the event type is pygame.KEYDOWN, indicating that a key has been pressed, we further check which key was pressed using event.key.

  • If the left arrow key (pygame.K_LEFT) is pressed and moving the Tetromino left (dx=-1) does not result in a collision, we decrement the x-coordinate of the Tetromino (current_tetromino["x"] -= 1).
  • Similarly, if the right arrow key (pygame.K_RIGHT) is pressed and moving the Tetromino right (dx=1) does not result in a collision, we increment the x-coordinate of the Tetromino (current_tetromino["x"] += 1).
  • If the down arrow key (pygame.K_DOWN) is pressed and moving the Tetromino down (dy=1) does not result in a collision, we increment the y-coordinate of the Tetromino (current_tetromino["y"] += 1).
  • If the up arrow key (pygame.K_UP) is pressed, indicating a request for rotation, we create a new Tetromino rotated 90 degrees clockwise. We check if the rotated Tetromino does not result in a collision with other Tetrominos or the grid boundaries. If it doesn’t, we update the shape of the current Tetromino to the shape of the rotated Tetromino.

Overall, this function allows players to control the movement and rotation of Tetrominos, facilitating interactive gameplay within the Tetris environment.

Updating the Game State

def update():
    global current_tetromino
    if is_collision(current_tetromino, dy=1):
        fallen_tetrominos.append(current_tetromino)
        current_tetromino = new_tetromino()
        update_grid()
        remove_completed_rows()
        if is_collision(current_tetromino):
            game_over()
    else:
        current_tetromino["y"] += 1

In this method, we define the process of updating the game state during each frame of the game loop.

We begin by declaring current_tetromino as a global variable to ensure it can be accessed and modified within the function.

We check if there is a collision between the current Tetromino and the game grid or other fallen Tetrominos when moving it down (dy=1). If a collision is detected, indicating that the Tetromino cannot move further down, we update the game state accordingly:

  • The current Tetromino is added to the list of fallen Tetrominos (fallen_tetrominos.append(current_tetromino)), indicating that it has landed on the grid.
  • We generate a new Tetromino (current_tetromino = new_tetromino()), which will become the next active Tetromino.
  • We update the game grid to incorporate the fallen Tetrominos (update_grid()).
  • We remove any completed rows from the game grid (remove_completed_rows()), potentially increasing the player’s score.
  • We check if the new current Tetromino immediately collides with other Tetrominos or the grid boundaries. If it does, indicating that the game is over, we call the game_over() function.

If no collision is detected when moving the current Tetromino down, we simply increment its y-coordinate (current_tetromino["y"] += 1), allowing it to continue falling within the game grid.

Overall, this function orchestrates the updating of the game state, including handling collisions, managing fallen Tetrominos, generating new Tetrominos, updating the game grid, and detecting game over conditions.

Handling Game Over

def game_over():
    font = pygame.font.SysFont(None, 50)
    game_over_text = font.render("GAME OVER", True, RED)
    screen.blit(game_over_text, (SCREEN_WIDTH // 2 - 150, SCREEN_HEIGHT // 2 - 25))
    pygame.display.flip()
    pygame.time.delay(2000)  # Display "GAME OVER" for 2 seconds
    pygame.quit()
    exit()

we define the behavior when the game is over, typically triggered when a Tetromino reaches the top of the game grid.

We first create a font object with a size of 50 using pygame.font.SysFont(None, 50). This font will be used to render the “GAME OVER” message.

Next, we create a text surface containing the message “GAME OVER” rendered in red color (RED) using the previously created font. The text surface is created with anti-aliasing enabled (True) to improve its appearance.

We then blit (i.e., draw) the “GAME OVER” text surface onto the game screen at the center horizontally (SCREEN_WIDTH // 2 - 150) and slightly above the center vertically (SCREEN_HEIGHT // 2 - 25). This ensures that the message is prominently displayed on the screen.

After displaying the “GAME OVER” message, we update the display using pygame.display.flip() to ensure that the changes are visible to the player.

We introduce a delay of 2000 milliseconds (pygame.time.delay(2000)) to keep the “GAME OVER” message on the screen for 2 seconds, allowing the player to acknowledge the end of the game.

Finally, we gracefully exit the game by calling pygame.quit() to quit the Pygame module and exit() to terminate the program.

Overall, this function provides a visual cue to the player when the game ends, displays the “GAME OVER” message on the screen, and exits the game after a brief delay.

Initializing Game State and Running the Game Loop

grid = [[0] * GRID_WIDTH for _ in range(GRID_HEIGHT)]
current_tetromino = new_tetromino()
fallen_tetrominos = []
score = 0

# Main game loop
while True:
    handle_input()
    update()
    draw(score)
    clock.tick(5)

In this section of code, we initialize the game state and execute the main game loop responsible for running the Tetris game.

  • grid: We initialize a 2D list representing the game grid. Each element in the grid is initially set to 0, indicating an empty cell. The dimensions of the grid are determined by GRID_WIDTH and GRID_HEIGHT.
  • current_tetromino: We initialize the variable current_tetromino by calling the new_tetromino() function, which generates a new Tetromino to be controlled by the player.
  • fallen_tetrominos: We initialize an empty list fallen_tetrominos to store Tetrominos that have landed and are no longer movable.
  • score: We initialize the variable score to keep track of the player’s score, starting from 0.
  • Main Game Loop: We enter a while loop that runs indefinitely (while True). This loop represents the core of the game, where the game state is updated and rendered repeatedly.
    • handle_input(): Within each iteration of the loop, we call the handle_input() function to process user input, allowing the player to control the Tetrominos.
    • update(): We call the update() function to update the game state based on the player’s input and any game mechanics, such as Tetromino movement and collision detection.
    • draw(score): We call the draw() function to render the current state of the game onto the screen, including the game grid, Tetrominos, and the player’s score.
    • clock.tick(5): We use clock.tick(5) to control the frame rate of the game. In this case, the game is set to run at 5 frames per second, ensuring smooth gameplay.

Overall, this section initializes the game state and sets up the main game loop, ensuring that the game continuously updates and renders the game state while running at a consistent frame rate.

Conclusion

Now, It’s time to gather all code blocks and compile our Tetris game script. To start the game, just type python3 tetris_game.py.

Bash
$ python3 tetris_game.py

This will launch the game window, allowing you to play the Tetris game.

tetris_game.py

Discover the entire source code on GitHub and enjoy the timeless Tetris game, made possible with Python and Pygame!


DarkLight
×