[python] do you want to make a Tetris yourself? It's implemented in 300 lines of code

Posted by darktimesrpg on Wed, 08 Dec 2021 22:52:27 +0100

[pygame] Python implements Tetris in less than 300 lines of code
This code is based on Python 3.6 and pyGame 1.9.4.

Tetris is one of the most classic games in childhood. When I first came into contact with pygame, I wanted to write a Tetris. But it seems difficult to think of operations such as rotation, docking and elimination. When you really finish writing, you will find that there are only 300 lines of code, which is not difficult.

Let's take a look at a screenshot of the game. It's a little ugly. Well, I don't have any art cells, but the main functions have been realized and can be played.

Now let's look at the implementation process.

appearance
The whole interface of Tetris is divided into two parts. One part is the game area on the left, and the other part is the display area on the right, showing score, speed, next box style, etc. There's no screenshot here. Just look at the picture.

Like greedy snakes, the game area is composed of small squares. In order to see it intuitively, I specially drew grid lines.

import sys
import pygame
from pygame.locals import *

SIZE = 30 # each small square size
BLOCK_HEIGHT = 20 # game area height
BLOCK_WIDTH = 10 # game area width
BORDER_WIDTH = 4 # game area border width
BORDER_COLOR = (40, 40, 200) # game area border color
SCREEN_WIDTH = SIZE * (BLOCK_WIDTH + 5) # game screen width
SCREEN_HEIGHT = SIZE * BLOCK_HEIGHT # game screen high
BG_COLOR = (40, 40, 60) # background color
BLACK = (0, 0, 0)

def print_text(screen, font, x, y, text, fcolor=(255, 255, 255)):

imgText = font.render(text, True, fcolor)
screen.blit(imgText, (x, y))

def main():

pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('Tetris')

font1 = pygame.font.SysFont('SimHei', 24)  # Bold 24
font_pos_x = BLOCK_WIDTH * SIZE + BORDER_WIDTH + 10  # The X coordinate of the font position in the information display area on the right
font1_height = int(font1.size('score')[1])

score = 0           # score

