Chat
Search
Ithy Logo

Creating a Fully Functional Tetris Game in Python Using Pygame

Step-by-Step Guide to Building Tetris with Falling Blocks, Scoring, and User Controls

tetris game setup

Key Takeaways

  • Structured Game Setup: Initialize Pygame, define the game grid, and set up the main game loop.
  • Tetromino Management: Define tetromino shapes, handle rotations, and manage piece spawning.
  • User Interaction & Scoring: Implement user controls, a scoring system, and display key game information.

1. Game Setup and Initialization

Setting Up the Environment

Begin by installing Pygame, initializing the library, and setting up the game window. Define essential constants such as screen dimensions, grid size, and colors for different elements.

Installing Pygame

Ensure you have Python installed. Install Pygame using pip:

pip install pygame

Initializing Pygame and Setting Up the Game Window

Import necessary modules, initialize Pygame, and create the main game window:

import pygame
import random

# Initialize Pygame
pygame.init()

# Constants
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
GRID_SIZE = 30
GRID_WIDTH = 10
GRID_HEIGHT = 20
SIDEBAR_WIDTH = 150
FPS = 60

# Colors (RGB)
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)
ORANGE  = (255,165, 0)

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

2. Defining Tetromino Shapes and Rotation Logic

Creating Tetromino Classes

Define the seven standard tetromino shapes (I, O, T, S, Z, J, L) using 2D lists. Assign distinct colors for each shape for visual differentiation.

Tetromino Shapes

Each tetromino is represented as a list of lists, indicating occupied cells:

Tetromino Shape Matrix Color
I [[1, 1, 1, 1]]
O [[1, 1], [1, 1]]
T [[1, 1, 1], [0, 1, 0]]
S [[0, 1, 1], [1, 1, 0]]
Z [[1, 1, 0], [0, 1, 1]]
J [[1, 0, 0], [1, 1, 1]]
L [[0, 0, 1], [1, 1, 1]]

Handling Rotation

Implement rotation logic to rotate tetromino shapes clockwise:

def rotate_shape(shape):
    return [list(row)[::-1] for row in zip(*shape)]

3. Game Mechanics

Falling Pieces and Collision Detection

Manage the falling of tetrominoes, detect collisions with the grid boundaries or other settled blocks, and lock pieces in place when they can no longer move down.

Falling Mechanism

Implement a function to make the current tetromino fall at a constant rate, increasing speed as the game progresses:

def update_game():
    global fall_time, fall_speed, current_tetromino
    fall_time += clock.get_rawtime()
    clock.tick()
    if fall_time / 1000 > fall_speed:
        if not move(current_tetromino, 0, 1):
            lock_tetromino(current_tetromino, grid)
            cleared = clear_lines(grid)
            score += cleared * 100
            current_tetromino = next_tetromino
            next_tetromino = Tetromino(random.choice(SHAPES))
            if not can_move(current_tetromino, grid, 0, 0):
                game_over = True
        fall_time = 0

Collision Detection

Check if the tetromino can move to a new position without colliding:

def can_move(tetromino, grid, dx, dy, rotated_shape=None):
    shape = rotated_shape if rotated_shape else tetromino.shape
    for y, row in enumerate(shape):
        for x, cell in enumerate(row):
            if cell:
                new_x = tetromino.x + x + dx
                new_y = tetromino.y + y + dy
                if new_x < 0 or new_x >= GRID_WIDTH or new_y >= GRID_HEIGHT:
                    return False
                if new_y >= 0 and grid[new_y][new_x]:
                    return False
    return True

Locking Tetrominoes

Once a tetromino cannot move further down, lock it into the grid:

def lock_tetromino(tetromino, grid):
    for y, row in enumerate(tetromino.shape):
        for x, cell in enumerate(row):
            if cell:
                grid[tetromino.y + y][tetromino.x + x] = tetromino.color

4. User Interface and Controls

Handling User Input

Implement controls to allow the user to move and rotate tetrominoes using arrow keys. Also, add functionality to perform a hard drop.

