🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
## 我们先搭建一个程序大致的框架,绘制一个游戏窗口。 导入程序所必要的模块,因为要随机产生一个方块,所以用到随机数模块random,time库是时间函数用来延时。 ```python import random, time, pygame, sys from pygame.locals import * ``` ## 设置俄罗斯方块使用的常量。 ``` python FPS = 25 # 设置屏幕刷新率 WINDOW_WIDTH = 640 # 设置窗口宽度 WINDOW_HEIGHT = 480 # 设置窗口高度 BOX_SIZE = 20 # 方格大小 # 放置俄罗斯方块窗口的大小 BOARD_WIDTH = 10 BOARD_HEIGHT = 20 BLANK = '.' # 代表空的形状 MOVESIDEWAYSFREQ = 0.15 # 若一直按下方向左键或右键那么每0.15秒方块才会继续移动 MOVEDOWNFREQ = 0.1 # 向下的频率 XMARGIN = int((WINDOWWIDTH - BOARDWIDTH * BOXSIZE) / 2) # x方向的边距 TOPMARGIN = WINDOWHEIGHT - (BOARDHEIGHT * BOXSIZE) - 5 # 距离窗口顶部的边距 ``` <div align="center"><img src="images/screenshot_1561344446084.png"/></div> ## 定义颜色 ```python # add color WHITE = (255, 255, 255) GRAY = (185, 185, 185) BLACK = ( 0, 0, 0) RED = (155, 0, 0) LIGHTRED = (175, 20, 20) GREEN = ( 0, 155, 0) LIGHTGREEN = ( 20, 175, 20) BLUE = ( 0, 0, 155) LIGHTBLUE = ( 20, 20, 175) YELLOW = (155, 155, 0) LIGHTYELLOW = (175, 175, 20) BORDERCOLOR = BLUE BGCOLOR = BLACK TEXTCOLOR = WHITE TEXTSHADOWCOLOR = GRAY COLORS = ( BLUE, GREEN, RED, YELLOW) LIGHTCOLORS = (LIGHTBLUE, LIGHTGREEN, LIGHTRED, LIGHTYELLOW) assert len(COLORS) == len(LIGHTCOLORS) # each color must have light color ``` 每个下落的俄罗斯方块随机有四种颜色:蓝色、绿色、红色和黄色。当我们绘制小方块的时候,会在边缘增加一个较浅的颜色,使方块看起来更有立体感。 ## 定义方块 方块的基本形状一共有以下七种: <div align="center"><img src="images/screenshot_1561102081007.png"/></div> 每种方块都能够旋转,所谓旋转,即是把方块顺时针旋转90°。我们创建数组来存储这些方块,像下面这样,“.”表示空白,“O”表示小方格。 例如:<br> ['.....', <br> '.....', <br> '..00.',<br> '.00..',<br> '.....']<br> 上面就是绘制的一个如下图的S型方块,: <div align="center"><img src="images/screenshot_1561359412952.png"/></div> 我们再将方块顺时针旋转90°,然后绘制其图形,写入数组中。下面的数组列出了所有写形状。 ```python T_SHAPE = [['.....', '..O..', '.OOO.', '.....', '.....'], ['.....', '..O..', '..OO.', '..O..', '.....'], ['.....', '.....', '.OOO.', '..O..', '.....'], ['.....', '..O..', '.OO..', '..O..', ''.....]] S_SHAPE = [['.....', '..... ', '..OO.', '.OO..', '.....'], ['.....', '..O..', '..OO.', '.....']] Z_SHAPE = ['.....', '.....', '.OO..', '..OO.', '.....'], ['.....', '..O..', '.OO..', '.O...', '.....']] J_SHAPE = [['.....', '.O...', '.OOO.', '.....', '.....'], ['.....', '..OO.', '..O..', '..O..’, '.....'], ['.....', '.....', '.OOO.', '...O.', '.....'], ['.....', '.....', '.OOO.', '...O.', '.....'], ['.....', '..O..', '..O..', '.OO..', '.....']] L_SHAPE = [['.....', '...O.', '.OOO.', '.....', '.....'], ['.....', '...O.', '.OOO.', '.....', '.....'], ['.....', '.....', '.OOO.', '.O...', '.....'], ['.....', '.OO..', '..O..', '..O..', '.....']] I_SHAPE = [['..O..', '..O..', '..O..', '..O..', '.....'], ['.....', '.....', 'OOOO.', '.....', '.....']] O_SHAPE = [['.....', '.....', '.OO..', '.OO..', '.....']] PIECES = { # 定义一个数据结构存储对应的形状 'T' : T_SHAPE, 'S' : S_SHAPE, 'Z' : Z_SHAPE, 'J' : J_SHAPE, 'L' : L_SHAPE, 'I' : I_SHAPE, 'O' : O_SHAPE } ``` ## main()方法 主函数中主要创建一写全局变量和游戏开始前显示一个开始画面,之后是游戏循环,循环中首先播放背景音乐,然后调用runGame()运行游戏,当游戏失败,runGame()就会返回到main()函数,这是会停止背景音乐并显示游戏失败的画面。当游戏这按下任意键,显示游戏失败的showTextScreen()函数就会返回到main()函数,游戏循环会再次开始然后继续下一次游戏。 ```python def main(): global FPSLOCK, DISPLAYSURF, BASICFONT, BIGFONT # 定义全局变量 pygame.init() # 初始化pygame FPSCLOCK = pygame.time.Clock() # 获得pygame时钟 DISPLAYSURF = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) # 设置窗口 BASICFONT = pygame.font.Font('freesansbold.ttf', 18) # 设置基础的字体 BIGFONT = pygame.font.Font('freesansbold.ttf', 100) # 设置大字体 pygame.display.set_caption('My Tetris') # 窗口标题 showTextScreen('Tetris') # 显示开始画面 while True: # 游戏主循环 pygame.mixer.music.load('tetrisb.mid') pygame.mixer.music.play(-1, 0.0) runGame() # 运行游戏 pygame.mixer.music.stop() # 退出游戏后,结束播放音乐 showTextScreen('Game Over') # 显示结束画面 ``` main函数主要创建游戏的窗口和设置播放背景音乐。然后调用runGame()函数开始游戏,当玩家游戏结束时,返回到main()中,然后打印游戏结束信息。 ## 基本模块 定义一个退出方法,以便需要的时候退出游戏。 ```python # 退出 def terminate(): pygame.quit() sys.exit() ``` 定义一个检测是否有按键被按下的方法,当处于开始、结束、暂停画面时,可以调用该方法,以便程序退出当前画面。 ```python # 检查是否有按键被按下 def checkForKeyPress(): # 通过事件队列寻找KEYUP事件 # 从事件队列删除KEYDOWN事件 checkForQuit() for event in pygame.event.get([KEYDOWN, KEYUP]): if event.type == KEYDOWN: continue return event.key return None ``` 考虑到我们需要在屏幕上显示文字,不妨直接创建相关方法,以便程序复用。此程序接收三个参数:要显示的文字、要显示文字的字体、要显示文字的颜色,它会返回相应的文本对象,以便使用。 ```python # 创建文本绘制对象 def makeTextObjs(text, font, color): surf = font.render(text, True, color) return surf, surf.get_rect() ``` 游戏开始画面、游戏结束画面以及游戏暂停画面可用一个方法来实现,以及游戏暂停画面可以用一个方法来实现,同时在循环中调用之前的checkForKeyPress()方法,以检测是否有按键被按下,从而实现是否从当前画面退出。 ```python # 显示开始、暂停、结束画面 def showTextScreen(text): # 这个函数用于在屏幕中央显示大文本,直到按下任意键 # 绘制文字阴影 titleSurf, titleRect = makeTextObjs(text, BIGFONT, TEXTSHADOWCOLOR) titleRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2)) DISPLAYSURF.blit(titleSurf, titleRect) # 绘制文字 titleSurf, titleRect = makeTextObjs(text, BIGFONT, TEXTCOLOR) titleRect.center = (int(WINDOWWIDTH / 2) - 3, int(WINDOWHEIGHT / 2) - 3) DISPLAYSURF.blit(titleSurf, titleRect) # 绘制额外的"Press a key to play."文字 pressKeySurf, pressKeyRect = makeTextObjs('Press a key to play.', BASICFONT, TEXTCOLOR) pressKeyRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2) + 100) DISPLAYSURF.blit(pressKeySurf, pressKeyRect) while checkForKeyPress() == None: pygame.display.update() FPSCLOCK.tick() ``` 在游戏开始之前,我们需要一个空的Board框,我们用一个二维列表表示Board框,列表中的每一项都为BLANK: ```python # 清空Board def getBlankBoard(): board = [] for i in range(BOARDWIDTH): board.append([BLANK] * BOARDHEIGHT) return board ``` 游戏运行时,每次都会有一个新的Piece从顶部落下,我们用一个字典来表示Piece,字典中的键包括Shape、相应Shape的方向、起始位置、以及颜色,获得新Piece的方法如下: ```python # 随机获得一个新的Piece(形状,方向,颜色) def getNewPiece(): shape = random.choice(list(PIECES.keys())) newPiece = {'shape': shape, 'rotation': random.randint(0, len(PIECES[shape]) - 1), 'x': int(BOARDWIDTH / 2) - int(TEMPLATEWIDTH / 2), # x居中 'y': -2, # y在屏幕的上方,小于0 'color': random.randint(0, len(COLORS)-1)} return newPiece ``` 获取新Piece之后,就要将它添加到Board框中,添加的原理就是将Board框中对应坐标的Box绘制成相应Piece的颜色: ```python # 将一个Piece添加到Board中 def addToBoard(board, piece): for x in range(TEMPLATEWIDTH): for y in range(TEMPLATEHEIGHT): if PIECES[piece['shape']][piece['rotation']][y][x] != BLANK: board[x + piece['x']][y + piece['y']] = piece['color'] ``` 当Piece着陆之后在Board外时,游戏结束,因此我们需要一个判断Board边界的方法: ```python # Board边界 def isOnBoard(x, y): return x >= 0 and x < BOARDWIDTH and y < BOARDHEIGHT ``` 刚刚那种判断游戏结束的方法,我们可以将其抽象出来,看成是检查Piece的当前位置是否合法,因为不光是在判断游戏结束时调用它,在调整Piece形状时也需要调用,判断调整形状后的Piece在当前位置中是否容得下。 ```python # Piece在当前的Board里是否是一个合法可用的位置 def isValidPosition(board, piece, adjX=0, adjY=0): # 若Piece在Board内并且无碰撞,则返回True for x in range(TEMPLATEWIDTH): for y in range(TEMPLATEHEIGHT): isAboveBoard = y + piece['y'] + adjY < 0 if isAboveBoard or PIECES[piece['shape']][piece['rotation']][y][x] == BLANK: continue if not isOnBoard(x + piece['x'] + adjX, y + piece['y'] + adjY): return False if board[x + piece['x'] + adjX][y + piece['y'] + adjY] != BLANK: return False return True ``` 此外,当游戏中某一行被填满时,我们将移除这一行,因此我们需要一个判断某行是否填满的方法,判断的原理就是看相应的这一行列表中的项是否为BLANK: ```python # 判断当前的这行是否被全部填满 def isCompleteLine(board, y): for x in range(BOARDWIDTH): if board\[x\]\[y\] == BLANK: return False return True ``` 还需要一个移除某行的方法,也就是将这一行上面的每一行都下降一行,同时还应该返回完成填满的总行数,这个值将作为玩家的分值,也就是说每成功移除一行,玩家分数加1: ```python # 检查每一行,移除完成填满的一行,将这一行上面的所有的都下降一行,返回完成填满的总行数 def removeCompleteLines(board): numLinesRemoved = 0 # 从-1开始从下往上检查每一行 y = BOARDHEIGHT - 1 while y >= 0: if isCompleteLine(board, y): for pullDownY in range(y, 0, -1): for x in range(BOARDWIDTH): board[x][pullDownY] = board[x][pullDownY-1] for x in range(BOARDWIDTH): board[x][0] = BLANK numLinesRemoved += 1 else: y -= 1 return numLinesRemoved ``` 随着游戏分数的越来越大,相应的等级也应越来越大,等级是当前消除行数除以10,我们假设刚开始时等级为1,随着等级的不断增大,下落频率应相应的减小,如下所示: ```python # 根据分数来计算等级和下落的频率 def calculateLevelAndFallFreq(score): level = int(score / 10) + 1 fallFreq = 0.27 - (level * 0.02) return level, fallFreq ``` ## runGame()方法 在游戏开始和Piece掉落之前,我们需要初始化一些跟游戏开始相关的变量。fallingPiece变量被赋值成当前掉落的变量,nextPiece变量被赋值成游戏者可以在屏幕NEXT区域看见的下一个Piece。 游戏主循环中,fallingPiece变量在Piece着陆后被设置成None。这意味着nextPiece变量中的下一个Piece应该被赋值给fallingPiece变量,然后一个随机的Piece又会被赋值给nextPiece变量。lastFallTime变量也被赋值成当前时间,这样我们就可以通过fallFreq变量控制Piece下落的频率。 事件循环主要处理当翻转方块、移动方块时或者暂停游戏时的一些事情。若游戏暂停,我们应该隐藏掉游戏界面以防止游戏者作弊(否则游戏者会看着画面思考怎么处理方块),用DISPLAYSURF.fill(BGCOLOR)就可以实现这个效果。停止按下方向键或ASD键会把movingLeft,movingRight,movingDown变量设置为False,表明游戏者不再想要在此方向上移动方块。当左方向键按下(而且往左移动是有效的,通过调用isVaildPosition()函数知道的),那么我们应该改变一个方块的位置使其向左移动一格。 如果方向键上或W键被按下,那么就会翻转方块,就是将储存在fallingPiece字典中的‘rotation’键的键值加1,然而,当增加的'rotation'键值大于所有当前类型方块的形状的数目的话(此变量储存在len(SHAPES[fallingPiece['shape']])变量中),那么它翻转到最初的形状。如果翻转后的形状因为其中的一些小方块已经超过边框的范围而无效,那么我们就要把它变回原来的形状通过将fallingPiece['rotation'])减去1,同理,按Q键执行反向翻转时则是加1。 当游戏者按下空格键,方块将会迅速的下落至着陆。程序首先需要找出到它着陆需要下降个多少个格子,其中有关moving的三个变量都要被设置为False(保证程序后面部分的代码知道游戏者已经停止了按下所有的方向键)。 方块自然下落的速率由lastFallTime变量决定。如果自从上个Piece掉落了一个格子后过去了足够的时间,那么我们就会再让Piece移动一个格子。 ```python # 运行游戏 def runGame(): # 在游戏开始前初始化变量 # 获得一个空的board board = getBlankBoard() lastMoveDownTime = time.time() # 最后向下移动的时刻 lastMoveSidewaysTime = time.time() #最后侧向移动的时刻 lastFallTime = time.time() # 最后的下降时间 # 是否可以 向下,向左,向右 # 注意:这里没有向上可用 movingDown = False movingLeft = False movingRight = False score = 0 # 分数 level, fallFreq = calculateLevelAndFallFreq(score) # 根据分数计算等级和下降的频率 fallingPiece = getNewPiece() # 获得新的形状(当前的形状) nextPiece = getNewPiece() # 获得下一个形状 while True: # 游戏循环体 if fallingPiece == None: # 当前没有下降的形状 # 重新获得新的形状和下一个形状 fallingPiece = nextPiece nextPiece = getNewPiece() lastFallTime = time.time() # 重置最后下降的时间 if not isValidPosition(board, fallingPiece): # 判断界面上是否还有空位(方块是否到顶),没有则结束游戏 return checkForQuit() # 检查是否有退出事件 for event in pygame.event.get(): # 事件处理循环 if event.type == KEYUP: # KEYUP事件处理 if (event.key == K_p): # 用户按P键暂停 DISPLAYSURF.fill(BGCOLOR) pygame.mixer.music.stop() #停止音乐 showTextScreen('Paused') # 显示暂停界面,直到按任意键继续 pygame.mixer.music.play(-1, 0.0) # 继续循环音乐 # 重置各种时间 lastFallTime = time.time() lastMoveDownTime = time.time() lastMoveSidewaysTime = time.time() elif (event.key == K_LEFT or event.key == K_a): movingLeft = False elif (event.key == K_RIGHT or event.key == K_d): movingRight = False elif (event.key == K_DOWN or event.key == K_s): movingDown = False elif event.type == KEYDOWN: # KEYDOWN事件处理 # 左右移动piece if (event.key == K_LEFT or event.key == K_a) and isValidPosition(board, fallingPiece, adjX=-1): fallingPiece['x'] -= 1 movingLeft = True movingRight = False lastMoveSidewaysTime = time.time() elif (event.key == K_RIGHT or event.key == K_d) and isValidPosition(board, fallingPiece, adjX=1): fallingPiece['x'] += 1 movingRight = True movingLeft = False lastMoveSidewaysTime = time.time() # UP或W键 旋转piece (在有空间旋转的前提下) # 正向旋转 elif (event.key == K_UP or event.key == K_w): fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(PIECES[fallingPiece['shape']]) if not isValidPosition(board, fallingPiece): fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(PIECES[fallingPiece['shape']]) # Q键,反向旋转 elif (event.key == K_q): fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(PIECES[fallingPiece['shape']]) if not isValidPosition(board, fallingPiece): fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(PIECES[fallingPiece['shape']]) # DOWN或S键 使piece下降得更快 elif (event.key == K_DOWN or event.key == K_s): movingDown = True if isValidPosition(board, fallingPiece, adjY=1): fallingPiece['y'] += 1 lastMoveDownTime = time.time() # 空格键,直接下降到最下面且可用的地方 elif event.key == K_SPACE: movingDown = False movingLeft = False movingRight = False for i in range(1, BOARDHEIGHT): if not isValidPosition(board, fallingPiece, adjY=i): break fallingPiece['y'] += i - 1 # 根据记录的用户输入方向的变量来移动piece if (movingLeft or movingRight) and time.time() - lastMoveSidewaysTime > MOVESIDEWAYSFREQ: if movingLeft and isValidPosition(board, fallingPiece, adjX=-1): fallingPiece['x'] -= 1 elif movingRight and isValidPosition(board, fallingPiece, adjX=1): fallingPiece['x'] += 1 lastMoveSidewaysTime = time.time() if movingDown and time.time() - lastMoveDownTime > MOVEDOWNFREQ and isValidPosition(board, fallingPiece, adjY=1): fallingPiece['y'] += 1 lastMoveDownTime = time.time() # 自动下降piece if time.time() - lastFallTime > fallFreq: if not isValidPosition(board, fallingPiece, adjY=1): addToBoard(board, fallingPiece) score += removeCompleteLines(board) level, fallFreq = calculateLevelAndFallFreq(score) fallingPiece = None else: fallingPiece['y'] += 1 lastFallTime = time.time() # 绘制屏幕上的所有东西 DISPLAYSURF.fill(BGCOLOR) drawBoard(board) drawStatus(score, level) drawNextPiece(nextPiece) if fallingPiece != None: drawPiece(fallingPiece) pygame.display.update() FPSCLOCK.tick(FPS) ``` ## 绘制屏幕 之前的编程,为了简化操作,我们使用的是Board的坐标,在绘制图形之前,我们需要把Box的坐标转换为相应的像素坐标: ```python # 根据Board的坐标转化成像素坐标 def convertToPixelCoords(boxx, boxy): return (XMARGIN + (boxx * BOXSIZE)), (TOPMARGIN + (boxy * BOXSIZE)) ``` 绘制Box: ```python # 绘制Box def drawBox(boxx, boxy, color, pixelx=None, pixely=None): # 使用Board的坐标绘制单个Box(一个Piece含有4个Box),若像素坐标pixelx、pixely被指定,则直接使用像素坐标(用于NextPiece区域) if color == BLANK: return if pixelx == None and pixely == None: pixelx, pixely = convertToPixelCoords(boxx, boxy) pygame.draw.rect(DISPLAYSURF, COLORS[color], (pixelx + 1, pixely + 1, BOXSIZE - 1, BOXSIZE - 1)) pygame.draw.rect(DISPLAYSURF, LIGHTCOLORS[color], (pixelx + 1, pixely + 1, BOXSIZE - 4, BOXSIZE - 4)) ``` 绘制Board主要包括绘制Board边框、绘制Board背景以及绘制Board中的Box: ```python# 绘制Board def drawBoard(board): # 绘制Board边框 pygame.draw.rect(DISPLAYSURF, BORDERCOLOR, (XMARGIN - 3, TOPMARGIN - 7, (BOARDWIDTH * BOXSIZE) + 8, (BOARDHEIGHT * BOXSIZE) + 8), 5) # 绘制Board背景 pygame.draw.rect(DISPLAYSURF, BGCOLOR, (XMARGIN, TOPMARGIN, BOXSIZE * BOARDWIDTH, BOXSIZE * BOARDHEIGHT)) # 绘制Board中的Box for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): drawBox(x, y, board[x][y]) ``` 绘制分数、等级等状态信息: ```python \# 绘制游戏分数、等级信息 def drawStatus(score, level): # 绘制分数文本 scoreSurf = BASICFONT.render('Score: %s' % score, True, TEXTCOLOR) scoreRect = scoreSurf.get\_rect() scoreRect.topleft = (WINDOWWIDTH - 150, 20) DISPLAYSURF.blit(scoreSurf, scoreRect) # 绘制等级文本 levelSurf = BASICFONT.render('Level: %s' % level, True, TEXTCOLOR) levelRect = levelSurf.get\_rect() levelRect.topleft = (WINDOWWIDTH - 150, 50) DISPLAYSURF.blit(levelSurf, levelRect) ``` 绘制Piece: ```python # 绘制各种形状Piece(S,Z,I,O,J,L,T) def drawPiece(piece, pixelx=None, pixely=None): shapeToDraw = PIECES[piece['shape']][piece['rotation']] if pixelx == None and pixely == None: # 若pixelx、pixely没有被指定,则使用piece数据结构中存储的位置 pixelx, pixely = convertToPixelCoords(piece['x'], piece['y']) # 绘制组成Piece的每个Box for x in range(TEMPLATEWIDTH): for y in range(TEMPLATEHEIGHT): if shapeToDraw[y][x] != BLANK: drawBox(None, None, piece['color'], pixelx + (x * BOXSIZE), pixely + (y * BOXSIZE)) ``` 绘制NextPiece区域显示的内容: ```python # 绘制提示信息,下一个Piece def drawNextPiece(piece): # 绘制"Next"文本 nextSurf = BASICFONT.render('Next:', True, TEXTCOLOR) nextRect = nextSurf.get_rect() nextRect.topleft = (WINDOWWIDTH - 120, 80) DISPLAYSURF.blit(nextSurf, nextRect) # 绘制NextPiece drawPiece(piece, pixelx=WINDOWWIDTH-120, pixely=100) ``` ## 尝试运行代码 ```python if __name__ == "__main__": main() ``` ## 执行程序 ``` sudo python tetris.py ```