使用 Python 和 Pygame 制作游戏:第四章到第五章

153 阅读1小时+

第四章:滑动拼图

原文:inventwithpython.com/pygame/chapter4.html

译者:飞龙

协议:CC BY-NC-SA 4.0

如何玩滑动拼图

棋盘是一个 4x4 的网格,有 15 个方块(从左到右编号为 1 到 15)和一个空白格。方块最初以随机位置开始,玩家必须将方块滑动到它们的原始顺序。

滑动拼图的源代码

此源代码可从invpy.com/slidepuzzle.py下载。如果出现任何错误消息,请查看错误消息中提到的行号,并检查代码中是否有任何拼写错误。您还可以将代码复制粘贴到invpy.com/diff/slidepuzzle的网络表单中,以查看您的代码与书中代码之间的差异。

# Slide Puzzle
# By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection)
# http://inventwithpython.com/pygame
# Creative Commons BY-NC-SA 3.0 US

import pygame, sys, random
from pygame.locals import *

# Create the constants (go ahead and experiment with different values)
BOARDWIDTH = 4  # number of columns in the board
BOARDHEIGHT = 4 # number of rows in the board
TILESIZE = 80
WINDOWWIDTH = 640
WINDOWHEIGHT = 480
FPS = 30
BLANK = None

#                 R    G    B
BLACK =         (  0,   0,   0)
WHITE =         (255, 255, 255)
BRIGHTBLUE =    (  050, 255)
DARKTURQUOISE = (  35473)
GREEN =         (  0, 204,   0)

BGCOLOR = DARKTURQUOISE
TILECOLOR = GREEN
TEXTCOLOR = WHITE
BORDERCOLOR = BRIGHTBLUE
BASICFONTSIZE = 20

BUTTONCOLOR = WHITE
BUTTONTEXTCOLOR = BLACK
MESSAGECOLOR = WHITE

XMARGIN = int((WINDOWWIDTH - (TILESIZE * BOARDWIDTH + (BOARDWIDTH - 1))) / 2)
YMARGIN = int((WINDOWHEIGHT - (TILESIZE * BOARDHEIGHT + (BOARDHEIGHT - 1))) / 2)

UP = 'up'
DOWN = 'down'
LEFT = 'left'
RIGHT = 'right'

def main():
    global FPSCLOCK, DISPLAYSURF, BASICFONT, RESET_SURF, RESET_RECT, NEW_SURF, NEW_RECT, SOLVE_SURF, SOLVE_RECT

    pygame.init()
    FPSCLOCK = pygame.time.Clock()
    DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
    pygame.display.set_caption('Slide Puzzle')
    BASICFONT = pygame.font.Font('freesansbold.ttf', BASICFONTSIZE)

    # Store the option buttons and their rectangles in OPTIONS.
    RESET_SURF, RESET_RECT = makeText('Reset',    TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 120, WINDOWHEIGHT - 90)
    NEW_SURF, NEW_RECT   = makeText('New Game', TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 120, WINDOWHEIGHT - 60)

    SOLVE_SURF, SOLVE_RECT = makeText('Solve',    TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 120, WINDOWHEIGHT - 30)

    mainBoard, solutionSeq = generateNewPuzzle(80)
    SOLVEDBOARD = getStartingBoard() # a solved board is the same as the board in a start state.
    allMoves = [] # list of moves made from the solved configuration

    while True: # main game loop
        slideTo = None # the direction, if any, a tile should slide
        msg = '' # contains the message to show in the upper left corner.
        if mainBoard == SOLVEDBOARD:
            msg = 'Solved!'

        drawBoard(mainBoard, msg)

        checkForQuit()
        for event in pygame.event.get(): # event handling loop
            if event.type == MOUSEBUTTONUP:
                spotx, spoty = getSpotClicked(mainBoard, event.pos[0], event.pos[1])

                if (spotx, spoty) == (None, None):
                    # check if the user clicked on an option button
                    if RESET_RECT.collidepoint(event.pos):
                        resetAnimation(mainBoard, allMoves) # clicked on Reset button
                        allMoves = []
                    elif NEW_RECT.collidepoint(event.pos):
                        mainBoard, solutionSeq = generateNewPuzzle(80) # clicked on New Game button
                        allMoves = []
                    elif SOLVE_RECT.collidepoint(event.pos):
                        resetAnimation(mainBoard, solutionSeq + allMoves) # clicked on Solve button
                        allMoves = []
                else:
                    # check if the clicked tile was next to the blank spot

                    blankx, blanky = getBlankPosition(mainBoard)
                    if spotx == blankx + 1 and spoty == blanky:
                        slideTo = LEFT
                    elif spotx == blankx - 1 and spoty == blanky:
                        slideTo = RIGHT
                    elif spotx == blankx and spoty == blanky + 1:
                        slideTo = UP
                    elif spotx == blankx and spoty == blanky - 1:
                        slideTo = DOWN

            elif event.type == KEYUP:
                # check if the user pressed a key to slide a tile
                if event.key in (K_LEFT, K_a) and isValidMove(mainBoard, LEFT):
                    slideTo = LEFT
                elif event.key in (K_RIGHT, K_d) and isValidMove(mainBoard, RIGHT):
                    slideTo = RIGHT
                elif event.key in (K_UP, K_w) and isValidMove(mainBoard, UP):
                    slideTo = UP
                elif event.key in (K_DOWN, K_s) and isValidMove(mainBoard, DOWN):
                    slideTo = DOWN

        if slideTo:
            slideAnimation(mainBoard, slideTo, 'Click tile or press arrow keys to slide.', 8) # show slide on screen
            makeMove(mainBoard, slideTo)
            allMoves.append(slideTo) # record the slide
        pygame.display.update()
        FPSCLOCK.tick(FPS)

def terminate():
    pygame.quit()
    sys.exit()

def checkForQuit():
    for event in pygame.event.get(QUIT): # get all the QUIT events
        terminate() # terminate if any QUIT events are present
    for event in pygame.event.get(KEYUP): # get all the KEYUP events
        if event.key == K_ESCAPE:
            terminate() # terminate if the KEYUP event was for the Esc key
        pygame.event.post(event) # put the other KEYUP event objects back

def getStartingBoard():
    # Return a board data structure with tiles in the solved state.
    # For example, if BOARDWIDTH and BOARDHEIGHT are both 3, this function
    # returns [[1, 4, 7], [2, 5, 8], [3, 6, None]]
    counter = 1
    board = []
    for x in range(BOARDWIDTH):
        column = []
        for y in range(BOARDHEIGHT):
            column.append(counter)
            counter += BOARDWIDTH
        board.append(column)
        counter -= BOARDWIDTH * (BOARDHEIGHT - 1) + BOARDWIDTH - 1

    board[BOARDWIDTH-1][BOARDHEIGHT-1] = None
    return board

def getBlankPosition(board):
    # Return the x and y of board coordinates of the blank space.
    for x in range(BOARDWIDTH):
        for y in range(BOARDHEIGHT):
            if board[x][y] == None:
                return (x, y)

def makeMove(board, move):
    # This function does not check if the move is valid.
    blankx, blanky = getBlankPosition(board)

    if move == UP:
        board[blankx][blanky], board[blankx][blanky + 1] = board[blankx][blanky + 1], board[blankx][blanky]
    elif move == DOWN:
        board[blankx][blanky], board[blankx][blanky - 1] = board[blankx][blanky - 1], board[blankx][blanky]
    elif move == LEFT:
        board[blankx][blanky], board[blankx + 1][blanky] = board[blankx + 1][blanky], board[blankx][blanky]
    elif move == RIGHT:
        board[blankx][blanky], board[blankx - 1][blanky] = board[blankx - 1][blanky], board[blankx][blanky]

def isValidMove(board, move):
    blankx, blanky = getBlankPosition(board)
    return (move == UP and blanky != len(board[0]) - 1) or \
           (move == DOWN and blanky != 0) or \
           (move == LEFT and blankx != len(board) - 1) or \
           (move == RIGHT and blankx != 0)

def getRandomMove(board, lastMove=None):
    # start with a full list of all four moves
    validMoves = [UP, DOWN, LEFT, RIGHT]

    # remove moves from the list as they are disqualified
    if lastMove == UP or not isValidMove(board, DOWN):
        validMoves.remove(DOWN)
    if lastMove == DOWN or not isValidMove(board, UP):
        validMoves.remove(UP)
    if lastMove == LEFT or not isValidMove(board, RIGHT):
        validMoves.remove(RIGHT)
    if lastMove == RIGHT or not isValidMove(board, LEFT):
        validMoves.remove(LEFT)

    # return a random move from the list of remaining moves
    return random.choice(validMoves)

def getLeftTopOfTile(tileX, tileY):
    left = XMARGIN + (tileX * TILESIZE) + (tileX - 1)
    top = YMARGIN + (tileY * TILESIZE) + (tileY - 1)
    return (left, top)

def getSpotClicked(board, x, y):
    # from the x & y pixel coordinates, get the x & y board coordinates
    for tileX in range(len(board)):
        for tileY in range(len(board[0])):
            left, top = getLeftTopOfTile(tileX, tileY)
            tileRect = pygame.Rect(left, top, TILESIZE, TILESIZE)
            if tileRect.collidepoint(x, y):
                return (tileX, tileY)
    return (None, None)

def drawTile(tilex, tiley, number, adjx=0, adjy=0):
    # draw a tile at board coordinates tilex and tiley, optionally a few
    # pixels over (determined by adjx and adjy)
    left, top = getLeftTopOfTile(tilex, tiley)
    pygame.draw.rect(DISPLAYSURF, TILECOLOR, (left + adjx, top + adjy, TILESIZE, TILESIZE))
    textSurf = BASICFONT.render(str(number), True, TEXTCOLOR)
    textRect = textSurf.get_rect()
    textRect.center = left + int(TILESIZE / 2) + adjx, top + int(TILESIZE / 2) + adjy
    DISPLAYSURF.blit(textSurf, textRect)

def makeText(text, color, bgcolor, top, left):
    # create the Surface and Rect objects for some text.
    textSurf = BASICFONT.render(text, True, color, bgcolor)
    textRect = textSurf.get_rect()
    textRect.topleft = (top, left)
    return (textSurf, textRect)

def drawBoard(board, message):
    DISPLAYSURF.fill(BGCOLOR)
    if message:
        textSurf, textRect = makeText(message, MESSAGECOLOR, BGCOLOR, 5, 5)
        DISPLAYSURF.blit(textSurf, textRect)

    for tilex in range(len(board)):
        for tiley in range(len(board[0])):
            if board[tilex][tiley]:
                drawTile(tilex, tiley, board[tilex][tiley])

    left, top = getLeftTopOfTile(0, 0)
    width = BOARDWIDTH * TILESIZE
    height = BOARDHEIGHT * TILESIZE
    pygame.draw.rect(DISPLAYSURF, BORDERCOLOR, (left - 5, top - 5, width + 11, height + 11), 4)

    DISPLAYSURF.blit(RESET_SURF, RESET_RECT)
    DISPLAYSURF.blit(NEW_SURF, NEW_RECT)
    DISPLAYSURF.blit(SOLVE_SURF, SOLVE_RECT)

