This comprehensive tutorial guides you through initializing Pygame, setting up the game screen, and implementing game mechanics such as piece rotation, line clearing, and scoring. Learn to handle player inputs, draw the game grid, and manage game states. By the end of this tutorial, you’ll have a fully functional Tetris game.
First, you need to install pygame if you haven’t already:
pip install pygame
Here’s a basic implementation of the Tetris game:
import pygame
import random
# Initialize Pygame
pygame.init()
# Screen dimensions
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 700
PLAY_WIDTH = 300 # meaning 300 // 10 = 30 width per block
PLAY_HEIGHT = 600 # meaning 600 // 20 = 20 height per block
BLOCK_SIZE = 30
# Top left coordinates of the play area
TOP_LEFT_X = (SCREEN_WIDTH - PLAY_WIDTH) // 2
TOP_LEFT_Y = SCREEN_HEIGHT - PLAY_HEIGHT - 50
# Shapes formats
S = [['.....',
'.....',
'..00.',
'.00..',
'.....'],
['.....',
'..0..',
'..00.',
'...0.',
'.....']]
Z = [['.....',
'.....',
'.00..',
'..00.',
'.....'],
['.....',
'..0..',
'.00..',
'.0...',
'.....']]
I = [['.....',
'..0..',
'..0..',
'..0..',
'..0..'],
['.....',
'0000.',
'.....',
'.....',
'.....']]
O = [['.....',
'.....',
'.00..',
'.00..',
'.....']]
J = [['.....',
'.0...',
'.000.',
'.....',
'.....'],
['.....',
'..00.',
'..0..',
'..0..',
'.....'],
['.....',
'.....',
'.000.',
'...0.',
'.....'],
['.....',
'..0..',
'..0..',
'.00..',
'.....']]
L = [['.....',
'...0.',
'.000.',
'.....',
'.....'],
['.....',
'..0..',
'..0..',
'..00.',
'.....'],
['.....',
'.....',
'.000.',
'.0...',
'.....'],
['.....',
'.00..',
'..0..',
'..0..',
'.....']]
T = [['.....',
'..0..',
'.000.',
'.....',
'.....'],
['.....',
'..0..',
'..00.',
'..0..',
'.....'],
['.....',
'.....',
'.000.',
'..0..',
'.....'],
['.....',
'..0..',
'.00..',
'..0..',
'.....']]
SHAPES = [S, Z, I, O, J, L, T]
SHAPE_COLORS = [(0, 255, 0), (255, 0, 0), (0, 255, 255), (255, 255, 0), (255, 165, 0), (0, 0, 255), (128, 0, 128)]
# Classes
class Piece(object):
def __init__(self, x, y, shape):
self.x = x
self.y = y
self.shape = shape
self.color = SHAPE_COLORS[SHAPES.index(shape)]
self.rotation = 0
def create_grid(locked_positions={}):
grid = [[(0,0,0) for x in range(10)] for y in range(20)]
for y in range(len(grid)):
for x in range(len(grid[y])):
if (x, y) in locked_positions:
c = locked_positions[(x, y)]
grid[y][x] = c
return grid
def convert_shape_format(shape):
positions = []
format = shape.shape[shape.rotation % len(shape.shape)]
for i, line in enumerate(format):
row = list(line)
for j, column in enumerate(row):
if column == '0':
positions.append((shape.x + j, shape.y + i))
for i, pos in enumerate(positions):
positions[i] = (pos[0] - 2, pos[1] - 4)
return positions
def valid_space(shape, grid):
accepted_positions = [[(x, y) for x in range(10) if grid[y][x] == (0, 0, 0)] for y in range(20)]
accepted_positions = [x for sub in accepted_positions for x in sub]
formatted = convert_shape_format(shape)
for pos in formatted:
if pos not in accepted_positions:
if pos[1] > -1:
return False
return True
def check_lost(positions):
for pos in positions:
x, y = pos
if y < 1:
return True
return False
def get_shape():
return Piece(5, 0, random.choice(SHAPES))
def draw_text_middle(text, size, color, surface):
font = pygame.font.Font(pygame.font.get_default_font(), size, bold=False, italic=True)
label = font.render(text, 1, color)
surface.blit(label, (TOP_LEFT_X + PLAY_WIDTH / 2 - (label.get_width() / 2), TOP_LEFT_Y + PLAY_HEIGHT / 2 - (label.get_height() / 2)))
def draw_grid(surface, grid):
for y in range(len(grid)):
pygame.draw.line(surface, (128,128,128), (TOP_LEFT_X, TOP_LEFT_Y + y*BLOCK_SIZE), (TOP_LEFT_X + PLAY_WIDTH, TOP_LEFT_Y + y * BLOCK_SIZE)) # horizontal lines
for x in range(len(grid[y])):
pygame.draw.line(surface, (128,128,128), (TOP_LEFT_X + x*BLOCK_SIZE, TOP_LEFT_Y), (TOP_LEFT_X + x * BLOCK_SIZE, TOP_LEFT_Y + PLAY_HEIGHT)) # vertical lines
def clear_rows(grid, locked):
inc = 0
for i in range(len(grid)-1, -1, -1):
row = grid[i]
if (0, 0, 0) not in row:
inc += 1
ind = i
for j in range(len(row)):
try:
del locked[(j, i)]
except:
continue
if inc > 0:
for key in sorted(list(locked), key=lambda k: k[1])[::-1]:
x, y = key
if y < ind:
newKey = (x, y + inc)
locked[newKey] = locked.pop(key)
return inc
def draw_next_shape(shape, surface):
font = pygame.font.Font(pygame.font.get_default_font(), 30)
label = font.render('Next Shape', 1, (255, 255, 255))
start_x = TOP_LEFT_X + PLAY_WIDTH + 50
start_y = TOP_LEFT_Y + (PLAY_HEIGHT / 2 - 100)
format = shape.shape[shape.rotation % len(shape.shape)]
for i, line in enumerate(format):
row = list(line)
for j, column in enumerate(row):
if column == '0':
pygame.draw.rect(surface, shape.color, (start_x + j*BLOCK_SIZE, start_y + i*BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE), 0)
surface.blit(label, (start_x + 10, start_y - 30))
def draw_window(surface, grid, score=0):
surface.fill((0, 0, 0))
pygame.font.init()
font = pygame.font.Font(pygame.font.get_default_font(), 60)
label = font.render('TETRIS', 1, (255, 255, 255))
surface.blit(label, (TOP_LEFT_X + PLAY_WIDTH / 2 - (label.get_width() / 2), 30))
font = pygame.font.Font(pygame.font.get_default_font(), 30)
label = font.render('Score: ' + str(score), 1, (255, 255, 255))
start_x = TOP_LEFT_X + PLAY_WIDTH + 50
start_y = TOP_LEFT_Y + (PLAY_HEIGHT / 2 - 100)
surface.blit(label, (start_x + 20, start_y + 160))
for y in range(len(grid)):
for x in range(len(grid[y])):
pygame.draw.rect(surface, grid[y][x], (TOP_LEFT_X + x*BLOCK_SIZE, TOP_LEFT_Y + y*BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE), 0)
draw_grid(surface, grid)
pygame.draw.rect(surface, (255, 0, 0), (TOP_LEFT_X, TOP_LEFT_Y, PLAY_WIDTH, PLAY_HEIGHT), 5)
def main():
locked_positions = {}
grid = create_grid(locked_positions)
change_piece = False
run = True
current_piece = get_shape()
next_piece = get_shape()
clock = pygame.time.Clock()
fall_time = 0
score = 0
while run:
grid = create_grid(locked_positions)
fall_speed = 0.27
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
pygame.display.quit()
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
current_piece.x -= 1
if not valid_space(current_piece, grid):
current_piece.x += 1
if event.key == pygame.K_RIGHT:
current_piece.x += 1
if not valid_space(current_piece, grid):
current_piece.x -= 1
if event.key == pygame.K_DOWN:
current_piece.y += 1
if not valid_space(current_piece, grid):
current_piece.y -= 1
if event.key == pygame.K_UP:
current_piece.rotation = (current_piece.rotation + 1) % len(current_piece.shape)
if not valid_space(current_piece, grid):
current_piece.rotation = (current_piece.rotation - 1) % len(current_piece.shape)
if event.key == pygame.K_SPACE:
while valid_space(current_piece, grid):
current_piece.y += 1
current_piece.y -= 1
shape_pos = convert_shape_format(current_piece)
for pos in shape_pos:
p = (pos[0], pos[1])
if p[1] > -1:
grid[p[1]][p[0]] = current_piece.color
if change_piece:
for pos in shape_pos:
p = (pos[0], pos[1])
locked_positions[(p[0], p[1])] = current_piece.color
current_piece = next_piece
next_piece = get_shape()
change_piece = False
score += clear_rows(grid, locked_positions) * 10
draw_window(screen, grid, score)
draw_next_shape(next_piece, screen)
pygame.display.update()
if not valid_space(current_piece, grid):
draw_text_middle("YOU LOST", 80, (255, 255, 255), screen)
pygame.display.update()
pygame.time.delay(1500)
run = False
fall_time += clock.get_rawtime()
clock.tick()
if fall_time / 1000 >= fall_speed:
fall_time = 0
current_piece.y += 1
if not valid_space(current_piece, grid):
current_piece.y -= 1
change_piece = True
pygame.display.quit()
def main_menu():
run = True
while run:
screen.fill((0, 0, 0))
draw_text_middle("Press Any Key To Play", 60, (255, 255, 255), screen)
pygame.display.update()
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if event.type == pygame.KEYDOWN:
main()
pygame.display.quit()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('Tetris')
main_menu()
import pygame
import random
import sys
# Initialize Pygame
pygame.init()
Here, we import necessary libraries. pygame is used for game development, random for randomizing pieces, and sys for system functions.
# Screen dimensions
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 700
PLAY_WIDTH = 300 # 300 pixels (10 blocks wide)
PLAY_HEIGHT = 600 # 600 pixels (20 blocks high)
BLOCK_SIZE = 30 # Each block is 30x30 pixels
# Top left coordinates of the play area
TOP_LEFT_X = (SCREEN_WIDTH - PLAY_WIDTH) // 2
TOP_LEFT_Y = SCREEN_HEIGHT - PLAY_HEIGHT - 50
These constants define the dimensions of the screen and the play area, as well as the size of each block. TOP_LEFT_X and TOP_LEFT_Y calculate where the play area should be positioned on the screen.
# Shape formats
S = [['.....',
'.....',
'..00.',
'.00..',
'.....'],
['.....',
'..0..',
'..00.',
'...0.',
'.....']]
# Other shapes omitted for brevity...
SHAPES = [S, Z, I, O, J, L, T]
SHAPE_COLORS = [(0, 255, 0), (255, 0, 0), (0, 255, 255), (255, 255, 0), (255, 165, 0), (0, 0, 255), (128, 0, 128)]
Each shape is represented as a list of strings. Each string represents a row, with . for empty space and 0 for the filled part of the shape. SHAPES holds all the shapes, and SHAPE_COLORS holds the corresponding colors for each shape.
class Piece:
def __init__(self, x, y, shape):
self.x = x
self.y = y
self.shape = shape
self.color = SHAPE_COLORS[SHAPES.index(shape)]
self.rotation = 0
The Piece class represents a Tetris piece. It holds the piece’s position, shape, color, and rotation state.
def create_grid(locked_positions={}):
grid = [[(0,0,0) for x in range(10)] for y in range(20)]
for y in range(len(grid)):
for x in range(len(grid[y])):
if (x, y) in locked_positions:
c = locked_positions[(x, y)]
grid[y][x] = c
return grid
create_grid initializes a 20×10 grid filled with black (empty) cells. If there are any locked positions (where pieces have landed), it updates the grid with the colors of those pieces.
def convert_shape_format(shape):
positions = []
format = shape.shape[shape.rotation % len(shape.shape)]
for i, line in enumerate(format):
row = list(line)
for j, column in enumerate(row):
if column == '0':
positions.append((shape.x + j, shape.y + i))
for i, pos in enumerate(positions):
positions[i] = (pos[0] - 2, pos[1] - 4)
return positions
convert_shape_format converts the shape format from the piece’s rotation state to actual grid positions.
def valid_space(shape, grid):
accepted_positions = [[(x, y) for x in range(10) if grid[y][x] == (0, 0, 0)] for y in range(20)]
accepted_positions = [x for sub in accepted_positions for x in sub]
formatted = convert_shape_format(shape)
for pos in formatted:
if pos not in accepted_positions:
if pos[1] > -1:
return False
return True
valid_space checks if the current piece’s position is within the bounds of the grid and not overlapping with other pieces.
def check_lost(positions):
for pos in positions:
x, y = pos
if y < 1:
return True
return False
check_lost checks if any piece has reached the top of the grid, which means the game is over.
def get_shape():
return Piece(5, 0, random.choice(SHAPES))
get_shape returns a new random piece positioned at the top center of the grid.
def draw_text_middle(text, size, color, surface):
font = pygame.font.Font(pygame.font.get_default_font(), size, bold=False, italic=True)
label = font.render(text, 1, color)
surface.blit(label, (TOP_LEFT_X + PLAY_WIDTH / 2 - (label.get_width() / 2), TOP_LEFT_Y + PLAY_HEIGHT / 2 - (label.get_height() / 2)))
draw_text_middle draws a centered text on the screen, used for displaying messages like “Game Over”.
def draw_grid(surface, grid):
for y in range(len(grid)):
pygame.draw.line(surface, (128,128,128), (TOP_LEFT_X, TOP_LEFT_Y + y*BLOCK_SIZE), (TOP_LEFT_X + PLAY_WIDTH, TOP_LEFT_Y + y * BLOCK_SIZE)) # horizontal lines
for x in range(len(grid[y])):
pygame.draw.line(surface, (128,128,128), (TOP_LEFT_X + x*BLOCK_SIZE, TOP_LEFT_Y), (TOP_LEFT_X + x * BLOCK_SIZE, TOP_LEFT_Y + PLAY_HEIGHT)) # vertical lines
draw_grid draws the grid lines on the play area.
def clear_rows(grid, locked):
inc = 0
for i in range(len(grid)-1, -1, -1):
row = grid[i]
if (0, 0, 0) not in row:
inc += 1
ind = i
for j in range(len(row)):
try:
del locked[(j, i)]
except:
continue
if inc > 0:
for key in sorted(list(locked), key=lambda k: k[1])[::-1]:
x, y = key
if y < ind:
newKey = (x, y + inc)
locked[newKey] = locked.pop(key)
return inc
clear_rows removes full rows from the grid, shifts the above rows down, and updates the locked positions.
def draw_next_shape(shape, surface):
font = pygame.font.Font(pygame.font.get_default_font(), 30)
label = font.render('Next Shape', 1, (255, 255, 255))
start_x = TOP_LEFT_X + PLAY_WIDTH + 50
start_y = TOP_LEFT_Y + (PLAY_HEIGHT / 2 - 100)
format = shape.shape[shape.rotation % len(shape.shape)]
for i, line in enumerate(format):
row = list(line)
for j, column in enumerate(row):
if column == '0':
pygame.draw.rect(surface, shape.color, (start_x + j*BLOCK_SIZE, start_y + i*BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE), 0)
surface.blit(label, (start_x + 10, start_y - 30))
draw_next_shape displays the next piece to be dropped on the right side of the screen.
def draw_window(surface, grid, score=0):
surface.fill((0, 0, 0))
pygame.font.init()
font = pygame.font.Font(pygame.font.get_default_font(), 60)
label = font.render('TETRIS', 1, (255, 255, 255))
surface.blit(label, (TOP_LEFT_X + PLAY_WIDTH / 2 - (label.get_width() / 2), 30))
font = pygame.font.Font(pygame.font.get_default_font(), 30)
label = font.render('Score: ' + str(score), 1, (255, 255, 255))
start_x = TOP_LEFT_X + PLAY_WIDTH + 50
start_y = TOP_LEFT_Y + (PLAY_HEIGHT / 2 - 100)
surface.blit(label, (start_x + 20, start_y + 160))
for y in range(len(grid)):
for x in range(len(grid[y])):
pygame.draw.rect(surface, grid[y][x], (TOP_LEFT_X + x*BLOCK_SIZE, TOP_LEFT_Y + y*BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE), 0)
draw_grid(surface, grid)
pygame.draw.rect(surface, (255, 0, 0), (TOP_LEFT_X, TOP_LEFT_Y, PLAY_WIDTH, PLAY_HEIGHT), 5)
draw_window draws the entire game window, including the play area, current score, and next piece.
def main():
locked_positions = {}
grid = create_grid(locked_positions)
change_piece = False
run = True
current_piece = get_shape()
next_piece = get_shape()
clock = pygame.time.Clock()
fall_time = 0
score = 0
while run:
grid = create_grid(locked_positions)
fall_speed = 0.27
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
pygame.display.quit()
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
current_piece.x -= 1
if not valid_space(current_piece, grid):
current_piece.x += 1
if event.key == pygame.K_RIGHT:
current_piece.x += 1
if not valid_space(current_piece, grid):
current_piece.x -= 1
if event.key == pygame.K_DOWN:
current_piece.y += 1
if not valid_space(current_piece, grid):
current_piece.y -= 1
if event.key == pygame.K_UP:
current_piece.rotation = (current_piece.rotation + 1) % len(current_piece.shape)
if not valid_space(current_piece, grid):
current_piece.rotation = (current_piece.rotation - 1) % len(current_piece.shape)
if event.key == pygame.K_SPACE:
while valid_space(current_piece, grid):
current_piece.y += 1
current_piece.y -= 1
shape_pos = convert_shape_format(current_piece)
for pos in shape_pos:
p = (pos[0], pos[1])
if p[1] > -1:
grid[p[1]][p[0]] = current_piece.color
if change_piece:
for pos in shape_pos:
p = (pos[0], pos[1])
locked_positions[(p[0], p[1])] = current_piece.color
current_piece = next_piece
next_piece = get_shape()
change_piece = False
score += clear_rows(grid, locked_positions) * 10
draw_window(screen, grid, score)
draw_next_shape(next_piece, screen)
pygame.display.update()
if not valid_space(current_piece, grid):
draw_text_middle("YOU LOST", 80, (255, 255, 255), screen)
pygame.display.update()
pygame.time.delay(1500)
run = False
fall_time += clock.get_rawtime()
clock.tick()
if fall_time / 1000 >= fall_speed:
fall_time = 0
current_piece.y += 1
if not valid_space(current_piece, grid):
current_piece.y -= 1
change_piece = True
pygame.display.quit()
def main_menu():
run = True
while run:
screen.fill((0, 0, 0))
draw_text_middle("Press Any Key To Play", 60, (255, 255, 255), screen)
pygame.display.update()
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if event.type == pygame.KEYDOWN:
main()
pygame.display.quit()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('Tetris')
main_menu()
current_piece is updated based on user input (left, right, down, up).shape_pos stores the current positions of the blocks of the piece.change_piece is True, it means the current piece has landed, and it updates the locked_positions dictionary with the landed piece’s blocks.current_piece, and next_piece is updated.clear_rows is called to remove full rows and update the locked positions.draw_window function updates the display with the current grid and score.draw_next_shape function shows the next piece that will drop.pygame.display.update().current_piece is not in a valid space at the start, the game is over.fall_time is incremented based on the clock’s raw time.fall_time exceeds fall_speed, the piece moves down by one unit.def main_menu():
run = True
while run:
screen.fill((0, 0, 0))
draw_text_middle("Press Any Key To Play", 60, (255, 255, 255), screen)
pygame.display.update()
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if event.type == pygame.KEYDOWN:
main()
pygame.display.quit()
The main_menu function displays a message prompting the player to press any key to start the game. Once a key is pressed, it calls the main function to start the game.
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('Tetris')
main_menu()
The game window is initialized, and the title is set to “Tetris”. The main_menu function is called to start the game loop.