How to Make a Tetris Game using PyGame in Python

Master the creation of a classic Tetris game using Pygame with this step-by-step tutorial. Learn to handle game logic, user input, and rendering while building your own customizable Tetris clone in Python.
  · 9 min read · Updated may 2023 · Game Development

Step up your coding game with AI-powered Code Explainer. Get insights like never before!

Tetris is a classic puzzle game where the objective is to arrange falling geometric pieces, called tetrominoes, to fill horizontal lines without leaving gaps. When a line is completely filled, it is cleared, and the player earns points. The game becomes progressively more challenging as the tetrominoes fall faster, making it harder to arrange them in time.

In this tutorial, we will walk you through the process of creating a simple Tetris game step by step using PyGame. We will cover the following topics:

  • Setting up the development environment and installing Pygame
  • Creating the game window and defining Tetris pieces
  • Implementing the Tetromino and Tetris classes to manage game logic
  • Handling user input and game events
  • Drawing the game state, score, and game over messages on the screen
  • Implementing the main game loop

By the end of this tutorial, you will have a fully functional Tetris game that you can play and customize. This project will also help you understand the basics of game development with Python and Pygame and serve as a foundation for creating other types of games. Here's how it'll look like:

Simple Tetris game with Pygame

Table of content:

Getting Started

Once you have Python installed, open your terminal or command prompt and enter the following command to install the Pygame library:

$ pip install pygame

Creating the Game Window

Now that our development environment is ready, let's start by creating the game window and defining the tetromino shapes.

First, we need to import the necessary modules for our Tetris game. Add the following lines to your Python script:

import sys
import pygame
import random

To use the Pygame library, we need to initialize it. Add the following line after the import statements:

pygame.init()

Defining Screen Dimensions, Colors, and Tetromino Shapes

Next, we need to define some constants for our game, such as screen dimensions, grid size, colors, and the shapes of the tetrominoes. Add the following lines to your script:

# Screen dimensions
WIDTH, HEIGHT = 800, 600
GRID_SIZE = 25

# Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
GREEN = (0, 255, 0)
COLORS = [RED, BLUE, GREEN]

# Tetromino shapes
SHAPES = [
    [
        ['.....',
         '.....',
         '.....',
         'OOOO.',
         '.....'],
        ['.....',
         '..O..',
         '..O..',
         '..O..',
         '..O..']
    ],
    [
        ['.....',
         '.....',
         '..O..',
         '.OOO.',
         '.....'],
        ['.....',
         '..O..',
         '.OO..',
         '..O..',
         '.....'],
        ['.....',
         '.....',
         '.OOO.',
         '..O..',
         '.....'],
        ['.....',
         '..O..',
         '..OO.',
         '..O..',
         '.....']
    ],
    [
        [
         '.....',
         '.....',
         '..OO.',
         '.OO..',
         '.....'],
        ['.....',
         '.....',
         '.OO..',
         '..OO.',
         '.....'],
        ['.....',
         '.O...',
         '.OO..',
         '..O..',
         '.....'],
        ['.....',
         '..O..',
         '.OO..',
         '.O...',
         '.....']
    ],
    [
        ['.....',
         '..O..',
         '..O.',
         '..OO.',
         '.....'],
        ['.....',
         '...O.',
         '.OOO.',
         '.....',
         '.....'],
        ['.....',
         '.OO..',
         '..O..',
         '..O..',
         '.....'],
        ['.....',
         '.....',
         '.OOO.',
         '.O...',
         '.....']
    ],
]

Here, we define the width and height of the game window, the grid size for the tetrominoes, and the colors we will use for the game (white, black, and red). The SHAPES list contains the tetromino shapes represented as strings, you're free to add/edit as you wish. You can also play around with the colors and grid size.

Implementing the Tetromino and Tetris Classes to Manage Game Logic

Now that we have our game window and tetromino shapes defined, let's create two classes to manage the game logic: the Tetromino class and the Tetris class.

Creating the Tetromino Class

The Tetromino class will represent a single tetromino piece, with properties for its position, shape, color, and rotation. Add the following code to your script:

class Tetromino:
    def __init__(self, x, y, shape):
        self.x = x
        self.y = y
        self.shape = shape
        self.color = random.choice(COLORS) # You can choose different colors for each shape
        self.rotation = 0

Creating the Tetris Class

The Tetris class will handle the main game logic. It will have methods for creating a new piece, checking if a move is valid, clearing lines, locking a piece, updating the game state, and drawing the game. Add the following code to your script:

class Tetris:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.grid = [[0 for _ in range(width)] for _ in range(height)]
        self.current_piece = self.new_piece()
        self.game_over = False
        self.score = 0  # Add score attribute

Let's make a method for making a new Tetromino piece:

    def new_piece(self):
        # Choose a random shape
        shape = random.choice(SHAPES)
        # Return a new Tetromino object
        return Tetromino(self.width // 2, 0, shape)

Next, the valid_move() method checks whether a piece can move to the given position:

    def valid_move(self, piece, x, y, rotation):
        """Check if the piece can move to the given position"""
        for i, row in enumerate(piece.shape[(piece.rotation + rotation) % len(piece.shape)]):
            for j, cell in enumerate(row):
                try:
                    if cell == 'O' and (self.grid[piece.y + i + y][piece.x + j + x] != 0):
                        return False
                except IndexError:
                    return False
        return True

The clear_lines() method clears the lines that are full and returns the number of cleared lines so we can calculate the score later:

    def clear_lines(self):
        """Clear the lines that are full and return the number of cleared lines"""
        lines_cleared = 0
        for i, row in enumerate(self.grid[:-1]):
            if all(cell != 0 for cell in row):
                lines_cleared += 1
                del self.grid[i]
                self.grid.insert(0, [0 for _ in range(self.width)])
        return lines_cleared

Now the method for locking the pieces:

    def lock_piece(self, piece):
        """Lock the piece in place and create a new piece"""
        for i, row in enumerate(piece.shape[piece.rotation % len(piece.shape)]):
            for j, cell in enumerate(row):
                if cell == 'O':
                    self.grid[piece.y + i][piece.x + j] = piece.color
        # Clear the lines and update the score
        lines_cleared = self.clear_lines()
        self.score += lines_cleared * 100  # Update the score based on the number of cleared lines
        # Create a new piece
        self.current_piece = self.new_piece()
        # Check if the game is over
        if not self.valid_move(self.current_piece, 0, 0, 0):
            self.game_over = True
        return lines_cleared

The update() function to move the tetromino one cell down:

    def update(self):
        """Move the tetromino down one cell"""
        if not self.game_over:
            if self.valid_move(self.current_piece, 0, 1, 0):
                self.current_piece.y += 1
            else:
                self.lock_piece(self.current_piece)

Finally, we make the method for drawing the game grid and the current piece:

    def draw(self, screen):
        """Draw the grid and the current piece"""
        for y, row in enumerate(self.grid):
            for x, cell in enumerate(row):
                if cell:
                    pygame.draw.rect(screen, cell, (x * GRID_SIZE, y * GRID_SIZE, GRID_SIZE - 1, GRID_SIZE - 1))

        if self.current_piece:
            for i, row in enumerate(self.current_piece.shape[self.current_piece.rotation % len(self.current_piece.shape)]):
                for j, cell in enumerate(row):
                    if cell == 'O':
                        pygame.draw.rect(screen, self.current_piece.color, ((self.current_piece.x + j) * GRID_SIZE, (self.current_piece.y + i) * GRID_SIZE, GRID_SIZE - 1, GRID_SIZE - 1))

Creating Helper Functions for Drawing Text in the Game

Now that we've made the Tetris class, let's make two helper functions that draw text, one for the score, and another for the "Game Over!":

def draw_score(screen, score, x, y):
    """Draw the score on the screen"""
    font = pygame.font.Font(None, 36)
    text = font.render(f"Score: {score}", True, WHITE)
    screen.blit(text, (x, y))
    
    
def draw_game_over(screen, x, y):
    """Draw the game over text on the screen"""
    font = pygame.font.Font(None, 48)
    text = font.render("Game Over", True, RED)
    screen.blit(text, (x, y))

These functions take four arguments (three in the case of draw_game_over()):

  • screen: the Pygame surface to draw on.
  • score: the current game score.
  • x: the x-coordinate of the position where the score will be displayed.
  • y: the y-coordinate of the position where the score will be displayed.

The functions create a font object using pygame.font.Font(None, 36), render the score text with the font.render() method, and then use screen.blit() to display the text on the screen.

Making the Game Loop

Before running the loop, we start by initializing Pygame, creating the clock object, and creating the game screen and the game, the Tetris object:

def main():
    # Initialize pygame
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption('Tetris')
    # Create a clock object
    clock = pygame.time.Clock()
    # Create a Tetris object
    game = Tetris(WIDTH // GRID_SIZE, HEIGHT // GRID_SIZE)

    fall_time = 0
    fall_speed = 50  # You can adjust this value to change the falling speed, it's in milliseconds

You can adjust the fall_speed to lower it (make it faster), or increase it to make it slower.

Here's the game loop:

    while True:
        # Fill the screen with black
        screen.fill(BLACK) 
        for event in pygame.event.get():
            # Check for the QUIT event
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            # Check for the KEYDOWN event
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_LEFT:
                    if game.valid_move(game.current_piece, -1, 0, 0):
                        game.current_piece.x -= 1 # Move the piece to the left
                if event.key == pygame.K_RIGHT:
                    if game.valid_move(game.current_piece, 1, 0, 0):
                        game.current_piece.x += 1 # Move the piece to the right
                if event.key == pygame.K_DOWN:
                    if game.valid_move(game.current_piece, 0, 1, 0):
                        game.current_piece.y += 1 # Move the piece down
                if event.key == pygame.K_UP:
                    if game.valid_move(game.current_piece, 0, 0, 1):
                        game.current_piece.rotation += 1 # Rotate the piece
                if event.key == pygame.K_SPACE:
                    while game.valid_move(game.current_piece, 0, 1, 0):
                        game.current_piece.y += 1 # Move the piece down until it hits the bottom
                    game.lock_piece(game.current_piece) # Lock the piece in place
        # Get the number of milliseconds since the last frame
        delta_time = clock.get_rawtime() 
        # Add the delta time to the fall time
        fall_time += delta_time 
        if fall_time >= fall_speed:
            # Move the piece down
            game.update()
            # Reset the fall time
            fall_time = 0
        # Draw the score on the screen
        draw_score(screen, game.score, 10, 10)
        # Draw the grid and the current piece
        game.draw(screen)
        if game.game_over:
            # Draw the "Game Over" message
            draw_game_over(screen, WIDTH // 2 - 100, HEIGHT // 2 - 30)  # Draw the "Game Over" message
            # You can add a "Press any key to restart" message here
            # Check for the KEYDOWN event
            if event.type == pygame.KEYDOWN:
                # Create a new Tetris object
                game = Tetris(WIDTH // GRID_SIZE, HEIGHT // GRID_SIZE)
        # Update the display
        pygame.display.flip()
        # Set the framerate
        clock.tick(60)

The main game loop is a while loop that continuously executes the following steps:

  1. Clear the screen by filling it with black.
  2. Process Pygame events, such as quitting the game or handling keyboard input for moving and rotating the Tetris pieces.
  3. Update the game state, including moving the current piece down by one row and locking the piece in place when it reaches the bottom.
  4. Draw the current score, the game grid, and the current piece on the screen.
  5. Display the "Game Over" message if the game is over.
  6. Update the Pygame display and control the frame rate with the clock object.

Final Words

That's it! This simple implementation of a Tetris game should now be functional. You can run the script to play the game and make any desired customizations to the gameplay, graphics, or controls.

You can get the complete code here.

Here are some related games built with Python:

Happy coding ♥

Want to code smarter? Our Python Code Assistant is waiting to help you. Try it now!

View Full Code Build My Python Code
Sharing is caring!



Read Also



Comment panel

    Got a coding query or need some guidance before you comment? Check out this Python Code Assistant for expert advice and handy tips. It's like having a coding tutor right in your fingertips!