def slideAnimation(board, direction, message, animationSpeed):
    # Note: This function does not check if the move is valid.

    blankx, blanky = getBlankPosition(board)
    if direction == UP:
        movex = blankx
        movey = blanky + 1
    elif direction == DOWN:
        movex = blankx
        movey = blanky - 1
    elif direction == LEFT:
        movex = blankx + 1
        movey = blanky
    elif direction == RIGHT:
        movex = blankx - 1
        movey = blanky

    # prepare the base surface
    drawBoard(board, message)
    baseSurf = DISPLAYSURF.copy()
    # draw a blank space over the moving tile on the baseSurf Surface.
    moveLeft, moveTop = getLeftTopOfTile(movex, movey)
    pygame.draw.rect(baseSurf, BGCOLOR, (moveLeft, moveTop, TILESIZE, TILESIZE))

    for i in range(0, TILESIZE, animationSpeed):
        # animate the tile sliding over
        checkForQuit()
        DISPLAYSURF.blit(baseSurf, (0, 0))
        if direction == UP:
            drawTile(movex, movey, board[movex][movey], 0, -i)
        if direction == DOWN:
            drawTile(movex, movey, board[movex][movey], 0, i)
        if direction == LEFT:
            drawTile(movex, movey, board[movex][movey], -i, 0)
        if direction == RIGHT:
            drawTile(movex, movey, board[movex][movey], i, 0)

        pygame.display.update()
        FPSCLOCK.tick(FPS)

def generateNewPuzzle(numSlides):
    # From a starting configuration, make numSlides number of moves (and
    # animate these moves).
    sequence = []
    board = getStartingBoard()
    drawBoard(board, '')
    pygame.display.update()
    pygame.time.wait(500) # pause 500 milliseconds for effect
    lastMove = None
    for i in range(numSlides):
        move = getRandomMove(board, lastMove)
        slideAnimation(board, move, 'Generating new puzzle...', int(TILESIZE / 3))
        makeMove(board, move)
        sequence.append(move)
        lastMove = move
    return (board, sequence)

def resetAnimation(board, allMoves):
    # make all of the moves in allMoves in reverse.
    revAllMoves = allMoves[:] # gets a copy of the list
    revAllMoves.reverse()

    for move in revAllMoves:
        if move == UP:
            oppositeMove = DOWN
        elif move == DOWN:
            oppositeMove = UP
        elif move == RIGHT:
            oppositeMove = LEFT
        elif move == LEFT:
            oppositeMove = RIGHT
        slideAnimation(board, oppositeMove, '', int(TILESIZE / 2))
        makeMove(board, oppositeMove)

if __name__ == '__main__':
    main()

第二节,与第一节相同

贪吃虫中的大部分代码与我们之前看过的游戏非常相似,特别是在代码开头设置常量的部分。

# Slide Puzzle
# By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection)
# http://inventwithpython.com/pygame
# Creative Commons BY-NC-SA 3.0 US

import pygame, sys, random
from pygame.locals import *

# Create the constants (go ahead and experiment with different values)
BOARDWIDTH = 4  # number of columns in the board
BOARDHEIGHT = 4 # number of rows in the board
TILESIZE = 80
WINDOWWIDTH = 640
WINDOWHEIGHT = 480
FPS = 30
BLANK = None

#                 R    G    B
BLACK =         (  0,   0,   0)
WHITE =         (255, 255, 255)
BRIGHTBLUE =    (  050, 255)
DARKTURQUOISE = (  35473)
GREEN =         (  0, 204,   0)

BGCOLOR = DARKTURQUOISE
TILECOLOR = GREEN
TEXTCOLOR = WHITE
BORDERCOLOR = BRIGHTBLUE
BASICFONTSIZE = 20

BUTTONCOLOR = WHITE
BUTTONTEXTCOLOR = BLACK
MESSAGECOLOR = WHITE

XMARGIN = int((WINDOWWIDTH - (TILESIZE * BOARDWIDTH + (BOARDWIDTH - 1))) / 2)
YMARGIN = int((WINDOWHEIGHT - (TILESIZE * BOARDHEIGHT + (BOARDHEIGHT - 1))) / 2)

UP = 'up'
DOWN = 'down'
LEFT = 'left'
RIGHT = 'right'

程序顶部的这段代码只是处理了所有基本模块的导入和创建常量。这就像上一章的记忆拼图游戏的开头一样。

设置按钮

def main():
    global FPSCLOCK, DISPLAYSURF, BASICFONT, RESET_SURF, RESET_RECT, NEW_SURF, NEW_RECT, SOLVE_SURF, SOLVE_RECT

    pygame.init()
    FPSCLOCK = pygame.time.Clock()
    DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
    pygame.display.set_caption('Slide Puzzle')
    BASICFONT = pygame.font.Font('freesansbold.ttf', BASICFONTSIZE)

    # Store the option buttons and their rectangles in OPTIONS.
    RESET_SURF, RESET_RECT = makeText('Reset',    TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 120, WINDOWHEIGHT - 90)
    NEW_SURF, NEW_RECT   = makeText('New Game', TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 120, WINDOWHEIGHT - 60)
    SOLVE_SURF, SOLVE_RECT = makeText('Solve',    TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 120, WINDOWHEIGHT - 30)

    mainBoard, solutionSeq = generateNewPuzzle(80)
    SOLVEDBOARD = getStartingBoard() # a solved board is the same as the board in a start state.

就像上一章一样,从 main() 函数调用的函数将在本章后面解释。现在,你只需要知道它们做什么以及它们返回什么值。你不需要知道它们是如何工作的。

main() 函数的第一部分将处理创建窗口、时钟对象和字体对象。makeText() 函数在程序的后面定义,但现在你只需要知道它返回一个 pygame.Surface 对象和一个 pygame.Rect 对象,可以用来制作可点击的按钮。滑动拼图游戏将有三个按钮:一个“重置”按钮,可以撤消玩家所做的任何移动,一个“新建”按钮,可以创建一个新的滑动拼图,以及一个“解决”按钮,可以为玩家解决拼图。

对于这个程序,我们将需要两个棋盘数据结构。一个棋盘将表示当前的游戏状态。另一个棋盘将使其方块处于“解决”状态,这意味着所有方块都按顺序排列。当当前游戏状态的棋盘与解决的棋盘完全相同时,我们就知道玩家赢了。(我们永远不会改变第二个棋盘。它只是用来比较当前游戏状态棋盘的。)

generateNewPuzzle() 将创建一个棋盘数据结构,它最初处于有序的解决状态,然后对其进行了 80 次随机滑动(因为我们向其传递了整数 80)。如果我们希望棋盘更加混乱,那么我们可以向其传递一个更大的整数。这将使棋盘变成一个随机混乱的状态,玩家将不得不解决它(这将存储在一个名为 mainBoard 的变量中)。generateNewBoard() 还返回了在其上执行的所有随机移动的列表(这将存储在一个名为 solutionSeq 的变量中)。

通过使用愚蠢的代码变得聪明

    allMoves = [] # list of moves made from the solved configuration

解决滑动拼图可能会非常棘手。我们可以让计算机来做,但这需要我们找出一个可以解决滑动拼图的算法。这将非常困难,并且需要大量的聪明和努力来将其放入这个程序中。

幸运的是,有一个更简单的方法。我们可以让计算机记住创建棋盘数据结构时所做的所有随机滑动,然后通过执行相反的滑动来解决棋盘。由于棋盘最初是处于解决状态的,撤消所有滑动将使其返回到解决状态。

例如,下面我们在页面左侧的棋盘上执行了一个“向右”滑动,这将使棋盘处于页面右侧的状态:

在正确的滑动之后,如果我们进行相反的滑动(向左滑动),那么板将恢复到原始状态。因此,要在进行几次滑动后恢复到原始状态,我们只需按相反的顺序进行相反的滑动。如果我们进行了右滑动,然后又进行了右滑动,然后进行了下滑动,我们将不得不进行上滑动、左滑动和左滑动来撤消这前三次滑动。这比编写一个可以简单地查看它们的当前状态来解决这些谜题的函数要容易得多。

主游戏循环

    while True: # main game loop
        slideTo = None # the direction, if any, a tile should slide
        msg = '' # contains the message to show in the upper left corner.
        if mainBoard == SOLVEDBOARD:
            msg = 'Solved!'

        drawBoard(mainBoard, msg)

在主游戏循环中,slideTo变量将跟踪玩家想要滑动瓷砖的方向(在游戏循环的开始时它开始为None,稍后设置),msg变量跟踪在窗口顶部显示的字符串。程序在第 64 行进行快速检查,看看板数据结构是否与存储在SOLVEDBOARD中的解决板数据结构具有相同的值。如果是,则msg变量更改为字符串'已解决!'。这将不会出现在屏幕上,直到调用drawBoard()将其绘制到DISPLAYSURF Surface 对象(在第 67 行执行)并调用pygame.display.update()将显示 Surface 对象绘制到实际计算机屏幕上(在游戏循环结束时的第 291 行执行)。

点击按钮

        checkForQuit()
        for event in pygame.event.get(): # event handling loop
            if event.type == MOUSEBUTTONUP:
                spotx, spoty = getSpotClicked(mainBoard, event.pos[0], event.pos[1])

                if (spotx, spoty) == (None, None):
                    # check if the user clicked on an option button
                    if RESET_RECT.collidepoint(event.pos):
                        resetAnimation(mainBoard, allMoves) # clicked on Reset button
                        allMoves = []
                    elif NEW_RECT.collidepoint(event.pos):
                        mainBoard, solutionSeq = generateNewPuzzle(80) # clicked on New Game button
                        allMoves = []
                    elif SOLVE_RECT.collidepoint(event.pos):
                        resetAnimation(mainBoard, solutionSeq + allMoves) # clicked on Solve button
                        allMoves = []

在进入事件循环之前,程序在第 69 行调用checkForQuit()来查看是否已创建任何QUIT事件(如果有,则终止程序)。为什么我们有一个单独的函数(checkForQuit()函数)来处理QUIT事件将在后面解释。第 70 行的for循环执行自上次调用pygame.event.get()以来创建的任何其他事件的事件处理代码(或者自程序启动以来,如果以前从未调用过pygame.event.get())。

如果事件类型是MOUSEBUTTONUP事件(即玩家在窗口的某个地方释放了鼠标按钮),那么我们将鼠标坐标传递给我们的getSpotClicked()函数,该函数将返回鼠标释放发生的板上位置的坐标。event.pos[0]是 X 坐标,event.pos[1]是 Y 坐标。

如果鼠标释放按钮没有发生在板上的空格之一上(但显然仍然发生在窗口的某个地方,因为创建了MOUSEBUTTONUP事件),那么getSpotClicked()将返回None。如果是这种情况,我们希望进行额外的检查,看看玩家是否可能点击了重置、新建或解决按钮(这些按钮不位于板上)。

这些按钮在窗口上的坐标存储在RESET_RECTNEW_RECTSOLVE_RECT变量中存储的pygame.Rect对象中。我们可以将事件对象的鼠标坐标传递给collidepoint()方法。如果鼠标坐标在 Rect 对象的区域内,则此方法将返回True,否则返回False

用鼠标滑动瓷砖

                else:
                    # check if the clicked tile was next to the blank spot

                    blankx, blanky = getBlankPosition(mainBoard)
                    if spotx == blankx + 1 and spoty == blanky:
                        slideTo = LEFT
                    elif spotx == blankx - 1 and spoty == blanky:
                        slideTo = RIGHT
                    elif spotx == blankx and spoty == blanky + 1:
                        slideTo = UP
                    elif spotx == blankx and spoty == blanky - 1:
                        slideTo = DOWN

如果getSpotClicked()没有返回(None, None),那么它将返回一个包含两个整数值的元组,表示点击的板上的位置的 X 和 Y 坐标。然后,第 89 到 96 行的ifelif语句检查被点击的位置是否是靠近空白位置的瓷砖(否则瓷砖将没有地方滑动)。

我们的getBlankPosition()函数将采取板数据结构并返回空白位置的 X 和 Y 板坐标,我们将其存储在变量blankxblanky中。如果用户点击的位置在空白处旁边,我们将使用应该滑动的值设置slideTo变量。

用键盘滑动瓷砖

            elif event.type == KEYUP:
                # check if the user pressed a key to slide a tile
                if event.key in (K_LEFT, K_a) and isValidMove(mainBoard, LEFT):
                    slideTo = LEFT
                elif event.key in (K_RIGHT, K_d) and isValidMove(mainBoard, RIGHT):
                    slideTo = RIGHT
                elif event.key in (K_UP, K_w) and isValidMove(mainBoard, UP):
                    slideTo = UP
                elif event.key in (K_DOWN, K_s) and isValidMove(mainBoard, DOWN):
                    slideTo = DOWN

