Creating a Fully Functional Tetris Game in Python Using Pygame
Step-by-Step Guide to Building Tetris with Falling Blocks, Scoring, and User Controls
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