Below is a walkthrough tutorial for creating a Tetris-style game in Python.
What I have used in this tutorial:
Visual Studio Code - IDE download from here
Python 3.10.5 64-bit - download from here
Pygame - use command:
pip install pygame
Any problems with set-up, feel free to comment below or else Stackoverflow usually has all the answers 😄
Step 1: Define the Blocks
Starting off easy - let's import the Pygame and Random modules. Using libraries gives you access to additional functionality. I then defined 7 colours that I chose from here. These set colours are used to fill the falling blocks.
import pygame
import random
colours = [
(255, 255, 255), # white
(0, 255, 0), # green
(0, 255, 255), # blue
(255, 255, 102), # yellow
(255, 0, 0), # red
(255, 0, 255), # pink
(153, 0, 153) # purple
]
As a reminder, traditional Tetris blocks come in the following shapes:
To define these coloured blocks we will use a class called 'Block'. Here we will set the initial position (x and y) as well as shape configurations (shapes and their rotations).
class Block:
position_x = 0
position_y = 0
block_shape = [
# cube
[[1, 2, 5, 6]],
# line
[[1, 5, 9, 13], [4, 5, 6, 7]],
# N shape
[[4, 5, 9, 10], [2, 6, 5, 9]],
[[6, 7, 9, 10], [1, 5, 6, 10]],
# T shape
[[1, 4, 5, 6], [1, 4, 5, 9], [4, 5, 6, 9], [1, 5, 6, 9]],
# L shape
[[1, 2, 5, 9], [0, 4, 5, 6], [1, 5, 9, 8], [4, 5, 6, 10]],
[[1, 2, 6, 10], [5, 6, 7, 9], [2, 6, 10, 11], [3, 5, 6, 7]],
]
# Constructor function
def __init__(self, position_x, position_y):
# Initialise the Block's variables
self.position_x = position_x
self.position_y = position_y
self.type = random.randint(0, len(self.block_shape) - 1)
self.color = random.randint(1, len(colours) - 1)
self.rotation = 0
def rotate(self):
self.rotation = (self.rotation + 1) % len(self.block_shape[self.type])
def current_block(self):
return self.block_shape[self.type][self.rotation]
You can play around with the shape grid below to add your own unique shapes.
Every class needs an init function. Here we choose a shape type and colour from the colours
and block_shape
list using random.randint(a, b) from the random module imported above to find a random integer between a and b.
As the shape falls, the player can rotate it based on the defined shape configurations using the rotate
function, current_block
returns the current shape and rotation.
Step 2: Game Play
To define how the game can be played, we must consider essential variables such as score
, game_state
, field
, height
, width
, position_x
, position_y
, zoom
, and block
. These variables will be used to store and manage the state and properties of the Tetris game.
class Tetris:
score = 0
game_state = "start"
field = []
height = 0
width = 0
position_x = 100
position_y = 60
zoom = 20
block = None
# 'level' is used to control some game logic and will be explained later
level = 2
def __init__(self, height, width):
self.height = height
self.width = width
self.field = []
self.score = 0
self.game_state = "start"
for i in range(height):
new_line = []
for j in range(width):
new_line.append(0)
self.field.append(new_line)
The following functions are required for gameplay:
new_block
method created new blocks using the Block class from above at position x = 3, y = 0.# Creates a new block and assigns it to the block variable of the game def new_block(self): self.block = Block(3, 0)
intersects
method checks if the current block is touching any existing blocks on the game field or if it goes beyond the boundaries of the field.# Checks for block contact def intersects(self): intersection = False for i in range(4): for j in range(4): if i * 4 + j in self.block.current_block(): if i + self.block.position_y > self.height - 1 or \ j + self.block.position_x > self.width - 1 or \ j + self.block.position_x < 0 or \ self.field[i + self.block.position_y][j + self.block.position_x] > 0: intersection = True return intersection
break_lines
method scans the game field and breaks any complete lines that are filled with blocks. It increments the score based on the number of lines cleared.# Removes completed lines def break_lines(self): lines = 0 for i in range(1, self.height): zeros = 0 for j in range(self.width): if self.field[i][j] == 0: zeros += 1 if zeros == 0: lines += 1 for i1 in range(i, 1, -1): for j in range(self.width): self.field[i1][j] = self.field[i1 - 1][j] self.score += lines ** 2
drop_block
method moves the current block down until it intersects with other blocks or reaches the bottom of the field. It then calls thefreeze
method to fix the block in place.# Block immediately drops to the lowest position def drop_block(self): while not self.intersects(): self.block.position_y += 1 self.block.position_y -= 1 self.freeze()
block_down
method moves the current block one step down and checks for intersections. If an intersection is detected, it adjusts the block's position and calls thefreeze
method.# Controls the falling movement of the blocks def block_down(self): self.block.position_y += 1 if self.intersects(): self.block.position_y -= 1 self.freeze()
freeze
method fixes the current block on the game field when it can no longer move down. It checks for completed lines, clears them, and updates the score. It then creates a new block using thenew_block
method and checks for an intersection to determine if the game is over.# Stops block movement def freeze(self): for i in range(4): for j in range(4): if i * 4 + j in self.block.current_block(): self.field[i + self.block.position_y][j + self.block.position_x] = self.block.color self.break_lines() self.new_block() if self.intersects(): self.game_state = "gameover"
move_horizontal
method moves the current block horizontally by a specified amount of delta x (dx
). It checks for intersections and adjusts the block's position if necessary.# Controls the left and right movement of the block def move_horizontal(self, dx): old_x = self.block.position_x self.block.position_x += dx if self.intersects(): self.block.position_x = old_x
rotate
method rotates the current block. It checks for intersections and reverts the rotation if an intersection occurs.# Uses Block's property to change the appearance of the block def rotate(self): old_rotation = self.block.rotation self.block.rotate() if self.intersects(): self.block.rotation = old_rotation
The full code for this section is as follows:
class Tetris:
score = 0
game_state = "start"
field = []
height = 0
width = 0
position_x = 100
position_y = 60
zoom = 20
block = None
# 'level' is used to control some game logic and will be explained later
level = 2
def __init__(self, height, width):
self.height = height
self.width = width
self.field = []
self.score = 0
self.game_state = "start"
for i in range(height):
new_line = []
for j in range(width):
new_line.append(0)
self.field.append(new_line)
# Creates a new block and assigns it to the block variable of the game
def new_block(self):
self.block = Block(3, 0)
# Checks for block contact
def intersects(self):
intersection = False
for i in range(4):
for j in range(4):
if i * 4 + j in self.block.current_block():
if i + self.block.position_y > self.height - 1 or \
j + self.block.position_x > self.width - 1 or \
j + self.block.position_x < 0 or \
self.field[i + self.block.position_y][j + self.block.position_x] > 0:
intersection = True
return intersection
# Removes completed lines
def break_lines(self):
lines = 0
for i in range(1, self.height):
zeros = 0
for j in range(self.width):
if self.field[i][j] == 0:
zeros += 1
if zeros == 0:
lines += 1
for i1 in range(i, 1, -1):
for j in range(self.width):
self.field[i1][j] = self.field[i1 - 1][j]
self.score += lines ** 2
# Block immediately drops to the lowest position
def drop_block(self):
while not self.intersects():
self.block.position_y += 1
self.block.position_y -= 1
self.freeze()
# Controls the falling movement of the blocks
def block_down(self):
self.block.position_y += 1
if self.intersects():
self.block.position_y -= 1
self.freeze()
# Stops block movement
def freeze(self):
for i in range(4):
for j in range(4):
if i * 4 + j in self.block.current_block():
self.field[i + self.block.position_y][j + self.block.position_x] = self.block.color
self.break_lines()
self.new_block()
if self.intersects():
self.game_state = "gameover"
# Controls the left and right movement of the block
def move_horizontal(self, dx):
old_x = self.block.position_x
self.block.position_x += dx
if self.intersects():
self.block.position_x = old_x
# Uses Block's property to change the appearance of the block
def rotate(self):
old_rotation = self.block.rotation
self.block.rotate()
if self.intersects():
self.block.rotation = old_rotation
Step 3: Pygame Logic
This is how the game is implemented using the Tetris
class and how it interacts with the Pygame library for graphics and user input.
Let's break down the code:
The game window constraints are set, with size as 400x500 pixels using
size = (400, 500)
.size = (400, 500) screen = pygame.display.set_mode(size) pygame.display.set_caption("Tetris")
The main game loop begins with
while not done
. This loop runs until the user closes the game window by clicking the close button. Inside the game loop, various events are handled, including user input and game logic updates such as:while not done: if game.block is None: game.new_block() counter += 1 if counter > 100000: counter = 0
If
game.block
isNone
, a new block is created usinggame.new
_block()
.if counter % (fps // game.level // 2) == 0 or pressing_down: if game.game_state == "start": game.go_down()
The game checks for user input events using
pygame.event.get()
. The code handles keyboard events usingpygame.KEYDOWN
andpygame.KEYUP
events. Depending on the key pressed, different actions are performed such as block rotation, block movement, or game restart. If the user clicks the close button (`pygame.QUIT` event), the loop is exited.for event in pygame.event.get(): if event.type == pygame.QUIT: done = True if event.type == pygame.KEYDOWN: if event.key == pygame.K_UP: game.rotate() if event.key == pygame.K_DOWN: pressing_down = True if event.key == pygame.K_LEFT: game.go_side(-1) if event.key == pygame.K_RIGHT: game.go_side(1) if event.key == pygame.K_SPACE: game.go_space() if event.key == pygame.K_ESCAPE: game.__init__(20, 10) if event.type == pygame.KEYUP: if event.key == pygame.K_DOWN: pressing_down = False
The game screen is filled with the colour black using
screen.fill(BLACK)
. The field and blocks are drawn on the screen using nested loops. The field is represented by a grid of rectangles, and each block is also drawn as a rectangle. The colour of each block is determined by thecolours
list defined at the start of the program.screen.fill(BLACK) for i in range(game.height): for j in range(game.width): pygame.draw.rect(screen, GREY, [game.position_x + game.zoom * j, game.position_y + game.zoom * i, game.zoom, game.zoom], 1) if game.field[i][j] > 0: pygame.draw.rect(screen, colours[game.field[i][j]], [game.position_x + game.zoom * j + 1, game.position_y + game.zoom * i + 1, game.zoom - 2, game.zoom - 1]) if game.block is not None: for i in range(4): for j in range(4): p = i * 4 + j if p in game.block.current_block(): pygame.draw.rect(screen, colours[game.block.color], [game.position_x + game.zoom * (j + game.block.position_x) + 1, game.position_y + game.zoom * (i + game.block.position_y) + 1, game.zoom - 2, game.zoom - 2])
The current score is rendered using the
font.render
function and displayed on the screen. If the game state is "gameover," the "Game Over" message is displayed on the screen.font = pygame.font.SysFont('Calibri', 25, True, False) font1 = pygame.font.SysFont('Calibri', 65, True, False) text = font.render("Score: " + str(game.score), True, GREEN) text_game_over = font1.render("Game Over", True, (255, 125, 0)) text_game_over1 = font1.render("Press ESC", True, (255, 215, 0)) screen.blit(text, [0, 0]) if game.game_state == "gameover": screen.blit(text_game_over, [20, 200]) screen.blit(text_game_over1, [25, 265])
Finally, the updated screen is displayed using
pygame.display.flip()
, and the frame rate is controlled usingclock.tick(fps)
. Once the game loop is exited, the Pygame library is shut down usingpygame.quit()
.pygame.display.flip() clock.tick(fps) #End of game pygame.quit()
The entire code for this section is as follows:
# Initialize the game engine
pygame.init()
# Define some colours for the game screen features (e.g. text)
GREEN = (0, 51, 0)
BLACK = (0, 0, 0)
GREY = (204, 204, 204)
# Set game area of play size and title
size = (400, 500)
screen = pygame.display.set_mode(size)
pygame.display.set_caption("Tetris")
# Loop until the user clicks the close button.
done = False
clock = pygame.time.Clock()
fps = 25
game = Tetris(20, 10)
counter = 0
pressing_down = False
while not done:
if game.block is None:
game.new_block()
counter += 1
if counter > 100000:
counter = 0
if counter % (fps // game.level // 2) == 0 or pressing_down:
if game.game_state == "start":
game.block_down()
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
game.rotate()
if event.key == pygame.K_DOWN:
pressing_down = True
if event.key == pygame.K_LEFT:
game.move_horizontal(-1)
if event.key == pygame.K_RIGHT:
game.move_horizontal(1)
if event.key == pygame.K_SPACE:
game.drop_block()
if event.key == pygame.K_ESCAPE:
game.__init__(20, 10)
if event.type == pygame.KEYUP:
if event.key == pygame.K_DOWN:
pressing_down = False
screen.fill(BLACK)
for i in range(game.height):
for j in range(game.width):
pygame.draw.rect(screen, GREY, [game.position_x + game.zoom * j, game.position_y + game.zoom * i, game.zoom, game.zoom], 1)
if game.field[i][j] > 0:
pygame.draw.rect(screen, colours[game.field[i][j]],
[game.position_x + game.zoom * j + 1, game.position_y + game.zoom * i + 1, game.zoom - 2, game.zoom - 1])
if game.block is not None:
for i in range(4):
for j in range(4):
p = i * 4 + j
if p in game.block.current_block():
pygame.draw.rect(screen, colours[game.block.color],
[game.position_x + game.zoom * (j + game.block.position_x) + 1,
game.position_y + game.zoom * (i + game.block.position_y) + 1,
game.zoom - 2, game.zoom - 2])
font = pygame.font.SysFont('Calibri', 25, True, False)
font1 = pygame.font.SysFont('Calibri', 65, True, False)
text = font.render("Score: " + str(game.score), True, GREEN)
text_game_over = font1.render("Game Over", True, (255, 125, 0))
text_game_over1 = font1.render("Press ESC", True, (255, 215, 0))
screen.blit(text, [0, 0])
if game.game_state == "gameover":
screen.blit(text_game_over, [20, 200])
screen.blit(text_game_over1, [25, 265])
pygame.display.flip()
clock.tick(fps)
pygame.quit()
~ coming soon ~
Below is a walkthrough tutorial for creating a Tetris-style game in C++.