• Writing Tetris in Python
  • Original author: Dr Pommes
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: z0gSh1u
  • Proofreader: Park-ma, zhengjian-L

Write a Tetris game in Python

Write a step-by-step guide to the Tetris game using Python’s PyGame library

In this tutorial, we will write a simple Tetris game using Python’s PyGame library. The algorithms are simple, but can be a bit challenging for beginners. We focus less on PyGame’s internals and more on the logic of the game. If you’re too lazy to read the whole article, you can simply copy and paste the code at the end.

The preparatory work

  1. Python 3. This can be downloaded from the official website.
  2. PyGame. Depending on the operating system you are using, open a command prompt or terminal and typepip install pygamepip3 install pygame.
  3. Basics of Python. Check out my other posts if you need to.

You may encounter some problems installing Python or PyGame, but that is beyond the scope of this article. See StackOverflow 🙂

I personally had a problem with not being able to display anything on the screen on the Mac, and this problem was solved by installing certain versions of PyGame: PIP install PyGame ==2.0.0.dev4.

Figure class

Let’s start with the Figure class. Our goal is to store the various types of graphs and their rotation results. We could certainly do this by matrix rotation, but that would make the problem too complicated.

So, we simply represent the graph with a list like this:

class Figure:
    figures = [
        [[1.5.9.13], [4.5.6.7]],
        [[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]],  
        [[1.4.5.6], [1.4.5.9], [4.5.6.9], [1.5.6.9]],
        [[1.2.5.6]],]Copy the code

Where the first dimension of the list stores the types of graphs and the second dimension stores their rotation results. The numbers in each element represent the positions filled as solid in the 4 × 4 matrix. For example, [1,5,9,13] represents a vertical line. For a better understanding, please refer to the picture above.

As an exercise, try adding some shapes that aren’t here, like the Zigzag.

The __init__ function looks like this:

class Figure:.def __init__(self, x, y):
        self.x = x
        self.y = y
        self.type = random.randint(0, len(self.figures) - 1)
        self.color = random.randint(1, len(colors) - 1)
        self.rotation = 0
Copy the code

Here, we randomly select a shape and color.

Also, we need to be able to quickly rotate the graph and get the current rotation result, so we give these two simple methods:

class Figure:.def image(self):
        return self.figures[self.type][self.rotation]

    def rotate(self):
        self.rotation = (self.rotation + 1) % len(self.figures[self.type])
Copy the code

Tetris class

Let’s initialize the game with some variables:

class Tetris:
    level = 2
    score = 0
    state = "start"
    field = []
    height = 0
    width = 0
    x = 100
    y = 60
    zoom = 20
    figure = None
Copy the code

Where state indicates whether we are still playing the game; Field indicates the field of play, zero indicates empty, and a color value indicates graphics (except for those still falling).

We initialize the game with this simple method:

class Tetris:.def __init__(self, height, width):
        self.height = height
        self.width = width
        for i in range(height):
            new_line = []
            for j in range(width):
                new_line.append(0)
            self.field.append(new_line)
Copy the code

This creates a field of size height x width.

It is easy to create a new shape and position it at coordinates (3, 0) :

class Tetris:.def new_figure(self):
        self.figure = Figure(3.0)
Copy the code

The more interesting function is to check whether the current falling graph intersects the fixed graph. This can happen when the graph goes left, right, down, or rotated.

class Tetris:.def intersects(self):
        intersection = False
        for i in range(4) :for j in range(4) :if i * 4 + j in self.figure.image():
                    if i + self.figure.y > self.height - 1 or \
                            j + self.figure.x > self.width - 1 or \
                            j + self.figure.x < 0 or \
                            self.field[i + self.figure.y][j + self.figure.x] > 0:
                        intersection = True
        return intersection
Copy the code

It’s simple: we walk through and examine each cell of the 4 × 4 matrix of the current graph, regardless of whether it goes beyond the boundaries of the game or coincides with the filled blocks of the field. We also check self.field[..] […] > 0, because that part of the field may have color. If it’s a zero there, that means it’s empty, and that’s fine.

With this function, we can check if we can move or rotate the graph. If it moves down and meets the intersection, then we are at the bottom, so we need to “freeze” this shape on the field:

class Tetris:.def freeze(self):
        for i in range(4) :for j in range(4) :if i * 4 + j in self.figure.image():
                    self.field[i + self.figure.y][j + self.figure.x] = self.figure.color
        self.break_lines()
        self.new_figure()
        if self.intersects():
            game.state = "gameover"
Copy the code

Once frozen, we need to check for filled horizontal lines that need to be deleted. Then create a new graph. If it meets the intersection as soon as it is created, it is Game Over.

Checking for filled horizontal lines is fairly straightforward, but note that deleting horizontal lines needs to be done from the bottom up:

class Tetris:.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
Copy the code

Now, we still need to move the method:

