Building a Simple Tetris Game with Pygame: A Beginner’s Guide

Welcome, aspiring game developers and Python enthusiasts! Have you ever wanted to create your own classic games? Tetris, with its simple yet addictive gameplay, is a fantastic project to start with. In this guide, we’ll walk through the process of building a very basic version of Tetris using Pygame, a popular library for making 2D games in Python. Don’t worry if you’re new to game development; we’ll explain everything in simple terms.

What is Tetris?

Tetris is a classic puzzle video game where different-shaped blocks, called Tetrominoes, fall from the top of the screen. Your goal is to rotate and move these blocks to form complete horizontal lines at the bottom of the screen. When a line is complete, it disappears, and you score points. The game ends when the blocks stack up and reach the top of the screen.

Why Pygame?

Pygame is a set of Python modules designed for writing video games. It provides functionalities for graphics, sound, input (keyboard, mouse, joystick), and more. It’s relatively easy to learn for beginners and is excellent for creating 2D games, making it perfect for our Tetris project!

Getting Started: Prerequisites

Before we dive into coding, you’ll need two things:

  • Python: Make sure you have Python installed on your computer. You can download it from the official Python website (python.org). We recommend Python 3.x.
  • Pygame: Once Python is installed, you can install Pygame using pip, Python’s package installer.

Open your terminal or command prompt and type:

pip install pygame

This command downloads and installs the Pygame library, making it available for your Python projects.

Core Concepts of Our Tetris Game

To build Tetris, we’ll need to understand a few fundamental concepts:

  1. The Game Window: This is where our game will be displayed.
  2. Colors: We’ll define various colors for our blocks and background.
  3. The Game Grid: Tetris is played on a grid, so we need a way to represent this in our code.
  4. Tetrominoes (Shapes): The seven different block shapes.
  5. Game Loop: The heart of any game, continuously updating and drawing everything.
  6. User Input: Handling keyboard presses to move and rotate blocks.
  7. Collision Detection: Checking if a block hits the bottom, another block, or the side walls.
  8. Line Clearing: Detecting and removing complete lines.

For this simple guide, we’ll focus on setting up the window, defining colors, creating the grid, representing shapes, and implementing basic drawing and movement within the game loop. Implementing full collision detection and line clearing can get quite complex for a beginner guide, but we’ll outline the logic.

Step 1: Setting up the Pygame Window and Basic Constants

Let’s start by importing Pygame, initializing it, and setting up our game window. We’ll also define some basic constants like screen dimensions and colors.

import pygame
import random

SCREEN_WIDTH = 300
SCREEN_HEIGHT = 600
BLOCK_SIZE = 30 # Each Tetris block will be 30x30 pixels

GRID_WIDTH = SCREEN_WIDTH // BLOCK_SIZE  # 10 blocks wide
GRID_HEIGHT = SCREEN_HEIGHT // BLOCK_SIZE # 20 blocks high

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (50, 50, 50)
LIGHT_GRAY = (100, 100, 100)

CYAN = (0, 255, 255)    # I-shape
BLUE = (0, 0, 255)      # J-shape
ORANGE = (255, 165, 0)  # L-shape
YELLOW = (255, 255, 0)  # O-shape
GREEN = (0, 255, 0)     # S-shape
PURPLE = (128, 0, 128)  # T-shape
RED = (255, 0, 0)       # Z-shape

TETROMINO_COLORS = [CYAN, BLUE, ORANGE, YELLOW, GREEN, PURPLE, RED]

pygame.init() # This function initializes all the Pygame modules needed for our game.
SCREEN = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) # Creates the game window.
pygame.display.set_caption("Simple Tetris") # Sets the title of the game window.
CLOCK = pygame.time.Clock() # This helps us control the game's frame rate.
  • import pygame: Imports the Pygame library.
  • import random: We’ll use this later to pick random Tetromino shapes.
  • SCREEN_WIDTH, SCREEN_HEIGHT: Define how wide and tall our game window will be in pixels.
  • BLOCK_SIZE: Sets the size of each individual block, making our game grid.
  • GRID_WIDTH, GRID_HEIGHT: Calculate how many blocks can fit across and down the screen.
  • Color Definitions: Standard RGB (Red, Green, Blue) tuples for easy color access.
  • pygame.init(): Always call this at the beginning of your Pygame program.
  • pygame.display.set_mode(...): Creates the actual window where your game will appear.
  • pygame.display.set_caption(...): Puts text on the window’s title bar.
  • pygame.time.Clock(): Used to manage the game’s frame rate, ensuring it runs smoothly on all computers.

Step 2: Defining Tetromino Shapes

Each Tetromino is made up of four blocks. We can represent their shapes as lists of coordinates, where each coordinate is an offset from a central point. For simplicity, we’ll define their initial rotations as well.

