Pygame implements Tetris in 200 lines of code


Source code address: PyGame Tetris

logical design

The logic of Tetris is very simple, that is, several blocks are put together and then fall. When they hit the surrounding walls, they cannot move. If a row is filled with squares, the row is deleted and all the squares above it are moved down one row.

In order to code this logic, a Boolean matrix can be used to represent specific block types. For example, long strip blocks can be represented by the following matrix.

BLOCK_I = [[0,1,0,0],
           [0,1,0,0],
           [0,1,0,0],
           [0,1,0,0]]

In actual operations, rotation operations are often encountered. The basic logic is to rotate the i i iline change − i -i icolumn, the rotation is as follows

def rotateBlock(block):
    newBlock = [[[] for _ in block] for b in block[0]]
    for i,row in enumerate(block, 1):
        for j,r in enumerate(row, 0):
            newBlock[j][-i] = r
    return newBlock

Define all the blocks and enclose them in the listBLOCK,

BLOCK_S = [[0,1,1],
           [1,1,0],
           [0,0,0]]

BLOCK_Z = [[1,1,0],
     [0,1,1],
     [0,0,0]]

# I型方块
BLOCK_I = [[0,1,0,0],
     [0,1,0,0],
     [0,1,0,0],
     [0,1,0,0]],

# O型方块
BLOCK_O = [[1,1],
            [1,1]]
# J型方块
BLOCK_J = [[1,0,0],
     [1,1,1],
     [0,0,0]]

# L型方块
BLOCK_L = [[0,0,1],
     [1,1,1],
     [0,0,0]]

# T型方块
BLOCK_T = [[0,1,0],
     [1,1,1],
     [0,0,0]]

BLOCKS = [BLOCK_S, BLOCK_I, BLOCK_J, BLOCK_L, BLOCK_O, BLOCK_T, BLOCK_Z]

With this, one of the blocks can be randomly generated. This process needs to introduce a little randomness.

import random
# 创建一个图形
def newBlock():
    block = random.choice(BLOCKS)
    for _ in range(random.randint(0,4)):
        block = rotateBlock(block)
    return block

Move logic

The principle is very simple, but if you want to write a playable game, you still need to pay attention to some details. Such as the behavior of Tetris when hitting a wall; the method of rotating blocks. Before implementing these specific problems, first set a few constants

SCREEN_WIDTH, SCREEN_HEIGHT = 450, 750
BLOCK_COL_NUM = 10  # 每行的方格数
BLOCK_ROW_NUM = 25  # 每列的方个数
SIZE = 30  # 每个小方格大小
BG_COLOR = (40, 40, 60)  # 背景色
RED = (200, 30, 30)  # 红色,GAME OVER 的字体颜色
WHITE = (255,255,255)   # 白色
BLACK = (0,0,0)         # 黑色
GREEN = (60, 128, 80)   # 绿色
STOP_LST = [[0 for _ in range(BLOCK_COL_NUM)]
               for _ in range(BLOCK_ROW_NUM)]


Next, implement the details of Tetris one by one. When moving the block left or right, if the block touches the wall, a determination function is needed. The code is as follows. When isR is True, it means moving to the right, otherwise it moves to the left. The judgment logic is very intuitive, just check whether it exceeds the boundary.

# 判断是否可以向左右移动
# isR为True,判断能否向右移动;isR为False,判断能否向左移动
def judgeMoveLR(block, stCol, isR):
    nCol = len(block[0])
    cols = range(nCol-1, -1, -1) if isR else range(nCol)
    for col in cols:
        lstCol = [line[col] for line in block]
        if 1 not in lstCol:
            continue
        if not 0 <= stCol + col < BLOCK_COL_NUM:
            return False
    return True

If the judgment is successful, you need to move the block, then according to its input to move left or right, you can construct a function

# 相左或者向右移动
def moveLR(block, stCol, key):
    isR = key == pygame.K_RIGHT
    stCol = stCol + 1 if isR else stCol - 1
    if judgeMoveLR(block ,stCol, isR):
        return 1 if isR else -1
    return 0