我们还可以让用户通过按键盘键来滑动瓷砖。第 100 至 107 行的ifelif语句允许用户通过按箭头键或 WASD 键(稍后解释)来设置slideTo变量。每个ifelif语句还都调用了isValidMove()来确保瓷砖可以朝那个方向滑动。(我们在鼠标点击时不必进行这个调用,因为对于相邻的空白空间的检查也会做同样的事情。)

“等于多个值中的一个”技巧与in运算符

表达式event.key in (K_LEFT, K_a)只是 Python 中的一个技巧,使代码更简单。这是一种说“如果event.key等于K_LEFTK_a中的一个,则评估为True”。以下两个表达式将以相同的方式进行评估:

event.key in (K_LEFT, K_a)

event.key == K_LEFT or event.key == K_a

当你需要检查一个值是否等于多个值中的一个时,使用这个技巧可以节省一些空间。以下两个表达式将以相同的方式进行评估:

spam == 'dog' or spam == 'cat' or spam == 'mouse' or spam == 'horse' or spam == 42 or spam == 'dingo'

spam in ('dog', 'cat', 'mouse', 'horse', 42, 'dingo')

WASD 和箭头键

W、A、S 和 D 键(合称 WASD 键,发音为“waz-dee”)在电脑游戏中通常用来做与箭头键相同的事情,只不过玩家可以使用左手(因为 WASD 键在键盘的左侧)。W 代表上,A 代表左,S 代表下,D 代表右。你可以很容易地记住这一点,因为 WASD 键的布局与箭头键相同:

实际执行瓷砖滑动

        if slideTo:
            slideAnimation(mainBoard, slideTo, 'Click tile or press arrow keys to slide.', 8) # show slide on screen
            makeMove(mainBoard, slideTo)
            allMoves.append(slideTo) # record the slide
        pygame.display.update()
        FPSCLOCK.tick(FPS)

现在所有事件都已处理完毕,我们应该更新游戏状态的变量,并在屏幕上显示新状态。如果slideTo已经设置(无论是由鼠标事件还是键盘事件处理代码),我们都可以调用slideAnimation()来执行滑动动画。参数是棋盘数据结构、滑动的方向、在滑动瓷砖时显示的消息以及滑动的速度。

在它返回后,我们需要更新实际的棋盘数据结构(由makeMove()函数完成),然后将滑动添加到迄今为止所有滑动的allMoves列表中。这样,如果玩家点击“重置”按钮,我们就知道如何撤消玩家的所有滑动。

IDLE 和终止 Pygame 程序

def terminate():
    pygame.quit()
    sys.exit()

这是一个我们可以调用的函数,它同时调用了pygame.quit()sys.exit()函数。这是一种语法糖,这样我们就不必记住调用这两个函数,只需要调用一个函数即可。

检查特定事件,并将事件发布到 Pygame 的事件队列

def checkForQuit():
    for event in pygame.event.get(QUIT): # get all the QUIT events
        terminate() # terminate if any QUIT events are present
    for event in pygame.event.get(KEYUP): # get all the KEYUP events
        if event.key == K_ESCAPE:
            terminate() # terminate if the KEYUP event was for the Esc key
        pygame.event.post(event) # put the other KEYUP event objects back

checkForQuit()函数将检查QUIT事件(或用户是否按下了 Esc 键),然后调用terminate()函数。但这有点棘手,需要一些解释。

Pygame 内部有自己的列表数据结构,它会在创建 Event 对象时将其附加到其中。这个数据结构称为事件队列。当调用pygame.event.get()函数而不带参数时,整个列表将被返回。但是,你可以传递一个常量,比如QUITpygame.event.get(),这样它就只会返回内部事件队列中的QUIT事件(如果有的话)。其余的事件将保留在事件队列中,以便下次调用pygame.event.get()时使用。

你应该注意,Pygame 的事件队列只能存储最多 127 个 Event 对象。如果你的程序不经常调用pygame.event.get(),并且队列填满了,那么发生的任何新事件都不会被添加到事件队列中。

第 123 行从 Pygame 的事件队列中提取了一个QUIT事件列表并返回它们。如果事件队列中有任何QUIT事件,程序将终止。

第 125 行从事件队列中提取所有KEYUP事件,并检查它们是否是 Esc 键。如果其中一个事件是,那么程序将终止。但是,除了 Esc 键之外,可能还有其他键的KEYUP事件。在这种情况下,我们需要将KEYUP事件放回 Pygame 的事件队列中。我们可以使用pygame.event.post()函数来实现这一点,该函数将传递给它的 Event 对象添加到 Pygame 事件队列的末尾。这样,当第 70 行调用pygame.event.get()时,非 Esc 键的KEYUP事件仍将存在。否则,对checkForQuit()的调用将“消耗”所有的KEYUP事件,这些事件将永远不会被处理。

pygame.event.post()函数也很方便,如果您希望程序将 Event 对象添加到 Pygame 事件队列中。

创建棋盘数据结构

def getStartingBoard():
    # Return a board data structure with tiles in the solved state.
    # For example, if BOARDWIDTH and BOARDHEIGHT are both 3, this function
    # returns [[1, 4, 7], [2, 5, 8], [3, 6, None]]
    counter = 1
    board = []
    for x in range(BOARDWIDTH):
        column = []
        for y in range(BOARDHEIGHT):
            column.append(counter)
            counter += BOARDWIDTH
        board.append(column)
        counter -= BOARDWIDTH * (BOARDHEIGHT - 1) + BOARDWIDTH - 1

    board[BOARDWIDTH-1][BOARDHEIGHT-1] = None
    return board

getStartingBoard()数据结构将创建并返回一个表示“已解决”棋盘的数据结构,其中所有编号的瓷砖都是有序的,空白瓷砖位于右下角。这与内存拼图游戏中的棋盘数据结构一样,都是使用嵌套的for循环完成的。

但是,请注意,第一列不会是[1, 2, 3],而是[1, 4, 7]。这是因为瓷砖上的数字是横向增加 1,而不是纵向增加。沿着列向下,数字按照棋盘宽度的大小增加(存储在BOARDWIDTH常量中)。我们将使用counter变量来跟踪应放在下一个瓷砖上的数字。当列中的瓷砖编号完成时,我们需要将counter设置为下一列开始的数字。

不跟踪空白位置

def getBlankPosition(board):
    # Return the x and y of board coordinates of the blank space.
    for x in range(BOARDWIDTH):
        for y in range(BOARDHEIGHT):
            if board[x][y] == None:
                return (x, y)

每当我们的代码需要找到空白空间的 XY 坐标时,我们可以创建一个函数,通过遍历整个棋盘来找到空白空间的坐标,而不是在每次滑动后跟踪空白空间的位置。None值在棋盘数据结构中用于表示空白空间。getBlankPosition()中的代码简单地使用嵌套的for循环来找到棋盘上的空白空间。

通过更新棋盘数据结构进行移动

def makeMove(board, move):
    # This function does not check if the move is valid.
    blankx, blanky = getBlankPosition(board)

    if move == UP:
        board[blankx][blanky], board[blankx][blanky + 1] = board[blankx][blanky + 1], board[blankx][blanky]
    elif move == DOWN:
        board[blankx][blanky], board[blankx][blanky - 1] = board[blankx][blanky - 1], board[blankx][blanky]
    elif move == LEFT:
        board[blankx][blanky], board[blankx + 1][blanky] = board[blankx + 1][blanky], board[blankx][blanky]
    elif move == RIGHT:
        board[blankx][blanky], board[blankx - 1][blanky] = board[blankx - 1][blanky], board[blankx][blanky]

board参数中的数据结构是一个表示所有瓷砖位置的二维列表。每当玩家进行移动时,程序都需要更新此数据结构。发生的情况是,瓷砖的值与空白空间的值交换。

makeMove()函数不必返回任何值,因为board参数是作为其参数传递的列表引用。这意味着我们在此函数中对board所做的任何更改都将应用于传递给makeMove()的列表值。(您可以在invpy.com/references上查看引用的概念。)

何时不使用断言

def isValidMove(board, move):
    blankx, blanky = getBlankPosition(board)
    return (move == UP and blanky != len(board[0]) - 1) or \
           (move == DOWN and blanky != 0) or \
           (move == LEFT and blankx != len(board) - 1) or \
           (move == RIGHT and blankx != 0)

isValidMove()函数接收一个棋盘数据结构和玩家想要进行的移动。如果移动是可能的,则返回值为True,如果不可能,则返回值为False。例如,您不能连续一百次将瓷砖向左滑动,因为最终空白空间将位于边缘,没有更多的瓷砖可以向左滑动。

移动是否有效取决于空白空间的位置。此函数调用getBlankPosition()来找到空白位置的 X 和 Y 坐标。第 173 至 176 行是一个带有单个表达式的return语句。在前三行的末尾的\斜杠告诉 Python 解释器这不是代码行的结尾(即使它在行的末尾)。这将使我们能够将“代码行”分成多行以使其看起来漂亮,而不是只有一行非常长且难以阅读。

因为括号中的这个表达式是由或运算符连接的,只需要其中一个为True整个表达式就为True。每个部分都检查预期的移动是什么,然后看空白空间的坐标是否允许该移动。

获取一个不那么随机的移动

def getRandomMove(board, lastMove=None):
    # start with a full list of all four moves
    validMoves = [UP, DOWN, LEFT, RIGHT]

    # remove moves from the list as they are disqualified
    if lastMove == UP or not isValidMove(board, DOWN):
        validMoves.remove(DOWN)
    if lastMove == DOWN or not isValidMove(board, UP):
        validMoves.remove(UP)
    if lastMove == LEFT or not isValidMove(board, RIGHT):
        validMoves.remove(RIGHT)
    if lastMove == RIGHT or not isValidMove(board, LEFT):
        validMoves.remove(LEFT)

    # return a random move from the list of remaining moves
    return random.choice(validMoves)

在游戏开始时,我们从解决的、有序的状态开始,通过随机滑动瓷砖来创建拼图。为了决定我们应该滑动哪个方向,我们将调用我们的getRandomMove()函数。通常我们可以使用random.choice()函数,并传递一个元组(UP, DOWN, LEFT, RIGHT),让 Python 简单地随机选择一个方向值。但是滑动拼图游戏有一个小限制,阻止我们选择纯随机数。

如果你有一个滑动拼图,将一个瓷砖向左滑动,然后将一个瓷砖向右滑动,你最终会得到与开始时完全相同的棋盘。进行相反的滑动是毫无意义的。此外,如果空白空间位于右下角,那么不可能将瓷砖向上或向左滑动。

getRandomMove()中的代码将考虑这些因素。为了防止函数选择上次移动的值,函数的调用者可以为lastMove参数传递一个方向值。第 181 行从存储在validMoves变量中的所有四个方向值的列表开始。如果lastMove值(如果未设置为None)则从validMoves中删除。根据空白空间是否在棋盘的边缘,第 184 到 191 行将从lastMove列表中删除其他方向值。

lastMove中剩下的值中,使用random.choice()随机选择一个值并返回。

将瓷砖坐标转换为像素坐标

def getLeftTopOfTile(tileX, tileY):
    left = XMARGIN + (tileX * TILESIZE) + (tileX - 1)
    top = YMARGIN + (tileY * TILESIZE) + (tileY - 1)
    return (left, top)

getLeftTopOfTile()函数将棋盘坐标转换为像素坐标。对于传入的棋盘 XY 坐标,该函数计算并返回该棋盘空间左上角的像素 XY 坐标。

从像素坐标转换为棋盘坐标

