How to Code a Tetris-Style Game in Python🧩

How to Code a Tetris-Style Game in Python🧩

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:

tetris.png

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.

tetris.png

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_blockmethod moves the current block down until it intersects with other blocks or reaches the bottom of the field. It then calls the freeze 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 the freeze 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 the new_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 is None, a new block is created using game.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 using pygame.KEYDOWN and pygame.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 the colours 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 using clock.tick(fps). Once the game loop is exited, the Pygame library is shut down using pygame.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++.