If the block hits the wall during the falling process, it will be a little troublesome, because some blocks may have been accumulated below. To do this, you need to know not only the information of the currently falling block, but also the information of the blocks that have been accumulated.

# 判断是否可以继续下落
def judgeMoveDown(block, stRow, stCol, stopLst):
    # 堆积方块的位置
    stopPosition = list()
    for row, line in enumerate(stopLst):
        for col, b in enumerate(line):
            if b: stopPosition.append((row, col))
    # 判断碰撞
    for row, line in enumerate(block):
        if 1 in line and stRow + row >= BLOCK_ROW_NUM:
            return False
        for col, b in enumerate(line):
            if b and (stRow + row, stCol + col) in stopPosition:
                return False
    return True

Finally, the three are combined to determine whether the block can continue to move.

def canMove(block, stRow, stCol, stopLst):
    flag = judgeMoveLR(block, stCol, False)
    flag &= judgeMoveLR(block, stCol, True)
    flag &= judgeMoveDown(block, stRow, stCol, stopLst)
    return flag

elimination and accumulation

The elimination logic is that when a row contains all 1s, delete the row and then add a list of all 0s at the top.

def judgeLines(stopLst):
    # 记录刚刚消除的行数
    i, N, count = 0, len(stopLst), 0
    for i in range(N):
        if 0 not in stopLst[i]:
            line = [0]*len(stopLst.pop(i))
            stopLst.insert(0, line)
            count += 10
    return count

The stacking logic is relatively simple, just set the stacking part position to 1.

# 将停止移动的block添加到堆积方块
def addToStopLst(stopLst, block, stRow, stCol):
    for row, line in enumerate(block):
        for col, b in enumerate(line):
            if b: stopLst[stRow + row][stCol + col] = 1

At this point, the operation logic of Tetris has been designed, and the next step is drawing.

Parameter logic

The falling speed of Tetris can distinguish different levels. The following is a scheme that uses scores to determine the falling speed. The speed value represents the delay time of the falling block.

def changeSpeed(score):
    scores = [20, 50, 100, 200]
    infos = "12345"
    speeds = [0.5, 0.4, 0.3, 0.2, 0.1]
    ind = bisect(scores, score)
    return infos[ind], speeds[ind]

Drawing logic

The first is background drawing, which mainly includes background color and grid drawing.

def drawBackground(screen):
    screen.fill(BG_COLOR)
    width = SIZE * BLOCK_COL_NUM
    pygame.draw.line(screen, BLACK, (width, 0), (width, SCREEN_HEIGHT), 4)
    for x in range(BLOCK_COL_NUM):
        pygame.draw.line(screen, BLACK, (x * SIZE, 0), (x * SIZE, SCREEN_HEIGHT), 1)
    for y in range(BLOCK_ROW_NUM):
        pygame.draw.line(screen, BLACK, (0, y * SIZE), (width, y * SIZE), 1)

Only the left window is used here, and the right side will be used to store prompt information, mainly including score, speed level and the next falling block.

def drawRight(screen, score, speedInfo):
    fontPath = r'C:\Windows\Fonts\msyh.ttc'
    font = pygame.font.Font(fontPath, 24)  # 黑体24

    posDct = {
    
    
        '得分: ':10, str(score): 50,
        '速度: ':100, speedInfo: 150,
        '下一个:':200
    }
    for k,v in posDct.items():
        msg = font.render(k, True, WHITE)
        screen.blit(msg, (BLOCK_COL_NUM * SIZE + 10, v))

Then there is the block drawing function. As the name suggests, drawBlock draws a block and drawBlocks draws a group of blocks.

def drawBlock(screen, row, col, stRow, stCol):
    stRow += row * SIZE
    stCol += SIZE * col
    rect = pygame.Rect(stCol, stRow, SIZE, SIZE)
    pygame.draw.rect(screen, GREEN, rect, 0)
    pygame.draw.rect(screen, BLACK, rect, 1)