def getSpotClicked(board, x, y):
    # from the x & y pixel coordinates, get the x & y board coordinates
    for tileX in range(len(board)):
        for tileY in range(len(board[0])):
            left, top = getLeftTopOfTile(tileX, tileY)
            tileRect = pygame.Rect(left, top, TILESIZE, TILESIZE)
            if tileRect.collidepoint(x, y):
                return (tileX, tileY)
    return (None, None)

getSpotClicked()函数与getLeftTopOfTile()相反,它将像素坐标转换为棋盘坐标。第 205 和 206 行的嵌套循环遍历了每个可能的 XY 棋盘坐标,如果传入的像素坐标在棋盘上的空间内,则返回这些棋盘坐标。由于所有的瓷砖都有在TILESIZE常量中设置的宽度和高度,我们可以创建一个表示棋盘空间的 Rect 对象,方法是获取棋盘空间左上角的像素坐标,然后使用collidepoint() Rect 方法来查看像素坐标是否在该 Rect 对象的区域内。

如果传入的像素坐标不在任何棋盘空间上,则返回值为(None, None)

绘制瓷砖

def drawTile(tilex, tiley, number, adjx=0, adjy=0):
    # draw a tile at board coordinates tilex and tiley, optionally a few
    # pixels over (determined by adjx and adjy)
    left, top = getLeftTopOfTile(tilex, tiley)
    pygame.draw.rect(DISPLAYSURF, TILECOLOR, (left + adjx, top + adjy, TILESIZE, TILESIZE))
    textSurf = BASICFONT.render(str(number), True, TEXTCOLOR)
    textRect = textSurf.get_rect()
    textRect.center = left + int(TILESIZE / 2) + adjx, top + int(TILESIZE / 2) + adjy
    DISPLAYSURF.blit(textSurf, textRect)

drawTile()函数将在棋盘上绘制一个带编号的瓷砖。tilextiley参数是瓷砖的棋盘坐标。数字参数是瓷砖编号的字符串(如'3''12')。adjxadjy关键字参数用于对瓷砖位置进行微小调整。例如,将5传递给adjx会使瓷砖出现在棋盘上tilextiley空间的右侧 5 像素处。将-10传递给adjx会使瓷砖出现在空间的左侧 10 像素处。

当我们需要在滑动中间绘制瓷砖时,这些调整值将非常方便。如果在调用drawTile()时没有传递这些参数的值,则默认设置为0。这意味着它们将正好在由tilextiley给出的棋盘空间上。

Pygame 绘图函数只使用像素坐标,因此第 217 行首先将tilextiley的棋盘坐标转换为像素坐标,我们将把它们存储在变量lefttop中(因为getLeftTopOfTile()返回左上角的坐标)。我们使用pygame.draw.rect()调用绘制瓦片的背景方块,同时在需要调整瓦片位置的情况下,将adjxadjy的值添加到lefttop中。

然后,第 219 到 222 行创建了具有数字文本的表面对象。一个用于表面对象的 Rect 对象被定位,然后用于将表面对象 blit 到显示表面。drawTile()函数不调用pygame.display.update()函数,因为调用drawTile()的人可能会想在将它们显示在屏幕上之前为棋盘的其余部分绘制更多的瓦片。

在屏幕上显示文本

def makeText(text, color, bgcolor, top, left):
    # create the Surface and Rect objects for some text.
    textSurf = BASICFONT.render(text, True, color, bgcolor)
    textRect = textSurf.get_rect()
    textRect.topleft = (top, left)
    return (textSurf, textRect)

makeText()函数处理创建用于在屏幕上定位文本的表面和 Rect 对象。我们可以只调用makeText()而不是每次想在屏幕上制作文本时都进行所有这些调用。这节省了我们程序中需要输入的数量。(尽管drawTile()自己调用render()get_rect(),因为它通过中心点而不是左上角点定位文本表面对象,并使用透明背景颜色。)

绘制棋盘

def drawBoard(board, message):
    DISPLAYSURF.fill(BGCOLOR)
    if message:
        textSurf, textRect = makeText(message, MESSAGECOLOR, BGCOLOR, 5, 5)
        DISPLAYSURF.blit(textSurf, textRect)

    for tilex in range(len(board)):
        for tiley in range(len(board[0])):
            if board[tilex][tiley]:
                drawTile(tilex, tiley, board[tilex][tiley])

这个函数处理绘制整个棋盘和所有的瓦片到DISPLAYSURF显示表面对象。第 234 行的fill()方法完全覆盖了以前在显示表面对象上绘制的任何东西,这样我们就可以从头开始。

第 235 到 237 行处理在窗口顶部绘制消息。我们用它来显示“生成新的谜题…”和其他我们想要在窗口顶部显示的文本。请记住,if语句条件认为空字符串是False值,因此如果消息设置为'',那么条件就是False,第 236 和 237 行将被跳过。

接下来,嵌套的for循环用于通过调用drawTile()函数将每个瓦片绘制到显示表面对象上。

绘制棋盘的边框

    left, top = getLeftTopOfTile(0, 0)
    width = BOARDWIDTH * TILESIZE
    height = BOARDHEIGHT * TILESIZE
    pygame.draw.rect(DISPLAYSURF, BORDERCOLOR, (left - 5, top - 5, width + 11, height + 11), 4)

第 244 到 247 行绘制了瓦片周围的边框。边框的左上角将位于棋盘坐标(0, 0)处的瓦片的左上角的左侧 5 像素和上侧 5 像素。边框的宽度和高度是根据棋盘的宽度和高度(存储在BOARDWIDTHBOARDHEIGHT常量中)乘以瓦片的大小(存储在TILESIZE常量中)计算的。

我们在第 247 行绘制的矩形将有 4 像素的厚度,所以我们将边框向左和向上移动 5 像素,以便线的厚度不会重叠在瓦片上。我们还将宽度和长度增加 11(这 11 个像素中的 5 个是为了补偿将矩形向左和向上移动)。

绘制按钮

    DISPLAYSURF.blit(RESET_SURF, RESET_RECT)
    DISPLAYSURF.blit(NEW_SURF, NEW_RECT)
    DISPLAYSURF.blit(SOLVE_SURF, SOLVE_RECT)

最后,我们在屏幕的一侧绘制按钮。这些按钮的文本和位置永远不会改变,这就是为什么它们在main()函数的开头被存储在常量变量中的原因。

动画化瓦片滑动

def slideAnimation(board, direction, message, animationSpeed):
    # Note: This function does not check if the move is valid.

    blankx, blanky = getBlankPosition(board)
    if direction == UP:
        movex = blankx
        movey = blanky + 1
    elif direction == DOWN:
        movex = blankx
        movey = blanky - 1
    elif direction == LEFT:
        movex = blankx + 1
        movey = blanky
    elif direction == RIGHT:
        movex = blankx - 1
        movey = blanky

我们的瓦片滑动动画代码需要计算的第一件事是空白空间在哪里,移动瓦片在哪里。第 255 行的注释提醒我们,调用slideAnimation()的代码应确保传递给方向参数的滑动是有效的移动。

空白空间的坐标来自于对getBlankPosition()的调用。根据这些坐标和滑动的方向,我们可以找出瓦片将滑动的 XY 棋盘坐标。这些坐标将存储在movexmovey变量中。

copy()表面方法

    # prepare the base surface
    drawBoard(board, message)
    baseSurf = DISPLAYSURF.copy()
    # draw a blank space over the moving tile on the baseSurf Surface.
    moveLeft, moveTop = getLeftTopOfTile(movex, movey)
    pygame.draw.rect(baseSurf, BGCOLOR, (moveLeft, moveTop, TILESIZE, TILESIZE))

Surface 对象的copy()方法将返回一个新的 Surface 对象,其上绘制了相同的图像。但它们是两个独立的 Surface 对象。调用copy()方法后,如果我们使用blit()或 Pygame 绘图函数在一个 Surface 对象上绘制,它不会改变另一个 Surface 对象上的图像。我们将这个副本存储在第 273 行的baseSurf变量中。

接下来,我们在将要滑动的板块上绘制另一个空白空间。这是因为当我们绘制滑动动画的每一帧时,我们将在baseSurf Surface 对象的不同部分上绘制滑动板块。如果我们没有在baseSurf Surface 上擦除移动的板块,那么当我们绘制滑动板块时,它仍然会在那里。在这种情况下,baseSurf Surface 将如下所示:

然后当我们在其上绘制“9”板块向上滑动时,它会是这个样子:

通过注释掉第 276 行并运行程序,您可以自行查看。

    for i in range(0, TILESIZE, animationSpeed):
        # animate the tile sliding over
        checkForQuit()
        DISPLAYSURF.blit(baseSurf, (0, 0))
        if direction == UP:
            drawTile(movex, movey, board[movex][movey], 0, -i)
        if direction == DOWN:
            drawTile(movex, movey, board[movex][movey], 0, i)
        if direction == LEFT:
            drawTile(movex, movey, board[movex][movey], -i, 0)
        if direction == RIGHT:
            drawTile(movex, movey, board[movex][movey], i, 0)

        pygame.display.update()
        FPSCLOCK.tick(FPS)

为了绘制滑动动画的帧,我们必须在显示 Surface 上绘制baseSurf Surface,然后在动画的每一帧上,将滑动板块绘制得越来越接近其最终位置,即原始空白空间的位置。相邻两个板块之间的间距与单个板块的大小相同,我们将其存储在TILESIZE中。代码使用for循环从0TILESIZE

通常情况下,这意味着我们会将板块绘制为 0 像素,然后在下一帧绘制为 1 像素,然后 2 像素,然后 3 像素,依此类推。每一帧将花费 1/30 秒。如果将TILESIZE设置为80(就像本书中的程序在第 12 行所做的那样),那么滑动一个板块将需要超过两秒半,这实际上有点慢。

因此,我们将使for循环每帧从0TILESIZE迭代几个像素。跳过的像素数存储在animationSpeed中,在调用slideAnimation()时传入。例如,如果animationSpeed设置为8,常量TILESIZE设置为80,那么for循环和range(0, TILESIZE, animationSpeed)将将i变量设置为值081624324048566472。(不包括80,因为range()函数的第二个参数是到达但不包括的。)这意味着整个滑动动画将在 10 帧内完成,这意味着它在 10/30 秒内完成(三分之一秒),因为游戏以 30 FPS 运行。

第 282 到 289 行确保我们以正确的方向绘制滑动的板块(基于direction变量的值)。动画完成后,函数返回。请注意,当动画正在进行时,用户创建的任何事件都不会被处理。这些事件将在下一次执行到main()函数的第 70 行或checkForQuit()函数中的代码时处理。

创建一个新的拼图

def generateNewPuzzle(numSlides):
    # From a starting configuration, make numSlides number of moves (and
    # animate these moves).
    sequence = []
    board = getStartingBoard()
    drawBoard(board, '')
    pygame.display.update()
    pygame.time.wait(500) # pause 500 milliseconds for effect

generateNewPuzzle()函数将在每个新游戏开始时调用。它将通过调用getStartingBoard()创建一个新的板数据结构,然后随机打乱它。generateNewPuzzle()的前几行获取板然后将其绘制到屏幕上(冻结半秒钟以让玩家看到新鲜的板片刻)。

    lastMove = None
    for i in range(numSlides):
        move = getRandomMove(board, lastMove)
        slideAnimation(board, move, 'Generating new puzzle...', int(TILESIZE / 3))
        makeMove(board, move)
        sequence.append(move)
        lastMove = move
    return (board, sequence)

numSlides参数将告诉函数要进行多少次这些随机移动。执行随机移动的代码是在第 305 行调用getRandomMove()来获取移动本身,然后调用slideAnimation()在屏幕上执行动画。因为执行滑动动画实际上并不会更新板数据结构,我们通过在第 307 行调用makeMove()来更新板。