TETROMINOES = {
    'I': [[(0, 1), (1, 1), (2, 1), (3, 1)], # Horizontal
          [(1, 0), (1, 1), (1, 2), (1, 3)]], # Vertical
    'J': [[(0, 0), (0, 1), (1, 1), (2, 1)],
          [(1, 0), (2, 0), (1, 1), (1, 2)],
          [(0, 1), (1, 1), (2, 1), (2, 2)],
          [(1, 0), (1, 1), (0, 2), (1, 2)]],
    'L': [[(2, 0), (0, 1), (1, 1), (2, 1)],
          [(1, 0), (1, 1), (1, 2), (2, 2)],
          [(0, 1), (1, 1), (2, 1), (0, 2)],
          [(0, 0), (1, 0), (1, 1), (1, 2)]],
    'O': [[(0, 0), (1, 0), (0, 1), (1, 1)]], # Only one rotation
    'S': [[(1, 0), (2, 0), (0, 1), (1, 1)],
          [(0, 0), (0, 1), (1, 1), (1, 2)]],
    'T': [[(1, 0), (0, 1), (1, 1), (2, 1)],
          [(1, 0), (0, 1), (1, 1), (1, 2)],
          [(0, 1), (1, 1), (2, 1), (1, 2)],
          [(1, 0), (1, 1), (2, 1), (1, 2)]],
    'Z': [[(0, 0), (1, 0), (1, 1), (2, 1)],
          [(1, 0), (0, 1), (1, 1), (0, 2)]]
}

TETROMINO_KEYS = list(TETROMINOES.keys()) # List of shape names for random selection
  • TETROMINOES: A dictionary where keys are the names of the shapes (like ‘I’, ‘J’, ‘L’) and values are lists of their possible rotations. Each rotation is itself a list of (x, y) tuples representing the relative positions of the blocks that make up the Tetromino.

Step 3: Drawing Functions

We need a way to draw individual blocks and the entire game grid.

def draw_block(surface, color, x, y):
    """Draws a single block on the given surface at (x, y) grid coordinates."""
    # Convert grid coordinates to pixel coordinates
    pixel_x = x * BLOCK_SIZE
    pixel_y = y * BLOCK_SIZE
    pygame.draw.rect(surface, color, (pixel_x, pixel_y, BLOCK_SIZE, BLOCK_SIZE), 0) # Fills the rectangle
    pygame.draw.rect(surface, LIGHT_GRAY, (pixel_x, pixel_y, BLOCK_SIZE, BLOCK_SIZE), 1) # Draws a border
  • draw_block(surface, color, x, y): This function takes a surface (our SCREEN), a color, and grid x, y coordinates. It converts these grid coordinates into pixel coordinates and then uses pygame.draw.rect to draw a filled rectangle (our block) and a lighter border around it.

Step 4: The Game Loop (Main Logic)

The game loop is where all the action happens. It continuously:
1. Handles Events: Checks for user input (keyboard, mouse).
2. Updates Game State: Moves blocks, checks for collisions, clears lines, etc.
3. Draws Everything: Renders the current state of the game to the screen.

