Make a Tetris game with python (1) - shapes and panels

I have always believed that writing games is an effective way for python beginners to seek advancement. Tetris is a very old game, but it is still quite difficult for python beginners.

At present, most of the implementations on the Internet are process-oriented. Although the functions can be realized, the scalability and maintainability of the entire program are very poor, and writing such a program will limit the improvement of us junior programmers. of.

We expect to be able to build code with clear structure, strong scalability, and easy maintenance, not a super hodgepodge of all logic and details.

final effect

Tetris

initial design

Game board (Board): 15 * 25 squares, the operation is carried out on the game board;

Shape: There are 7 shapes in total: L, J, T, Z, S, I, O

Style (Style): Each shape can be rotated, and it will present different shapes (or directions) after rotation. For example, L shape has 4 shapes.

Cell: The game board and shapes are composed of cells, the smallest unit. In the preliminary idea, the grid has two states: filled and blank. It seems that we can use simple 0 and 1 to represent the grid, that is, 1 means filled and 0 means blank; but after a little thought, the state of the grid is actually There are 3 types: 1. Active squares; 2. Inactive squares (landing squares); 3. Blank squares.

So we represent the three states in this way (the binary representation is used here to make it more regular, in fact, it is the three values ​​of 0, 1, and 2):

# CELL STATE
BLANK  = 0b00
ACTIVE = 0b01
LANDED = 0b10

Each Style form is composed of BLANK and ACTIVE state representations of squares, so a simple two-dimensional array can be represented. For example, the four forms of L shape can be expressed as:

s1 = [[1, 1, 1], 
      [1, 0, 0]]

s2 = [[1, 1], 
      [0, 1], 
      [0, 1]]
 
s3 = [[0, 0, 1], 
      [1, 1, 1]]

s4 = [[1, 0], 
      [1, 0], 
      [1, 1]]

In this way, the shape L and the four forms can be expressed as:

L = [s1, s2, s3, s4]

And so on, all 7 shapes and their different forms can be expressed as constants of the following form:

# SHAPES
L = [[(1, 1, 1), (1, 0, 0)], [(1, 1), (0, 1), (0, 1)], [(0, 0, 1), (1, 1, 1)], [(1, 0), (1, 0), (1, 1)]]
J = [[(1, 1, 1), (0, 0, 1)], [(0, 1), (0, 1), (1, 1)], [(1, 0, 0), (1, 1, 1)], [(1, 1), (1, 0), (1, 0)]]
T = [[(0, 1, 0), (1, 1, 1)], [(1, 0), (1, 1), (1, 0)], [(1, 1, 1), (0, 1, 0)], [(0, 1), (1, 1), (0, 1)]]
Z = [[(1, 1, 0), (0, 1, 1)], [(0, 1), (1, 1), (1, 0)]]
S = [[(0, 1, 1), (1, 1, 0)], [(1, 0), (1, 1), (0, 1)]]
I = [[(1,), (1,), (1,), (1,)], [(1, 1, 1, 1)]]
O = [[(1, 1), (1, 1)]]

Let's look at the game panel (Board). The game board is a collection of squares, and a two-dimensional array is also used to represent the game board. For example, the following is a 5 * 5 blank game board:

board = [[0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0]]

Any square in the panel can be represented by coordinates (x, y), obtained by:

cell = board[y][x]

How does the shape "draw" on the board? Because the squares of the shape are all active squares, the way the L shape is displayed on the panel is as follows:

board = [[0, 0, 0, 0, 0],
         [0, 1, 1, 1, 0],
         [0, 1, 0, 0, 0],
         [0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0]]

After the shape lands, the grid state of the shape changes to LANDED, and the board becomes as follows:

board = [[0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0],
         [0, 2, 2, 2, 0],
         [0, 2, 0, 0, 0]]

We noticed that although the game panel can be represented by a two-dimensional array, it should still need to define some general behaviors, such as getting or setting a certain coordinate cell state, how to "draw" the entire shape on the panel, and clearing line, judging whether it has reached the top, etc. possible behaviors, so we create a Board class:

class Board:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.current_shape = None
        self.init_cells()

    def __str__(self):
        return str(self.cells)

    def init_cells(self):
        self.cells = []
        for _ in range(self.height):
            self.cells.append([BLANK] * self.width)

    def get_cell(self, x, y):
        return self.cells[y][x]

    def set_cell(self, x, y, state=ACTIVE):
        self.cells[y][x] = state

    def _blit_shape(self, state):
        for offset_y, row in enumerate(self.current_shape.style):
            for offset_x, value in enumerate(row):
                if value:
                    self.set_cell(self.current_shape.x + offset_x, 
                        self.current_shape.y + offset_y, state)

    @property
    def is_over_top(self):
        for cell in self.cells[0]:
            if cell:
                return True
        return False

    def find_full_rows(self):
        index = []
        for y in range(self.height -1, -1, -1):
            if sum(self.cells[y]) == LANDED * self.width:
                index.append(y)
        return index

    def clear_full_rows(self):
        index = self.find_full_rows()
        if (l := len(index)) > 0:
            for y in index:
                self.cells.pop(y)
            for _ in range(l):
                self.cells.insert(0, [BLANK] * self.width)

    def update(self):
        for y in range(self.height):
            for x in range(self.width):
                if self.cells[y][x] == ACTIVE:
                    self.cells[y][x] = 0
        if self.current_shape:
            self._blit_shape(ACTIVE)

illustrate:

  1. The game will end when the shape reaches the top, so an is_over_top attribute is defined here to determine whether it is at the top;
  2. Because the game will eliminate the full row, find_full_rows is used to find the full row and return the index of the full row, where the index must be in descending order; clear_full_rows is used to clear the full row, because the full row index is in descending order, so the order of pop is also from Back to front, so that the index will not be messed up;
  3. A property is added here, current_shape, which indicates the active shape in the current panel (grid value is 1);
  4. The _blit_shape method is used to "draw" the current shape on the board. If the state attribute is LANDED, it means that the shape has landed;
  5. The last update method is used to refresh the panel: the "moving part" is the active shape on the panel, so each refresh will clear the active shape (that is, set the grid with the state of ACTIVE to BLANK), after clearing Draw it again - because the shape will have a new position or a new style at this time.

Next build the Shape class:

class Shape:
    def __init__(self, style):
        self.styles = styles[style]
        self.styles_len = len(self.styles)
        self.i = 0
        self.board = None
        self.x = 0
        self.y = 0

    def __repr__(self):
        return str(self.style)

    @staticmethod
    def random_shape():
        return Shape(random.choice('LJTZSIO'))

    @property
    def i(self):
        return self._i
    @i.setter
    def i(self, value):
        self._i = value
        self.style = self.styles[value]
        self.length = len(self.style[0])
        self.height = len(self.style)

    @property
    def next_style(self):
        if self.i + 1 >= self.styles_len:
            return self.styles[0]
        return self.styles[self.i + 1]

    @property
    def margin_left(self):
        return self.x

    @property
    def margin_right(self):
        return self.board.width - self.x - 1

    @property
    def margin_bottom(self):
        return self.board.height - self.y - 1
    
    def move_left(self, value=1):
        self.x -= value

    def move_right(self, value=1):
        self.x += value

    def move_down(self, value=1):
        self.y += value

    def land(self):
        self.board._blit_shape(LANDED)
        self.board.current_shape = None

    def rotate(self):
        if (self.i + 1) >= self.styles_len:
            self.i = 0
        else:
            self.i += 1

illustrate:

  1. We don't need to build a special class for style, but treat style as an attribute; the styles attribute represents all the shapes of the shape, and the related i attribute indicates the index of the current style in the styles list. Once the value of i is set value, the current style will change accordingly; next_style indicates the shape of the next flip;
  2. The board attribute represents the board to which the current shape belongs, and x and y represent the position coordinates;
  3. Several margin attributes indicate the distance between the current shape and the edge of the panel;
  4. Several move methods represent the left, right and down movement of the shape; the rotate method represents flipping; the land method represents landing
  5. The static method random_shape will create a random shape instance

The two most basic classes have been built, and the next step is to implement the game logic. See you in the next chapter~

Guess you like

Origin blog.csdn.net/weixin_49775731/article/details/126601191