我们需要跟踪每个随机移动,以便玩家稍后可以点击“解决”按钮,并让程序撤销所有这些随机移动。(“通过使用愚蠢的代码变得聪明”部分讨论了我们为什么以及如何这样做。)所以移动被附加到第 308 行的sequence移动列表中。

然后我们将随机移动存储在名为lastMove的变量中,这将在下一次迭代中传递给getRandomMove()。这可以防止下一个随机移动撤销我们刚刚执行的随机移动。

所有这些需要发生numSlides次,所以我们将第 305 行到 309 行放在一个for循环中。当棋盘被打乱后,我们返回棋盘数据结构,以及在其上进行的随机移动的列表。

动画化棋盘重置

def resetAnimation(board, allMoves):
    # make all of the moves in allMoves in reverse.
    revAllMoves = allMoves[:] # gets a copy of the list
    revAllMoves.reverse()

    for move in revAllMoves:
        if move == UP:
            oppositeMove = DOWN
        elif move == DOWN:
            oppositeMove = UP
        elif move == RIGHT:
            oppositeMove = LEFT
        elif move == LEFT:
            oppositeMove = RIGHT
        slideAnimation(board, oppositeMove, '', int(TILESIZE / 2))
        makeMove(board, oppositeMove)

当玩家点击“重置”或“解决”时,滑动拼图游戏程序需要撤消对棋盘所做的所有移动。幻灯片的方向值列表将作为参数传递给allMoves参数。

第 315 行使用列表切片来创建allMoves列表的副本。记住,如果你在:之前不指定数字,那么 Python 会假定切片应该从列表的开头开始。如果你在:之后不指定数字,那么 Python 会假定切片应该一直到列表的末尾。所以allMoves[:]创建了整个allMoves列表的切片。这样可以创建实际列表的副本存储在revAllMoves中,而不仅仅是列表引用的副本。(详情请参阅invpy.com/references。)

为了撤消allMoves中的所有移动,我们需要按相反的顺序执行allMoves中的移动。有一个名为reverse()的列表方法,它会颠倒列表中项目的顺序。我们在第 316 行调用这个方法来颠倒revAllMoves列表的顺序。

在第 318 行的for循环遍历方向值的列表。记住,我们需要相反的移动,所以从第 319 行到 326 行的ifelif语句设置了oppositeMove变量中的正确方向值。然后我们调用slideAnimation()执行动画,以及makeMove()来更新棋盘数据结构。

if __name__ == '__main__':
    main()

就像在记忆拼图游戏中一样,在执行所有def语句以创建所有函数之后,我们调用main()函数来开始程序的主要部分。

这就是滑动拼图程序的全部内容!但让我们谈谈在这个游戏中出现的一些一般编程概念。

时间与内存的权衡

当然,有几种不同的方法可以编写滑动拼图游戏,使其看起来和行为方式完全相同,尽管代码不同。一个任务的程序可能有许多不同的编写方式。最常见的区别是在执行时间和内存使用之间进行权衡。

通常,程序运行得越快,就越好。这对于需要进行大量计算的程序尤其如此,无论是科学天气模拟器还是需要绘制大量详细的 3D 图形的游戏。同时,尽可能少地使用内存也是很好的。程序使用的变量越多,列表越大,它所占用的内存就越多。(你可以在invpy.com/profiling找到如何测量程序的内存使用和执行时间。)

现在,这本书中的程序还不够大且复杂,不需要担心节约内存或优化执行时间。但随着你成为更有技巧的程序员,这可能需要考虑的事情。

例如,考虑getBlankPosition()函数。这个函数需要时间来运行,因为它需要遍历所有可能的棋盘坐标来找到空白空间的位置。相反,我们可以只有一个blankspacexblankspacey变量,它们将具有这些 XY 坐标,这样我们就不必每次想知道它在哪里时都要遍历整个棋盘。(我们还需要代码,每当进行移动时更新blankspacexblankspacey变量。这段代码可以放在makeMove()中。)使用这些变量会占用更多内存,但它们会节省执行时间,使您的程序运行更快。

另一个例子是,我们在SOLVEDBOARD变量中保留了一个解决状态的棋盘数据结构,以便我们可以将当前棋盘与SOLVEDBOARD进行比较,以查看玩家是否已经解决了谜题。每次我们想要进行这个检查时,我们可以调用getStartingBoard()函数并将返回的值与当前棋盘进行比较。然后我们就不需要SOLVEDBOARD变量了。这会为我们节省一点内存,但是我们的程序会花更长的时间运行,因为它每次进行这个检查时都会重新创建解决状态的棋盘数据结构。

然而,有一件事您必须记住。编写可读性强的代码是一项非常重要的技能。可读性强的代码是易于理解的代码,尤其是对于没有编写代码的程序员。如果另一个程序员可以查看您程序的源代码并且毫不费力地弄清楚它的作用,那么该程序就是非常易读的。可读性很重要,因为当您想要修复错误或添加新功能到您的程序时(错误和新功能总是会出现),那么拥有一个易读的程序会使这些任务变得更加容易。

没有人在乎几个字节

还有一件事可能在这本书中似乎有点愚蠢,但许多人会对此感到困惑。您应该知道,使用像xnum这样的短变量名,而不是像blankxnumSlides这样更长、更具描述性的变量名,在程序实际运行时并不会节省内存。使用这些更长的变量名更好,因为它们会使您的程序更易读。

您可能会想出一些聪明的技巧,以节省一些内存。一个技巧是,当您不再需要一个变量时,您可以重新使用该变量名称以用于不同的目的,而不仅仅是使用两个不同命名的变量。

尽量避免这种诱惑。通常,这些技巧会降低代码的可读性,并使调试程序变得更加困难。现代计算机有数十亿字节的内存,而在程序中节省几个字节并不值得为了人类程序员更加混乱而使代码更加混乱。

没有人在乎几百万纳秒

同样,有时您可以以某种方式重新排列代码,使其稍微快一些,减少几个纳秒。这些技巧通常也会使代码更难阅读。考虑到在您阅读这句话所花费的时间内已经过去了数十亿纳秒,程序执行时间节省几个纳秒并不会被玩家注意到。

总结

除了使用 Surface 对象的copy()方法之外,本章没有介绍任何记忆迷题游戏没有使用的新 Pygame 编程概念。只要了解一些不同的概念,您就可以创建完全不同的游戏。

为了练习,您可以从invpy.com/buggy/slidepuzzle下载 Sliding Puzzle 程序的有错误的版本。

第五章:模拟

原文:inventwithpython.com/pygame/chapter5.html

译者:飞龙

协议:CC BY-NC-SA 4.0

如何玩模拟

模拟是 Simon 游戏的克隆版。屏幕上有四个彩色按钮。按钮以某种随机模式亮起,然后玩家必须按照正确的顺序重复这个模式。每次玩家成功模拟模式,模式就会变得更长。玩家尽可能长时间地匹配模式。

模拟源代码

可以从以下网址下载此源代码:invpy.com/simulate.py。如果出现任何错误消息,请查看错误消息中提到的行号,并检查代码中是否有任何拼写错误。您还可以将代码复制粘贴到invpy.com/diff/simulate的 Web 表单中,以查看您的代码与书中代码之间的差异。

您可以从以下网址下载此程序使用的四个声音文件:

# Simulate (a Simon clone)
# By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection)
# http://inventwithpython.com/pygame
# Creative Commons BY-NC-SA 3.0 US

  import random, sys, time, pygame
  from pygame.locals import *

  FPS = 30
 WINDOWWIDTH = 640
 WINDOWHEIGHT = 480
 FLASHSPEED = 500 # in milliseconds
 FLASHDELAY = 200 # in milliseconds
 BUTTONSIZE = 200
 BUTTONGAPSIZE = 20
 TIMEOUT = 4 # seconds before game over if no button is pushed.

 #                R    G    B
 WHITE        = (255, 255, 255)
 BLACK        = (  0,   0,   0)
 BRIGHTRED    = (255,   0,   0)
 RED          = (155,   0,   0)
 BRIGHTGREEN  = (  0, 255,   0)
 GREEN        = (  0, 155,   0)
 BRIGHTBLUE   = (  0,   0, 255)
 BLUE         = (  0,   0, 155)
 BRIGHTYELLOW = (255, 255,   0)
 YELLOW       = (155, 155,   0)
 DARKGRAY     = ( 404040)
 bgColor = BLACK

 XMARGIN = int((WINDOWWIDTH - (2 * BUTTONSIZE) - BUTTONGAPSIZE) / 2)
 YMARGIN = int((WINDOWHEIGHT - (2 * BUTTONSIZE) - BUTTONGAPSIZE) / 2)

# Rect objects for each of the four buttons
 YELLOWRECT = pygame.Rect(XMARGIN, YMARGIN, BUTTONSIZE, BUTTONSIZE)
 BLUERECT   = pygame.Rect(XMARGIN + BUTTONSIZE + BUTTONGAPSIZE, YMARGIN, BUTTONSIZE, BUTTONSIZE)
 REDRECT    = pygame.Rect(XMARGIN, YMARGIN + BUTTONSIZE + BUTTONGAPSIZE, BUTTONSIZE, BUTTONSIZE)
 GREENRECT  = pygame.Rect(XMARGIN + BUTTONSIZE + BUTTONGAPSIZE, YMARGIN + BUTTONSIZE + BUTTONGAPSIZE, BUTTONSIZE, BUTTONSIZE)

def main():
     global FPSCLOCK, DISPLAYSURF, BASICFONT, BEEP1, BEEP2, BEEP3, BEEP4

     pygame.init()
     FPSCLOCK = pygame.time.Clock()
     DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
     pygame.display.set_caption('Simulate')

     BASICFONT = pygame.font.Font('freesansbold.ttf', 16)

    infoSurf = BASICFONT.render('Match the pattern by clicking on the button or using the Q, W, A, S keys.', 1, DARKGRAY)
     infoRect = infoSurf.get_rect()
     infoRect.topleft = (10, WINDOWHEIGHT - 25)
     # load the sound files
     BEEP1 = pygame.mixer.Sound('beep1.ogg')
     BEEP2 = pygame.mixer.Sound('beep2.ogg')
     BEEP3 = pygame.mixer.Sound('beep3.ogg')
     BEEP4 = pygame.mixer.Sound('beep4.ogg')

    # Initialize some variables for a new game
     pattern = [] # stores the pattern of colors
     currentStep = 0 # the color the player must push next
     lastClickTime = 0 # timestamp of the player's last button push
     score = 0
     # when False, the pattern is playing. when True, waiting for the player to click a colored button:
     waitingForInput = False

    while True: # main game loop
         clickedButton = None # button that was clicked (set to YELLOW, RED, GREEN, or BLUE)
         DISPLAYSURF.fill(bgColor)
         drawButtons()

         scoreSurf = BASICFONT.render('Score: ' + str(score), 1, WHITE)
         scoreRect = scoreSurf.get_rect()
         scoreRect.topleft = (WINDOWWIDTH - 100, 10)
         DISPLAYSURF.blit(scoreSurf, scoreRect)

        DISPLAYSURF.blit(infoSurf, infoRect)

        checkForQuit()
         for event in pygame.event.get(): # event handling loop
             if event.type == MOUSEBUTTONUP:
                 mousex, mousey = event.pos
                 clickedButton = getButtonClicked(mousex, mousey)
            elif event.type == KEYDOWN:
                if event.key == K_q:
                     clickedButton = YELLOW
                 elif event.key == K_w:
                     clickedButton = BLUE
                 elif event.key == K_a:
                     clickedButton = RED
                 elif event.key == K_s:
                     clickedButton = GREEN

        if not waitingForInput:
             # play the pattern
             pygame.display.update()
            pygame.time.wait(1000)
            pattern.append(random.choice((YELLOW, BLUE, RED, GREEN)))
            for button in pattern:
                flashButtonAnimation(button)
                pygame.time.wait(FLASHDELAY)
            waitingForInput = True
        else:
            # wait for the player to enter buttons
            if clickedButton and clickedButton == pattern[currentStep]:
                # pushed the correct button
                flashButtonAnimation(clickedButton)
                currentStep += 1
                lastClickTime = time.time()

                if currentStep == len(pattern):
                    # pushed the last button in the pattern
                    changeBackgroundAnimation()
                    score += 1
                    waitingForInput = False
                    currentStep = 0 # reset back to first step

            elif (clickedButton and clickedButton != pattern[currentStep]) or (currentStep != 0 and time.time() - TIMEOUT > lastClickTime):
                # pushed the incorrect button, or has timed out
                gameOverAnimation()
                # reset the variables for a new game:
                pattern = []
                currentStep = 0
                waitingForInput = False
                score = 0
                pygame.time.wait(1000)
                changeBackgroundAnimation()

        pygame.display.update()
        FPSCLOCK.tick(FPS)