while True:
    for event in pygame.event.get():
        if event.type == QUIT:
            sys.exit()

    # Fill background color
    screen.fill(BG_COLOR)
    # Draw game area separator
    pygame.draw.line(screen, BORDER_COLOR,
                     (SIZE * BLOCK_WIDTH + BORDER_WIDTH // 2, 0),
                     (SIZE * BLOCK_WIDTH + BORDER_WIDTH // 2, SCREEN_HEIGHT), BORDER_WIDTH)
    # Draw grid lines and vertical lines
    for x in range(BLOCK_WIDTH):
        pygame.draw.line(screen, BLACK, (x * SIZE, 0), (x * SIZE, SCREEN_HEIGHT), 1)
    # Draw grid lines and horizontal lines
    for y in range(BLOCK_HEIGHT):
        pygame.draw.line(screen, BLACK, (0, y * SIZE), (BLOCK_WIDTH * SIZE, y * SIZE), 1)

    print_text(screen, font1, font_pos_x, 10, f'score: ')
    print_text(screen, font1, font_pos_x, 10 + font1_height + 6, f'{score}')
    print_text(screen, font1, font_pos_x, 20 + (font1_height + 6) * 2, f'speed: ')
    print_text(screen, font1, font_pos_x, 20 + (font1_height + 6) * 3, f'{score // 10000}')
    print_text(screen, font1, font_pos_x, 30 + (font1_height + 6) * 4, f'next:')

    pygame.display.flip()

if name == '__main__':

main()

block
The next step is to define the box. There are seven shapes of the box:


Type I

Type O

T-type

S type

Type Z

L-type


Type J
I made several changes here, because the maximum length of the box is a long strip, which is 4 squares, so I used 4 squares uniformly × 4. This is OK, but later found inconvenient.

For the sake of intuition, the square is directly defined by a two-dimensional array, where. Represents empty and 0 represents solid. (use. To indicate blank is for intuitive viewing. If you use spaces, you will not be able to see clearly.)
For example, line I, with 4 × 4. The square is defined as

['.0..',
'.0..',
'.0..',
'.0..']
and

['....',
'....',
'0000',
'....']
The most difficult thing about the box is to realize the rotation function. For example, type I has two forms: horizontal and vertical. The so-called rotation, on the surface, is to rotate the box 90 ° clockwise, but in practice, we don't need to really realize this "rotation" effect.

In the final implementation, we draw these graphics on the interface, and every time we refresh, all the contents on the interface will be cleared and redrawn, so the rotation is only to draw the current box, not the previous shape, but the rotated shape.

For example, this type I is defined as 4 × 4, but actually only 1 is needed × 4 or 4 × 1 is OK. The rest of the space is empty. Unlike T-type, T-type is not a rectangle. If you define it with a rectangle, two positions must be empty. Then, it is really necessary to define type I as 4 × 4?

The answer is yes. Think about it. If it's 4 × A bar of 1 that turns into 1 × 4 vertical bar, how to determine this position? It seems a little difficult. But if it's 4 × For the square of 4, we only need to fix the starting point coordinate (upper left corner) unchanged and put the vertical bar of 4 × 4 directly replace the 4 of the horizontal bar × 4 area, does it realize rotation? And the location is easy to calculate.

On the other hand, it can not be rotated in some cases. For example, type I vertical bars cannot be rotated when they are close to the left and right frames. I'm impressed with that, I'm sure. But for other shapes, I'm not sure. I searched Baidu and found a web version of Tetris to play. I found that it's not allowed. For example:

It cannot be rotated when it is close to the right border. It's really annoying to judge every shape. Starting with the definition of block, it can be implemented very simply.

For example, a vertical bar row is defined as:

['.0..',
'.0..',
'.0..',
'.0..']
The vertical bar can be pasted, so when it is on the far left, the X-axis coordinate is - 1, because the left vertical row in the definition is empty. We only need to determine that the shape defined by the box (including the empty part) can be rotated only when it is completely within the game area.

What I said before is all defined as 4 × 4. It's not good. That's why it's impossible to make this judgment for other shapes such as T-shape. Therefore, for T-shape and other shapes, we can define it as 3 × 3 format:

['.0.',
'000',
'...']
There is also a situation that cannot rotate, that is, the position after rotation has been occupied by other blocks. In addition, this judgment should be made for falling and moving left and right. Since these are consistent, we can use the same method to judge.

First define a game_ The area variable is used to store the current state of the whole game area:

game_area = [['.'] * BLOCK_WIDTH for _ in range(BLOCK_HEIGHT)]
The initial state is all empty, so you can initialize it all with.
In addition, some variables are needed to define the status of the current drop box

cur_block = None # current drop box
cur_pos_x, cur_pos_y = 0, 0 # coordinates of the current falling square
Blocks are defined in the form of a two-dimensional array, and there are empty rows and columns. If we traverse this two-dimensional array to determine whether its area has been occupied by other blocks in the current game area, this can be achieved. Let's consider another case, a vertical bar, the left row is empty, and the empty row can move out of the game area. How to judge this? Every time you move left, judge whether the left row is empty? This is too much trouble. And the squares are fixed, so we can define them in advance. The final block is defined as follows:

from collections import namedtuple

Point = namedtuple('Point', 'X Y')
Block = namedtuple('Block', 'template start_pos end_pos name next')

S-shaped square

S_BLOCK = [Block(['.00',

              '00.',
              '...'], Point(0, 0), Point(2, 1), 'S', 1),
       Block(['0..',
              '00.',
              '.0.'], Point(0, 0), Point(1, 2), 'S', 0)]

The block needs to include two methods: obtaining a random block and obtaining the rotated block when rotating

BLOCKS = {'O': O_BLOCK,

      'I': I_BLOCK,
      'Z': Z_BLOCK,
      'T': T_BLOCK,
      'L': L_BLOCK,
      'S': S_BLOCK,
      'J': J_BLOCK}

def get_block():

block_name = random.choice('OIZTLSJ')
b = BLOCKS[block_name]
idx = random.randint(0, len(b) - 1)
return b[idx]

Gets the rotated square

def get_next_block(block):

b = BLOCKS[block.name]
return b[block.next]

The method of judging whether it can rotate, fall and move is also easy to realize

def _judge(pos_x, pos_y, block):

nonlocal game_area
for _i in range(block.start_pos.Y, block.end_pos.Y + 1):
    if pos_y + block.end_pos.Y >= BLOCK_HEIGHT:
        return False
    for _j in range(block.start_pos.X, block.end_pos.X + 1):
        if pos_y + _i >= 0 and block.template[_i][_j] != '.' and game_area[pos_y + _i][pos_x + _j] != '.':
            return False
return True

stop
The last problem is docking. When the box falls to the bottom or meets another box, it can't fall. I call this "docking", and a name is more convenient to say.

The first thing is to judge whether it can be docked. After docking occurs, draw the non empty point of the current box to the game area. In short, cur_ The non empty points of the block are copied to the game according to the corresponding positions_ Go to the area. And calculate whether a row is fully filled, and if it is fully filled, it will be eliminated.

def _dock():

nonlocal cur_block, next_block, game_area, cur_pos_x, cur_pos_y, game_over
for _i in range(cur_block.start_pos.Y, cur_block.end_pos.Y + 1):
    for _j in range(cur_block.start_pos.X, cur_block.end_pos.X + 1):
        if cur_block.template[_i][_j] != '.':
            game_area[cur_pos_y + _i][cur_pos_x + _j] = '0'
if cur_pos_y + cur_block.start_pos.Y <= 0:
    game_over = True
else:
    # Calculation elimination
    remove_idxs = []
    for _i in range(cur_block.start_pos.Y, cur_block.end_pos.Y + 1):
        if all(_x == '0' for _x in game_area[cur_pos_y + _i]):
            remove_idxs.append(cur_pos_y + _i)
    if remove_idxs:
        # eliminate
        _i = _j = remove_idxs[-1]
        while _i >= 0:
            while _j in remove_idxs:
                _j -= 1
            if _j < 0:
                game_area[_i] = ['.'] * BLOCK_WIDTH
            else:
                game_area[_i] = game_area[_j]
            _i -= 1
            _j -= 1
    cur_block = next_block
    next_block = blocks.get_block()
    cur_pos_x, cur_pos_y = (BLOCK_WIDTH - cur_block.end_pos.X - 1) // 2, -1 - cur_block.end_pos.Y

So far, the main function of the whole Tetris has been completed.

Many parameters here can be adjusted. For example, if you feel uncomfortable rotating, you can directly adjust the definition of the box without changing the code logic.

Topics: Python