class Tetris:.def go_space(self):
        while not self.intersects():
            self.figure.y += 1
        self.figure.y -= 1
        self.freeze()

    def go_down(self):
        self.figure.y += 1
        if self.intersects():
            self.figure.y -= 1
            self.freeze()

    def go_side(self, dx):
        old_x = self.figure.x
        self.figure.x += dx
        if self.intersects():
            self.figure.x = old_x

    def rotate(self):
        old_rotation = self.figure.rotation
        self.figure.rotate()
        if self.intersects():
            self.figure.rotation = old_rotation
Copy the code

As you can see, the go_space method repeats go_DOWN, but it moves down until it reaches the bottom of the scene or some fixed shape.

And in each method, we remember the previous position, change the coordinates, and then check to see if it intersects. If it intersects, we go back to the previous state.

PyGame and the complete code

We’re almost done!

There is still some game loop and PyGame logic left. So let’s take a look at the complete code:

import pygame
import random

colors = [
    (0.0.0),
    (120.37.179),
    (100.179.179),
    (80.34.22),
    (80.134.22),
    (180.34.22),
    (180.34.122),]class Figure:
    x = 0
    y = 0

    figures = [
        [[1.5.9.13], [4.5.6.7]],
        [[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]],
        [[1.4.5.6], [1.4.5.9], [4.5.6.9], [1.5.6.9]],
        [[1.2.5.6]],]def __init__(self, x, y):
        self.x = x
        self.y = y
        self.type = random.randint(0, len(self.figures) - 1)
        self.color = random.randint(1, len(colors) - 1)
        self.rotation = 0

    def image(self):
        return self.figures[self.type][self.rotation]

    def rotate(self):
        self.rotation = (self.rotation + 1) % len(self.figures[self.type])


class Tetris:
    level = 2
    score = 0
    state = "start"
    field = []
    height = 0
    width = 0
    x = 100
    y = 60
    zoom = 20
    figure = None

    def __init__(self, height, width):
        self.height = height
        self.width = width
        for i in range(height):
            new_line = []
            for j in range(width):
                new_line.append(0)
            self.field.append(new_line)

    def new_figure(self):
        self.figure = Figure(3.0)

    def intersects(self):
        intersection = False
        for i in range(4) :for j in range(4) :if i * 4 + j in self.figure.image():
                    if i + self.figure.y > self.height - 1 or \
                            j + self.figure.x > self.width - 1 or \
                            j + self.figure.x < 0 or \
                            self.field[i + self.figure.y][j + self.figure.x] > 0:
                        intersection = True
        return intersection

    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

    def go_space(self):
        while not self.intersects():
            self.figure.y += 1
        self.figure.y -= 1
        self.freeze()

    def go_down(self):
        self.figure.y += 1
        if self.intersects():
            self.figure.y -= 1
            self.freeze()

    def freeze(self):
        for i in range(4) :for j in range(4) :if i * 4 + j in self.figure.image():
                    self.field[i + self.figure.y][j + self.figure.x] = self.figure.color
        self.break_lines()
        self.new_figure()
        if self.intersects():
            game.state = "gameover"

    def go_side(self, dx):
        old_x = self.figure.x
        self.figure.x += dx
        if self.intersects():
            self.figure.x = old_x

    def rotate(self):
        old_rotation = self.figure.rotation
        self.figure.rotate()
        if self.intersects():
            self.figure.rotation = old_rotation


Initialize the game engine
pygame.init()

# Define some colors
BLACK = (0.0.0)
WHITE = (255.255.255)
GRAY = (128.128.128)

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.figure is None:
        game.new_figure()
    counter += 1
    if counter > 100000:
        counter = 0

    if counter % (fps // game.level // 2) = =0 or pressing_down:
        if game.state == "start":
            game.go_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.go_side(- 1)
            if event.key == pygame.K_RIGHT:
                game.go_side(1)
            if event.key == pygame.K_SPACE:
                game.go_space()
        if event.type == pygame.KEYUP:
            if event.key == pygame.K_DOWN:
                pressing_down = False

    screen.fill(WHITE)

    for i in range(game.height):
        for j in range(game.width):
            pygame.draw.rect(screen, GRAY, [game.x + game.zoom * j, game.y + game.zoom * i, game.zoom, game.zoom], 1)
            if game.field[i][j] > 0:
                pygame.draw.rect(screen, colors[game.field[i][j]],
                                 [game.x + game.zoom * j + 1, game.y + game.zoom * i + 1, game.zoom - 2, game.zoom - 1])

    if game.figure is not None:
        for i in range(4) :for j in range(4):
                p = i * 4 + j
                if p in game.figure.image():
                    pygame.draw.rect(screen, colors[game.figure.color],
                                     [game.x + game.zoom * (j + game.figure.x) + 1,
                                      game.y + game.zoom * (i + game.figure.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, BLACK)
    text_game_over = font1.render("Game Over :( ".True, (255.0.0))

    screen.blit(text, [0.0])
    if game.state == "gameover":
        screen.blit(text_game_over, [10.200])

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

pygame.quit()
Copy the code

Try copying and pasting it into a py file. Run, and enjoy the game! 🙂

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.