def terminate():
    pygame.quit()
    sys.exit()

def checkForQuit():
    for event in pygame.event.get(QUIT): # get all the QUIT events
        terminate() # terminate if any QUIT events are present
    for event in pygame.event.get(KEYUP): # get all the KEYUP events
        if event.key == K_ESCAPE:
            terminate() # terminate if the KEYUP event was for the Esc key
        pygame.event.post(event) # put the other KEYUP event objects back

def flashButtonAnimation(color, animationSpeed=50):
    if color == YELLOW:
        sound = BEEP1
        flashColor = BRIGHTYELLOW
        rectangle = YELLOWRECT
    elif color == BLUE:
        sound = BEEP2
        flashColor = BRIGHTBLUE
        rectangle = BLUERECT
    elif color == RED:
        sound = BEEP3
        flashColor = BRIGHTRED
        rectangle = REDRECT
    elif color == GREEN:
        sound = BEEP4
        flashColor = BRIGHTGREEN
        rectangle = GREENRECT

    origSurf = DISPLAYSURF.copy()
    flashSurf = pygame.Surface((BUTTONSIZE, BUTTONSIZE))
    flashSurf = flashSurf.convert_alpha()
    r, g, b = flashColor
    sound.play()
    for start, end, step in ((0, 255, 1), (255, 0, -1)): # animation loop
        for alpha in range(start, end, animationSpeed * step):
            checkForQuit()
            DISPLAYSURF.blit(origSurf, (0, 0))
            flashSurf.fill((r, g, b, alpha))
            DISPLAYSURF.blit(flashSurf, rectangle.topleft)
            pygame.display.update()
            FPSCLOCK.tick(FPS)
    DISPLAYSURF.blit(origSurf, (0, 0))

def drawButtons():
    pygame.draw.rect(DISPLAYSURF, YELLOW, YELLOWRECT)
    pygame.draw.rect(DISPLAYSURF, BLUE,   BLUERECT)
    pygame.draw.rect(DISPLAYSURF, RED,    REDRECT)
    pygame.draw.rect(DISPLAYSURF, GREEN,  GREENRECT)

def changeBackgroundAnimation(animationSpeed=40):
    global bgColor
    newBgColor = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))

    newBgSurf = pygame.Surface((WINDOWWIDTH, WINDOWHEIGHT))
    newBgSurf = newBgSurf.convert_alpha()
    r, g, b = newBgColor
    for alpha in range(0, 255, animationSpeed): # animation loop
        checkForQuit()
        DISPLAYSURF.fill(bgColor)

        newBgSurf.fill((r, g, b, alpha))
        DISPLAYSURF.blit(newBgSurf, (0, 0))

        drawButtons() # redraw the buttons on top of the tint

        pygame.display.update()
        FPSCLOCK.tick(FPS)
    bgColor = newBgColor

def gameOverAnimation(color=WHITE, animationSpeed=50):
    # play all beeps at once, then flash the background
    origSurf = DISPLAYSURF.copy()
    flashSurf = pygame.Surface(DISPLAYSURF.get_size())
    flashSurf = flashSurf.convert_alpha()
    BEEP1.play() # play all four beeps at the same time, roughly.
    BEEP2.play()
    BEEP3.play()
    BEEP4.play()
    r, g, b = color
    for i in range(3): # do the flash 3 times
        for start, end, step in ((0, 255, 1), (255, 0, -1)):
            # The first iteration in this loop sets the following for loop
            # to go from 0 to 255, the second from 255 to 0.
            for alpha in range(start, end, animationSpeed * step): # animation loop
                # alpha means transparency. 255 is opaque, 0 is invisible
                checkForQuit()
                flashSurf.fill((r, g, b, alpha))
                DISPLAYSURF.blit(origSurf, (0, 0))
                DISPLAYSURF.blit(flashSurf, (0, 0))
                drawButtons()
                pygame.display.update()
                FPSCLOCK.tick(FPS)

def getButtonClicked(x, y):
    if YELLOWRECT.collidepoint( (x, y) ):
        return YELLOW
    elif BLUERECT.collidepoint( (x, y) ):
        return BLUE
    elif REDRECT.collidepoint( (x, y) ):
        return RED
    elif GREENRECT.collidepoint( (x, y) ):
        return GREEN
    return None

if __name__ == '__main__':
    main()

通常的起始工作

# Simulate (a Simon clone)
# By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection)
# http://inventwithpython.com/pygame
# Creative Commons BY-NC-SA 3.0 US

  import random, sys, time, pygame
  from pygame.locals import *

  FPS = 30
 WINDOWWIDTH = 640
 WINDOWHEIGHT = 480
 FLASHSPEED = 500 # in milliseconds
 FLASHDELAY = 200 # in milliseconds
 BUTTONSIZE = 200
 BUTTONGAPSIZE = 20
 TIMEOUT = 4 # seconds before game over if no button is pushed.

 #                R    G    B
 WHITE        = (255, 255, 255)
 BLACK        = (  0,   0,   0)
 BRIGHTRED    = (255,   0,   0)
 RED          = (155,   0,   0)
 BRIGHTGREEN  = (  0, 255,   0)
 GREEN        = (  0, 155,   0)
 BRIGHTBLUE   = (  0,   0, 255)
 BLUE         = (  0,   0, 155)
 BRIGHTYELLOW = (255, 255,   0)
 YELLOW       = (155, 155,   0)
 DARKGRAY     = ( 404040)
 bgColor = BLACK

 XMARGIN = int((WINDOWWIDTH - (2 * BUTTONSIZE) - BUTTONGAPSIZE) / 2)
 YMARGIN = int((WINDOWHEIGHT - (2 * BUTTONSIZE) - BUTTONGAPSIZE) / 2)

在这里,我们设置了通常的常量,用于以后可能要修改的事物,例如四个按钮的大小,按钮用于的颜色阴影(当按钮亮起时使用的明亮颜色)以及玩家在游戏超时之前必须按下序列中的下一个按钮的时间。

设置按钮

 # Rect objects for each of the four buttons
 YELLOWRECT = pygame.Rect(XMARGIN, YMARGIN, BUTTONSIZE, BUTTONSIZE)
 BLUERECT   = pygame.Rect(XMARGIN + BUTTONSIZE + BUTTONGAPSIZE, YMARGIN, BUTTONSIZE, BUTTONSIZE)
 REDRECT    = pygame.Rect(XMARGIN, YMARGIN + BUTTONSIZE + BUTTONGAPSIZE, BUTTONSIZE, BUTTONSIZE)
 GREENRECT  = pygame.Rect(XMARGIN + BUTTONSIZE + BUTTONGAPSIZE, YMARGIN + BUTTONSIZE + BUTTONGAPSIZE, BUTTONSIZE, BUTTONSIZE)

就像滑动拼图游戏中的“重置”、“解决”和“新游戏”按钮一样,模拟游戏有四个矩形区域和处理玩家在这些区域内点击时的代码。程序将需要 Rect 对象来表示四个按钮的区域,以便可以在它们上调用collidepoint()方法。第 36 至 39 行设置了这些 Rect 对象的适当坐标和大小。

main()函数

 def main():
     global FPSCLOCK, DISPLAYSURF, BASICFONT, BEEP1, BEEP2, BEEP3, BEEP4

     pygame.init()
     FPSCLOCK = pygame.time.Clock()
     DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
     pygame.display.set_caption('Simulate')

     BASICFONT = pygame.font.Font('freesansbold.ttf', 16)

    infoSurf = BASICFONT.render('Match the pattern by clicking on the button or using the Q, W, A, S keys.', 1, DARKGRAY)
     infoRect = infoSurf.get_rect()
     infoRect.topleft = (10, WINDOWHEIGHT - 25)
     # load the sound files
     BEEP1 = pygame.mixer.Sound('beep1.ogg')
     BEEP2 = pygame.mixer.Sound('beep2.ogg')
     BEEP3 = pygame.mixer.Sound('beep3.ogg')
     BEEP4 = pygame.mixer.Sound('beep4.ogg')

main()函数将实现程序的大部分内容,并在需要时调用其他函数。通常的 Pygame 设置函数被调用来初始化库,创建一个 Clock 对象,创建一个窗口,设置标题,并创建一个 Font 对象,用于在窗口上显示分数和说明。这些函数调用创建的对象将存储在全局变量中,以便它们可以在其他函数中使用。但它们基本上是常量,因为其中的值从不改变。

第 55 至 58 行将加载声音文件,以便模拟可以在玩家点击每个按钮时播放声音效果。pygame.mixer.Sound()构造函数将返回一个 Sound 对象,我们将其存储在变量BEEP1BEEP4中,这些变量在第 42 行被设置为全局变量。

本程序中使用的一些局部变量

     # Initialize some variables for a new game
     pattern = [] # stores the pattern of colors
     currentStep = 0 # the color the player must push next
     lastClickTime = 0 # timestamp of the player's last button push
     score = 0
     # when False, the pattern is playing. when True, waiting for the player to click a colored button:
     waitingForInput = False

pattern变量将是一个颜色值列表(YELLOWREDBLUEGREEN),用于跟踪玩家必须记住的模式。例如,如果模式的值是[RED, RED, YELLOW, RED, BLUE, BLUE, RED, GREEN],那么玩家首先必须点击红色按钮两次,然后是黄色按钮,然后是红色按钮,依此类推,直到最后的绿色按钮。当玩家完成每一轮时,一个新的随机颜色将被添加到列表的末尾。

currentStep变量将跟踪玩家必须点击的模式列表中的颜色。如果currentStep0pattern[GREEN, RED, RED, YELLOW],那么玩家必须点击绿色按钮。如果他们点击其他按钮,代码将导致游戏结束。

有一个TIMEOUT常量,使玩家在一定时间内点击模式中的下一个按钮,否则代码会导致游戏结束。为了检查自上次按钮点击以来是否已经过了足够的时间,lastClickTime变量需要跟踪玩家上次点击按钮的时间。(Python 有一个名为 time 的模块和一个time.time()函数来返回当前时间。这将在后面解释。)

也许很难相信,score变量跟踪得分。难以置信!

我们的程序还有两种模式。要么程序正在为玩家播放按钮的模式(在这种情况下,waitingForInput设置为False),要么程序已经完成了模式的播放,并正在等待用户按正确顺序点击按钮(在这种情况下,waitingForInput设置为True)。

绘制棋盘和处理输入

     while True: # main game loop
         clickedButton = None # button that was clicked (set to YELLOW, RED, GREEN, or BLUE)
         DISPLAYSURF.fill(bgColor)
         drawButtons()

         scoreSurf = BASICFONT.render('Score: ' + str(score), 1, WHITE)
         scoreRect = scoreSurf.get_rect()
         scoreRect.topleft = (WINDOWWIDTH - 100, 10)
         DISPLAYSURF.blit(scoreSurf, scoreRect)

        DISPLAYSURF.blit(infoSurf, infoRect)

第 68 行是主游戏循环的开始。clickedButton在每次迭代开始时将被重置为None。如果在此迭代期间点击了按钮,那么clickedButton将被设置为与按钮匹配的颜色值之一(YELLOWREDGREENBLUE)。

