Tetromino (Python, Albert Sweigart, 2012)
Tetromino | |
---|---|
开发 | Albert Sweigart |
游戏平台 | Python(Pygame) |
发行时间 | 2012年 |
游戏信息 | |
预览块数 | 1 |
场地大小 | 10 × 20 |
暂存块 | 无 |
硬降 | 无(但有瞬降) |
旋转系统 | 专用 |
| |
| |
|
Tetromino 是一款 Python 四连方块游戏。
该游戏是 Making Games with Python & Pygame 一书第七章的内容。
该游戏的代码和编程讲解为许多其他的 Python 四连方块游戏提供了支持。
玩法
升级、得分。
开局 1 级,每消 10 行升级,得分等于总消行数。
方块自动下落间隔 = (0.27 - 等级 × 0.02) 秒。
方块堆超出第 20 行的部分消失。
死亡判定:重叠死亡。
操作
A/D 或左右键横移,Q 逆时针旋转,W 或上键顺时针旋转,S 或下键软降,空格键瞬降。
横移和软降可以叠加长按,旋转和瞬降只能单点但可插入长按。
单纯长按可跨块,被插入其他操作后不可跨块。
方块环境配置
七种方块都是纯色,在红黄蓝绿四种颜色当中随机取一种。
|
各碰撞箱的入场位置如上图所示。
其中,X 格坐标为 (6,21)。
方块入场时朝向随机。
以下图组中,每一组的最左图取横向入场朝向,右图是依次顺时针旋转的朝向:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Tetromino (Python, Albert Sweigart, 2012) 没有踢墙。
第七章的讲解
此处围绕原文调整了部分内容。
第一部分:游戏概念和源代码 |
游戏方法和六个术语
Tetromino 是“俄罗斯方块”的一个克隆版本。不同形状的四连方块从屏幕顶上落下,玩家要尽量控制它们填满整行消除(更上方的行随之下落),直到方块堆填满场地、新方块入场空间不足。
Tetromino 的源代码 |
第二部分:初始设置代码 |
通常初始设置
import random, time, pygame, sys from pygame.locals import * FPS = 25 WINDOWWIDTH = 640 WINDOWHEIGHT = 480 BOXSIZE = 20 BOARDWIDTH = 10 BOARDHEIGHT = 20 BLANK = '.' 首先,布置方块游戏的常量。 长按操作时间常量初始设置 MOVESIDEWAYSFREQ = 0.15 MOVEDOWNFREQ = 0.1 横移 ARR = 0.15 秒,软降 ARR = 0.1 秒。 窗口布局初始设置 XMARGIN = int((WINDOWWIDTH - BOARDWIDTH * BOXSIZE) / 2) TOPMARGIN = WINDOWHEIGHT - (BOARDHEIGHT * BOXSIZE) - 5 游戏场地靠下居中。 颜色初始设置 # R G B 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) # 断言深色数等于浅色数 为方块安排红黄蓝绿这四种颜色。 |
第三部分:方块模板 |
TEMPLATEWIDTH = 5 TEMPLATEHEIGHT = 5 S_SHAPE_TEMPLATE = [['.....', '.....', '..OO.', '.OO..', '.....'], ['.....', '..O..', '..OO.', '...O.', '.....']] Z_SHAPE_TEMPLATE = [['.....', '.....', '.OO..', '..OO.', '.....'], ['.....', '..O..', '.OO..', '.O...', '.....']] I_SHAPE_TEMPLATE = [['..O..', '..O..', '..O..', '..O..', '.....'], ['.....', '.....', 'OOOO.', '.....', '.....']] O_SHAPE_TEMPLATE = [['.....', '.....', '.OO..', '.OO..', '.....']] J_SHAPE_TEMPLATE = [['.....', '.O...', '.OOO.', '.....', '.....'], ['.....', '..OO.', '..O..', '..O..', '.....'], ['.....', '.....', '.OOO.', '...O.', '.....'], ['.....', '..O..', '..O..', '.OO..', '.....']] L_SHAPE_TEMPLATE = [['.....', '...O.', '.OOO.', '.....', '.....'], ['.....', '..O..', '..O..', '..OO.', '.....'], ['.....', '.....', '.OOO.', '.O...', '.....'], ['.....', '.OO..', '..O..', '..O..', '.....']] T_SHAPE_TEMPLATE = [['.....', '..O..', '.OOO.', '.....', '.....'], ['.....', '..O..', '..OO.', '..O..', '.....'], ['.....', '.....', '.OOO.', '..O..', '.....'], ['.....', '..O..', '.OO..', '..O..', '.....']] 这一步是在定义方块的碰撞箱。 SHAPES = {'S': S_SHAPE_TEMPLATE, 'Z': Z_SHAPE_TEMPLATE, 'J': J_SHAPE_TEMPLATE, 'L': L_SHAPE_TEMPLATE, 'I': I_SHAPE_TEMPLATE, 'O': O_SHAPE_TEMPLATE, 'T': T_SHAPE_TEMPLATE} 游戏中有七种方块,每一种方块的模板又常有多个列表。 |
第四部分:定义 main() |
def main(): global FPSCLOCK, DISPLAYSURF, BASICFONT, BIGFONT pygame.init() FPSCLOCK = pygame.time.Clock() DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) BASICFONT = pygame.font.Font('freesansbold.ttf', 18) BIGFONT = pygame.font.Font('freesansbold.ttf', 100) pygame.display.set_caption('Tetromino') showTextScreen('Tetromino') 这一步是在定义每秒帧数计时器、标题画面和游戏字体。 while True: # 游戏循环 if random.randint(0, 1) == 0: pygame.mixer.music.load('tetrisb.mid') else: pygame.mixer.music.load('tetrisc.mid') pygame.mixer.music.play(-1, 0.0) runGame() pygame.mixer.music.stop() showTextScreen('Game Over') 这一步是先背景音乐随机二选一,再用 runGame() 函数执行实际游戏的全部代码。 |
第五部分:定义 runGame() |
开局的变量 def runGame(): # 设置游戏开局的初始变量 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() 方块的三向移动情况可用布尔值表示,ARR 情况可用 pygame 的时间控制模块表示,开局分数可用数值“0”表示。 方块的循环 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: 对于一个用键盘操作的游戏,玩家的操作分按键和松键两种。[注 3] if (event.key == K_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: # 横移方块 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_UP or event.key == K_w): fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(SHAPES[fallingPiece['shape']]) if not isValidPosition(board, fallingPiece): fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(SHAPES[fallingPiece['shape']]) elif (event.key == K_q): # 另一个方向的旋转 fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(SHAPES[fallingPiece['shape']]) if not isValidPosition(board, fallingPiece): fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(SHAPES[fallingPiece['shape']]) 从人类认知方向顺序的习惯上说,如果方块游戏支持双向旋转,旋转状态序号 +1 对应顺时针旋转,-1 逆时针。 软降方块 # 用下键使方块更快地降落 elif (event.key == K_DOWN or event.key == K_s): movingDown = True if isValidPosition(board, fallingPiece, adjY=1): fallingPiece['y'] += 1 lastMoveDownTime = time.time() 和横移同理,方块 y 值 +=1,lastMoveDownTime = time.time() 函数用来实现软降 ARR。 瞬降方块(寻底) # 使当前方块一路降到底 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 从代码上说,“瞬降”是方块纵坐标突变的现象。 按键移动 # 处理用户输入按键所引起的方块移动 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() 按下方向键时,除了方块坐标改变,游戏程序还要捕获按键瞬间的时刻信息,以便做出 DAS 的效果。 重力(自然下落) # 每隔一定时间,方块自动下落一格 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() 重力本身会变化,由 lastFallTime 这个变量控制。 画面实显 # 将这一切显示在画面上 DISPLAYSURF.fill(BGCOLOR) drawBoard(board) drawStatus(score, level) drawNextPiece(nextPiece) if fallingPiece != None: drawPiece(fallingPiece) pygame.display.update() FPSCLOCK.tick(FPS) 画面实显是由函数完成的,按不同部分的显示内容加以分工。 |
第六部分:定义其他函数 |
制作文本对象 def makeTextObjs(text, font, color): surf = font.render(text, True, color) return surf, surf.get_rect() 这个函数能方便文字和图像的显示。 终止程序 def terminate(): pygame.quit() sys.exit() 使玩家有条件手动(例如松开 Esc 键的时候)终止程序。 检测玩家的操作 def checkForKeyPress(): # 遍历事件序列,寻找松键事件 # 抓取松键事件,将其移出事件序列 checkForQuit() for event in pygame.event.get([KEYDOWN, KEYUP]): if event.type == KEYDOWN: continue return event.key return None 如前文所述,操作分松键和按键两种。 显示文本 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) # 绘制提示文本 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() 游戏的前中后三个阶段都涉及到文本显示,它们内容不同、性质相同,只需 showTextScreen() 一个函数即可完成。 检测程序终止 def checkForQuit(): for event in pygame.event.get(QUIT): # 获取所有的退出(QUIT)事件 terminate() # 只要有退出事件,程序就终止 for event in pygame.event.get(KEYUP): # 获取所有的松键事件 if event.key == K_ESCAPE: terminate() # 只要 Esc 松键,程序就终止 pygame.event.post(event) # 如果不终止,就把其他松键的事件对象放回事件序列 “程序终止”可按手动与否来划分。 分数和升级 def calculateLevelAndFallFreq(score): # 在分数的基础上返回当前的等级和方块自动下落间隔 level = int(score / 10) + 1 fallFreq = 0.27 - (level * 0.02) return level, fallFreq 分数、等级和方块自动下落间隔都是用数值表示的,只需列出等式,不需调用新的函数。 生成新方块 def getNewPiece(): # 返回新方块,颜色和朝向随机 shape = random.choice(list(SHAPES.keys())) newPiece = {'shape': shape, 'rotation': random.randint(0, len(SHAPES[shape]) - 1), 'x': int(BOARDWIDTH / 2) - int(TEMPLATEWIDTH / 2), 'y': -2, # 从场地上方入场(y < 0) 'color': random.randint(0, len(COLORS)-1)} return newPiece 刚入场的新方块(newPiece)由字典数据类型表示,它有五个键值对:形状、旋转状态、横坐标、纵坐标、颜色。 把当前方块添加至方块堆 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'] 方块着陆后,调用 addToBoard() 函数,将方块数据结构添加至场地数据结构。 清空场地 def getBlankBoard(): # 创建空白的场地数据结构并返回值 board = [] for i in range(BOARDWIDTH): board.append([BLANK] * BOARDHEIGHT) return board 场地数据结构是列表值的列表。 检查方块的出界和重叠 def isOnBoard(x, y): return x >= 0 and x < BOARDWIDTH and y < BOARDHEIGHT 这是一个检测横纵坐标的函数,它能帮助判断方块的出界情况。 def isValidPosition(board, piece, adjX=0, adjY=0): # 方块不出界也不重叠就返回真 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 “有效的”方块运动目标位置需要全是场内的空格。这就有了另外两个要求: 检查和执行消行 def isCompleteLine(board, y): # 一行全是格子、没有空隙,就返回“真” for x in range(BOARDWIDTH): if board[x][y] == BLANK: return False return True 一行 10 格全部填满,这一行的消除就是“真”。 def removeCompleteLines(board): # 移除场上填满的行,上方内容随之下移,返回被消行数的值 numLinesRemoved = 0 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 # 整个循环的下一次迭代取相同的 y 值,更高处同时发生的消行一并移除 else: y -= 1 # 继续检查更上方的一行 return numLinesRemoved 消行的逻辑:自下而上地遍历全场方块堆数据结构,一行之内查不到空格就消除,更上方的格子下移,补上全空的顶行。 把场地坐标转换成像素坐标 def convertToPixelCoords(boxx, boxy): # 把场地的 xy 转换成屏幕上的 xy return (XMARGIN + (boxx * BOXSIZE)), (TOPMARGIN + (boxy * BOXSIZE)) 场地的方块堆坐标只是一个概念,基数全是 1,如果不加以放大,玩家是很难看清楚的。 显示格子 def drawBox(boxx, boxy, color, pixelx=None, pixely=None): # 按 xy 坐标在场上画一个格子(每个方块有四个格子) # 如果已经指定 pixelx 和 pixely,就画出预览块 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)) 方块图像显示在两处:场内方块、预览块。 显示场内的所有内容 def drawBoard(board): # 绘制场地的边线 pygame.draw.rect(DISPLAYSURF, BORDERCOLOR, (XMARGIN - 3, TOPMARGIN - 7, (BOARDWIDTH * BOXSIZE) + 8, (BOARDHEIGHT * BOXSIZE) + 8), 5) # 填充场地的背景 pygame.draw.rect(DISPLAYSURF, BGCOLOR, (XMARGIN, TOPMARGIN, BOXSIZE * BOARDWIDTH, BOXSIZE * BOARDHEIGHT)) # 在场上一个个地绘制出格子 for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): drawBox(x, y, board[x][y]) 都通过 DISPLAYSURF 实现。 显示分数和等级 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) 分数和等级都是会变的字符内容,用字符串格式化(%s)的形式表示。 显示方块 def drawPiece(piece, pixelx=None, pixely=None): shapeToDraw = PIECES[piece['shape']][piece['rotation']] if pixelx == None and pixely == None: # 和“显示格子”同理,按场内方块和预览块分开讨论 pixelx, pixely = convertToPixelCoords(piece['x'], piece['y']) # 绘制全部四个格子,做出四连方块 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)) “显示格子”针对的是场地,“显示方块”针对的则是 5×5 模板。 显示预览块 def drawNextPiece(piece): # 绘制“NEXT:”文本 nextSurf = BASICFONT.render('Next:', True, TEXTCOLOR) nextRect = nextSurf.get_rect() nextRect.topleft = (WINDOWWIDTH - 120, 80) DISPLAYSURF.blit(nextSurf, nextRect) # 绘制预览块 drawPiece(piece, pixelx=WINDOWWIDTH-120, pixely=100) 和前面几处同理。 运行模块 if __name__ == '__main__': main() 如果直接运行“tetromino.py”,游戏程序就执行“main()”的内容。 |
其他说明
这个游戏需要加载名为 tetrisb 和 tetrisc 的两个 mid 文件。
作者 Albert Sweigart 在 invpy.com 提供了这两个文件的下载:tetrisb.mid、tetrisc.mid
或者,将任意合规的 mid 文件改名置于同一目录,也可以正常游戏。
“tetromino.py”还有两个改版,一个是五连方块,一个是单格方块。
获取地址:本条目第一个外链的原文正文的最后两个链接。
注释
- ↑ 实际场宽 = 格数 × 格宽
- ↑ 作为解释性语言,Python 不像 C++、Java 等编译性语言那样必须有一个 main 函数。在 Python 中写 def main() 更多地是在强调接下来要定义的内容将会在某种意义上居于主要地位。例如,此处是在定义 Tetromino 进入游戏后的标题画面的一些东西,它们“居于主要地位”。
- ↑ 对游戏程序而言,“松键”是重要的判断依据。例如,因为松开了向左,所以方块才停止向左移动。
- ↑ 对负数有效。例:J 块逆时针旋转,(-1) % 4 = (-1) * 4 + 3,结果为旋转状态四。
- ↑ pygame 模块的二维场地的直角坐标系的第一象限在原点的右下方。
- ↑ 字色已在“颜色初始设置”中定义。
在后面的场地上色部分,边线和背景同理上色。 - ↑ 包括松键操作。如果是长按着操作输掉游戏的,只要把键松开,下一局就会开始。
- ↑ 游戏背景是黑色的,这个内缩 1 像素相当于利用背景的黑色完成了方块的黑色边线。
外链
- Making Games with Python & Pygame 第七章 - Tetromino
- Making Games with Python & Pygame 的 pdf 电子书(完整版)
此书还有滑动拼图、宝石迷阵等另外十个游戏的 Pygame 开发实例