def drawBlocks(screen, block, stRow, stCol):
    for row, line in enumerate(block):
        for col, b in enumerate(line):
            if not b: continue
            drawBlock(screen, row, col, stRow, stCol)

Finally, if the game fails, a failure message will pop up

def drawGamerOver(screen):
    fontPath = r'C:\Windows\Fonts\msyh.ttc'
    font = pygame.font.Font(fontPath, 72)

    w, h = font.size('GAME OVER')
    msg = font.render('GAME OVER', True, RED)
    screen.blit(msg, ((SCREEN_WIDTH - w) // 2, (SCREEN_HEIGHT - h) // 2))

    # 显示"鼠标点击任意位置,再来一局"
    font = pygame.font.Font(fontPath, 24)
    w, h = font.size('点击任意位置,再来一局')
    msg = font.render('点击任意位置,再来一局', True, RED)
    screen.blit(msg, ((SCREEN_WIDTH - w) // 2, (SCREEN_HEIGHT - h) // 2 + 80))

main program

The main program content is as follows, in which initBlock is used to initialize the block. Since everyone is familiar with Tetris and all the sub-functions called have been explained, the process will not be described in detail.

def initBlock(nextBlock=None):
    stRow, stCol = -2, 4
    nowBlock = nextBlock if nextBlock else newBlock()
    nextBlock = newBlock()
    return stRow, stCol, nowBlock, nextBlock

def main():
    pygame.init()
    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pygame.display.set_caption('俄罗斯方块')

    # 初始化当前图形的位置,当前图形,以及下一个图形
    stRow, stCol, nowBlock, nextBlock = initBlock()
    last_time = time.time()
    speed, speedInfo = 0.5, '1'
    score, gameOver = 0, False
    stopLst = deepcopy(STOP_LST)
    clock = pygame.time.Clock()

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                if event.key in [pygame.K_LEFT, pygame.K_RIGHT]:
                    stCol += moveLR(nowBlock, stCol, event.key)
                elif event.key == pygame.K_UP:
                    rotBlock = rotateBlock(nowBlock)
                    if canMove(rotBlock, stRow, stCol, stopLst):
                        nowBlock = rotBlock
                elif event.key == pygame.K_DOWN:
                    # 判断是否可以向下移动,如果碰到底部或者其它的图形就不能移动了
                    while judgeMoveDown(nowBlock, stRow + 1, stCol, stopLst):
                        stRow += 1
            if gameOver:
                stRow, stCol, nowBlock, nextBlock = initBlock()
                stopLst = deepcopy(STOP_LST)
                score = 0
                gameOver = False

        # 判断是否修改当前图形显示的起始行
        if not gameOver and time.time() - last_time > speed:
            last_time = time.time()
            # 可以向下移动的情形
            if judgeMoveDown(nowBlock, stRow + 1, stCol, stopLst):
                stRow += 1
            else:
                # 不可向下移动,则需新建一个block然后下落
                addToStopLst(stopLst, nowBlock, stRow, stCol)
                score += judgeLines(stopLst)
                gameOver =  1 in stopLst[0]             # 第一行中间有1则游戏结束
                speedInfo, speed = changeSpeed(score)   # 调整速度
                stRow, stCol, nowBlock, nextBlock = initBlock(nextBlock)

        drawBackground(screen)
        drawRight(screen, score, speedInfo)
        drawBlocks(screen, nowBlock, stRow*SIZE, stCol*SIZE)    # 当前方块
        drawBlocks(screen, stopLst, 0, 0)                       # 堆积方块
        drawBlocks(screen, nextBlock, 300, 320)                 # 下一个方块

        # 显示游戏结束画面
        if gameOver:
            drawGamerOver(screen)

        pygame.display.update()
        clock.tick(60)  # 通过一定的延时,实现1秒钟能够循环60次

The final game effect is as follows

Insert image description here

Guess you like

Origin blog.csdn.net/m0_37816922/article/details/134902678