fill()方法在第 70 行被调用,以重新绘制整个显示表面,这样我们就可以从头开始绘制。四个彩色按钮是通过调用drawButtons()(稍后解释)来绘制的。然后在 73 到 76 行创建了得分的文本。

还将有文本告诉玩家他们当前的得分是多少。与 51 行对指示文本的render()方法不同,得分的文本会发生变化。它起初是'Score: 0',然后变成'Score: 1',然后变成'Score: 2',依此类推。这就是为什么我们在游戏循环内部的 73 行调用render()方法来创建新的 Surface 对象。由于指示文本(“按照模式匹配…”)永远不会改变,所以我们只需要在 50 行游戏循环外部调用一次render()

检查鼠标点击

         checkForQuit()
         for event in pygame.event.get(): # event handling loop
             if event.type == MOUSEBUTTONUP:
                 mousex, mousey = event.pos
                 clickedButton = getButtonClicked(mousex, mousey)

第 80 行快速检查是否有任何QUIT事件,然后第 81 行是事件处理循环的开始。任何鼠标点击的 XY 坐标将存储在mousexmousey变量中。如果鼠标点击在四个按钮之一上,那么我们的getButtonClicked()函数将返回被点击的按钮的颜色对象(否则返回None)。

检查键盘按键

             elif event.type == KEYDOWN:
                if event.key == K_q:
                     clickedButton = YELLOW
                 elif event.key == K_w:
                     clickedButton = BLUE
                 elif event.key == K_a:
                     clickedButton = RED
                 elif event.key == K_s:
                     clickedButton = GREEN

第 85 到 93 行检查是否有任何KEYDOWN事件(当用户在键盘上按键时创建)。Q、W、A 和 S 键对应按钮,因为它们在键盘上呈正方形排列。

Q 键位于四个键盘键的左上方,就像屏幕上的黄色按钮位于左上方一样,所以我们将按下 Q 键与点击黄色按钮相同。我们可以通过将clickedButton变量设置为常量变量YELLOW中的值来实现这一点。我们也可以对其他三个键做同样的操作。这样,用户可以用鼠标或键盘玩模拟游戏。

游戏循环的两种状态

         if not waitingForInput:
             # play the pattern
             pygame.display.update()
            pygame.time.wait(1000)
            pattern.append(random.choice((YELLOW, BLUE, RED, GREEN)))
            for button in pattern:
                flashButtonAnimation(button)
                pygame.time.wait(FLASHDELAY)
            waitingForInput = True

程序可以处于两种不同的“模式”或“状态”。当waitingForInputFalse时,程序将显示模式的动画。当waitingForInputTrue时,程序将等待用户选择按钮。

第 97 到 105 行将涵盖程序显示模式动画的情况。由于这是在游戏开始或玩家完成模式时完成的,第 101 行将向模式列表添加一个随机颜色,使模式变长一步。然后第 102 到 104 行循环遍历模式列表中的每个值,并调用flashButtonAnimation()使该按钮发光。在所有按钮都发光完成后,程序将waitingForInput变量设置为True

弄清楚玩家是否按下了正确的按钮

        else:
            # wait for the player to enter buttons
            if clickedButton and clickedButton == pattern[currentStep]:
                # pushed the correct button
                flashButtonAnimation(clickedButton)
                currentStep += 1
                lastClickTime = time.time()

如果waitingForInputTrue,那么第 106 行的else语句中的代码将执行。第 108 行检查玩家是否在游戏循环的这次迭代中点击了一个按钮,以及该按钮是否是正确的。currentStep变量跟踪模式列表中玩家下一个应该点击的按钮的索引。

例如,如果模式设置为[YELLOW, RED, RED],并且currentStep变量设置为0(就像玩家刚开始游戏时一样),那么玩家应该点击的正确按钮将是pattern[0](黄色按钮)。

如果玩家点击了正确的按钮,我们希望通过调用flashButtonAnimation()来闪烁玩家点击的按钮,然后增加currentStep到下一步,然后更新lastClickTime变量到当前时间。(time.time()函数返回自 1970 年 1 月 1 日以来的秒数的浮点值,因此我们可以用它来跟踪时间。)

                if currentStep == len(pattern):
                    # pushed the last button in the pattern
                    changeBackgroundAnimation()
                    score += 1
                    waitingForInput = False
                    currentStep = 0 # reset back to first step

第 114 到 119 行位于从第 106 行开始的else语句内。如果执行在该else语句内部,我们知道玩家点击了一个按钮,而且这是正确的按钮。第 114 行检查是否这是模式列表中的最后一个正确的按钮,通过检查存储在currentStep中的整数是否等于模式列表中的值数量。

如果这是True,那么我们希望通过调用changeBackgroundAnimation()来改变背景颜色。这是让玩家知道他们已经完全正确输入整个模式的简单方法。分数增加,currentStep设置回0waitingForInput变量设置为False,这样在游戏循环的下一次迭代中,代码将向模式列表添加一个新的颜色值,然后闪烁按钮。

            elif (clickedButton and clickedButton != pattern[currentStep]) or (currentStep != 0 and time.time() - TIMEOUT > lastClickTime):

如果玩家没有点击正确的按钮,第 121 行的elif语句处理了玩家点击错误按钮或者玩家等待太久没有点击按钮的情况。无论哪种情况,我们都需要显示“游戏结束”动画并开始新游戏。

elif语句的条件中的(clickedButton and clickedButton != pattern[currentStep])部分检查是否点击了一个按钮,并且是错误的按钮。您可以将此与第 108 行的if语句的条件clickedButton and clickedButton == pattern[currentStep]进行比较,如果玩家点击了一个按钮,并且这是正确的按钮,则评估为True

第 121 行elif条件的另一部分是(currentStep != 0 and time.time() - TIMEOUT > lastClickTime)。这处理确保玩家没有“超时”的情况。请注意,该条件的这一部分有两个由and关键字连接的表达式。这意味着and关键字的两侧都需要评估为True

为了“超时”,它不能是玩家的第一个按钮点击。但一旦他们开始点击按钮,他们必须快速点击按钮,直到他们输入整个模式(或者点击了错误的模式并得到了“游戏结束”)。如果currentStep != 0True,那么我们知道玩家已经开始点击按钮。

时代时间

此外,为了“超时”,当前时间(由time.time()返回)减去四秒(因为4存储在TIMEOUT中)必须大于上次点击按钮的时间(存储在lastClickTime中)。time.time() - TIMEOUT > lastClickTime之所以有效,是因为时代时间的工作原理。时代时间(也称为 Unix 时代时间)是自 1970 年 1 月 1 日以来的秒数。这个日期被称为 Unix 时代。

例如,当我从交互式 shell 中运行time.time()(不要忘记首先导入 time 模块),它看起来像这样:

>>> import time
>>> time.time()
1320460242.118

这个数字的意思是,time.time()函数被调用的时刻距离 1970 年 1 月 1 日午夜已经超过了 1,320,460,242 秒。(这相当于 2011 年 11 月 4 日晚上 7 点 30 分 42 秒。您可以在invpy.com/epochtime学习如何将 Unix 纪元时间转换为常规英文时间)

如果我稍后从交互式 shell 中调用time.time(),可能会看起来像这样:

>>> time.time()
1320460261.315

从 Unix 纪元的午夜开始的 1320460261.315 秒是 2011 年 11 月 4 日晚上 7 点 31 分 01 秒。(实际上,如果您想要精确的话,是 7 点 31 分 0.315 秒。)

如果我们必须处理字符串,处理时间将会很困难。如果我们只有字符串值'7:30:42 PM''7:31:01 PM'进行比较,很难知道已经过去了 19 秒。但是使用纪元时间,只需要减去整数1320460261.315 - 1320460242.118,得到19.197000026702881。这个值是这两个时间之间的秒数。(额外的0.000026702881来自于使用浮点数进行数学运算时发生的非常小的舍入误差。它们只会偶尔发生,通常太微小而不值得关注。您可以在invpy.com/roundingerrors了解更多关于浮点数舍入误差的信息。)

回到第 121 行,如果time.time() - TIMEOUT > lastClickTime的值为True,那么自time.time()被调用并存储在lastClickTime以来已经过去了 4 秒以上。如果值为False,则已经过去了不到 4 秒。

                # pushed the incorrect button, or has timed out
                gameOverAnimation()
                # reset the variables for a new game:
                pattern = []
                currentStep = 0
                waitingForInput = False
                score = 0
                pygame.time.wait(1000)
                changeBackgroundAnimation()

如果玩家点击了错误的按钮或者超时了,程序应该播放“游戏结束”的动画,然后重置变量以开始新游戏。这包括将pattern列表设置为空列表,currentStep设置为0waitingForInput设置为False,然后将score设置为0。稍作暂停,然后设置新的背景颜色,以提示玩家新游戏的开始,新游戏将在游戏循环的下一次迭代中开始。

将面板绘制到屏幕上

        pygame.display.update()
        FPSCLOCK.tick(FPS)

就像其他游戏程序一样,在游戏循环中最后要做的事情是将显示 Surface 对象绘制到屏幕上,并调用tick()方法。

同样的terminate()函数

def terminate():
    pygame.quit()
    sys.exit()

def checkForQuit():
    for event in pygame.event.get(QUIT): # get all the QUIT events
        terminate() # terminate if any QUIT events are present
    for event in pygame.event.get(KEYUP): # get all the KEYUP events
        if event.key == K_ESCAPE:
            terminate() # terminate if the KEYUP event was for the Esc key
        pygame.event.post(event) # put the other KEYUP event objects back

terminate()checkForQuit()函数在滑动拼图章节中被使用和解释过,所以我们将跳过再次描述它们。

重复使用常量变量

def flashButtonAnimation(color, animationSpeed=50):
    if color == YELLOW:
        sound = BEEP1
        flashColor = BRIGHTYELLOW
        rectangle = YELLOWRECT
    elif color == BLUE:
        sound = BEEP2
        flashColor = BRIGHTBLUE
        rectangle = BLUERECT
    elif color == RED:
        sound = BEEP3
        flashColor = BRIGHTRED
        rectangle = REDRECT
    elif color == GREEN:
        sound = BEEP4
        flashColor = BRIGHTGREEN
        rectangle = GREENRECT

根据传递给颜色参数的 Color 值的不同,声音、明亮闪光的颜色和闪光的矩形区域也会有所不同。第 151 到 166 行根据color参数中的值设置了三个本地变量:soundflashColorrectangle

闪烁按钮的动画

    origSurf = DISPLAYSURF.copy()
    flashSurf = pygame.Surface((BUTTONSIZE, BUTTONSIZE))
    flashSurf = flashSurf.convert_alpha()
    r, g, b = flashColor
    sound.play()

闪烁按钮的动画过程很简单:在每一帧动画上,首先绘制正常的面板,然后在上面绘制闪烁的按钮的明亮颜色版本。明亮颜色的 alpha 值在动画的第一帧开始时为0,然后在每一帧后慢慢增加,直到完全不透明,明亮颜色版本完全覆盖了正常按钮颜色。这将使它看起来像按钮慢慢变亮。

变亮是动画的第一部分。第二部分是按钮变暗。这是用相同的代码完成的,只是在每一帧中,alpha 值不是增加,而是减少。随着 alpha 值越来越低,覆盖在上面的明亮颜色将变得越来越不可见,直到只剩下原始的板子和暗淡的颜色可见。

要在代码中执行此操作,第 168 行创建显示 Surface 对象的副本并将其存储在origSurf中。第 169 行创建一个新的 Surface 对象,大小与单个按钮相同,并将其存储在flashSurf中。在flashSurf上调用convert_alpha()方法,以便 Surface 对象可以在其上绘制透明颜色(否则,我们使用的 Color 对象中的 alpha 值将被忽略并自动假定为 255)。在您自己的游戏程序中,如果您在使颜色透明度工作时遇到问题,请确保已在任何具有透明颜色的 Surface 对象上调用了convert_alpha()方法。