User Input

Handle user events for movement and rotation:

for event in pygame.event.get():
    if event.type == pygame.QUIT:
        running = False
    if event.type == pygame.KEYDOWN:
        if event.key == pygame.K_LEFT and can_move(current_tetromino, grid, -1, 0):
            current_tetromino.x -= 1
        elif event.key == pygame.K_RIGHT and can_move(current_tetromino, grid, 1, 0):
            current_tetromino.x += 1
        elif event.key == pygame.K_DOWN and can_move(current_tetromino, grid, 0, 1):
            current_tetromino.y += 1
        elif event.key == pygame.K_UP:
            rotated = rotate_shape(current_tetromino.shape)
            if can_move(current_tetromino, grid, 0, 0, rotated):
                current_tetromino.shape = rotated
        elif event.key == pygame.K_SPACE:
            while can_move(current_tetromino, grid, 0, 1):
                current_tetromino.y += 1
            lock_tetromino(current_tetromino, grid)
            cleared = clear_lines(grid)
            score += cleared * 100
            current_tetromino = next_tetromino
            next_tetromino = Tetromino(random.choice(SHAPES))
            if not can_move(current_tetromino, grid, 0, 0):
                game_over = True

Displaying Score and Next Piece

Show the current score and the next tetromino to give players foresight:

Scoring System

Implement a scoring system that rewards line clears:

score += cleared * 100

Next Piece Preview

Display the next tetromino in a sidebar:

def draw_next_piece(screen, tetromino):
    font = pygame.font.SysFont('comicsans', 30)
    label = font.render('Next Piece', 1, WHITE)
    screen.blit(label, (GRID_WIDTH * GRID_SIZE + 20, 20))
    for y, row in enumerate(tetromino.shape):
        for x, cell in enumerate(row):
            if cell:
                pygame.draw.rect(screen, tetromino.color, 
                                 (GRID_WIDTH * GRID_SIZE + 20 + x * GRID_SIZE, 
                                  60 + y * GRID_SIZE, GRID_SIZE, GRID_SIZE))

Start Button and Game Over Screen

Add a start button to begin the game and display a game over message when the game ends:

start_button = pygame.Rect(GRID_WIDTH * GRID_SIZE + 50, GRID_HEIGHT * GRID_SIZE // 2 - 25, 100, 50)

# Within the event loop
if event.type == pygame.MOUSEBUTTONDOWN and show_start_screen:
    if start_button.collidepoint(event.pos):
        show_start_screen = False

# Drawing the start button
pygame.draw.rect(screen, WHITE, start_button)
font = pygame.font.SysFont('comicsans', 30)
text = font.render('Start Game', True, BLACK)
screen.blit(text, (start_button.x + 10, start_button.y + 10))

5. Rendering and Display

Drawing the Game Elements

Use Pygame's drawing functions to render the grid, tetrominoes, next piece, and score on the screen:

Drawing the Grid and Tetrominoes

def draw_grid(screen, grid):
    for y in range(GRID_HEIGHT):
        for x in range(GRID_WIDTH):
            cell = grid[y][x]
            if cell:
                pygame.draw.rect(screen, cell, (x * GRID_SIZE, y * GRID_SIZE, GRID_SIZE, GRID_SIZE))
            pygame.draw.rect(screen, GRAY, (x * GRID_SIZE, y * GRID_SIZE, GRID_SIZE, GRID_SIZE), 1)

def draw_tetromino(screen, tetromino):
    for y, row in enumerate(tetromino.shape):
        for x, cell in enumerate(row):
            if cell:
                pygame.draw.rect(screen, tetromino.color, 
                                 ((tetromino.x + x) * GRID_SIZE, 
                                  (tetromino.y + y) * GRID_SIZE, GRID_SIZE, GRID_SIZE))

Rendering Updates

Continuously update the display within the game loop:

pygame.display.flip()
clock.tick(FPS)

6. Comprehensive Code Implementation

Complete Tetris Game Code

Below is the full Python code for a Tetris game using Pygame that incorporates all the necessary features:

import pygame
import random

# Initialize Pygame
pygame.init()

# Constants
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
GRID_SIZE = 30
GRID_WIDTH = 10
GRID_HEIGHT = 20
SIDEBAR_WIDTH = 150
FPS = 60

# Colors (RGB)
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)
ORANGE  = (255,165, 0)