def main():
    game_over = False
    current_piece = None
    current_x = 0
    current_y = 0
    current_rotation = 0
    current_color = None

    # Represents the fallen blocks on the grid
    # A 2D list where each element stores the color of the block at that position, or None if empty.
    game_grid = [[None for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]

    # --- Game Loop ---
    running = True
    while running:
        # 1. Event Handling
        for event in pygame.event.get():
            if event.type == pygame.QUIT: # User clicked the 'X' to close the window
                running = False
            elif event.type == pygame.KEYDOWN: # A key was pressed down
                if event.key == pygame.K_LEFT:
                    # Move piece left (need to add collision check later)
                    current_x -= 1
                elif event.key == pygame.K_RIGHT:
                    # Move piece right (need to add collision check later)
                    current_x += 1
                elif event.key == pygame.K_DOWN:
                    # Speed up piece fall (need to add collision check later)
                    current_y += 1
                elif event.key == pygame.K_UP:
                    # Rotate piece (need to add collision check later)
                    current_rotation = (current_rotation + 1) % len(TETROMINOES[current_piece[0]])

        # 2. Update Game State (Simplified for now)
        # If no current piece, create a new one
        if current_piece is None:
            piece_type = random.choice(TETROMINO_KEYS)
            current_piece = TETROMINOES[piece_type]
            current_color = TETROMINO_COLORS[TETROMINO_KEYS.index(piece_type)]
            current_x = GRID_WIDTH // 2 - 2 # Start roughly in the middle
            current_y = 0
            current_rotation = 0

        # Simulate gravity (piece falls slowly)
        # In a real game, this would be based on a timer
        # For this simple example, we'll just move it down every few frames or on a timer event.
        # For now, let's make it fall one block down every 60 frames (1 second at 60 FPS)
        if pygame.time.get_ticks() % 60 == 0: # This is a very basic way to simulate fall. Better to use a timer.
             current_y += 1

        # --- Basic Collision Check (Highly simplified) ---
        # For a full game, you'd check if the piece hits the bottom or other blocks.
        # If current_y goes beyond GRID_HEIGHT, or if piece collides, it 'lands'.
        # For simplicity, if it goes too low, reset it and add to grid.
        if current_y + len(current_piece[current_rotation]) > GRID_HEIGHT:
            # Piece landed, 'lock' it into the game_grid
            for dx, dy in current_piece[current_rotation]:
                if 0 <= current_x + dx < GRID_WIDTH and 0 <= current_y + dy -1 < GRID_HEIGHT:
                    game_grid[current_y + dy -1][current_x + dx] = current_color # Place block one step up
            current_piece = None # Get a new piece
            current_y = 0
            current_x = GRID_WIDTH // 2 - 2

        # 3. Drawing
        SCREEN.fill(BLACK) # Fill the background with black

        # Draw the grid lines
        for x in range(0, SCREEN_WIDTH, BLOCK_SIZE):
            pygame.draw.line(SCREEN, GRAY, (x, 0), (x, SCREEN_HEIGHT))
        for y in range(0, SCREEN_HEIGHT, BLOCK_SIZE):
            pygame.draw.line(SCREEN, GRAY, (0, y), (SCREEN_WIDTH, y))

        # Draw landed blocks
        for y_grid in range(GRID_HEIGHT):
            for x_grid in range(GRID_WIDTH):
                if game_grid[y_grid][x_grid] is not None:
                    draw_block(SCREEN, game_grid[y_grid][x_grid], x_grid, y_grid)

        # Draw the current falling piece
        if current_piece:
            for dx, dy in current_piece[current_rotation]:
                draw_block(SCREEN, current_color, current_x + dx, current_y + dy)

        # 4. Update the display
        pygame.display.flip() # Makes everything drawn visible on the screen.
        CLOCK.tick(60) # Limits the game to 60 frames per second.

    pygame.quit() # Uninitializes Pygame when the loop ends.

if __name__ == "__main__":
    main()
  • main() function: Encapsulates our game logic.
  • game_over: A flag to track if the game has ended.
  • current_piece: Stores the current falling Tetromino’s shape data.
  • current_x, current_y: The current position (top-left block) of the falling Tetromino on the grid.
  • current_rotation: Which rotation of the current Tetromino is active.
  • game_grid: A 2D list representing our playing field. Each cell will either be None (empty) or hold the color of a landed block.
  • while running:: This is our game loop. It continues as long as running is True.
  • pygame.event.get(): Gathers all recent user inputs and system events.
  • pygame.QUIT: Triggered when the user clicks the close button on the window.
  • pygame.KEYDOWN: Triggered when a key is pressed. We check event.key to see which key it was (e.g., pygame.K_LEFT for the left arrow key).
  • SCREEN.fill(BLACK): Clears the screen each frame by filling it with black. Without this, previous drawings would remain.
  • Drawing Grid Lines: We draw light gray lines to show the grid.
  • Drawing Landed Blocks: We iterate through game_grid and draw any blocks that have landed.
  • Drawing Current Piece: We draw the currently falling Tetromino using its current_x, current_y, and current_rotation.
  • pygame.display.flip(): Updates the entire screen to show what we’ve just drawn.
  • CLOCK.tick(60): Tells Pygame to pause briefly if the game is running too fast, aiming for 60 frames per second. This ensures consistent game speed.
  • pygame.quit(): Cleans up Pygame resources when the game loop finishes.

Expanding Your Game (Next Steps)

This is a very basic foundation. To make it a full Tetris game, you would need to add:

  • Robust Collision Detection: Check if the current piece can legally move or rotate without overlapping with other landed blocks or going out of bounds.
  • Landing Logic: When a piece can no longer fall, “lock” it into the game_grid (which our simplified code does, but needs more robust checking).
  • Line Clearing: After a piece lands, check if any horizontal lines are fully filled. If so, remove them and shift all blocks above down.
  • Scoring System: Keep track of the player’s score.
  • Game Over Condition: If a new piece spawns and immediately collides with existing blocks, the game is over.
  • Next Piece Display: Show the player what the next falling Tetromino will be.
  • Hold Piece: Allow players to “hold” a piece for later use.

Conclusion

You’ve just set up the basic framework for a Tetris game using Pygame! While our example is simplified, you now understand the core concepts: setting up the window, defining shapes, handling user input, and the continuous game loop. This is an excellent starting point for diving deeper into game development. Don’t hesitate to experiment with the code, add new features, and make it your own! Happy coding!

Comments

Leave a Reply