第 171 行创建名为rgb的单独局部变量,用于存储存储在flashColor中的元组的各个 RGB 值。这只是一些语法糖,使该函数中的其余代码更容易阅读。在开始执行按钮闪烁动画之前,第 172 行将播放该按钮的声音效果。声音效果开始播放后,程序执行会继续进行,因此声音将在按钮闪烁动画期间播放。

    for start, end, step in ((0, 255, 1), (255, 0, -1)): # animation loop
        for alpha in range(start, end, animationSpeed * step):
            checkForQuit()
            DISPLAYSURF.blit(origSurf, (0, 0))
            flashSurf.fill((r, g, b, alpha))
            DISPLAYSURF.blit(flashSurf, rectangle.topleft)
            pygame.display.update()
            FPSCLOCK.tick(FPS)
    DISPLAYSURF.blit(origSurf, (0, 0))

请记住,为了执行动画,我们首先要用从0255的递增 alpha 值绘制flashSurf以执行动画的变亮部分。然后为了执行变暗,我们希望 alpha 值从2550。我们可以使用以下代码执行:

    for alpha in range(0, 255, animationSpeed): # brightening
        checkForQuit()
        DISPLAYSURF.blit(origSurf, (0, 0))
        flashSurf.fill((r, g, b, alpha))
        DISPLAYSURF.blit(flashSurf, rectangle.topleft)
        pygame.display.update()
        FPSCLOCK.tick(FPS)
    for alpha in range(255, 0, -animationSpeed): # dimming
        checkForQuit()
        DISPLAYSURF.blit(origSurf, (0, 0))
        flashSurf.fill((r, g, b, alpha))
        DISPLAYSURF.blit(flashSurf, rectangle.topleft)
        pygame.display.update()
        FPSCLOCK.tick(FPS)

但请注意,for循环内的代码处理绘制帧并且彼此相同。如果我们像上面那样编写代码,那么第一个for循环将处理动画的变亮部分(其中 alpha 值从0255),第二个for循环将处理动画的变暗部分(其中 alpha 值从2550)。请注意,对于第二个for循环,range()调用的第三个参数是一个负数。

每当我们有相同的代码时,我们可能可以缩短我们的代码,这样我们就不必重复它。这就是我们在第 173 行的for循环中所做的,它为第 174 行的range()调用提供了不同的值:

    for start, end, step in ((0, 255, 1), (255, 0, -1)): # animation loop
        for alpha in range(start, end, animationSpeed * step):

在第 173 行的for循环的第一次迭代中,start设置为0end设置为255step设置为1。这样,当执行第 174 行的for循环时,它调用range(0, 255, animationSpeed)。 (请注意,animationSpeed * 1animationSpeed相同。将一个数字乘以1会给我们相同的数字。)

然后,第 174 行的for循环执行并执行变亮动画。

在第 173 行的for循环的第二次迭代(始终有两次且仅有两次此内部for循环的迭代)中,start设置为255end设置为0step设置为-1。当执行第 174 行的for循环时,它调用range(255, 0, -animationSpeed)。 (请注意,animationSpeed * -1评估为-animationSpeed,因为将任何数字乘以-1都会返回该数字的负形式。)

这样,我们就不必有两个单独的for循环,并重复其中的所有代码。以下是再次在第 174 行的for循环内的代码:

            checkForQuit()
            DISPLAYSURF.blit(origSurf, (0, 0))
            flashSurf.fill((r, g, b, alpha))
            DISPLAYSURF.blit(flashSurf, rectangle.topleft)
            pygame.display.update()
            FPSCLOCK.tick(FPS)
    DISPLAYSURF.blit(origSurf, (0, 0))

我们检查任何QUIT事件(以防用户在动画期间尝试关闭程序),然后将origSurf Surface 贴到显示 Surface 上。然后我们通过调用fill()(提供我们在第 171 行得到的颜色的rgb值和for循环在alpha变量中设置的 alpha 值)来绘制flashSurf Surface。然后将flashSurf Surface 贴到显示 Surface 上。

然后,为了使显示 Surface 显示在屏幕上,第 179 行调用了pygame.display.update()。为了确保动画不会以计算机可以绘制的速度播放,我们通过调用tick()方法添加了短暂的暂停。(如果要看到闪烁动画播放得非常慢,请将低数字(如 1 或 2)作为tick()的参数,而不是FPS。)

绘制按钮

def drawButtons():
    pygame.draw.rect(DISPLAYSURF, YELLOW, YELLOWRECT)
    pygame.draw.rect(DISPLAYSURF, BLUE,   BLUERECT)
    pygame.draw.rect(DISPLAYSURF, RED,    REDRECT)
    pygame.draw.rect(DISPLAYSURF, GREEN,  GREENRECT)

每个按钮只是一个特定颜色的矩形放在特定位置,我们只需调用pygame.draw.rect()四次来在显示表面上绘制按钮。我们使用的 Color 对象和 Rect 对象永远不会改变,这就是为什么我们将它们存储在像YELLOWYELLOWRECT这样的常量变量中。

背景变化动画

def changeBackgroundAnimation(animationSpeed=40):
    global bgColor
    newBgColor = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))

    newBgSurf = pygame.Surface((WINDOWWIDTH, WINDOWHEIGHT))
    newBgSurf = newBgSurf.convert_alpha()
    r, g, b = newBgColor
    for alpha in range(0, 255, animationSpeed): # animation loop
        checkForQuit()
        DISPLAYSURF.fill(bgColor)

        newBgSurf.fill((r, g, b, alpha))
        DISPLAYSURF.blit(newBgSurf, (0, 0))

        drawButtons() # redraw the buttons on top of the tint

        pygame.display.update()
        FPSCLOCK.tick(FPS)
    bgColor = newBgColor

每当玩家完成正确输入整个模式时,背景颜色变化动画就会发生。在从第 198 行开始的每次循环迭代中,整个显示表面都必须重新绘制(与越来越不透明的新背景颜色混合,直到背景完全被新颜色覆盖)。循环的每次迭代所做的步骤是:

  • 第 200 行用旧的背景颜色(存储在bgColor中)填充整个显示表面(存储在DISPLAYSURF中)。

  • 第 202 行用不同的 Surface 对象(存储在newBgSurf中)填充新背景颜色的 RGB 值(并且每次迭代时 alpha 透明度值都会改变,因为这是第 198 行上的for循环所做的)。

  • 第 203 行然后将newBgSurf Surface 绘制到DISPLAYSURF中的显示表面上。我们之所以没有直接在DISPLAYSURF上绘制半透明的新背景颜色,是因为fill()方法只会替换表面上的颜色,而blit()方法会混合颜色。

  • 现在我们已经按照自己的意愿设置了背景,我们将在第 205 行调用drawButtons()来在其上绘制按钮。

  • 第 207 和 208 行只是将显示表面绘制到屏幕上并添加了一个暂停。

changeBackgroundAnimation()函数开头有一个global语句的原因是bgColor变量,因为这个函数通过第 209 行的赋值语句修改了变量的内容。任何函数都可以读取全局变量的值,而不需要指定global语句。

如果该函数为全局变量分配一个值而没有global语句,那么 Python 认为该变量是一个局部变量,只是恰好与全局变量同名。main()函数使用bgColor变量,但不需要为它添加全局语句,因为它只读取bgColor的内容,main()函数从不为bgColor分配新值。这个概念在invpy.com/global上有更详细的解释。

游戏结束动画

def gameOverAnimation(color=WHITE, animationSpeed=50):
    # play all beeps at once, then flash the background
    origSurf = DISPLAYSURF.copy()
    flashSurf = pygame.Surface(DISPLAYSURF.get_size())
    flashSurf = flashSurf.convert_alpha()
    BEEP1.play() # play all four beeps at the same time, roughly.
    BEEP2.play()
    BEEP3.play()
    BEEP4.play()
    r, g, b = color
    for i in range(3): # do the flash 3 times

下一行(下面的第 223 行)的for循环的每次迭代都会执行一次闪烁。为了完成三次闪烁,我们将所有代码放在一个具有三次迭代的for循环中。如果您想要更多或更少的闪烁,那么请更改传递给第 222 行的range()的整数。

        for start, end, step in ((0, 255, 1), (255, 0, -1)):

第 223 行上的for循环与第 173 行上的完全相同。startendstep变量将在下一个for循环(第 224 行)中用于控制alpha变量的变化。如果需要刷新自己对这些循环的工作原理的话,请重新阅读“按钮闪烁动画”部分。

            # The first iteration in this loop sets the following for loop
            # to go from 0 to 255, the second from 255 to 0.
            for alpha in range(start, end, animationSpeed * step): # animation loop
                # alpha means transparency. 255 is opaque, 0 is invisible
                checkForQuit()
                flashSurf.fill((r, g, b, alpha))
                DISPLAYSURF.blit(origSurf, (0, 0))
                DISPLAYSURF.blit(flashSurf, (0, 0))
                drawButtons()
                pygame.display.update()
                FPSCLOCK.tick(FPS)

这个动画循环与“背景变化动画”部分中的以前的闪烁动画代码相同。存储在origSurf中的原始 Surface 对象被绘制在显示表面上,然后flashSurf(上面涂上新的闪烁颜色)被 blitted 到显示表面上。背景颜色设置完成后,按钮在第 232 行上方绘制。最后,通过调用pygame.display.update()将显示表面绘制到屏幕上。

第 226 行上的for循环调整了每帧动画使用的颜色的 alpha 值(起初增加,然后减少)。

从像素坐标转换为按钮

def getButtonClicked(x, y):
    if YELLOWRECT.collidepoint( (x, y) ):
        return YELLOW
    elif BLUERECT.collidepoint( (x, y) ):
        return BLUE
    elif REDRECT.collidepoint( (x, y) ):
        return RED
    elif GREENRECT.collidepoint( (x, y) ):
        return GREEN
    return None

if __name__ == '__main__':
    main()

getButtonClicked()函数简单地接受 XY 像素坐标并返回值YELLOWBLUEREDGREEN,如果其中一个按钮被点击,或者如果 XY 像素坐标不在四个按钮之上则返回None

显式胜于隐式

您可能已经注意到getButtonClicked()的代码以在第 247 行以return None语句结束。这可能看起来像一个奇怪的输入,因为如果所有函数根本没有return语句,它们都会返回None。我们本来可以完全省略第 47 行,程序仍然会以完全相同的方式工作。那么为什么要写它呢?

通常当一个函数到达结尾并隐式地返回None值(也就是说,没有明确的return语句表明它返回None)调用它的代码并不关心返回值。所有函数调用都必须返回一个值(这样它们才能被计算为某个值并成为表达式的一部分),但我们的代码并不总是使用返回值。

例如,想想print()函数。从技术上讲,这个函数返回None值,但我们从来不关心它:

>>> spam = print('Hello')
Hello
>>> spam == None
True
>>> 

然而,当getButtonClicked()返回None时,这意味着传递给它的坐标不在四个按钮之上。为了清楚地表明在这种情况下从getButtonClicked()返回了值None,我们在函数末尾有了return None行。

为了使您的代码更易读,最好让您的代码明确(也就是说,明确地陈述某事,即使它可能是显而易见的),而不是隐含的(也就是说,让阅读代码的人知道它的工作方式,而不是直接告诉他们)。事实上,“显式胜于隐式”是 Python Koans 中的一个。

Koans 是一组关于如何编写良好代码的小格言。在 Python 交互式 shell 中有一个彩蛋(也就是一个小小的隐藏惊喜),如果你尝试导入一个名为this的模块,它会显示“Python 禅宗”Koans。在交互式 shell 中试一试:

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

如果您想了解这些个别 Koans 的更多含义,请访问invpy.com/zen