# Tetromino shapes
SHAPES = [
    [[1, 1, 1, 1]],  # I
    [[1, 1], [1, 1]],  # O
    [[1, 1, 1], [0,1,0]],  # T
    [[0,1,1], [1,1,0]],  # S
    [[1,1,0], [0,1,1]],  # Z
    [[1,0,0], [1,1,1]],  # J
    [[0,0,1], [1,1,1]]   # L
]
COLORS = [CYAN, YELLOW, MAGENTA, GREEN, RED, BLUE, ORANGE]

# Function to rotate shapes
def rotate_shape(shape):
    return [list(row)[::-1] for row in zip(*shape)]

class Tetromino:
    def __init__(self, shape):
        self.shape = shape
        self.color = COLORS[SHAPES.index(shape)]
        self.x = GRID_WIDTH // 2 - len(shape[0]) // 2
        self.y = 0
        self.rotation = 0

    def rotate(self):
        self.shape = rotate_shape(self.shape)

def create_grid():
    return [[0 for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]

def can_move(tetromino, grid, dx, dy, rotated_shape=None):
    shape = rotated_shape if rotated_shape else tetromino.shape
    for y, row in enumerate(shape):
        for x, cell in enumerate(row):
            if cell:
                new_x = tetromino.x + x + dx
                new_y = tetromino.y + y + dy
                if new_x < 0 or new_x >= GRID_WIDTH or new_y >= GRID_HEIGHT:
                    return False
                if new_y >= 0 and grid[new_y][new_x]:
                    return False
    return True

def lock_tetromino(tetromino, grid):
    for y, row in enumerate(tetromino.shape):
        for x, cell in enumerate(row):
            if cell:
                grid[tetromino.y + y][tetromino.x + x] = tetromino.color

def clear_lines(grid):
    lines_cleared = 0
    for i in range(len(grid)-1, -1, -1):
        if 0 not in grid[i]:
            del grid[i]
            grid.insert(0, [0 for _ in range(GRID_WIDTH)])
            lines_cleared +=1
    return lines_cleared

def draw_grid(screen, grid):
    for y in range(GRID_HEIGHT):
        for x in range(GRID_WIDTH):
            cell = grid[y][x]
            if cell:
                pygame.draw.rect(screen, cell, (x * GRID_SIZE, y * GRID_SIZE, GRID_SIZE, GRID_SIZE))
            pygame.draw.rect(screen, GRAY, (x * GRID_SIZE, y * GRID_SIZE, GRID_SIZE, GRID_SIZE), 1)

def draw_tetromino(screen, tetromino):
    for y, row in enumerate(tetromino.shape):
        for x, cell in enumerate(row):
            if cell:
                pygame.draw.rect(screen, tetromino.color, 
                                 ((tetromino.x + x) * GRID_SIZE, 
                                  (tetromino.y + y) * GRID_SIZE, GRID_SIZE, GRID_SIZE))

def draw_next_piece(screen, tetromino):
    font = pygame.font.SysFont('comicsans', 30)
    label = font.render('Next Piece', 1, WHITE)
    screen.blit(label, (GRID_WIDTH * GRID_SIZE + 20, 20))
    for y, row in enumerate(tetromino.shape):
        for x, cell in enumerate(row):
            if cell:
                pygame.draw.rect(screen, tetromino.color, 
                                 (GRID_WIDTH * GRID_SIZE + 20 + x * GRID_SIZE, 
                                  60 + y * GRID_SIZE, GRID_SIZE, GRID_SIZE))

def main():
    screen = pygame.display.set_mode((GRID_WIDTH * GRID_SIZE + SIDEBAR_WIDTH, GRID_HEIGHT * GRID_SIZE))
    pygame.display.set_caption('Tetris')
    clock = pygame.time.Clock()
    grid = create_grid()
    current_tetromino = Tetromino(random.choice(SHAPES))
    next_tetromino = Tetromino(random.choice(SHAPES))
    fall_time = 0
    fall_speed = 0.5  # seconds
    score = 0
    game_over = False
    show_start_screen = True

    # Start button
    start_button = pygame.Rect(GRID_WIDTH * GRID_SIZE + 50, GRID_HEIGHT * GRID_SIZE // 2 - 25, 100, 50)

    while True:
        dt = clock.tick(FPS) / 1000  # Delta time in seconds
        if not show_start_screen:
            fall_time += dt

            # Piece falling
            if fall_time > fall_speed:
                fall_time = 0
                if not can_move(current_tetromino, grid, 0, 1):
                    lock_tetromino(current_tetromino, grid)
                    cleared = clear_lines(grid)
                    score += cleared * 100
                    current_tetromino = next_tetromino
                    next_tetromino = Tetromino(random.choice(SHAPES))
                    if not can_move(current_tetromino, grid, 0, 0):
                        game_over = True
                else:
                    current_tetromino.y +=1

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                return
            if event.type == pygame.MOUSEBUTTONDOWN and show_start_screen:
                if start_button.collidepoint(event.pos):
                    show_start_screen = False
            if event.type == pygame.KEYDOWN and not show_start_screen and not game_over:
                if event.key == pygame.K_LEFT:
                    if can_move(current_tetromino, grid, -1, 0):
                        current_tetromino.x -=1
                elif event.key == pygame.K_RIGHT:
                    if can_move(current_tetromino, grid, 1, 0):
                        current_tetromino.x +=1
                elif event.key == pygame.K_DOWN:
                    if can_move(current_tetromino, grid, 0,1):
                        current_tetromino.y +=1
                elif event.key == pygame.K_UP:
                    rotated = rotate_shape(current_tetromino.shape)
                    if can_move(current_tetromino, grid, 0,0, rotated_shape=rotated):
                        current_tetromino.shape = rotated
                elif event.key == pygame.K_SPACE:
                    while can_move(current_tetromino, grid, 0,1):
                        current_tetromino.y +=1
                    lock_tetromino(current_tetromino, grid)
                    cleared = clear_lines(grid)
                    score += cleared * 100
                    current_tetromino = next_tetromino
                    next_tetromino = Tetromino(random.choice(SHAPES))
                    if not can_move(current_tetromino, grid, 0, 0):
                        game_over = True

        screen.fill(BLACK)

        if show_start_screen:
            pygame.draw.rect(screen, WHITE, start_button)
            font = pygame.font.SysFont('comicsans', 30)
            text = font.render('Start Game', True, BLACK)
            screen.blit(text, (start_button.x + 10, start_button.y + 10))
        else:
            draw_grid(screen, grid)
            draw_tetromino(screen, current_tetromino)
            draw_next_piece(screen, next_tetromino)
            font = pygame.font.SysFont('comicsans', 30)
            score_text = font.render(f"Score: {score}", True, WHITE)
            screen.blit(score_text, (GRID_WIDTH * GRID_SIZE + 20, 200))

            if game_over:
                font = pygame.font.SysFont('comicsans', 60)
                over_text = font.render("GAME OVER", True, RED)
                screen.blit(over_text, (50, GRID_HEIGHT * GRID_SIZE // 2 - 30))

        pygame.display.flip()

if __name__ == "__main__":
    main()
    pygame.quit()

7. Conclusion

By following this comprehensive guide, you can build a fully functional Tetris game in Python using Pygame. The implementation covers game setup, tetromino management, user controls, scoring, and user interface elements like the next piece preview and start button. Further enhancements can include adding sound effects, increasing difficulty levels, and tracking high scores to enrich the gaming experience.

References


Last updated February 10, 2025
Ask Ithy AI
Export Article
Delete Article