第九章:推星星
如何玩推星星
推星星是 Sokoban 或“箱子推动者”的克隆。玩家位于一个房间,里面有几颗星星。房间中的一些瓷砖精灵上有星星标记。玩家必须想办法将星星推到有星星标记的瓷砖上。如果墙壁或其他星星在其后面,玩家就不能推动星星。玩家不能拉星星,所以如果星星被推到角落,玩家将不得不重新开始级别。当所有星星都被推到星星标记的地板瓷砖上时,级别完成,下一个级别开始。
每个级别由 2D 网格瓷砖图像组成。瓷砖精灵是相同大小的图像,可以相邻放置以形成更复杂的图像。有了一些地板和墙砖,我们可以创建许多有趣形状和大小的级别。
级别文件未包含在源代码中。相反,您可以自己创建级别文件或下载级别文件。可以从invpy.com/starPusherLevels.txt
下载包含 201 个级别的级别文件。运行推星星程序时,请确保此级别文件与 starpusher.py 文件在同一文件夹中。否则,您将收到此错误消息:AssertionError: Cannot find the level file: starPusherLevels.txt
级别设计最初由 David W. Skinner 制作。您可以从他的网站sneezingtiger.com/sokoban/levels.html
下载更多谜题。
推星星的源代码
此源代码可从invpy.com/starpusher.py
下载。如果出现任何错误消息,请查看错误消息中提到的行号,并检查代码中是否有任何拼写错误。您还可以将代码复制并粘贴到invpy.com/diff/starpusher
的网络表单中,以查看您的代码与书中代码之间的差异。
级别文件可从invpy.com/starPusherLevels.txt
下载。瓷砖可从invpy.com/starPusherImages.zip
下载。
此外,就像松鼠、草地和敌人在《松鼠吃松鼠》游戏中的“对象”一样,当我在本章中说“地图对象”、“游戏状态对象”或“级别对象”时,我并不是指面向对象编程意义上的对象。这些“对象”实际上只是字典值,但由于它们代表游戏世界中的事物,因此更容易将它们称为对象。
# Star Pusher (a Sokoban 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, copy, os, pygame from pygame.locals import * FPS = 30 # frames per second to update the screen WINWIDTH = 800 # width of the program's window, in pixels WINHEIGHT = 600 # height in pixels HALF_WINWIDTH = int(WINWIDTH / 2) HALF_WINHEIGHT = int(WINHEIGHT / 2) # The total width and height of each tile in pixels. TILEWIDTH = 50 TILEHEIGHT = 85 TILEFLOORHEIGHT = 45 CAM_MOVE_SPEED = 5 # how many pixels per frame the camera moves # The percentage of outdoor tiles that have additional # decoration on them, such as a tree or rock. OUTSIDE_DECORATION_PCT = 20 BRIGHTBLUE = ( 0, 170, 255) WHITE = (255, 255, 255) BGCOLOR = BRIGHTBLUE TEXTCOLOR = WHITE UP = 'up' DOWN = 'down' LEFT = 'left' RIGHT = 'right' def main(): global FPSCLOCK, DISPLAYSURF, IMAGESDICT, TILEMAPPING, OUTSIDEDECOMAPPING, BASICFONT, PLAYERIMAGES, currentImage # Pygame initialization and basic set up of the global variables. pygame.init() FPSCLOCK = pygame.time.Clock() # Because the Surface object stored in DISPLAYSURF was returned # from the pygame.display.set_mode() function, this is the # Surface object that is drawn to the actual computer screen # when pygame.display.update() is called. DISPLAYSURF = pygame.display.set_mode((WINWIDTH, WINHEIGHT)) pygame.display.set_caption('Star Pusher') BASICFONT = pygame.font.Font('freesansbold.ttf', 18) # A global dict value that will contain all the Pygame # Surface objects returned by pygame.image.load(). IMAGESDICT = {'uncovered goal': pygame.image.load('RedSelector.png'), 'covered goal': pygame.image.load('Selector.png'), 'star': pygame.image.load('Star.png'), 'corner': pygame.image.load('Wall Block Tall.png'), 'wall': pygame.image.load('Wood Block Tall.png'), 'inside floor': pygame.image.load('Plain Block.png'), 'outside floor': pygame.image.load('Grass Block.png'), 'title': pygame.image.load('star_title.png'), 'solved': pygame.image.load('star_solved.png'), 'princess': pygame.image.load('princess.png'), 'boy': pygame.image.load('boy.png'), 'catgirl': pygame.image.load('catgirl.png'), 'horngirl': pygame.image.load('horngirl.png'), 'pinkgirl': pygame.image.load('pinkgirl.png'), 'rock': pygame.image.load('Rock.png'), 'short tree': pygame.image.load('Tree_Short.png'), 'tall tree': pygame.image.load('Tree_Tall.png'), 'ugly tree': pygame.image.load('Tree_Ugly.png')} # These dict values are global, and map the character that appears # in the level file to the Surface object it represents. TILEMAPPING = {'x': IMAGESDICT['corner'], '#': IMAGESDICT['wall'], 'o': IMAGESDICT['inside floor'], ' ': IMAGESDICT['outside floor']} OUTSIDEDECOMAPPING = {'1': IMAGESDICT['rock'], '2': IMAGESDICT['short tree'], '3': IMAGESDICT['tall tree'], '4': IMAGESDICT['ugly tree']} # PLAYERIMAGES is a list of all possible characters the player can be. # currentImage is the index of the player's current player image. currentImage = 0 PLAYERIMAGES = [IMAGESDICT['princess'], IMAGESDICT['boy'], IMAGESDICT['catgirl'], IMAGESDICT['horngirl'], IMAGESDICT['pinkgirl']] startScreen() # show the title screen until the user presses a key # Read in the levels from the text file. See the readLevelsFile() for # details on the format of this file and how to make your own levels. levels = readLevelsFile('starPusherLevels.txt') currentLevelIndex = 0 # The main game loop. This loop runs a single level, when the user # finishes that level, the next/previous level is loaded. while True: # main game loop # Run the level to actually start playing the game: result = runLevel(levels, currentLevelIndex) if result in ('solved', 'next'): # Go to the next level. currentLevelIndex += 1 if currentLevelIndex >= len(levels): # If there are no more levels, go back to the first one. currentLevelIndex = 0 elif result == 'back': # Go to the previous level. currentLevelIndex -= 1 if currentLevelIndex < 0: # If there are no previous levels, go to the last one. currentLevelIndex = len(levels)-1 elif result == 'reset': pass # Do nothing. Loop re-calls runLevel() to reset the level def runLevel(levels, levelNum): global currentImage levelObj = levels[levelnum] mapObj = decorateMap(levelObj['mapObj'], levelObj['startState']['player']) gameStateObj = copy.deepcopy(levelObj['startState']) mapNeedsRedraw = True # set to True to call drawMap() levelSurf = BASICFONT.render('Level %s of %s' % (levelObj['levelNum'] + 1, totalNumOfLevels), 1, TEXTCOLOR) levelRect = levelSurf.get_rect() levelRect.bottomleft = (20, WINHEIGHT - 35) mapWidth = len(mapObj) * TILEWIDTH mapHeight = (len(mapObj[0]) - 1) * (TILEHEIGHT - TILEFLOORHEIGHT) + TILEHEIGHT MAX_CAM_X_PAN = abs(HALF_WINHEIGHT - int(mapHeight / 2)) + TILEWIDTH MAX_CAM_Y_PAN = abs(HALF_WINWIDTH - int(mapWidth / 2)) + TILEHEIGHT levelIsComplete = False # Track how much the camera has moved: cameraOffsetX = 0 cameraOffsetY = 0 # Track if the keys to move the camera are being held down: cameraUp = False cameraDown = False cameraLeft = False cameraRight = False while True: # main game loop # Reset these variables: playerMoveTo = None keyPressed = False for event in pygame.event.get(): # event handling loop if event.type == QUIT: # Player clicked the "X" at the corner of the window. terminate() elif event.type == KEYDOWN: # Handle key presses keyPressed = True if event.key == K_LEFT: playerMoveTo = LEFT elif event.key == K_RIGHT: playerMoveTo = RIGHT elif event.key == K_UP: playerMoveTo = UP elif event.key == K_DOWN: playerMoveTo = DOWN # Set the camera move mode. elif event.key == K_a: cameraLeft = True elif event.key == K_d: cameraRight = True elif event.key == K_w: cameraUp = True elif event.key == K_s: cameraDown = True elif event.key == K_n: return 'next' elif event.key == K_b: return 'back' elif event.key == K_ESCAPE: terminate() # Esc key quits. elif event.key == K_BACKSPACE: return 'reset' # Reset the level. elif event.key == K_p: # Change the player image to the next one. currentImage += 1 if currentImage >= len(PLAYERIMAGES): # After the last player image, use the first one. currentImage = 0 mapNeedsRedraw = True elif event.type == KEYUP: # Unset the camera move mode. if event.key == K_a: cameraLeft = False elif event.key == K_d: cameraRight = False elif event.key == K_w: cameraUp = False elif event.key == K_s: cameraDown = False if playerMoveTo != None and not levelIsComplete: # If the player pushed a key to move, make the move # (if possible) and push any stars that are pushable. moved = makeMove(mapObj, gameStateObj, playerMoveTo) if moved: # increment the step counter. gameStateObj['stepCounter'] += 1 mapNeedsRedraw = True if isLevelFinished(levelObj, gameStateObj): # level is solved, we should show the "Solved!" image. levelIsComplete = True keyPressed = False DISPLAYSURF.fill(BGCOLOR) if mapNeedsRedraw: mapSurf = drawMap(mapObj, gameStateObj, levelObj['goals']) mapNeedsRedraw = False if cameraUp and cameraOffsetY < MAX_CAM_X_PAN: cameraOffsetY += CAM_MOVE_SPEED elif cameraDown and cameraOffsetY > -MAX_CAM_X_PAN: cameraOffsetY -= CAM_MOVE_SPEED if cameraLeft and cameraOffsetX < MAX_CAM_Y_PAN: cameraOffsetX += CAM_MOVE_SPEED elif cameraRight and cameraOffsetX > -MAX_CAM_Y_PAN: cameraOffsetX -= CAM_MOVE_SPEED # Adjust mapSurf's Rect object based on the camera offset. mapSurfRect = mapSurf.get_rect() mapSurfRect.center = (HALF_WINWIDTH + cameraOffsetX, HALF_WINHEIGHT + cameraOffsetY) # Draw mapSurf to the DISPLAYSURF Surface object. DISPLAYSURF.blit(mapSurf, mapSurfRect) DISPLAYSURF.blit(levelSurf, levelRect) stepSurf = BASICFONT.render('Steps: %s' % (gameStateObj['stepCounter']), 1, TEXTCOLOR) stepRect = stepSurf.get_rect() stepRect.bottomleft = (20, WINHEIGHT - 10) DISPLAYSURF.blit(stepSurf, stepRect) if levelIsComplete: # is solved, show the "Solved!" image until the player # has pressed a key. solvedRect = IMAGESDICT['solved'].get_rect() solvedRect.center = (HALF_WINWIDTH, HALF_WINHEIGHT) DISPLAYSURF.blit(IMAGESDICT['solved'], solvedRect) if keyPressed: return 'solved' pygame.display.update() # draw DISPLAYSURF to the screen. FPSCLOCK.tick() def decorateMap(mapObj, startxy): """Makes a copy of the given map object and modifies it. Here is what is done to it: * Walls that are corners are turned into corner pieces. * The outside/inside floor tile distinction is made. * Tree/rock decorations are randomly added to the outside tiles. Returns the decorated map object.""" startx, starty = startxy # Syntactic sugar # Copy the map object so we don't modify the original passed mapObjCopy = copy.deepcopy(mapObj) # Remove the non-wall characters from the map data for x in range(len(mapObjCopy)): for y in range(len(mapObjCopy[0])): if mapObjCopy[x][y] in ('$', '.', '@', '+', '*'): mapObjCopy[x][y] = ' ' # Flood fill to determine inside/outside floor tiles. floodFill(mapObjCopy, startx, starty, ' ', 'o') # Convert the adjoined walls into corner tiles. for x in range(len(mapObjCopy)): for y in range(len(mapObjCopy[0])): if mapObjCopy[x][y] == '#': if (isWall(mapObjCopy, x, y-1) and isWall(mapObjCopy, x+1, y)) or \ (isWall(mapObjCopy, x+1, y) and isWall(mapObjCopy, x, y+1)) or \ (isWall(mapObjCopy, x, y+1) and isWall(mapObjCopy, x-1, y)) or \ (isWall(mapObjCopy, x-1, y) and isWall(mapObjCopy, x, y-1)): mapObjCopy[x][y] = 'x' elif mapObjCopy[x][y] == ' ' and random.randint(0, 99) < OUTSIDE_DECORATION_PCT: mapObjCopy[x][y] = random.choice(list(OUTSIDEDECOMAPPING.keys())) return mapObjCopy def isBlocked(mapObj, gameStateObj, x, y): """Returns True if the (x, y) position on the map is blocked by a wall or star, otherwise return False.""" if isWall(mapObj, x, y): return True elif x < 0 or x >= len(mapObj) or y < 0 or y >= len(mapObj[x]): return True # x and y aren't actually on the map. elif (x, y) in gameStateObj['stars']: return True # a star is blocking return False def makeMove(mapObj, gameStateObj, playerMoveTo): """Given a map and game state object, see if it is possible for the player to make the given move. If it is, then change the player's position (and the position of any pushed star). If not, do nothing. Returns True if the player moved, otherwise False.""" # Make sure the player can move in the direction they want. playerx, playery = gameStateObj['player'] # This variable is "syntactic sugar". Typing "stars" is more # readable than typing "gameStateObj['stars']" in our code. stars = gameStateObj['stars'] # The code for handling each of the directions is so similar aside # from adding or subtracting 1 to the x/y coordinates. We can # simplify it by using the xOffset and yOffset variables. if playerMoveTo == UP: xOffset = 0 yOffset = -1 elif playerMoveTo == RIGHT: xOffset = 1 yOffset = 0 elif playerMoveTo == DOWN: xOffset = 0 yOffset = 1 elif playerMoveTo == LEFT: xOffset = -1 yOffset = 0 # See if the player can move in that direction. if isWall(mapObj, playerx + xOffset, playery + yOffset): return False else: if (playerx + xOffset, playery + yOffset) in stars: # There is a star in the way, see if the player can push it. if not isBlocked(mapObj, gameStateObj, playerx + (xOffset*2), playery + (yOffset*2)): # Move the star. ind = stars.index((playerx + xOffset, playery + yOffset)) stars[ind] = (stars[ind][0] + xOffset, stars[ind][1] + yOffset) else: return False # Move the player upwards. gameStateObj['player'] = (playerx + xOffset, playery + yOffset) return True def startScreen(): """Display the start screen (which has the title and instructions) until the player presses a key. Returns None.""" # Position the title image. titleRect = IMAGESDICT['title'].get_rect() topCoord = 50 # topCoord tracks where to position the top of the text titleRect.top = topCoord titleRect.centerx = HALF_WINWIDTH topCoord += titleRect.height # Unfortunately, Pygame's font & text system only shows one line at # a time, so we can't use strings with \n newline characters in them. # So we will use a list with each line in it. instructionText = ['Push the stars over the marks.', 'Arrow keys to move, WASD for camera control, P to change character.', 'Backspace to reset level, Esc to quit.', 'N for next level, B to go back a level.'] # Start with drawing a blank color to the entire window: DISPLAYSURF.fill(BGCOLOR) # Draw the title image to the window: DISPLAYSURF.blit(IMAGESDICT['title'], titleRect) # Position and draw the text. for i in range(len(instructionText)): instSurf = BASICFONT.render(instructionText[i], 1, TEXTCOLOR) instRect = instSurf.get_rect() topCoord += 10 # 10 pixels will go in between each line of text. instRect.top = topCoord instRect.centerx = HALF_WINWIDTH topCoord += instRect.height # Adjust for the height of the line. DISPLAYSURF.blit(instSurf, instRect) while True: # Main loop for the start screen. for event in pygame.event.get(): if event.type == QUIT: terminate() elif event.type == KEYDOWN: if event.key == K_ESCAPE: terminate() return # user has pressed a key, so return. # Display the DISPLAYSURF contents to the actual screen. pygame.display.update() FPSCLOCK.tick() def readLevelsFile(filename): assert os.path.exists(filename), 'Cannot find the level file: %s' % (filename) mapFile = open(filename, 'r') # Each level must end with a blank line content = mapFile.readlines() + ['\r\n'] mapFile.close() levels = [] # Will contain a list of level objects. levelNum = 0 mapTextLines = [] # contains the lines for a single level's map. mapObj = [] # the map object made from the data in mapTextLines for lineNum in range(len(content)): # Process each line that was in the level file. line = content[lineNum].rstrip('\r\n') if ';' in line: # Ignore the ; lines, they're comments in the level file. line = line[:line.find(';')] if line != '': # This line is part of the map. mapTextLines.append(line) elif line == '' and len(mapTextLines) > 0: # A blank line indicates the end of a level's map in the file. # Convert the text in mapTextLines into a level object. # Find the longest row in the map. maxWidth = -1 for i in range(len(mapTextLines)): if len(mapTextLines[i]) > maxWidth: maxWidth = len(mapTextLines[i]) # Add spaces to the ends of the shorter rows. This # ensures the map will be rectangular. for i in range(len(mapTextLines)): mapTextLines[i] += ' ' * (maxWidth - len(mapTextLines[i])) # Convert mapTextLines to a map object. for x in range(len(mapTextLines[0])): mapObj.append([]) for y in range(len(mapTextLines)): for x in range(maxWidth): mapObj[x].append(mapTextLines[y][x]) # Loop through the spaces in the map and find the @, ., and $ # characters for the starting game state. startx = None # The x and y for the player's starting position starty = None goals = [] # list of (x, y) tuples for each goal. stars = [] # list of (x, y) for each star's starting position. for x in range(maxWidth): for y in range(len(mapObj[x])): if mapObj[x][y] in ('@', '+'): # '@' is player, '+' is player & goal startx = x starty = y if mapObj[x][y] in ('.', '+', '*'): # '.' is goal, '*' is star & goal goals.append((x, y)) if mapObj[x][y] in ('$', '*'): # '$' is star stars.append((x, y)) # Basic level design sanity checks: assert startx != None and starty != None, 'Level %s (around line %s) in %s is missing a "@" or "+" to mark the start point.' % (levelNum+1, lineNum, filename) assert len(goals) > 0, 'Level %s (around line %s) in %s must have at least one goal.' % (levelNum+1, lineNum, filename) assert len(stars) >= len(goals), 'Level %s (around line %s) in %s is impossible to solve. It has %s goals but only %s stars.' % (levelNum+1, lineNum, filename, len(goals), len(stars)) # Create level object and starting game state object. gameStateObj = {'player': (startx, starty), 'stepCounter': 0, 'stars': stars} levelObj = {'width': maxWidth, 'height': len(mapObj), 'mapObj': mapObj, 'goals': goals, 'startState': gameStateObj} levels.append(levelObj) # Reset the variables for reading the next map. mapTextLines = [] mapObj = [] gameStateObj = {} levelNum += 1 return levels 511. 512. def floodFill(mapObj, x, y, oldCharacter, newCharacter): """Changes any values matching oldCharacter on the map object to newCharacter at the (x, y) position, and does the same for the positions to the left, right, down, and up of (x, y), recursively.""" # In this game, the flood fill algorithm creates the inside/outside # floor distinction. This is a "recursive" function. # For more info on the Flood Fill algorithm, see: # http://en.wikipedia.org/wiki/Flood_fill if mapObj[x][y] == oldCharacter: mapObj[x][y] = newCharacter if x < len(mapObj) - 1 and mapObj[x+1][y] == oldCharacter: floodFill(mapObj, x+1, y, oldCharacter, newCharacter) # call right if x > 0 and mapObj[x-1][y] == oldCharacter: floodFill(mapObj, x-1, y, oldCharacter, newCharacter) # call left if y < len(mapObj[x]) - 1 and mapObj[x][y+1] == oldCharacter: floodFill(mapObj, x, y+1, oldCharacter, newCharacter) # call down if y > 0 and mapObj[x][y-1] == oldCharacter: floodFill(mapObj, x, y-1, oldCharacter, newCharacter) # call up def drawMap(mapObj, gameStateObj, goals): """Draws the map to a Surface object, including the player and stars. This function does not call pygame.display.update(), nor does it draw the "Level" and "Steps" text in the corner.""" # mapSurf will be the single Surface object that the tiles are drawn # on, so that it is easy to position the entire map on the DISPLAYSURF # Surface object. First, the width and height must be calculated. mapSurfWidth = len(mapObj) * TILEWIDTH mapSurfHeight = (len(mapObj[0]) - 1) * (TILEHEIGHT - TILEFLOORHEIGHT) + TILEHEIGHT mapSurf = pygame.Surface((mapSurfWidth, mapSurfHeight)) mapSurf.fill(BGCOLOR) # start with a blank color on the surface. # Draw the tile sprites onto this surface. for x in range(len(mapObj)): for y in range(len(mapObj[x])): spaceRect = pygame.Rect((x * TILEWIDTH, y * (TILEHEIGHT - TILEFLOORHEIGHT), TILEWIDTH, TILEHEIGHT)) if mapObj[x][y] in TILEMAPPING: baseTile = TILEMAPPING[mapObj[x][y]] elif mapObj[x][y] in OUTSIDEDECOMAPPING: baseTile = TILEMAPPING[' '] # First draw the base ground/wall tile. mapSurf.blit(baseTile, spaceRect) if mapObj[x][y] in OUTSIDEDECOMAPPING: # Draw any tree/rock decorations that are on this tile. mapSurf.blit(OUTSIDEDECOMAPPING[mapObj[x][y]], spaceRect) elif (x, y) in gameStateObj['stars']: if (x, y) in goals: # A goal AND star are on this space, draw goal first. mapSurf.blit(IMAGESDICT['covered goal'], spaceRect) # Then draw the star sprite. mapSurf.blit(IMAGESDICT['star'], spaceRect) elif (x, y) in goals: # Draw a goal without a star on it. mapSurf.blit(IMAGESDICT['uncovered goal'], spaceRect) # Last draw the player on the board. if (x, y) == gameStateObj['player']: # Note: The value "currentImage" refers # to a key in "PLAYERIMAGES" which has the # specific player image we want to show. mapSurf.blit(PLAYERIMAGES[currentImage], spaceRect) return mapSurf def isLevelFinished(levelObj, gameStateObj): """Returns True if all the goals have stars in them.""" for goal in levelObj['goals']: if goal not in gameStateObj['stars']: # Found a space with a goal but no star on it. return False return True def terminate(): pygame.quit() sys.exit() if __name__ == '__main__': main()
初始设置
# Star Pusher (a Sokoban 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, copy, os, pygame from pygame.locals import * FPS = 30 # frames per second to update the screen WINWIDTH = 800 # width of the program's window, in pixels WINHEIGHT = 600 # height in pixels HALF_WINWIDTH = int(WINWIDTH / 2) HALF_WINHEIGHT = int(WINHEIGHT / 2) # The total width and height of each tile in pixels. TILEWIDTH = 50 TILEHEIGHT = 85 TILEFLOORHEIGHT = 45 CAM_MOVE_SPEED = 5 # how many pixels per frame the camera moves # The percentage of outdoor tiles that have additional # decoration on them, such as a tree or rock. OUTSIDE_DECORATION_PCT = 20 BRIGHTBLUE = ( 0, 170, 255) WHITE = (255, 255, 255) BGCOLOR = BRIGHTBLUE TEXTCOLOR = WHITE UP = 'up' DOWN = 'down' LEFT = 'left' RIGHT = 'right'
这些常数在程序的各个部分中使用。 TILEWIDTH
和 TILEHEIGHT
变量显示每个瓷砖图像的宽度为 50 像素,高度为 85 像素。但是,这些瓷砖在屏幕上绘制时会重叠。(稍后会解释。) TILEFLOORHEIGHT
指的是表示地板的瓷砖部分高 45 像素。这是一个简单地板图像的示意图:
房间外的草地瓷砖有时会添加额外的装饰(如树木或岩石)。 OUTSIDE_DECORATION_PCT
常数显示这些瓷砖中将随机有这些装饰的百分比。
def main(): global FPSCLOCK, DISPLAYSURF, IMAGESDICT, TILEMAPPING, OUTSIDEDECOMAPPING, BASICFONT, PLAYERIMAGES, currentImage # Pygame initialization and basic set up of the global variables. pygame.init() FPSCLOCK = pygame.time.Clock() # Because the Surface object stored in DISPLAYSURF was returned # from the pygame.display.set_mode() function, this is the # Surface object that is drawn to the actual computer screen # when pygame.display.update() is called. DISPLAYSURF = pygame.display.set_mode((WINWIDTH, WINHEIGHT)) pygame.display.set_caption('Star Pusher') BASICFONT = pygame.font.Font('freesansbold.ttf', 18)
这是程序开始时发生的通常 Pygame 设置。
# A global dict value that will contain all the Pygame # Surface objects returned by pygame.image.load(). IMAGESDICT = {'uncovered goal': pygame.image.load('RedSelector.png'), 'covered goal': pygame.image.load('Selector.png'), 'star': pygame.image.load('Star.png'), 'corner': pygame.image.load('Wall Block Tall.png'), 'wall': pygame.image.load('Wood Block Tall.png'), 'inside floor': pygame.image.load('Plain Block.png'), 'outside floor': pygame.image.load('Grass Block.png'), 'title': pygame.image.load('star_title.png'), 'solved': pygame.image.load('star_solved.png'), 'princess': pygame.image.load('princess.png'), 'boy': pygame.image.load('boy.png'), 'catgirl': pygame.image.load('catgirl.png'), 'horngirl': pygame.image.load('horngirl.png'), 'pinkgirl': pygame.image.load('pinkgirl.png'), 'rock': pygame.image.load('Rock.png'), 'short tree': pygame.image.load('Tree_Short.png'), 'tall tree': pygame.image.load('Tree_Tall.png'), 'ugly tree': pygame.image.load('Tree_Ugly.png')}
IMAGESDICT
是一个字典,其中存储了所有加载的图像。这样在其他函数中使用起来更容易,因为只需要将IMAGESDICT
变量设为全局变量。如果我们将每个图像存储在单独的变量中,那么所有 18 个变量(用于此游戏中使用的 18 个图像)都需要设为全局变量。包含所有 Surface 对象的字典与图像更容易处理。
# These dict values are global, and map the character that appears # in the level file to the Surface object it represents. TILEMAPPING = {'x': IMAGESDICT['corner'], '#': IMAGESDICT['wall'], 'o': IMAGESDICT['inside floor'], ' ': IMAGESDICT['outside floor']}
地图的数据结构只是一个由单个字符字符串组成的二维列表。TILEMAPPING
字典将地图数据结构中使用的字符链接到它们代表的图像。(这将在drawMap()
函数的解释中更清楚。)
OUTSIDEDECOMAPPING = {'1': IMAGESDICT['rock'], '2': IMAGESDICT['short tree'], '3': IMAGESDICT['tall tree'], '4': IMAGESDICT['ugly tree']}
OUTSIDEDECOMAPPING
也是一个字典,将地图数据结构中使用的字符链接到加载的图像。“外部装饰”图像绘制在室外草地砖上方。
# PLAYERIMAGES is a list of all possible characters the player can be. # currentImage is the index of the player's current player image. currentImage = 0 PLAYERIMAGES = [IMAGESDICT['princess'], IMAGESDICT['boy'], IMAGESDICT['catgirl'], IMAGESDICT['horngirl'], IMAGESDICT['pinkgirl']]
PLAYERIMAGES
列表存储了玩家使用的图像。currentImage
变量跟踪当前选择的玩家图像的索引。例如,当currentImage
设置为0
时,屏幕上会绘制PLAYERIMAGES[0]
,也就是“公主”玩家图像。
startScreen() # show the title screen until the user presses a key # Read in the levels from the text file. See the readLevelsFile() for # details on the format of this file and how to make your own levels. levels = readLevelsFile('starPusherLevels.txt') currentLevelIndex = 0
startScreen()
函数将持续显示初始启动屏幕(其中还包括游戏说明),直到玩家按下键。当玩家按下键时,startScreen()
函数返回并从关卡文件中读取关卡。玩家从第一关开始,这是关卡列表中索引为0
的关卡对象。
# The main game loop. This loop runs a single level, when the user # finishes that level, the next/previous level is loaded. while True: # main game loop # Run the level to actually start playing the game: result = runLevel(levels, currentLevelIndex)
runLevel()
函数处理游戏的所有动作。它接收一个关卡对象列表和要玩的关卡在该列表中的整数索引。当玩家完成关卡时,runLevel()
将返回以下字符串之一:'solved'
(因为玩家已经将所有星星放在目标上),'next'
(因为玩家想跳到下一关),'back'
(因为玩家想回到上一关),和'reset'
(因为玩家想重新开始当前关卡,也许是因为他们把星星推到了角落里)。
if result in ('solved', 'next'): # Go to the next level. currentLevelIndex += 1 if currentLevelIndex >= len(levels): # If there are no more levels, go back to the first one. currentLevelIndex = 0 elif result == 'back': # Go to the previous level. currentLevelIndex -= 1 if currentLevelIndex < 0: # If there are no previous levels, go to the last one. currentLevelIndex = len(levels)-1
如果runLevel()
返回字符串'solved'
或'next'
,则需要将levelNum
增加1
。如果这将levelNum
增加到超出关卡数量,则将levelNum
设置回0
。
如果返回'back'
,则levelNum
减1
。如果这使其小于0
,则将其设置为最后一关(即len(levels)-1
)。
elif result == 'reset': pass # Do nothing. Loop re-calls runLevel() to reset the level
如果返回值是'reset'
,则代码不执行任何操作。pass
语句不执行任何操作(类似于注释),但是需要因为 Python 解释器在elif
语句后期望一个缩进的代码行。
我们可以完全从源代码中删除第 119 和 120 行,程序仍然可以正常工作。我们在这里包含它的原因是为了程序的可读性,这样如果以后对代码进行更改,我们不会忘记runLevel()
也可以返回字符串'reset'
。
def runLevel(levels, levelNum): global currentImage levelObj = levels[levelnum] mapObj = decorateMap(levelObj['mapObj'], levelObj['startState']['player']) gameStateObj = copy.deepcopy(levelObj['startState'])
关卡列表包含了从关卡文件中加载的所有关卡对象。当前关卡的关卡对象(即levelNum
设置的值)存储在levelObj
变量中。从decorateMap()
函数返回一个地图对象(它区分室内和室外瓷砖,并用树木和岩石装饰室外瓷砖)。并且为了跟踪玩家玩这个关卡时的游戏状态,使用copy.deepcopy()
函数创建了存储在levelObj
中的游戏状态对象的副本。
游戏状态对象的副本是因为存储在levelObj['startState']
中的游戏状态对象代表了关卡开始时的游戏状态,我们不希望修改它。否则,如果玩家重新开始关卡,该关卡的原始游戏状态将丢失。
copy.deepcopy()
函数被使用是因为游戏状态对象是一个包含元组的字典。但从技术上讲,字典包含对元组的引用。(引用在invpy.com/references
中有详细解释。)使用赋值语句来复制字典将复制引用而不是它们所指向的值,因此复制和原始字典仍然指向相同的元组。
copy.deepcopy()
函数通过复制字典中的实际元组来解决了这个问题。这样我们可以保证改变一个字典不会影响另一个字典。
mapNeedsRedraw = True # set to True to call drawMap() levelSurf = BASICFONT.render('Level %s of %s' % (levelObj['levelNum'] + 1, totalNumOfLevels), 1, TEXTCOLOR) levelRect = levelSurf.get_rect() levelRect.bottomleft = (20, WINHEIGHT - 35) mapWidth = len(mapObj) * TILEWIDTH mapHeight = (len(mapObj[0]) - 1) * (TILEHEIGHT - TILEFLOORHEIGHT) + TILEHEIGHT MAX_CAM_X_PAN = abs(HALF_WINHEIGHT - int(mapHeight / 2)) + TILEWIDTH MAX_CAM_Y_PAN = abs(HALF_WINWIDTH - int(mapWidth / 2)) + TILEHEIGHT levelIsComplete = False # Track how much the camera has moved: cameraOffsetX = 0 cameraOffsetY = 0 # Track if the keys to move the camera are being held down: cameraUp = False cameraDown = False cameraLeft = False cameraRight = False
在开始玩一个关卡时设置了更多的变量。mapWidth
和 mapHeight
变量是地图的像素大小。计算 mapHeight
的表达式有点复杂,因为瓷砖彼此重叠。只有底部一行瓷砖是完整的高度(这解释了表达式中的 + TILEHEIGHT
部分),所有其他行的瓷砖(数量为 (len(mapObj[0]) - 1)
)都有轻微的重叠。这意味着它们实际上每个只有 (TILEHEIGHT - TILEFLOORHEIGHT)
像素高。
《推星星》中的摄像头可以独立于玩家在地图上移动。这就是为什么摄像头需要自己的一组“移动”变量:cameraUp
、cameraDown
、cameraLeft
和 cameraRight
。cameraOffsetX
和 cameraOffsetY
变量跟踪摄像头的位置。
while True: # main game loop # Reset these variables: playerMoveTo = None keyPressed = False for event in pygame.event.get(): # event handling loop if event.type == QUIT: # Player clicked the "X" at the corner of the window. terminate()
playerMoveTo
变量将被设置为玩家打算在地图上移动玩家角色的方向常量。keyPressed
变量跟踪在游戏循环的这次迭代中是否按下了任何键。稍后在玩家解决了关卡时会检查这个变量。
elif event.type == KEYDOWN: # Handle key presses keyPressed = True if event.key == K_LEFT: playerMoveTo = LEFT elif event.key == K_RIGHT: playerMoveTo = RIGHT elif event.key == K_UP: playerMoveTo = UP elif event.key == K_DOWN: playerMoveTo = DOWN # Set the camera move mode. elif event.key == K_a: cameraLeft = True elif event.key == K_d: cameraRight = True elif event.key == K_w: cameraUp = True elif event.key == K_s: cameraDown = True elif event.key == K_n: return 'next' elif event.key == K_b: return 'back' elif event.key == K_ESCAPE: terminate() # Esc key quits. elif event.key == K_BACKSPACE: return 'reset' # Reset the level. elif event.key == K_p: # Change the player image to the next one. currentImage += 1 if currentImage >= len(PLAYERIMAGES): # After the last player image, use the first one. currentImage = 0 mapNeedsRedraw = True elif event.type == KEYUP: # Unset the camera move mode. if event.key == K_a: cameraLeft = False elif event.key == K_d: cameraRight = False elif event.key == K_w: cameraUp = False elif event.key == K_s: cameraDown = False
这段代码处理了按下各种键时要做什么。
if playerMoveTo != None and not levelIsComplete: # If the player pushed a key to move, make the move # (if possible) and push any stars that are pushable. moved = makeMove(mapObj, gameStateObj, playerMoveTo) if moved: # increment the step counter. gameStateObj['stepCounter'] += 1 mapNeedsRedraw = True if isLevelFinished(levelObj, gameStateObj): # level is solved, we should show the "Solved!" image. levelIsComplete = True keyPressed = False
如果 playerMoveTo
变量不再设置为 None
,那么我们知道玩家打算移动。对 makeMove()
的调用处理了改变 gameStateObj
中玩家位置的 XY 坐标,以及推动任何星星。makeMove()
的返回值存储在 moved
中。如果这个值是 True
,那么玩家角色就朝那个方向移动了。如果值是 False
,那么玩家一定试图移动到一个墙上,或者推动一个背后有东西的星星。在这种情况下,玩家无法移动,地图上的任何东西都不会改变。
DISPLAYSURF.fill(BGCOLOR) if mapNeedsRedraw: mapSurf = drawMap(mapObj, gameStateObj, levelObj['goals']) mapNeedsRedraw = False
地图不需要在游戏循环的每次迭代中重新绘制。事实上,这个游戏程序已经足够复杂,这样做会导致游戏略微(但是可察觉的)减速。地图只有在发生变化时(比如玩家移动或推动星星)才需要重新绘制。因此,mapSurf
变量中的 Surface 对象只有在 mapNeedsRedraw
变量被设置为 True
时才会通过调用 drawMap()
函数进行更新。
在第 225 行绘制地图后,mapNeedsRedraw
变量被设置为 False
。如果想要看到程序在游戏循环的每次迭代中绘制而变慢,可以注释掉第 226 行并重新运行程序。你会注意到移动摄像头会明显变慢。
if cameraUp and cameraOffsetY < MAX_CAM_X_PAN: cameraOffsetY += CAM_MOVE_SPEED elif cameraDown and cameraOffsetY > -MAX_CAM_X_PAN: cameraOffsetY -= CAM_MOVE_SPEED if cameraLeft and cameraOffsetX < MAX_CAM_Y_PAN: cameraOffsetX += CAM_MOVE_SPEED elif cameraRight and cameraOffsetX > -MAX_CAM_Y_PAN: cameraOffsetX -= CAM_MOVE_SPEED
如果摄像头移动变量被设置为 True
,并且摄像头没有超过由 MAX_CAM_X_PAN
和 MAX_CAM_Y_PAN
设置的边界,那么摄像头位置(存储在 cameraOffsetX
和 cameraOffsetY
中)应该移动 CAM_MOVE_SPEED
像素。
请注意,在第 228 行和第 230 行有一个 if
和 elif
语句用于上下移动摄像头,然后在第 232 行和第 234 行有一个单独的 if
和 elif
语句。这样,用户可以同时在垂直和水平方向上移动摄像头。如果第 232 行是一个 elif
语句,这是不可能的。
# Adjust mapSurf's Rect object based on the camera offset. mapSurfRect = mapSurf.get_rect() mapSurfRect.center = (HALF_WINWIDTH + cameraOffsetX, HALF_WINHEIGHT + cameraOffsetY) # Draw mapSurf to the DISPLAYSURF Surface object. DISPLAYSURF.blit(mapSurf, mapSurfRect) DISPLAYSURF.blit(levelSurf, levelRect) stepSurf = BASICFONT.render('Steps: %s' % (gameStateObj['stepCounter']), 1, TEXTCOLOR) stepRect = stepSurf.get_rect() stepRect.bottomleft = (20, WINHEIGHT - 10) DISPLAYSURF.blit(stepSurf, stepRect) if levelIsComplete: # is solved, show the "Solved!" image until the player # has pressed a key. solvedRect = IMAGESDICT['solved'].get_rect() solvedRect.center = (HALF_WINWIDTH, HALF_WINHEIGHT) DISPLAYSURF.blit(IMAGESDICT['solved'], solvedRect) if keyPressed: return 'solved' pygame.display.update() # draw DISPLAYSURF to the screen. FPSCLOCK.tick()
237 到 261 行定位摄像头并将地图和其他图形绘制到DISPLAYSURF
中。如果关卡已解决,则胜利图形也会绘制在其他所有内容之上。如果用户在此迭代期间按下键,则keyPressed
变量将设置为True
,此时runLevel()
函数将返回。
def isWall(mapObj, x, y): """Returns True if the (x, y) position on the map is a wall, otherwise return False.""" if x < 0 or x >= len(mapObj) or y < 0 or y >= len(mapObj[x]): return False # x and y aren't actually on the map. elif mapObj[x][y] in ('#', 'x'): return True # wall is blocking return False
isWall()
函数在地图对象的 XY 坐标处返回True
,如果有墙壁。墙壁对象在地图对象中表示为'x'
或'#'
字符串。
def decorateMap(mapObj, startxy): """Makes a copy of the given map object and modifies it. Here is what is done to it: * Walls that are corners are turned into corner pieces. * The outside/inside floor tile distinction is made. * Tree/rock decorations are randomly added to the outside tiles. Returns the decorated map object.""" startx, starty = startxy # Syntactic sugar # Copy the map object so we don't modify the original passed mapObjCopy = copy.deepcopy(mapObj)
decorateMap()
函数改变了数据结构mapObj
,使其不像地图文件中那样简单。decorateMap()
改变的三件事在函数顶部的注释中有解释。
# Remove the non-wall characters from the map data for x in range(len(mapObjCopy)): for y in range(len(mapObjCopy[0])): if mapObjCopy[x][y] in ('$', '.', '@', '+', '*'): mapObjCopy[x][y] = ' '
地图对象具有表示玩家、目标和星星位置的字符。这些对于地图对象是必要的(它们在地图文件读取后存储在其他数据结构中),因此它们被转换为空格。
# Flood fill to determine inside/outside floor tiles. floodFill(mapObjCopy, startx, starty, ' ', 'o')
floodFill()
函数将把墙壁内的所有瓷砖从' '
字符更改为'o'
字符。它使用一种称为递归的编程概念来实现这一点,这在本章后面的“递归函数”部分中有解释。
# Convert the adjoined walls into corner tiles. for x in range(len(mapObjCopy)): for y in range(len(mapObjCopy[0])): if mapObjCopy[x][y] == '#': if (isWall(mapObjCopy, x, y-1) and isWall(mapObjCopy, x+1, y)) or \ (isWall(mapObjCopy, x+1, y) and isWall(mapObjCopy, x, y+1)) or \ (isWall(mapObjCopy, x, y+1) and isWall(mapObjCopy, x-1, y)) or \ (isWall(mapObjCopy, x-1, y) and isWall(mapObjCopy, x, y-1)): mapObjCopy[x][y] = 'x' elif mapObjCopy[x][y] == ' ' and random.randint(0, 99) < OUTSIDE_DECORATION_PCT: mapObjCopy[x][y] = random.choice(list(OUTSIDEDECOMAPPING.keys())) return mapObjCopy
301 行的大型多行if
语句检查当前 XY 坐标处的墙壁瓷砖是否是角落墙瓷砖,方法是检查是否有相邻的墙瓷砖形成角落形状。如果是,地图对象中表示普通墙壁的'#'
字符串将被更改为表示角落墙瓷砖的'x'
字符串。
def isBlocked(mapObj, gameStateObj, x, y): """Returns True if the (x, y) position on the map is blocked by a wall or star, otherwise return False.""" if isWall(mapObj, x, y): return True elif x < 0 or x >= len(mapObj) or y < 0 or y >= len(mapObj[x]): return True # x and y aren't actually on the map. elif (x, y) in gameStateObj['stars']: return True # a star is blocking return False
地图上的空格会被阻塞的三种情况:如果有星星、墙壁,或者空格的坐标超出地图的边缘。isBlocked()
函数检查这三种情况,如果 XY 坐标被阻塞则返回True
,否则返回False
。
def makeMove(mapObj, gameStateObj, playerMoveTo): """Given a map and game state object, see if it is possible for the player to make the given move. If it is, then change the player's position (and the position of any pushed star). If not, do nothing. Returns True if the player moved, otherwise False.""" # Make sure the player can move in the direction they want. playerx, playery = gameStateObj['player'] # This variable is "syntactic sugar". Typing "stars" is more # readable than typing "gameStateObj['stars']" in our code. stars = gameStateObj['stars'] # The code for handling each of the directions is so similar aside # from adding or subtracting 1 to the x/y coordinates. We can # simplify it by using the xOffset and yOffset variables. if playerMoveTo == UP: xOffset = 0 yOffset = -1 elif playerMoveTo == RIGHT: xOffset = 1 yOffset = 0 elif playerMoveTo == DOWN: xOffset = 0 yOffset = 1 elif playerMoveTo == LEFT: xOffset = -1 yOffset = 0 # See if the player can move in that direction. if isWall(mapObj, playerx + xOffset, playery + yOffset): return False else: if (playerx + xOffset, playery + yOffset) in stars: # There is a star in the way, see if the player can push it. if not isBlocked(mapObj, gameStateObj, playerx + (xOffset*2), playery + (yOffset*2)): # Move the star. ind = stars.index((playerx + xOffset, playery + yOffset)) stars[ind] = (stars[ind][0] + xOffset, stars[ind][1] + yOffset) else: return False # Move the player upwards. gameStateObj['player'] = (playerx + xOffset, playery + yOffset) return True
makeMove()
函数检查移动玩家是否是有效移动。只要没有墙壁阻挡路径,或者星星后面有墙壁或星星,玩家就可以朝那个方向移动。gameStateObj
变量将被更新以反映这一点,并且将返回True
值告诉函数的调用者玩家已经移动。
如果玩家想要移动的空间中有星星,那么星星的位置也会改变,并且这些信息也会更新到gameStateObj
变量中。这就是“推星星”的实现方式。
如果玩家被阻止朝所需方向移动,则不会修改gameStateObj
,函数返回False
。
def startScreen(): """Display the start screen (which has the title and instructions) until the player presses a key. Returns None.""" # Position the title image. titleRect = IMAGESDICT['title'].get_rect() topCoord = 50 # topCoord tracks where to position the top of the text titleRect.top = topCoord titleRect.centerx = HALF_WINWIDTH topCoord += titleRect.height # Unfortunately, Pygame's font & text system only shows one line at # a time, so we can't use strings with \n newline characters in them. # So we will use a list with each line in it. instructionText = ['Push the stars over the marks.', 'Arrow keys to move, WASD for camera control, P to change character.', 'Backspace to reset level, Esc to quit.', 'N for next level, B to go back a level.']
startScreen()
函数需要在窗口中心显示几行不同的文本。我们将每行存储为instructionText
列表中的字符串。标题图像(存储在IMAGESDICT['title']
中,作为一个 Surface 对象(最初从star_title.png文件加载))将被定位在窗口顶部 50 像素处。这是因为整数50
被存储在 383 行的topCoord
变量中。topCoord
变量将跟踪标题图像和指示文本的 Y 轴定位。X 轴始终设置为使图像和文本居中,就像 385 行中的标题图像一样。
386 行,topCoord
变量增加了该图像的高度。这样我们就可以修改图像,而启动屏幕代码不必更改。
# Start with drawing a blank color to the entire window: DISPLAYSURF.fill(BGCOLOR) # Draw the title image to the window: DISPLAYSURF.blit(IMAGESDICT['title'], titleRect) # Position and draw the text. for i in range(len(instructionText)): instSurf = BASICFONT.render(instructionText[i], 1, TEXTCOLOR) instRect = instSurf.get_rect() topCoord += 10 # 10 pixels will go in between each line of text. instRect.top = topCoord instRect.centerx = HALF_WINWIDTH topCoord += instRect.height # Adjust for the height of the line. DISPLAYSURF.blit(instSurf, instRect)
400 行是标题图像被绘制到显示表面对象的地方。从 403 行开始的for
循环将渲染、定位和绘制instructionText
循环中的每个指示字符串。topCoord
变量将始终按照先前渲染文本的大小(409 行)和额外的 10 个像素(406 行)递增,以便文本行之间有 10 像素的间隔。
while True: # Main loop for the start screen. for event in pygame.event.get(): if event.type == QUIT: terminate() elif event.type == KEYDOWN: if event.key == K_ESCAPE: terminate() return # user has pressed a key, so return. # Display the DISPLAYSURF contents to the actual screen. pygame.display.update() FPSCLOCK.tick()
在startScreen()
中有一个游戏循环,从第 412 行开始处理指示程序是否应终止或从startScreen()
函数返回的事件。直到玩家执行其中一个操作,循环将继续调用pygame.display.update()
和FPSCLOCK.tick()
以保持开始屏幕显示在屏幕上。
Star Pusher 中的数据结构
Star Pusher 对级别、地图和游戏状态数据结构有特定的格式。
“游戏状态”数据结构
游戏状态对象将是一个带有三个键的字典:‘player’、‘stepCounter’和’stars’。
- 键为’player’的值将是当前玩家 XY 位置的两个整数元组。
- 键为’stepCounter’的值将是一个整数,用于跟踪玩家在本级别中移动了多少步(这样玩家可以尝试以更少的步骤解决谜题)。
- 键为’stars’的值是当前级别上每颗星星的 XY 值的两个整数元组的列表。
“地图”数据结构
地图数据结构只是一个二维列表,其中使用的两个索引表示地图的 X 和 Y 坐标。列表中每个索引处的值是一个表示该地图上每个空间的标题的单个字符字符串:
- ‘#’ - 一个木墙。
- ‘x’ - 一个角落的墙。
- ‘@’ - 本级别玩家的起始空间。
- ‘.’ - 一个目标空间。
- ‘$’ - 一个星星在级别开始时所在的空间。
- ‘+’ - 一个有目标和起始玩家空间的空间。
- ‘*’ - 一个在级别开始时有一个目标和一颗星星的空间。
- ’ ’ - 一个草地户外空间。
- ‘o’ - 一个内部地板空间。(这是一个小写字母 O,不是零。)
- ‘1’ - 草地上的岩石。
- ‘2’ - 草地上的矮树。
- ‘3’ - 草地上的高树。
- ‘4’ - 草地上的丑陋树。
“级别”数据结构
级别对象包含一个游戏状态对象(将在级别刚开始时使用的状态)、一个地图对象和一些其他值。级别对象本身是一个带有以下键的字典:
- 键为’width’的值是整数,表示整个地图有多少个瓷砖宽。
- 键为’height’的值是整数,表示整个地图有多少个瓷砖高。
- 键为’mapObj’的值是这个级别的地图对象。
- 键为’goals’的值是一个包含每个目标空间在地图上 XY 坐标的两个整数元组的列表。
- 键为’startState’的值是一个游戏状态对象,用于显示级别开始时星星和玩家的起始位置。
读写文本文件
Python 有用于从玩家硬盘读取文件的函数。这对于让单独的文件保存每个级别的所有数据将非常有用。这也是一个好主意,因为为了获得新的级别,玩家不必更改游戏的源代码,而是可以只下载新的级别文件。
文本文件和二进制文件
文本文件是包含简单文本数据的文件。在 Windows 中,文本文件是由记事本应用程序、Ubuntu 上的 Gedit 和 Mac OS X 上的 TextEdit 创建的。还有许多其他称为文本编辑器的程序可以创建和修改文本文件。IDLE 自己的文件编辑器是一个文本编辑器。
文本编辑器和文字处理器(如 Microsoft Word、OpenOffice Writer 或 iWork Pages)之间的区别在于文本编辑器只有文本。您无法设置文本的字体、大小或颜色。(IDLE 会根据 Python 代码的类型自动设置文本的颜色,但您无法自行更改,因此它仍然是一个文本编辑器。)文本和二进制文件之间的区别对于这个游戏程序并不重要,但您可以在invpy.com/textbinary
上阅读相关内容。您只需要知道这一章和 Star Pusher 程序只处理文本文件。
写入文件
要创建一个文件,调用open()
函数并传递两个参数:一个字符串作为文件名,另一个字符串'w'
告诉open()
函数您要以“写”模式打开文件。open()
函数返回一个文件对象:
>>> textFile = open('hello.txt', 'w') >>>
如果您从交互式 shell 中运行此代码,此函数创建的hello.txt文件将在 python.exe 程序所在的同一文件夹中创建(在 Windows 上,这可能是 C:\Python32)。如果open()
函数是从.py 程序中调用的,则文件将在.py 文件所在的同一文件夹中创建。
“写”模式告诉open()
创建文件(如果文件不存在)。如果文件存在,open()
将删除该文件并创建一个新的空文件。这就像赋值语句可以创建一个新变量,或者覆盖已存在变量中的当前值一样。**这可能有些危险。**如果意外将一个重要文件的文件名发送给open()
函数,并将'w'
作为第二个参数,它将被删除。这可能导致必须重新安装计算机操作系统和/或发射核导弹。
文件对象有一个名为write()
的方法,可用于向文件写入文本。只需像将字符串传递给print()
函数一样将其传递给write()
。不同之处在于write()
不会自动在字符串末尾添加换行符('\n'
)。如果要添加换行符,必须在字符串中包含它:
>>> textFile = open('hello.txt', 'w') >>> textFile.write('This will be the content of the file.\nHello world!\n') >>>
要告诉 Python 您已经完成向该文件写入内容,应调用文件对象的close()
方法。(尽管 Python 会在程序结束时自动关闭任何打开的文件对象。)
>>> textFile.close()
从文件中读取
要读取文件的内容,将字符串'r'
传递给open()
函数,而不是'w'
。然后在文件对象上调用readlines()
方法来读取文件的内容。最后,通过调用close()
方法关闭文件。
>>> textFile = open('hello.txt', 'r') >>> content = textFile.readlines() >>> textFile.close()
readlines()
方法返回一个字符串列表:文件中每一行的一个字符串。
>>> content ['This will be the content of the file.\n', 'Hello world!\n'] >>>
如果要重新读取该文件的内容,必须在文件对象上调用close()
并重新打开它。
作为readlines()
的替代方案,您还可以调用read()
方法,它将返回文件的整个内容作为单个字符串值:
>>> textFile = open('hello.txt', 'r') >>> content = textFile.read() >>> content 'This will be the content of the file.\nHello world!\n'
顺便说一句,如果省略open()
函数的第二个参数,Python 将假定您要以读模式打开文件。因此,open('foobar.txt', 'r')
和open('foobar.txt')
做的事情完全相同。
关于 Star Pusher 地图文件格式
我们需要特定格式的级别文本文件。哪些字符代表墙壁、星星或玩家的起始位置?如果我们有多个级别的地图,如何知道一个级别的地图何时结束,下一个级别何时开始?
幸运的是,我们将使用的地图文件格式已经为我们定义好了。有许多 Sokoban 游戏(您可以在invpy.com/sokobanclones
找到更多),它们都使用相同的地图文件格式。如果您从invpy.com/starPusherLevels.txt
下载关卡文件并在文本编辑器中打开,您会看到类似于这样的内容:
; Star Pusher (Sokoban clone) ; http://inventwithpython.com/blog ; By Al Sweigart [email protected] ; ; Everything after the ; is a comment and will be ignored by the game that ; reads in this file. ; ; The format is described at: ; http://sokobano.de/wiki/index.php?title=Level_format ; @ - The starting position of the player. ; $ - The starting position for a pushable star. ; . - A goal where a star needs to be pushed. ; + - Player & goal ; * - Star & goal ; (space) - an empty open space. ; # - A wall. ; ; Level maps are separated by a blank line (I like to use a ; at the start ; of the line since it is more visible.) ; ; I tried to use the same format as other people use for their Sokoban games, ; so that loading new levels is easy. Just place the levels in a text file ; and name it "starPusherLevels.txt" (after renaming this file, of course). ; Starting demo level: ######## ## # # . # # $ # # .$@$. # ####$ # #. # # ## #####
文件顶部的注释解释了文件的格式。加载第一个级别时,它看起来像这样:
def readLevelsFile(filename): assert os.path.exists(filename), 'Cannot find the level file: %s' % (filename)
os.path.exists()
函数将返回True
,如果由传递给函数的字符串指定的文件存在。如果不存在,os.path.exists()
将返回False
。
mapFile = open(filename, 'r') # Each level must end with a blank line content = mapFile.readlines() + ['\r\n'] mapFile.close() levels = [] # Will contain a list of level objects. levelNum = 0 mapTextLines = [] # contains the lines for a single level's map. mapObj = [] # the map object made from the data in mapTextLines
用于读取文件的级别文件的文件对象存储在mapFile
中。级别文件的所有文本都存储在content
变量中的字符串列表中,并在末尾添加了一个空行。(稍后会解释为什么这样做。)
创建级别对象后,它们将存储在levels
列表中。levelNum
变量将跟踪级别文件中找到的级别数量。mapTextLines
列表将是content
列表中单个地图的字符串列表(与content
存储级别文件中所有地图的字符串方式相反)。mapObj
变量将是一个二维列表。
for lineNum in range(len(content)): # Process each line that was in the level file. line = content[lineNum].rstrip('\r\n')
第 437 行的for
循环将逐行遍历从级别文件中读取的每一行。行号将存储在lineNum
中,行的文本字符串将存储在行中。字符串末尾的任何换行符将被剥离。
if ';' in line: # Ignore the ; lines, they're comments in the level file. line = line[:line.find(';')]
地图文件中分号后存在的任何文本都被视为注释并被忽略。这就像 Python 注释的#
符号一样。为了确保我们的代码不会意外地将注释视为地图的一部分,line
变量被修改,以便它只包含分号之前(但不包括)的文本。请记住,这只是更改content
列表中的字符串,而不是更改硬盘上的级别文件。
if line != '': # This line is part of the map. mapTextLines.append(line)
地图文件中可以有多个级别的地图。mapTextLines
列表将包含当前加载的级别的地图文件中的文本行。只要当前行不为空,该行将被附加到mapTextLines
的末尾。
elif line == '' and len(mapTextLines) > 0: # A blank line indicates the end of a level's map in the file. # Convert the text in mapTextLines into a level object.
当地图文件中有空行时,表示当前级别的地图已结束。未来的文本行将用于后续级别。但是,请注意,mapTextLines
中必须至少有一行,以便多个连续的空行不被视为多个级别的起始和结束。
# Find the longest row in the map. maxWidth = -1 for i in range(len(mapTextLines)): if len(mapTextLines[i]) > maxWidth: maxWidth = len(mapTextLines[i])
mapTextLines
中的所有字符串都需要具有相同的长度(以便它们形成一个矩形),因此它们应该用额外的空格填充,直到它们的长度与最长的字符串一样长。for
循环遍历mapTextLines
中的每个字符串,并在找到新的最长字符串时更新maxWidth
。执行完此循环后,maxWidth
变量将设置为mapTextLines
中最长字符串的长度。
# Add spaces to the ends of the shorter rows. This # ensures the map will be rectangular. for i in range(len(mapTextLines)): mapTextLines[i] += ' ' * (maxWidth - len(mapTextLines[i]))
第 459 行的for
循环再次遍历mapTextLines
中的字符串,这次是为了添加足够的空格字符,以使每个字符串的长度与maxWidth
一样长。
# Convert mapTextLines to a map object. for x in range(len(mapTextLines[0])): mapObj.append([]) for y in range(len(mapTextLines)): for x in range(maxWidth): mapObj[x].append(mapTextLines[y][x])
mapTextLines
变量只存储一个字符串列表。(列表中的每个字符串代表一行,字符串中的每个字符代表不同列的字符。这就是为什么第 467 行的 Y 和 X 索引被颠倒,就像 Tetromino 游戏中的SHAPES
数据结构一样。)但是地图对象将是一个单字符字符串的列表的列表,以便mapObj[x][y]
引用 XY 坐标处的瓦片。第 463 行的for
循环为mapTextLines
中的每一列添加一个空列表到mapObj
中。
嵌套的for
循环在第 465 和 466 行将使用单个字符字符串填充这些列表,以表示地图上的每个瓦片。这创建了 Star Pusher 使用的地图对象。
# Loop through the spaces in the map and find the @, ., and $ # characters for the starting game state. startx = None # The x and y for the player's starting position starty = None goals = [] # list of (x, y) tuples for each goal. stars = [] # list of (x, y) for each star's starting position. for x in range(maxWidth): for y in range(len(mapObj[x])): if mapObj[x][y] in ('@', '+'): # '@' is player, '+' is player & goal startx = x starty = y if mapObj[x][y] in ('.', '+', '*'): # '.' is goal, '*' is star & goal goals.append((x, y)) if mapObj[x][y] in ('$', '*'): # '$' is star stars.append((x, y))
创建地图对象后,第 475 和 476 行的嵌套for
循环将遍历每个空格,以找到 XY 坐标的三个事物:
- 玩家的起始位置。这将存储在
startx
和starty
变量中,然后稍后在第 494 行存储在游戏状态对象中。 - 所有星星的起始位置将存储在
stars
列表中,该列表稍后将存储在第 496 行的游戏状态对象中。 - 所有目标的位置。这些将存储在
goals
列表中,稍后将在第 500 行存储在级别对象中。
请记住,游戏状态对象包含所有可能发生变化的事物。这就是为什么玩家的位置存储在其中(因为玩家可以四处移动),星星也存储在其中(因为玩家可以推动星星)。但是目标存储在级别对象中,因为它们永远不会移动。
# Basic level design sanity checks: assert startx != None and starty != None, 'Level %s (around line %s) in %s is missing a "@" or "+" to mark the start point.' % (levelNum+1, lineNum, filename) assert len(goals) > 0, 'Level %s (around line %s) in %s must have at least one goal.' % (levelNum+1, lineNum, filename) assert len(stars) >= len(goals), 'Level %s (around line %s) in %s is impossible to solve. It has %s goals but only %s stars.' % (levelNum+1, lineNum, filename, len(goals), len(stars))
此时,级别已经被读取并处理。为了确保这个级别能够正常工作,必须通过一些断言。如果这些断言的条件中有任何一个为False
,那么 Python 将产生一个错误(使用assert
语句中的字符串)来指出级别文件的问题。
第一条断言在第 489 行检查,以确保地图上某处列出了玩家的起点。第二条断言在第 490 行检查,以确保地图上至少有一个目标(或更多)。第 491 行的第三个断言检查,以确保每个目标至少有一个星星(但允许星星的数量多于目标)。
# Create level object and starting game state object. gameStateObj = {'player': (startx, starty), 'stepCounter': 0, 'stars': stars} levelObj = {'width': maxWidth, 'height': len(mapObj), 'mapObj': mapObj, 'goals': goals, 'startState': gameStateObj} levels.append(levelObj)
最后,这些对象被存储在游戏状态对象中,游戏状态对象本身存储在级别对象中。级别对象被添加到级别对象列表中的 503 行。当所有地图都被处理完毕时,readLevelsFile()
函数将返回这个levels
列表。
# Reset the variables for reading the next map. mapTextLines = [] mapObj = [] gameStateObj = {} levelNum += 1 return levels
现在这个级别已经处理完毕,mapTextLines
、mapObj
和gameStateObj
的变量应该被重置为空值,以便下一个级别从级别文件中读取。levelNum
变量也会增加 1,以便下一个级别的级别编号。
递归函数
在学习floodFill()
函数的工作原理之前,你需要了解递归。递归是一个简单的概念:递归函数就是调用自身的函数,就像下面程序中的函数一样:(不过不要在每行开头输入字母)
A. def passFortyTwoWhenYouCallThisFunction(param): B. print('Start of function.') C. if param != 42: D. print('You did not pass 42 when you called this function.') E. print('Fine. I will do it myself.') F. passFortyTwoWhenYouCallThisFunction(42) # this is the recursive call G. if param == 42: H. print('Thank you for passing 42 when you called this function.') I. print('End of function.') passFortyTwoWhenYouCallThisFunction(41)
(在你自己的程序中,不要让函数的名称像passFortyTwoWhenYouCallThisFunction()
那么长。我只是在愚蠢和傻里愚蠢。愚蠢。)
当你运行这个程序时,def
语句在 A 行执行时定义了函数。执行的下一行代码是 K 行,它调用passFortyTwoWhenYouCallThisFunction()
并传递(哇!)41
。结果,函数在 F 行调用自身并传递 42。我们称这个调用为递归调用。
这是我们程序的输出:
Start of function. You did not pass 42 when you called this function. Fine. I will do it myself. Start of function. Thank you for passing 42 when you called this function. End of function. End of function.
注意,“函数开始。”和“函数结束。”文本出现了两次。让我们弄清楚到底发生了什么,以及发生的顺序。
在 K 行,函数被调用并传递 41 作为参数。B 行打印出“函数开始”。C 行的条件将是True
(因为41 != 42
),所以 C 行和 D 行将打印出它们的消息。然后 F 行将递归调用函数并传递 42 作为参数。因此,执行再次从 B 行开始,并打印出“函数开始”。C 行的条件这次是False
,所以它跳到 G 行并发现条件为True
。这导致 H 行被调用并在屏幕上显示“谢谢……”。然后函数的最后一行,I 行,将执行打印出“函数结束”。函数返回到调用它的行。
但请记住,调用函数的代码行是 F 行。在这个原始调用中,参数被设置为41
。代码继续到 G 行并检查条件,这是False
(因为41 == 42
是False
),所以它跳过了 H 行的print()
调用。相反,它运行了 I 行的print()
调用,使“函数结束。”再次显示。
由于已经到达函数的末尾,它返回到调用此函数调用的代码行,这是 K 行。在 K 行之后没有更多的代码行,所以程序终止。
请注意,局部变量不仅仅是函数的局部变量,而是特定函数调用的局部变量。
堆栈溢出
每次调用函数时,Python 解释器都会记住是哪一行代码进行了调用。这样,当函数返回时,Python 就知道从哪里恢复执行。记住这一点会占用一点内存。这通常不是什么大问题,但看看这段代码:
def funky(): funky() funky()
如果您运行此程序,将会得到大量输出,看起来像这样:
... File "C:\test67.py", line 2, in funky funky() File "C:\test67.py", line 2, in funky funky() File "C:\test67.py", line 2, in funky funky() File "C:\test67.py", line 2, in funky funky() File "C:\test67.py", line 2, in funky funky() RuntimeError: maximum recursion depth exceeded
funky()
函数什么也不做,只是调用自身。然后在那个调用中,函数再次调用自身。然后再次调用自身,一次又一次。每次调用自身时,Python 都必须记住是哪一行代码发起了调用,以便在函数返回时可以在那里恢复执行。但funky()
函数永远不会返回,它只是不断地调用自身。
这就像无限循环错误一样,程序一直运行而不停止。为了防止内存耗尽,Python 将在调用深度达到 1000 次后引发错误并使程序崩溃。这种类型的错误称为堆栈溢出。
即使没有递归函数,这段代码也会导致堆栈溢出:
def spam(): eggs() def eggs(): spam() spam()
当您运行此程序时,会导致如下错误:
... File "C:\test67.py", line 2, in spam eggs() File "C:\test67.py", line 5, in eggs spam() File "C:\test67.py", line 2, in spam eggs() File "C:\test67.py", line 5, in eggs spam() File "C:\test67.py", line 2, in spam eggs() RuntimeError: maximum recursion depth exceeded
使用基本情况预防堆栈溢出
为了防止堆栈溢出错误,必须有一个基本情况,函数在那里停止进行新的递归调用。如果没有基本情况,那么函数调用将永远不会停止,最终会发生堆栈溢出。这是一个具有基本情况的递归函数的示例。基本情况是当 param 参数等于 2 时。
def fizz(param): print(param) if param == 2: return fizz(param - 1) fizz(5)
当您运行此程序时,输出将如下所示:
5 4 3 2
这个程序没有堆栈溢出错误,因为一旦 param 参数设置为2
,if
语句的条件将为True
,函数将返回,然后其余的调用也将依次返回。
尽管如果您的代码永远不会达到基本情况,那么这将导致堆栈溢出。如果我们将fizz(5)
调用更改为fizz(0)
,那么程序的输出将如下所示:
File "C:\rectest.py", line 5, in fizz fizz(param - 1) File "C:\rectest.py", line 5, in fizz fizz(param - 1) File "C:\rectest.py", line 5, in fizz fizz(param - 1) File "C:\rectest.py", line 2, in fizz print(param) RuntimeError: maximum recursion depth exceeded
递归调用和基本情况将用于执行泛洪填充算法,接下来将对其进行描述。
泛洪填充算法
泛洪填充算法用于在 Star Pusher 中将级别墙壁内部的所有地板瓷砖更改为使用“内部地板”瓷砖图像,而不是“外部地板”瓷砖(默认情况下地图上的所有瓷砖都是如此)。原始的floodFill()
调用在第 295 行。它将任何用’ ‘字符串表示的瓷砖(表示室外地板)转换为’o’`字符串(表示室内地板)。
def floodFill(mapObj, x, y, oldCharacter, newCharacter): """Changes any values matching oldCharacter on the map object to newCharacter at the (x, y) position, and does the same for the positions to the left, right, down, and up of (x, y), recursively.""" # In this game, the flood fill algorithm creates the inside/outside # floor distinction. This is a "recursive" function. # For more info on the Flood Fill algorithm, see: # http://en.wikipedia.org/wiki/Flood_fill if mapObj[x][y] == oldCharacter: mapObj[x][y] = newCharacter
第 522 和 523 行将传递给floodFill()
的 XY 坐标处的瓷砖转换为newCharacter
字符串,如果它最初与oldCharacter
字符串相同。
if x < len(mapObj) - 1 and mapObj[x+1][y] == oldCharacter: floodFill(mapObj, x+1, y, oldCharacter, newCharacter) # call right if x > 0 and mapObj[x-1][y] == oldCharacter: floodFill(mapObj, x-1, y, oldCharacter, newCharacter) # call left if y < len(mapObj[x]) - 1 and mapObj[x][y+1] == oldCharacter: floodFill(mapObj, x, y+1, oldCharacter, newCharacter) # call down if y > 0 and mapObj[x][y-1] == oldCharacter: floodFill(mapObj, x, y-1, oldCharacter, newCharacter) # call up
这四个if
语句检查 XY 坐标右侧、左侧、下方和上方的瓷砖是否与oldCharacter
相同,如果是,则对floodFill()
进行递归调用。
为了更好地理解floodFill()
函数的工作原理,这里有一个不使用递归调用,而是使用 XY 坐标列表来跟踪地图上应该被检查并可能更改为newCharacter
的空格的版本。
def floodFill(mapObj, x, y, oldCharacter, newCharacter): spacesToCheck = [] if mapObj[x][y] == oldCharacter: spacesToCheck.append((x, y)) while spacesToCheck != []: x, y = spacesToCheck.pop() mapObj[x][y] = newCharacter if x < len(mapObj) - 1 and mapObj[x+1][y] == oldCharacter: spacesToCheck.append((x+1, y)) # check right if x > 0 and mapObj[x-1][y] == oldCharacter: spacesToCheck.append((x-1, y)) # check left if y < len(mapObj[x]) - 1 and mapObj[x][y+1] == oldCharacter: spacesToCheck.append((x, y+1)) # check down if y > 0 and mapObj[x][y-1] == oldCharacter: spacesToCheck.append((x, y-1)) # check up
如果您想阅读一个更详细的关于递归的教程,以猫和僵尸为例,请访问invpy.com/recursivezombies
。
绘制地图
def drawMap(mapObj, gameStateObj, goals): """Draws the map to a Surface object, including the player and stars. This function does not call pygame.display.update(), nor does it draw the "Level" and "Steps" text in the corner.""" # mapSurf will be the single Surface object that the tiles are drawn # on, so that it is easy to position the entire map on the DISPLAYSURF # Surface object. First, the width and height must be calculated. mapSurfWidth = len(mapObj) * TILEWIDTH mapSurfHeight = (len(mapObj[0]) - 1) * (TILEHEIGHT - TILEFLOORHEIGHT) + TILEHEIGHT mapSurf = pygame.Surface((mapSurfWidth, mapSurfHeight)) mapSurf.fill(BGCOLOR) # start with a blank color on the surface.
drawMap()
函数将返回一个 Surface 对象,上面绘制了整个地图(以及玩家和星星)。需要从mapObj
计算出这个 Surface 所需的宽度和高度(在第 543 和 544 行完成)。在第 545 行创建了将绘制所有内容的 Surface 对象。首先,在第 546 行将整个 Surface 对象绘制为背景颜色。
# Draw the tile sprites onto this surface. for x in range(len(mapObj)): for y in range(len(mapObj[x])): spaceRect = pygame.Rect((x * TILEWIDTH, y * (TILEHEIGHT - TILEFLOORHEIGHT), TILEWIDTH, TILEHEIGHT))
第 549 和 550 行的嵌套for
循环将遍历地图上的每个可能的 XY 坐标,并在该位置绘制适当的瓷砖图像。
if mapObj[x][y] in TILEMAPPING: baseTile = TILEMAPPING[mapObj[x][y]] elif mapObj[x][y] in OUTSIDEDECOMAPPING: baseTile = TILEMAPPING[' '] # First draw the base ground/wall tile. mapSurf.blit(baseTile, spaceRect)
baseTile
变量设置为要在迭代当前 XY 坐标处绘制的瓷砖图像的 Surface 对象。如果单字符字符串在OUTSIDEDECOMAPPING
字典中,则将使用TILEMAPPING[' ']
(基本室外地板瓷砖的单字符字符串)。
if mapObj[x][y] in OUTSIDEDECOMAPPING: # Draw any tree/rock decorations that are on this tile. mapSurf.blit(OUTSIDEDECOMAPPING[mapObj[x][y]], spaceRect)
此外,如果瓷砖在OUTSIDEDECOMAPPING
字典中列出,相应的树木或岩石图像应该绘制在刚刚在该 XY 坐标处绘制的瓷砖上。
elif (x, y) in gameStateObj['stars']: if (x, y) in goals: # A goal AND star are on this space, draw goal first. mapSurf.blit(IMAGESDICT['covered goal'], spaceRect) # Then draw the star sprite. mapSurf.blit(IMAGESDICT['star'], spaceRect)
如果地图上的此 XY 坐标处有一个星星(可以通过检查gameStateObj['stars']
列表中的(x, y)
是否存在来找到),那么应该在此 XY 坐标处绘制一个星星(在第 568 行完成)。在绘制星星之前,代码应该首先检查此位置是否也有一个目标,如果是的话,应该先绘制“覆盖的目标”瓷砖。
elif (x, y) in goals: # Draw a goal without a star on it. mapSurf.blit(IMAGESDICT['uncovered goal'], spaceRect)
如果地图上的此 XY 坐标处有一个目标,那么“未覆盖的目标”应该绘制在瓷砖的顶部。绘制未覆盖的目标是因为如果执行已经到达第 569 行的elif
语句,我们知道第 563 行的elif
语句的条件为False
,并且在此 XY 坐标处也没有星星。
# Last draw the player on the board. if (x, y) == gameStateObj['player']: # Note: The value "currentImage" refers # to a key in "PLAYERIMAGES" which has the # specific player image we want to show. mapSurf.blit(PLAYERIMAGES[currentImage], spaceRect) return mapSurf
最后,drawMap()
函数检查玩家是否位于此 XY 坐标,如果是,则玩家的图像将覆盖在瓷砖上。第 580 行位于从第 549 行和 550 行开始的嵌套for
循环之外,因此在返回 Surface 对象时,整个地图已经绘制在上面。
检查关卡是否完成。
def isLevelFinished(levelObj, gameStateObj): """Returns True if all the goals have stars in them.""" for goal in levelObj['goals']: if goal not in gameStateObj['stars']: # Found a space with a goal but no star on it. return False return True
isLevelFinished()
函数在所有目标都被星星覆盖时返回True
。有些关卡可能有比目标更多的星星,因此重要的是检查所有目标是否被星星覆盖,而不是检查所有星星是否覆盖了目标。
第 585 行的for
循环遍历levelObj['goals']
中的目标(这是每个目标的 XY 坐标元组列表),并检查gameStateObj['stars']
列表中是否有相同的 XY 坐标的星星(not in
运算符在这里起作用,因为gameStateObj['stars']
是这些相同 XY 坐标的元组列表)。代码第一次发现一个没有星星的目标在相同的位置时,函数返回False
。
如果它通过了所有的目标并在每个目标上找到了一个星星,isLevelFinished()
返回True
。
def terminate(): pygame.quit() sys.exit()
这个terminate()
函数与之前的所有程序中的函数相同。
if __name__ == '__main__': main()
在定义了所有函数之后,调用第 602 行的main()
函数开始游戏。
总结
在松鼠吃松鼠游戏中,游戏世界非常简单:只是一个无限的绿色平原,上面随机散布着草图像。推星星游戏引入了新的东西:具有独特设计的具有瓷砖图形的关卡。为了将这些关卡以计算机可读的格式存储,它们被输入到文本文件中,并且程序中的代码读取这些文件并为关卡创建数据结构。
实际上,推星星程序不仅仅是一个简单的单一地图游戏,更像是一个基于关卡文件加载自定义地图的系统。通过修改关卡文件,我们可以改变游戏世界中墙壁、星星和目标出现的位置。推星星程序可以处理关卡文件设置的任何配置(只要通过确保地图合理的assert
语句)。
您甚至不需要知道如何编写 Python 代码来制作自己的关卡。修改starPusherLevels.txt文件的文本编辑程序是任何人都需要拥有自己的推星星游戏关卡编辑器的全部。
为了进行额外的编程练习,您可以从invpy.com/buggy/starpusher
下载推星星的有 bug 版本,并尝试找出如何修复这些 bug。
第十章:四个额外游戏
原文:
inventwithpython.com/pygame/chapter10.html
译者:飞龙
本章包括四个额外游戏的源代码。不幸的是,本章中只有源代码(包括注释),没有对代码的详细解释。到目前为止,您可以通过查看源代码和注释来玩这些游戏并弄清楚代码的工作原理。
这些游戏包括:
- Flippy – 一个“Othello”克隆,玩家试图翻转计算机 AI 玩家的方块。
- Ink Spill – 一个使用泛洪填充算法的“Flood It”克隆。
- Four in a Row – 一个“Connect Four”克隆,与计算机 AI 玩家对战。
- Gemgem – 一个“Bejeweled”克隆,玩家交换宝石以尝试获得三个相同的宝石排成一行。
如果您对本书中的源代码有任何疑问,请随时通过电子邮件联系作者[email protected]。
如果您想练习修复错误,这些程序的错误版本也是可用的:
Flippy,一个“Othello”克隆
Othello,也被称为 Reversi,是一个 8x8 的棋盘,棋子一面是黑色,另一面是白色。起始棋盘如图 10-1 所示。每个玩家轮流放置自己颜色的新方块。任何处于新方块和同色其他方块之间的对手方块都会被翻转。游戏的目标是尽可能多地拥有自己颜色的方块。例如,图 10-2 是当白方在 5, 6 处放置一个新的白色方块时的情况。
Reversi 游戏的起始棋盘上有两个白色方块和两个黑色方块。 | 白方放置一个新方块。 |
5, 5 处的黑色方块位于新的白色方块和已有的白色方块 5, 4 之间。该黑色方块被翻转并成为新的白色方块,使得棋盘看起来像图 10-3。黑方接下来也进行类似的移动,在 4, 6 处放置一个黑色方块,翻转了 5, 4 处的白色方块。这导致了一个看起来像图 10-4 的棋盘。
白方的移动将翻转黑方的一个方块。 | 黑方放置一个新方块,翻转白方的一个方块。 |
只要它们处于玩家新方块和已有方块之间,所有方向上的方块都会被翻转。在图 10-5 中,白方在 3, 6 处放置一个方块,并在两个方向上翻转了黑色方块(由线标记)。结果如图 10-6 所示。
白方在 3, 6 处的第二步将翻转两个黑方的方块。 | 白方的第二步后的棋盘。 |
正如您所看到的,每个玩家可以在一两步内迅速占据棋盘上大部分方块。玩家必须始终进行至少占据一个方块的移动。游戏在玩家无法进行移动或者棋盘完全填满时结束。拥有最多自己颜色方块的玩家获胜。
您可以从维基百科了解更多关于 Reversi 的信息:en.wikipedia.org/wiki/Reversi
这个游戏的文本版本使用print()
和input()
而不是 Pygame,出现在“用 Python 发明自己的计算机游戏”第 15 章。您可以阅读该章节了解计算机 AI 算法是如何组合的。inventwithpython.com/chapter15.html
这个游戏的电脑 AI 非常出色,因为计算机很容易模拟每一种可能的走法,并选择翻转最多瓷砖的走法。每当我玩的时候,它通常都会打败我。
Flippy 的源代码
此源代码可从invpy.com/flippy.py
下载。
Flippy 使用的图像文件可以从invpy.com/flippyimages.zip
下载。
# Flippy (an Othello or Reversi clone) # By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection) # http://inventwithpython.com/pygame # Released under a "Simplified BSD" license # Based on the "reversi.py" code that originally appeared in "Invent # Your Own Computer Games with Python", chapter 15: # http://inventwithpython.com/chapter15.html import random, sys, pygame, time, copy from pygame.locals import * FPS = 10 # frames per second to update the screen WINDOWWIDTH = 640 # width of the program's window, in pixels WINDOWHEIGHT = 480 # height in pixels SPACESIZE = 50 # width & height of each space on the board, in pixels BOARDWIDTH = 8 # how many columns of spaces on the game board BOARDHEIGHT = 8 # how many rows of spaces on the game board WHITE_TILE = 'WHITE_TILE' # an arbitrary but unique value BLACK_TILE = 'BLACK_TILE' # an arbitrary but unique value EMPTY_SPACE = 'EMPTY_SPACE' # an arbitrary but unique value HINT_TILE = 'HINT_TILE' # an arbitrary but unique value ANIMATIONSPEED = 25 # integer from 1 to 100, higher is faster animation # Amount of space on the left & right side (XMARGIN) or above and below # (YMARGIN) the game board, in pixels. XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * SPACESIZE)) / 2) YMARGIN = int((WINDOWHEIGHT - (BOARDHEIGHT * SPACESIZE)) / 2) # R G B WHITE = (255, 255, 255) BLACK = ( 0, 0, 0) GREEN = ( 0, 155, 0) BRIGHTBLUE = ( 0, 50, 255) BROWN = (174, 94, 0) TEXTBGCOLOR1 = BRIGHTBLUE TEXTBGCOLOR2 = GREEN GRIDLINECOLOR = BLACK TEXTCOLOR = WHITE HINTCOLOR = BROWN def main(): global MAINCLOCK, DISPLAYSURF, FONT, BIGFONT, BGIMAGE pygame.init() MAINCLOCK = pygame.time.Clock() DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) pygame.display.set_caption('Flippy') FONT = pygame.font.Font('freesansbold.ttf', 16) BIGFONT = pygame.font.Font('freesansbold.ttf', 32) # Set up the background image. boardImage = pygame.image.load('flippyboard.png') # Use smoothscale() to stretch the board image to fit the entire board: boardImage = pygame.transform.smoothscale(boardImage, (BOARDWIDTH * SPACESIZE, BOARDHEIGHT * SPACESIZE)) boardImageRect = boardImage.get_rect() boardImageRect.topleft = (XMARGIN, YMARGIN) BGIMAGE = pygame.image.load('flippybackground.png') # Use smoothscale() to stretch the background image to fit the entire window: BGIMAGE = pygame.transform.smoothscale(BGIMAGE, (WINDOWWIDTH, WINDOWHEIGHT)) BGIMAGE.blit(boardImage, boardImageRect) # Run the main game. while True: if runGame() == False: break def runGame(): # Plays a single game of reversi each time this function is called. # Reset the board and game. mainBoard = getNewBoard() resetBoard(mainBoard) showHints = False turn = random.choice(['computer', 'player']) # Draw the starting board and ask the player what color they want. drawBoard(mainBoard) playerTile, computerTile = enterPlayerTile() # Make the Surface and Rect objects for the "New Game" and "Hints" buttons newGameSurf = FONT.render('New Game', True, TEXTCOLOR, TEXTBGCOLOR2) newGameRect = newGameSurf.get_rect() newGameRect.topright = (WINDOWWIDTH - 8, 10) hintsSurf = FONT.render('Hints', True, TEXTCOLOR, TEXTBGCOLOR2) hintsRect = hintsSurf.get_rect() hintsRect.topright = (WINDOWWIDTH - 8, 40) while True: # main game loop # Keep looping for player and computer's turns. if turn == 'player': # Player's turn: if getValidMoves(mainBoard, playerTile) == []: # If it's the player's turn but they # can't move, then end the game. break movexy = None while movexy == None: # Keep looping until the player clicks on a valid space. # Determine which board data structure to use for display. if showHints: boardToDraw = getBoardWithValidMoves(mainBoard, playerTile) else: boardToDraw = mainBoard checkForQuit() for event in pygame.event.get(): # event handling loop if event.type == MOUSEBUTTONUP: # Handle mouse click events mousex, mousey = event.pos if newGameRect.collidepoint( (mousex, mousey) ): # Start a new game return True elif hintsRect.collidepoint( (mousex, mousey) ): # Toggle hints mode showHints = not showHints # movexy is set to a two-item tuple XY coordinate, or None value movexy = getSpaceClicked(mousex, mousey) if movexy != None and not isValidMove(mainBoard, playerTile, movexy[0], movexy[1]): movexy = None # Draw the game board. drawBoard(boardToDraw) drawInfo(boardToDraw, playerTile, computerTile, turn) # Draw the "New Game" and "Hints" buttons. DISPLAYSURF.blit(newGameSurf, newGameRect) DISPLAYSURF.blit(hintsSurf, hintsRect) MAINCLOCK.tick(FPS) pygame.display.update() # Make the move and end the turn. makeMove(mainBoard, playerTile, movexy[0], movexy[1], True) if getValidMoves(mainBoard, computerTile) != []: # Only set for the computer's turn if it can make a move. turn = 'computer' else: # Computer's turn: if getValidMoves(mainBoard, computerTile) == []: # If it was set to be the computer's turn but # they can't move, then end the game. break # Draw the board. drawBoard(mainBoard) drawInfo(mainBoard, playerTile, computerTile, turn) # Draw the "New Game" and "Hints" buttons. DISPLAYSURF.blit(newGameSurf, newGameRect) DISPLAYSURF.blit(hintsSurf, hintsRect) # Make it look like the computer is thinking by pausing a bit. pauseUntil = time.time() + random.randint(5, 15) * 0.1 while time.time() < pauseUntil: pygame.display.update() # Make the move and end the turn. x, y = getComputerMove(mainBoard, computerTile) makeMove(mainBoard, computerTile, x, y, True) if getValidMoves(mainBoard, playerTile) != []: # Only set for the player's turn if they can make a move. turn = 'player' # Display the final score. drawBoard(mainBoard) scores = getScoreOfBoard(mainBoard) # Determine the text of the message to display. if scores[playerTile] > scores[computerTile]: text = 'You beat the computer by %s points! Congratulations!' % \ (scores[playerTile] - scores[computerTile]) elif scores[playerTile] < scores[computerTile]: text = 'You lost. The computer beat you by %s points.' % \ (scores[computerTile] - scores[playerTile]) else: text = 'The game was a tie!' textSurf = FONT.render(text, True, TEXTCOLOR, TEXTBGCOLOR1) textRect = textSurf.get_rect() textRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2)) DISPLAYSURF.blit(textSurf, textRect) # Display the "Play again?" text with Yes and No buttons. text2Surf = BIGFONT.render('Play again?', True, TEXTCOLOR, TEXTBGCOLOR1) text2Rect = text2Surf.get_rect() text2Rect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2) + 50) # Make "Yes" button. yesSurf = BIGFONT.render('Yes', True, TEXTCOLOR, TEXTBGCOLOR1) yesRect = yesSurf.get_rect() yesRect.center = (int(WINDOWWIDTH / 2) - 60, int(WINDOWHEIGHT / 2) + 90) # Make "No" button. noSurf = BIGFONT.render('No', True, TEXTCOLOR, TEXTBGCOLOR1) noRect = noSurf.get_rect() noRect.center = (int(WINDOWWIDTH / 2) + 60, int(WINDOWHEIGHT / 2) + 90) while True: # Process events until the user clicks on Yes or No. checkForQuit() for event in pygame.event.get(): # event handling loop if event.type == MOUSEBUTTONUP: mousex, mousey = event.pos if yesRect.collidepoint( (mousex, mousey) ): return True elif noRect.collidepoint( (mousex, mousey) ): return False DISPLAYSURF.blit(textSurf, textRect) DISPLAYSURF.blit(text2Surf, text2Rect) DISPLAYSURF.blit(yesSurf, yesRect) DISPLAYSURF.blit(noSurf, noRect) pygame.display.update() MAINCLOCK.tick(FPS) def translateBoardToPixelCoord(x, y): return XMARGIN + x * SPACESIZE + int(SPACESIZE / 2), YMARGIN + y * SPACESIZE + int(SPACESIZE / 2) def animateTileChange(tilesToFlip, tileColor, additionalTile): # Draw the additional tile that was just laid down. (Otherwise we'd # have to completely redraw the board & the board info.) if tileColor == WHITE_TILE: additionalTileColor = WHITE else: additionalTileColor = BLACK additionalTileX, additionalTileY = translateBoardToPixelCoord(additionalTile[0], additionalTile[1]) pygame.draw.circle(DISPLAYSURF, additionalTileColor, (additionalTileX, additionalTileY), int(SPACESIZE / 2) - 4) pygame.display.update() for rgbValues in range(0, 255, int(ANIMATIONSPEED * 2.55)): if rgbValues > 255: rgbValues = 255 elif rgbValues < 0: rgbValues = 0 if tileColor == WHITE_TILE: color = tuple([rgbValues] * 3) # rgbValues goes from 0 to 255 elif tileColor == BLACK_TILE: color = tuple([255 - rgbValues] * 3) # rgbValues goes from 255 to 0 for x, y in tilesToFlip: centerx, centery = translateBoardToPixelCoord(x, y) pygame.draw.circle(DISPLAYSURF, color, (centerx, centery), int(SPACESIZE / 2) - 4) pygame.display.update() MAINCLOCK.tick(FPS) checkForQuit() def drawBoard(board): # Draw background of board. DISPLAYSURF.blit(BGIMAGE, BGIMAGE.get_rect()) # Draw grid lines of the board. for x in range(BOARDWIDTH + 1): # Draw the horizontal lines. startx = (x * SPACESIZE) + XMARGIN starty = YMARGIN endx = (x * SPACESIZE) + XMARGIN endy = YMARGIN + (BOARDHEIGHT * SPACESIZE) pygame.draw.line(DISPLAYSURF, GRIDLINECOLOR, (startx, starty), (endx, endy)) for y in range(BOARDHEIGHT + 1): # Draw the vertical lines. startx = XMARGIN starty = (y * SPACESIZE) + YMARGIN endx = XMARGIN + (BOARDWIDTH * SPACESIZE) endy = (y * SPACESIZE) + YMARGIN pygame.draw.line(DISPLAYSURF, GRIDLINECOLOR, (startx, starty), (endx, endy)) # Draw the black & white tiles or hint spots. for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): centerx, centery = translateBoardToPixelCoord(x, y) if board[x][y] == WHITE_TILE or board[x][y] == BLACK_TILE: if board[x][y] == WHITE_TILE: tileColor = WHITE else: tileColor = BLACK pygame.draw.circle(DISPLAYSURF, tileColor, (centerx, centery), int(SPACESIZE / 2) - 4) if board[x][y] == HINT_TILE: pygame.draw.rect(DISPLAYSURF, HINTCOLOR, (centerx - 4, centery - 4, 8, 8)) def getSpaceClicked(mousex, mousey): # Return a tuple of two integers of the board space coordinates where # the mouse was clicked. (Or returns None not in any space.) for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): if mousex > x * SPACESIZE + XMARGIN and \ mousex < (x + 1) * SPACESIZE + XMARGIN and \ mousey > y * SPACESIZE + YMARGIN and \ mousey < (y + 1) * SPACESIZE + YMARGIN: return (x, y) return None def drawInfo(board, playerTile, computerTile, turn): # Draws scores and whose turn it is at the bottom of the screen. scores = getScoreOfBoard(board) scoreSurf = FONT.render("Player Score: %s Computer Score: %s %s's Turn" % (str(scores[playerTile]), str(scores[computerTile]), turn.title()), True, TEXTCOLOR) scoreRect = scoreSurf.get_rect() scoreRect.bottomleft = (10, WINDOWHEIGHT - 5) DISPLAYSURF.blit(scoreSurf, scoreRect) def resetBoard(board): # Blanks out the board it is passed, and sets up starting tiles. for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): board[x][y] = EMPTY_SPACE # Add starting pieces to the center board[3][3] = WHITE_TILE board[3][4] = BLACK_TILE board[4][3] = BLACK_TILE board[4][4] = WHITE_TILE def getNewBoard(): # Creates a brand new, empty board data structure. board = [] for i in range(BOARDWIDTH): board.append([EMPTY_SPACE] * BOARDHEIGHT) return board def isValidMove(board, tile, xstart, ystart): # Returns False if the player's move is invalid. If it is a valid # move, returns a list of spaces of the captured pieces. if board[xstart][ystart] != EMPTY_SPACE or not isOnBoard(xstart, ystart): return False board[xstart][ystart] = tile # temporarily set the tile on the board. if tile == WHITE_TILE: otherTile = BLACK_TILE else: otherTile = WHITE_TILE tilesToFlip = [] # check each of the eight directions: for xdirection, ydirection in [[0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1]]: x, y = xstart, ystart x += xdirection y += ydirection if isOnBoard(x, y) and board[x][y] == otherTile: # The piece belongs to the other player next to our piece. x += xdirection y += ydirection if not isOnBoard(x, y): continue while board[x][y] == otherTile: x += xdirection y += ydirection if not isOnBoard(x, y): break # break out of while loop, continue in for loop if not isOnBoard(x, y): continue if board[x][y] == tile: # There are pieces to flip over. Go in the reverse # direction until we reach the original space, noting all # the tiles along the way. while True: x -= xdirection y -= ydirection if x == xstart and y == ystart: break tilesToFlip.append([x, y]) board[xstart][ystart] = EMPTY_SPACE # make space empty if len(tilesToFlip) == 0: # If no tiles flipped, this move is invalid return False return tilesToFlip def isOnBoard(x, y): # Returns True if the coordinates are located on the board. return x >= 0 and x < BOARDWIDTH and y >= 0 and y < BOARDHEIGHT def getBoardWithValidMoves(board, tile): # Returns a new board with hint markings. dupeBoard = copy.deepcopy(board) for x, y in getValidMoves(dupeBoard, tile): dupeBoard[x][y] = HINT_TILE return dupeBoard def getValidMoves(board, tile): # Returns a list of (x,y) tuples of all valid moves. validMoves = [] for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): if isValidMove(board, tile, x, y) != False: validMoves.append((x, y)) return validMoves def getScoreOfBoard(board): # Determine the score by counting the tiles. xscore = 0 oscore = 0 for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): if board[x][y] == WHITE_TILE: xscore += 1 if board[x][y] == BLACK_TILE: oscore += 1 return {WHITE_TILE:xscore, BLACK_TILE:oscore} def enterPlayerTile(): # Draws the text and handles the mouse click events for letting # the player choose which color they want to be. Returns # [WHITE_TILE, BLACK_TILE] if the player chooses to be White, # [BLACK_TILE, WHITE_TILE] if Black. # Create the text. textSurf = FONT.render('Do you want to be white or black?', True, TEXTCOLOR, TEXTBGCOLOR1) textRect = textSurf.get_rect() textRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2)) xSurf = BIGFONT.render('White', True, TEXTCOLOR, TEXTBGCOLOR1) xRect = xSurf.get_rect() xRect.center = (int(WINDOWWIDTH / 2) - 60, int(WINDOWHEIGHT / 2) + 40) oSurf = BIGFONT.render('Black', True, TEXTCOLOR, TEXTBGCOLOR1) oRect = oSurf.get_rect() oRect.center = (int(WINDOWWIDTH / 2) + 60, int(WINDOWHEIGHT / 2) + 40) while True: # Keep looping until the player has clicked on a color. checkForQuit() for event in pygame.event.get(): # event handling loop if event.type == MOUSEBUTTONUP: mousex, mousey = event.pos if xRect.collidepoint( (mousex, mousey) ): return [WHITE_TILE, BLACK_TILE] elif oRect.collidepoint( (mousex, mousey) ): return [BLACK_TILE, WHITE_TILE] # Draw the screen. DISPLAYSURF.blit(textSurf, textRect) DISPLAYSURF.blit(xSurf, xRect) DISPLAYSURF.blit(oSurf, oRect) pygame.display.update() MAINCLOCK.tick(FPS) def makeMove(board, tile, xstart, ystart, realMove=False): # Place the tile on the board at xstart, ystart, and flip tiles # Returns False if this is an invalid move, True if it is valid. tilesToFlip = isValidMove(board, tile, xstart, ystart) if tilesToFlip == False: return False board[xstart][ystart] = tile if realMove: animateTileChange(tilesToFlip, tile, (xstart, ystart)) for x, y in tilesToFlip: board[x][y] = tile return True def isOnCorner(x, y): # Returns True if the position is in one of the four corners. return (x == 0 and y == 0) or \ (x == BOARDWIDTH and y == 0) or \ (x == 0 and y == BOARDHEIGHT) or \ (x == BOARDWIDTH and y == BOARDHEIGHT) def getComputerMove(board, computerTile): # Given a board and the computer's tile, determine where to # move and return that move as a [x, y] list. possibleMoves = getValidMoves(board, computerTile) # randomize the order of the possible moves random.shuffle(possibleMoves) # always go for a corner if available. for x, y in possibleMoves: if isOnCorner(x, y): return [x, y] # Go through all possible moves and remember the best scoring move bestScore = -1 for x, y in possibleMoves: dupeBoard = copy.deepcopy(board) makeMove(dupeBoard, computerTile, x, y) score = getScoreOfBoard(dupeBoard)[computerTile] if score > bestScore: bestMove = [x, y] bestScore = score return bestMove def checkForQuit(): for event in pygame.event.get((QUIT, KEYUP)): # event handling loop if event.type == QUIT or (event.type == KEYUP and event.key == K_ESCAPE): pygame.quit() sys.exit() if __name__ == '__main__': main()
Ink Spill,一个“Flood It”克隆
游戏“Flood It”从一个填满彩色瓷砖的棋盘开始。在每一轮中,玩家选择一个新颜色来涂抹左上角的瓷砖,以及相邻的相同颜色的瓷砖。这个游戏使用了泛洪填充算法(在 Star Pusher 章节中有描述)。游戏的目标是在用完所有回合之前将整个棋盘变成单一颜色。
这个游戏还有一个设置屏幕,玩家可以更改棋盘的大小和游戏的难度。如果玩家对颜色感到厌倦,他们还可以切换到其他几种颜色方案。
Ink Spill 的源代码
此源代码可从invpy.com/inkspill.py
下载。
Flippy 使用的图像文件可以从invpy.com/inkspillimages.zip
下载。
# Ink Spill (a Flood It clone) # http://inventwithpython.com/pygame # By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection) # Released under a "Simplified BSD" license import random, sys, webbrowser, copy, pygame from pygame.locals import * # There are different box sizes, number of boxes, and # life depending on the "board size" setting selected. SMALLBOXSIZE = 60 # size is in pixels MEDIUMBOXSIZE = 20 LARGEBOXSIZE = 11 SMALLBOARDSIZE = 6 # size is in boxes MEDIUMBOARDSIZE = 17 LARGEBOARDSIZE = 30 SMALLMAXLIFE = 10 # number of turns MEDIUMMAXLIFE = 30 LARGEMAXLIFE = 64 FPS = 30 WINDOWWIDTH = 640 WINDOWHEIGHT = 480 boxSize = MEDIUMBOXSIZE PALETTEGAPSIZE = 10 PALETTESIZE = 45 EASY = 0 # arbitrary but unique value MEDIUM = 1 # arbitrary but unique value HARD = 2 # arbitrary but unique value difficulty = MEDIUM # game starts in "medium" mode maxLife = MEDIUMMAXLIFE boardWidth = MEDIUMBOARDSIZE boardHeight = MEDIUMBOARDSIZE # R G B WHITE = (255, 255, 255) DARKGRAY = ( 70, 70, 70) BLACK = ( 0, 0, 0) RED = (255, 0, 0) GREEN = ( 0, 255, 0) BLUE = ( 0, 0, 255) YELLOW = (255, 255, 0) ORANGE = (255, 128, 0) PURPLE = (255, 0, 255) # The first color in each scheme is the background color, the next six are the palette colors. COLORSCHEMES = (((150, 200, 255), RED, GREEN, BLUE, YELLOW, ORANGE, PURPLE), ((0, 155, 104), (97, 215, 164), (228, 0, 69), (0, 125, 50), (204, 246, 0), (148, 0, 45), (241, 109, 149)), ((195, 179, 0), (255, 239, 115), (255, 226, 0), (147, 3, 167), (24, 38, 176), (166, 147, 0), (197, 97, 211)), ((85, 0, 0), (155, 39, 102), (0, 201, 13), (255, 118, 0), (206, 0, 113), (0, 130, 9), (255, 180, 115)), ((191, 159, 64), (183, 182, 208), (4, 31, 183), (167, 184, 45), (122, 128, 212), (37, 204, 7), (88, 155, 213)), ((200, 33, 205), (116, 252, 185), (68, 56, 56), (52, 238, 83), (23, 149, 195), (222, 157, 227), (212, 86, 185))) for i in range(len(COLORSCHEMES)): assert len(COLORSCHEMES[i]) == 7, 'Color scheme %s does not have exactly 7 colors.' % (i) bgColor = COLORSCHEMES[0][0] paletteColors = COLORSCHEMES[0][1:] def main(): global FPSCLOCK, DISPLAYSURF, LOGOIMAGE, SPOTIMAGE, SETTINGSIMAGE, SETTINGSBUTTONIMAGE, RESETBUTTONIMAGE pygame.init() FPSCLOCK = pygame.time.Clock() DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) # Load images LOGOIMAGE = pygame.image.load('inkspilllogo.png') SPOTIMAGE = pygame.image.load('inkspillspot.png') SETTINGSIMAGE = pygame.image.load('inkspillsettings.png') SETTINGSBUTTONIMAGE = pygame.image.load('inkspillsettingsbutton.png') RESETBUTTONIMAGE = pygame.image.load('inkspillresetbutton.png') pygame.display.set_caption('Ink Spill') mousex = 0 mousey = 0 mainBoard = generateRandomBoard(boardWidth, boardHeight, difficulty) life = maxLife lastPaletteClicked = None while True: # main game loop paletteClicked = None resetGame = False # Draw the screen. DISPLAYSURF.fill(bgColor) drawLogoAndButtons() drawBoard(mainBoard) drawLifeMeter(life) drawPalettes() checkForQuit() for event in pygame.event.get(): # event handling loop if event.type == MOUSEBUTTONUP: mousex, mousey = event.pos if pygame.Rect(WINDOWWIDTH - SETTINGSBUTTONIMAGE.get_width(), WINDOWHEIGHT - SETTINGSBUTTONIMAGE.get_height(), SETTINGSBUTTONIMAGE.get_width(), SETTINGSBUTTONIMAGE.get_height()).collidepoint(mousex, mousey): resetGame = showSettingsScreen() # clicked on Settings button elif pygame.Rect(WINDOWWIDTH - RESETBUTTONIMAGE.get_width(), WINDOWHEIGHT - SETTINGSBUTTONIMAGE.get_height() - RESETBUTTONIMAGE.get_height(), RESETBUTTONIMAGE.get_width(), RESETBUTTONIMAGE.get_height()).collidepoint(mousex, mousey): resetGame = True # clicked on Reset button else: # check if a palette button was clicked paletteClicked = getColorOfPaletteAt(mousex, mousey) if paletteClicked != None and paletteClicked != lastPaletteClicked: # a palette button was clicked that is different from the # last palette button clicked (this check prevents the player # from accidentally clicking the same palette twice) lastPaletteClicked = paletteClicked floodAnimation(mainBoard, paletteClicked) life -= 1 resetGame = False if hasWon(mainBoard): for i in range(4): # flash border 4 times flashBorderAnimation(WHITE, mainBoard) resetGame = True pygame.time.wait(2000) # pause so the player can bask in victory elif life == 0: # life is zero, so player has lost drawLifeMeter(0) pygame.display.update() pygame.time.wait(400) for i in range(4): flashBorderAnimation(BLACK, mainBoard) resetGame = True pygame.time.wait(2000) # pause so the player can suffer in their defeat if resetGame: # start a new game mainBoard = generateRandomBoard(boardWidth, boardHeight, difficulty) life = maxLife lastPaletteClicked = None pygame.display.update() FPSCLOCK.tick(FPS) def checkForQuit(): # Terminates the program if there are any QUIT or escape key events. for event in pygame.event.get(QUIT): # get all the QUIT events pygame.quit() # terminate if any QUIT events are present sys.exit() for event in pygame.event.get(KEYUP): # get all the KEYUP events if event.key == K_ESCAPE: pygame.quit() # terminate if the KEYUP event was for the Esc sys.exit() pygame.event.post(event) # put the other KEYUP event objects back def hasWon(board): # if the entire board is the same color, player has won for x in range(boardWidth): for y in range(boardHeight): if board[x][y] != board[0][0]: return False # found a different color, player has not won return True def showSettingsScreen(): global difficulty, boxSize, boardWidth, boardHeight, maxLife, paletteColors, bgColor # The pixel coordinates in this function were obtained by loading # the inkspillsettings.png image into a graphics editor and reading # the pixel coordinates from there. Handy trick. origDifficulty = difficulty origBoxSize = boxSize screenNeedsRedraw = True while True: if screenNeedsRedraw: DISPLAYSURF.fill(bgColor) DISPLAYSURF.blit(SETTINGSIMAGE, (0,0)) # place the ink spot marker next to the selected difficulty if difficulty == EASY: DISPLAYSURF.blit(SPOTIMAGE, (30, 4)) if difficulty == MEDIUM: DISPLAYSURF.blit(SPOTIMAGE, (8, 41)) if difficulty == HARD: DISPLAYSURF.blit(SPOTIMAGE, (30, 76)) # place the ink spot marker next to the selected size if boxSize == SMALLBOXSIZE: DISPLAYSURF.blit(SPOTIMAGE, (22, 150)) if boxSize == MEDIUMBOXSIZE: DISPLAYSURF.blit(SPOTIMAGE, (11, 185)) if boxSize == LARGEBOXSIZE: DISPLAYSURF.blit(SPOTIMAGE, (24, 220)) for i in range(len(COLORSCHEMES)): drawColorSchemeBoxes(500, i * 60 + 30, i) pygame.display.update() screenNeedsRedraw = False # by default, don't redraw the screen for event in pygame.event.get(): # event handling loop if event.type == QUIT: pygame.quit() sys.exit() elif event.type == KEYUP: if event.key == K_ESCAPE: # Esc key on settings screen goes back to game return not (origDifficulty == difficulty and origBoxSize == boxSize) elif event.type == MOUSEBUTTONUP: screenNeedsRedraw = True # screen should be redrawn mousex, mousey = event.pos # syntactic sugar # check for clicks on the difficulty buttons if pygame.Rect(74, 16, 111, 30).collidepoint(mousex, mousey): difficulty = EASY elif pygame.Rect(53, 50, 104, 29).collidepoint(mousex, mousey): difficulty = MEDIUM elif pygame.Rect(72, 85, 65, 31).collidepoint(mousex, mousey): difficulty = HARD # check for clicks on the size buttons elif pygame.Rect(63, 156, 84, 31).collidepoint(mousex, mousey): # small board size setting: boxSize = SMALLBOXSIZE boardWidth = SMALLBOARDSIZE boardHeight = SMALLBOARDSIZE maxLife = SMALLMAXLIFE elif pygame.Rect(52, 192, 106,32).collidepoint(mousex, mousey): # medium board size setting: boxSize = MEDIUMBOXSIZE boardWidth = MEDIUMBOARDSIZE boardHeight = MEDIUMBOARDSIZE maxLife = MEDIUMMAXLIFE elif pygame.Rect(67, 228, 58, 37).collidepoint(mousex, mousey): # large board size setting: boxSize = LARGEBOXSIZE boardWidth = LARGEBOARDSIZE boardHeight = LARGEBOARDSIZE maxLife = LARGEMAXLIFE elif pygame.Rect(14, 299, 371, 97).collidepoint(mousex, mousey): # clicked on the "learn programming" ad webbrowser.open('http://inventwithpython.com') # opens a web browser elif pygame.Rect(178, 418, 215, 34).collidepoint(mousex, mousey): # clicked on the "back to game" button return not (origDifficulty == difficulty and origBoxSize == boxSize) for i in range(len(COLORSCHEMES)): # clicked on a color scheme button if pygame.Rect(500, 30 + i * 60, MEDIUMBOXSIZE * 3, MEDIUMBOXSIZE * 2).collidepoint(mousex, mousey): bgColor = COLORSCHEMES[i][0] paletteColors = COLORSCHEMES[i][1:] def drawColorSchemeBoxes(x, y, schemeNum): # Draws the color scheme boxes that appear on the "Settings" screen. for boxy in range(2): for boxx in range(3): pygame.draw.rect(DISPLAYSURF, COLORSCHEMES[schemeNum][3 * boxy + boxx + 1], (x + MEDIUMBOXSIZE * boxx, y + MEDIUMBOXSIZE * boxy, MEDIUMBOXSIZE, MEDIUMBOXSIZE)) if paletteColors == COLORSCHEMES[schemeNum][1:]: # put the ink spot next to the selected color scheme DISPLAYSURF.blit(SPOTIMAGE, (x - 50, y)) def flashBorderAnimation(color, board, animationSpeed=30): origSurf = DISPLAYSURF.copy() flashSurf = pygame.Surface(DISPLAYSURF.get_size()) flashSurf = flashSurf.convert_alpha() for start, end, step in ((0, 256, 1), (255, 0, -1)): # the first iteration on the outer loop will set the inner loop # to have transparency go from 0 to 255, the second iteration will # have it go from 255 to 0\. This is the "flash". for transparency in range(start, end, animationSpeed * step): DISPLAYSURF.blit(origSurf, (0, 0)) r, g, b = color flashSurf.fill((r, g, b, transparency)) DISPLAYSURF.blit(flashSurf, (0, 0)) drawBoard(board) # draw board ON TOP OF the transparency layer pygame.display.update() FPSCLOCK.tick(FPS) DISPLAYSURF.blit(origSurf, (0, 0)) # redraw the original surface def floodAnimation(board, paletteClicked, animationSpeed=25): origBoard = copy.deepcopy(board) floodFill(board, board[0][0], paletteClicked, 0, 0) for transparency in range(0, 255, animationSpeed): # The "new" board slowly become opaque over the original board. drawBoard(origBoard) drawBoard(board, transparency) pygame.display.update() FPSCLOCK.tick(FPS) def generateRandomBoard(width, height, difficulty=MEDIUM): # Creates a board data structure with random colors for each box. board = [] for x in range(width): column = [] for y in range(height): column.append(random.randint(0, len(paletteColors) - 1)) board.append(column) # Make board easier by setting some boxes to same color as a neighbor. # Determine how many boxes to change. if difficulty == EASY: if boxSize == SMALLBOXSIZE: boxesToChange = 100 else: boxesToChange = 1500 elif difficulty == MEDIUM: if boxSize == SMALLBOXSIZE: boxesToChange = 5 else: boxesToChange = 200 else: boxesToChange = 0 # Change neighbor's colors: for i in range(boxesToChange): # Randomly choose a box whose color to copy x = random.randint(1, width-2) y = random.randint(1, height-2) # Randomly choose neighbors to change. direction = random.randint(0, 3) if direction == 0: # change left and up neighbor board[x-1][y] = board[x][y] board[x][y-1] = board[x][y] elif direction == 1: # change right and down neighbor board[x+1][y] = board[x][y] board[x][y+1] = board[x][y] elif direction == 2: # change right and up neighbor board[x][y-1] = board[x][y] board[x+1][y] = board[x][y] else: # change left and down neighbor board[x][y+1] = board[x][y] board[x-1][y] = board[x][y] return board def drawLogoAndButtons(): # draw the Ink Spill logo and Settings and Reset buttons. DISPLAYSURF.blit(LOGOIMAGE, (WINDOWWIDTH - LOGOIMAGE.get_width(), 0)) DISPLAYSURF.blit(SETTINGSBUTTONIMAGE, (WINDOWWIDTH - SETTINGSBUTTONIMAGE.get_width(), WINDOWHEIGHT - SETTINGSBUTTONIMAGE.get_height())) DISPLAYSURF.blit(RESETBUTTONIMAGE, (WINDOWWIDTH - RESETBUTTONIMAGE.get_width(), WINDOWHEIGHT - SETTINGSBUTTONIMAGE.get_height() - RESETBUTTONIMAGE.get_height())) def drawBoard(board, transparency=255): # The colored squares are drawn to a temporary surface which is then # drawn to the DISPLAYSURF surface. This is done so we can draw the # squares with transparency on top of DISPLAYSURF as it currently is. tempSurf = pygame.Surface(DISPLAYSURF.get_size()) tempSurf = tempSurf.convert_alpha() tempSurf.fill((0, 0, 0, 0)) for x in range(boardWidth): for y in range(boardHeight): left, top = leftTopPixelCoordOfBox(x, y) r, g, b = paletteColors[board[x][y]] pygame.draw.rect(tempSurf, (r, g, b, transparency), (left, top, boxSize, boxSize)) left, top = leftTopPixelCoordOfBox(0, 0) pygame.draw.rect(tempSurf, BLACK, (left-1, top-1, boxSize * boardWidth + 1, boxSize * boardHeight + 1), 1) DISPLAYSURF.blit(tempSurf, (0, 0)) def drawPalettes(): # Draws the six color palettes at the bottom of the screen. numColors = len(paletteColors) xmargin = int((WINDOWWIDTH - ((PALETTESIZE * numColors) + (PALETTEGAPSIZE * (numColors - 1)))) / 2) for i in range(numColors): left = xmargin + (i * PALETTESIZE) + (i * PALETTEGAPSIZE) top = WINDOWHEIGHT - PALETTESIZE - 10 pygame.draw.rect(DISPLAYSURF, paletteColors[i], (left, top, PALETTESIZE, PALETTESIZE)) pygame.draw.rect(DISPLAYSURF, bgColor, (left + 2, top + 2, PALETTESIZE - 4, PALETTESIZE - 4), 2) def drawLifeMeter(currentLife): lifeBoxSize = int((WINDOWHEIGHT - 40) / maxLife) # Draw background color of life meter. pygame.draw.rect(DISPLAYSURF, bgColor, (20, 20, 20, 20 + (maxLife * lifeBoxSize))) for i in range(maxLife): if currentLife >= (maxLife - i): # draw a solid red box pygame.draw.rect(DISPLAYSURF, RED, (20, 20 + (i * lifeBoxSize), 20, lifeBoxSize)) pygame.draw.rect(DISPLAYSURF, WHITE, (20, 20 + (i * lifeBoxSize), 20, lifeBoxSize), 1) # draw white outline def getColorOfPaletteAt(x, y): # Returns the index of the color in paletteColors that the x and y parameters # are over. Returns None if x and y are not over any palette. numColors = len(paletteColors) xmargin = int((WINDOWWIDTH - ((PALETTESIZE * numColors) + (PALETTEGAPSIZE * (numColors - 1)))) / 2) top = WINDOWHEIGHT - PALETTESIZE - 10 for i in range(numColors): # Find out if the mouse click is inside any of the palettes. left = xmargin + (i * PALETTESIZE) + (i * PALETTEGAPSIZE) r = pygame.Rect(left, top, PALETTESIZE, PALETTESIZE) if r.collidepoint(x, y): return i return None # no palette exists at these x, y coordinates def floodFill(board, oldColor, newColor, x, y): # This is the flood fill algorithm. if oldColor == newColor or board[x][y] != oldColor: return board[x][y] = newColor # change the color of the current box # Make the recursive call for any neighboring boxes: if x > 0: floodFill(board, oldColor, newColor, x - 1, y) # on box to the left if x < boardWidth - 1: floodFill(board, oldColor, newColor, x + 1, y) # on box to the right if y > 0: floodFill(board, oldColor, newColor, x, y - 1) # on box to up if y < boardHeight - 1: floodFill(board, oldColor, newColor, x, y + 1) # on box to down def leftTopPixelCoordOfBox(boxx, boxy): # Returns the x and y of the left-topmost pixel of the xth & yth box. xmargin = int((WINDOWWIDTH - (boardWidth * boxSize)) / 2) ymargin = int((WINDOWHEIGHT - (boardHeight * boxSize)) / 2) return (boxx * boxSize + xmargin, boxy * boxSize + ymargin) if __name__ == '__main__': main()
四子连珠,一个“四子连珠”克隆
游戏“四子连珠”有一个 7x6 的棋盘,玩家轮流从棋盘顶部放置标记。标记将从每列的顶部掉落,并停在底部或该列的最顶部标记上。当四个标记水平、垂直或对角线排成一行时,玩家获胜。
这个游戏的 AI 非常出色。它模拟了它可以做的每一种可能的走法,然后模拟了人类玩家对每一种走法的可能响应,然后模拟了它可以对此做出的每一种可能的走法,然后模拟了人类玩家对每一种走法的可能响应!经过所有这些思考,计算机确定了哪一步最有可能导致它获胜。
所以电脑有点难以战胜。我通常输给它。
由于您可以在自己的回合上进行七种可能的走法(除非某些列已满),对手可以进行七种可能的走法,对此有七种可能的走法,对此有七种可能的走法,这意味着在每一回合,计算机都在考虑 7 x 7 x 7 x 7 = 2,401 种可能的走法。您可以通过将DIFFICULTY
常量设置为更高的数字来让计算机进一步考虑游戏,但是当我将其设置为大于2
的值时,计算机需要很长时间来计算自己的回合。
您还可以通过将DIFFICULTY
设置为1
来降低电脑的难度。然后,计算机只考虑自己的每一步和玩家对这些步骤的可能响应。如果将DIFFICULTY
设置为0
,那么计算机将失去所有智能,只会进行随机移动。
四子连珠的源代码
此源代码可从invpy.com/fourinarow.py
下载。
Flippy 使用的图像文件可以从invpy.com/fourinarowimages.zip
下载。
# Four-In-A-Row (a Connect Four clone) # By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection) # http://inventwithpython.com/pygame # Released under a "Simplified BSD" license import random, copy, sys, pygame from pygame.locals import * BOARDWIDTH = 7 # how many spaces wide the board is BOARDHEIGHT = 6 # how many spaces tall the board is assert BOARDWIDTH >= 4 and BOARDHEIGHT >= 4, 'Board must be at least 4x4.' DIFFICULTY = 2 # how many moves to look ahead. (>2 is usually too slow) SPACESIZE = 50 # size of the tokens and individual board spaces in pixels FPS = 30 # frames per second to update the screen WINDOWWIDTH = 640 # width of the program's window, in pixels WINDOWHEIGHT = 480 # height in pixels XMARGIN = int((WINDOWWIDTH - BOARDWIDTH * SPACESIZE) / 2) YMARGIN = int((WINDOWHEIGHT - BOARDHEIGHT * SPACESIZE) / 2) BRIGHTBLUE = (0, 50, 255) WHITE = (255, 255, 255) BGCOLOR = BRIGHTBLUE TEXTCOLOR = WHITE RED = 'red' BLACK = 'black' EMPTY = None HUMAN = 'human' COMPUTER = 'computer' def main(): global FPSCLOCK, DISPLAYSURF, REDPILERECT, BLACKPILERECT, REDTOKENIMG global BLACKTOKENIMG, BOARDIMG, ARROWIMG, ARROWRECT, HUMANWINNERIMG global COMPUTERWINNERIMG, WINNERRECT, TIEWINNERIMG pygame.init() FPSCLOCK = pygame.time.Clock() DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) pygame.display.set_caption('Four in a Row') REDPILERECT = pygame.Rect(int(SPACESIZE / 2), WINDOWHEIGHT - int(3 * SPACESIZE / 2), SPACESIZE, SPACESIZE) BLACKPILERECT = pygame.Rect(WINDOWWIDTH - int(3 * SPACESIZE / 2), WINDOWHEIGHT - int(3 * SPACESIZE / 2), SPACESIZE, SPACESIZE) REDTOKENIMG = pygame.image.load('4row_red.png') REDTOKENIMG = pygame.transform.smoothscale(REDTOKENIMG, (SPACESIZE, SPACESIZE)) BLACKTOKENIMG = pygame.image.load('4row_black.png') BLACKTOKENIMG = pygame.transform.smoothscale(BLACKTOKENIMG, (SPACESIZE, SPACESIZE)) BOARDIMG = pygame.image.load('4row_board.png') BOARDIMG = pygame.transform.smoothscale(BOARDIMG, (SPACESIZE, SPACESIZE)) HUMANWINNERIMG = pygame.image.load('4row_humanwinner.png') COMPUTERWINNERIMG = pygame.image.load('4row_computerwinner.png') TIEWINNERIMG = pygame.image.load('4row_tie.png') WINNERRECT = HUMANWINNERIMG.get_rect() WINNERRECT.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2)) ARROWIMG = pygame.image.load('4row_arrow.png') ARROWRECT = ARROWIMG.get_rect() ARROWRECT.left = REDPILERECT.right + 10 ARROWRECT.centery = REDPILERECT.centery isFirstGame = True while True: runGame(isFirstGame) isFirstGame = False def runGame(isFirstGame): if isFirstGame: # Let the computer go first on the first game, so the player # can see how the tokens are dragged from the token piles. turn = COMPUTER showHelp = True else: # Randomly choose who goes first. if random.randint(0, 1) == 0: turn = COMPUTER else: turn = HUMAN showHelp = False # Set up a blank board data structure. mainBoard = getNewBoard() while True: # main game loop if turn == HUMAN: # Human player's turn. getHumanMove(mainBoard, showHelp) if showHelp: # turn off help arrow after the first move showHelp = False if isWinner(mainBoard, RED): winnerImg = HUMANWINNERIMG break turn = COMPUTER # switch to other player's turn else: # Computer player's turn. column = getComputerMove(mainBoard) animateComputerMoving(mainBoard, column) makeMove(mainBoard, BLACK, column) if isWinner(mainBoard, BLACK): winnerImg = COMPUTERWINNERIMG break turn = HUMAN # switch to other player's turn if isBoardFull(mainBoard): # A completely filled board means it's a tie. winnerImg = TIEWINNERIMG break while True: # Keep looping until player clicks the mouse or quits. drawBoard(mainBoard) DISPLAYSURF.blit(winnerImg, WINNERRECT) pygame.display.update() FPSCLOCK.tick() for event in pygame.event.get(): # event handling loop if event.type == QUIT or (event.type == KEYUP and event.key == K_ESCAPE): pygame.quit() sys.exit() elif event.type == MOUSEBUTTONUP: return def makeMove(board, player, column): lowest = getLowestEmptySpace(board, column) if lowest != -1: board[column][lowest] = player def drawBoard(board, extraToken=None): DISPLAYSURF.fill(BGCOLOR) # draw tokens spaceRect = pygame.Rect(0, 0, SPACESIZE, SPACESIZE) for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): spaceRect.topleft = (XMARGIN + (x * SPACESIZE), YMARGIN + (y * SPACESIZE)) if board[x][y] == RED: DISPLAYSURF.blit(REDTOKENIMG, spaceRect) elif board[x][y] == BLACK: DISPLAYSURF.blit(BLACKTOKENIMG, spaceRect) # draw the extra token if extraToken != None: if extraToken['color'] == RED: DISPLAYSURF.blit(REDTOKENIMG, (extraToken['x'], extraToken['y'], SPACESIZE, SPACESIZE)) elif extraToken['color'] == BLACK: DISPLAYSURF.blit(BLACKTOKENIMG, (extraToken['x'], extraToken['y'], SPACESIZE, SPACESIZE)) # draw board over the tokens for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): spaceRect.topleft = (XMARGIN + (x * SPACESIZE), YMARGIN + (y * SPACESIZE)) DISPLAYSURF.blit(BOARDIMG, spaceRect) # draw the red and black tokens off to the side DISPLAYSURF.blit(REDTOKENIMG, REDPILERECT) # red on the left DISPLAYSURF.blit(BLACKTOKENIMG, BLACKPILERECT) # black on the right def getNewBoard(): board = [] for x in range(BOARDWIDTH): board.append([EMPTY] * BOARDHEIGHT) return board def getHumanMove(board, isFirstMove): draggingToken = False tokenx, tokeny = None, None while True: for event in pygame.event.get(): # event handling loop if event.type == QUIT: pygame.quit() sys.exit() elif event.type == MOUSEBUTTONDOWN and not draggingToken and REDPILERECT.collidepoint(event.pos): # start of dragging on red token pile. draggingToken = True tokenx, tokeny = event.pos elif event.type == MOUSEMOTION and draggingToken: # update the position of the red token being dragged tokenx, tokeny = event.pos elif event.type == MOUSEBUTTONUP and draggingToken: # let go of the token being dragged if tokeny < YMARGIN and tokenx > XMARGIN and tokenx < WINDOWWIDTH - XMARGIN: # let go at the top of the screen. column = int((tokenx - XMARGIN) / SPACESIZE) if isValidMove(board, column): animateDroppingToken(board, column, RED) board[column][getLowestEmptySpace(board, column)] = RED drawBoard(board) pygame.display.update() return tokenx, tokeny = None, None draggingToken = False if tokenx != None and tokeny != None: drawBoard(board, {'x':tokenx - int(SPACESIZE / 2), 'y':tokeny - int(SPACESIZE / 2), 'color':RED}) else: drawBoard(board) if isFirstMove: # Show the help arrow for the player's first move. DISPLAYSURF.blit(ARROWIMG, ARROWRECT) pygame.display.update() FPSCLOCK.tick() def animateDroppingToken(board, column, color): x = XMARGIN + column * SPACESIZE y = YMARGIN - SPACESIZE dropSpeed = 1.0 lowestEmptySpace = getLowestEmptySpace(board, column) while True: y += int(dropSpeed) dropSpeed += 0.5 if int((y - YMARGIN) / SPACESIZE) >= lowestEmptySpace: return drawBoard(board, {'x':x, 'y':y, 'color':color}) pygame.display.update() FPSCLOCK.tick() def animateComputerMoving(board, column): x = BLACKPILERECT.left y = BLACKPILERECT.top speed = 1.0 # moving the black tile up while y > (YMARGIN - SPACESIZE): y -= int(speed) speed += 0.5 drawBoard(board, {'x':x, 'y':y, 'color':BLACK}) pygame.display.update() FPSCLOCK.tick() # moving the black tile over y = YMARGIN - SPACESIZE speed = 1.0 while x > (XMARGIN + column * SPACESIZE): x -= int(speed) speed += 0.5 drawBoard(board, {'x':x, 'y':y, 'color':BLACK}) pygame.display.update() FPSCLOCK.tick() # dropping the black tile animateDroppingToken(board, column, BLACK) def getComputerMove(board): potentialMoves = getPotentialMoves(board, BLACK, DIFFICULTY) # get the best fitness from the potential moves bestMoveFitness = -1 for i in range(BOARDWIDTH): if potentialMoves[i] > bestMoveFitness and isValidMove(board, i): bestMoveFitness = potentialMoves[i] # find all potential moves that have this best fitness bestMoves = [] for i in range(len(potentialMoves)): if potentialMoves[i] == bestMoveFitness and isValidMove(board, i): bestMoves.append(i) return random.choice(bestMoves) def getPotentialMoves(board, tile, lookAhead): if lookAhead == 0 or isBoardFull(board): return [0] * BOARDWIDTH if tile == RED: enemyTile = BLACK else: enemyTile = RED # Figure out the best move to make. potentialMoves = [0] * BOARDWIDTH for firstMove in range(BOARDWIDTH): dupeBoard = copy.deepcopy(board) if not isValidMove(dupeBoard, firstMove): continue makeMove(dupeBoard, tile, firstMove) if isWinner(dupeBoard, tile): # a winning move automatically gets a perfect fitness potentialMoves[firstMove] = 1 break # don't bother calculating other moves else: # do other player's counter moves and determine best one if isBoardFull(dupeBoard): potentialMoves[firstMove] = 0 else: for counterMove in range(BOARDWIDTH): dupeBoard2 = copy.deepcopy(dupeBoard) if not isValidMove(dupeBoard2, counterMove): continue makeMove(dupeBoard2, enemyTile, counterMove) if isWinner(dupeBoard2, enemyTile): # a losing move automatically gets the worst fitness potentialMoves[firstMove] = -1 break else: # do the recursive call to getPotentialMoves() results = getPotentialMoves(dupeBoard2, tile, lookAhead - 1) potentialMoves[firstMove] += (sum(results) / BOARDWIDTH) / BOARDWIDTH return potentialMoves def getLowestEmptySpace(board, column): # Return the row number of the lowest empty row in the given column. for y in range(BOARDHEIGHT-1, -1, -1): if board[column][y] == EMPTY: return y return -1 def isValidMove(board, column): # Returns True if there is an empty space in the given column. # Otherwise returns False. if column < 0 or column >= (BOARDWIDTH) or board[column][0] != EMPTY: return False return True def isBoardFull(board): # Returns True if there are no empty spaces anywhere on the board. for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): if board[x][y] == EMPTY: return False return True def isWinner(board, tile): # check horizontal spaces for x in range(BOARDWIDTH - 3): for y in range(BOARDHEIGHT): if board[x][y] == tile and board[x+1][y] == tile and board[x+2][y] == tile and board[x+3][y] == tile: return True # check vertical spaces for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT - 3): if board[x][y] == tile and board[x][y+1] == tile and board[x][y+2] == tile and board[x][y+3] == tile: return True # check / diagonal spaces for x in range(BOARDWIDTH - 3): for y in range(3, BOARDHEIGHT): if board[x][y] == tile and board[x+1][y-1] == tile and board[x+2][y-2] == tile and board[x+3][y-3] == tile: return True # check \ diagonal spaces for x in range(BOARDWIDTH - 3): for y in range(BOARDHEIGHT - 3): if board[x][y] == tile and board[x+1][y+1] == tile and board[x+2][y+2] == tile and board[x+3][y+3] == tile: return True return False if __name__ == '__main__': main()
Gemgem,一个“宝石迷阵”克隆
“宝石迷阵”是一个宝石掉落填满棋盘的游戏。玩家可以交换任意两个相邻的宝石,尝试在一行中匹配三个宝石(垂直或水平,但不是对角线)。匹配的宝石然后消失,为从顶部掉落新宝石让路。匹配超过三个宝石,或引发宝石匹配的连锁反应将获得更多分数。玩家的分数会随时间缓慢下降,因此玩家必须不断进行新的匹配。当棋盘上无法进行匹配时,游戏结束。
Gemgem 的源代码
这个源代码可以从invpy.com/gemgem.py
下载。
Flippy 使用的图像文件可以从invpy.com/gemgemimages.zip
下载。
# Gemgem (a Bejeweled clone) # By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection) # http://inventwithpython.com/pygame # Released under a "Simplified BSD" license """ This program has "gem data structures", which are basically dictionaries with the following keys: 'x' and 'y' - The location of the gem on the board. 0,0 is the top left. There is also a ROWABOVEBOARD row that 'y' can be set to, to indicate that it is above the board. 'direction' - one of the four constant variables UP, DOWN, LEFT, RIGHT. This is the direction the gem is moving. 'imageNum' - The integer index into GEMIMAGES to denote which image this gem uses. """ import random, time, pygame, sys, copy from pygame.locals import * FPS = 30 # frames per second to update the screen WINDOWWIDTH = 600 # width of the program's window, in pixels WINDOWHEIGHT = 600 # height in pixels BOARDWIDTH = 8 # how many columns in the board BOARDHEIGHT = 8 # how many rows in the board GEMIMAGESIZE = 64 # width & height of each space in pixels # NUMGEMIMAGES is the number of gem types. You will need .png image # files named gem0.png, gem1.png, etc. up to gem(N-1).png. NUMGEMIMAGES = 7 assert NUMGEMIMAGES >= 5 # game needs at least 5 types of gems to work # NUMMATCHSOUNDS is the number of different sounds to choose from when # a match is made. The .wav files are named match0.wav, match1.wav, etc. NUMMATCHSOUNDS = 6 MOVERATE = 25 # 1 to 100, larger num means faster animations DEDUCTSPEED = 0.8 # reduces score by 1 point every DEDUCTSPEED seconds. # R G B PURPLE = (255, 0, 255) LIGHTBLUE = (170, 190, 255) BLUE = ( 0, 0, 255) RED = (255, 100, 100) BLACK = ( 0, 0, 0) BROWN = ( 85, 65, 0) HIGHLIGHTCOLOR = PURPLE # color of the selected gem's border BGCOLOR = LIGHTBLUE # background color on the screen GRIDCOLOR = BLUE # color of the game board GAMEOVERCOLOR = RED # color of the "Game over" text. GAMEOVERBGCOLOR = BLACK # background color of the "Game over" text. SCORECOLOR = BROWN # color of the text for the player's score # The amount of space to the sides of the board to the edge of the window # is used several times, so calculate it once here and store in variables. XMARGIN = int((WINDOWWIDTH - GEMIMAGESIZE * BOARDWIDTH) / 2) YMARGIN = int((WINDOWHEIGHT - GEMIMAGESIZE * BOARDHEIGHT) / 2) # constants for direction values UP = 'up' DOWN = 'down' LEFT = 'left' RIGHT = 'right' EMPTY_SPACE = -1 # an arbitrary, nonpositive value ROWABOVEBOARD = 'row above board' # an arbitrary, noninteger value def main(): global FPSCLOCK, DISPLAYSURF, GEMIMAGES, GAMESOUNDS, BASICFONT, BOARDRECTS # Initial set up. pygame.init() FPSCLOCK = pygame.time.Clock() DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) pygame.display.set_caption('Gemgem') BASICFONT = pygame.font.Font('freesansbold.ttf', 36) # Load the images GEMIMAGES = [] for i in range(1, NUMGEMIMAGES+1): gemImage = pygame.image.load('gem%s.png' % i) if gemImage.get_size() != (GEMIMAGESIZE, GEMIMAGESIZE): gemImage = pygame.transform.smoothscale(gemImage, (GEMIMAGESIZE, GEMIMAGESIZE)) GEMIMAGES.append(gemImage) # Load the sounds. GAMESOUNDS = {} GAMESOUNDS['bad swap'] = pygame.mixer.Sound('badswap.wav') GAMESOUNDS['match'] = [] for i in range(NUMMATCHSOUNDS): GAMESOUNDS['match'].append(pygame.mixer.Sound('match%s.wav' % i)) # Create pygame.Rect objects for each board space to # do board-coordinate-to-pixel-coordinate conversions. BOARDRECTS = [] for x in range(BOARDWIDTH): BOARDRECTS.append([]) for y in range(BOARDHEIGHT): r = pygame.Rect((XMARGIN + (x * GEMIMAGESIZE), YMARGIN + (y * GEMIMAGESIZE), GEMIMAGESIZE, GEMIMAGESIZE)) BOARDRECTS[x].append(r) while True: runGame() def runGame(): # Plays through a single game. When the game is over, this function returns. # initialize the board gameBoard = getBlankBoard() score = 0 fillBoardAndAnimate(gameBoard, [], score) # Drop the initial gems. # initialize variables for the start of a new game firstSelectedGem = None lastMouseDownX = None lastMouseDownY = None gameIsOver = False lastScoreDeduction = time.time() clickContinueTextSurf = None while True: # main game loop clickedSpace = None for event in pygame.event.get(): # event handling loop if event.type == QUIT or (event.type == KEYUP and event.key == K_ESCAPE): pygame.quit() sys.exit() elif event.type == KEYUP and event.key == K_BACKSPACE: return # start a new game elif event.type == MOUSEBUTTONUP: if gameIsOver: return # after games ends, click to start a new game if event.pos == (lastMouseDownX, lastMouseDownY): # This event is a mouse click, not the end of a mouse drag. clickedSpace = checkForGemClick(event.pos) else: # this is the end of a mouse drag firstSelectedGem = checkForGemClick((lastMouseDownX, lastMouseDownY)) clickedSpace = checkForGemClick(event.pos) if not firstSelectedGem or not clickedSpace: # if not part of a valid drag, deselect both firstSelectedGem = None clickedSpace = None elif event.type == MOUSEBUTTONDOWN: # this is the start of a mouse click or mouse drag lastMouseDownX, lastMouseDownY = event.pos if clickedSpace and not firstSelectedGem: # This was the first gem clicked on. firstSelectedGem = clickedSpace elif clickedSpace and firstSelectedGem: # Two gems have been clicked on and selected. Swap the gems. firstSwappingGem, secondSwappingGem = getSwappingGems(gameBoard, firstSelectedGem, clickedSpace) if firstSwappingGem == None and secondSwappingGem == None: # If both are None, then the gems were not adjacent firstSelectedGem = None # deselect the first gem continue # Show the swap animation on the screen. boardCopy = getBoardCopyMinusGems(gameBoard, (firstSwappingGem, secondSwappingGem)) animateMovingGems(boardCopy, [firstSwappingGem, secondSwappingGem], [], score) # Swap the gems in the board data structure. gameBoard[firstSwappingGem['x']][firstSwappingGem['y']] = secondSwappingGem['imageNum'] gameBoard[secondSwappingGem['x']][secondSwappingGem['y']] = firstSwappingGem['imageNum'] # See if this is a matching move. matchedGems = findMatchingGems(gameBoard) if matchedGems == []: # Was not a matching move; swap the gems back GAMESOUNDS['bad swap'].play() animateMovingGems(boardCopy, [firstSwappingGem, secondSwappingGem], [], score) gameBoard[firstSwappingGem['x']][firstSwappingGem['y']] = firstSwappingGem['imageNum'] gameBoard[secondSwappingGem['x']][secondSwappingGem['y']] = secondSwappingGem['imageNum'] else: # This was a matching move. scoreAdd = 0 while matchedGems != []: # Remove matched gems, then pull down the board. # points is a list of dicts that tells fillBoardAndAnimate() # where on the screen to display text to show how many # points the player got. points is a list because if # the player gets multiple matches, then multiple points text should appear. points = [] for gemSet in matchedGems: scoreAdd += (10 + (len(gemSet) - 3) * 10) for gem in gemSet: gameBoard[gem[0]][gem[1]] = EMPTY_SPACE points.append({'points': scoreAdd, 'x': gem[0] * GEMIMAGESIZE + XMARGIN, 'y': gem[1] * GEMIMAGESIZE + YMARGIN}) random.choice(GAMESOUNDS['match']).play() score += scoreAdd # Drop the new gems. fillBoardAndAnimate(gameBoard, points, score) # Check if there are any new matches. matchedGems = findMatchingGems(gameBoard) firstSelectedGem = None if not canMakeMove(gameBoard): gameIsOver = True # Draw the board. DISPLAYSURF.fill(BGCOLOR) drawBoard(gameBoard) if firstSelectedGem != None: highlightSpace(firstSelectedGem['x'], firstSelectedGem['y']) if gameIsOver: if clickContinueTextSurf == None: # Only render the text once. In future iterations, just # use the Surface object already in clickContinueTextSurf clickContinueTextSurf = BASICFONT.render('Final Score: %s (Click to continue)' % (score), 1, GAMEOVERCOLOR, GAMEOVERBGCOLOR) clickContinueTextRect = clickContinueTextSurf.get_rect() clickContinueTextRect.center = int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2) DISPLAYSURF.blit(clickContinueTextSurf, clickContinueTextRect) elif score > 0 and time.time() - lastScoreDeduction > DEDUCTSPEED: # score drops over time score -= 1 lastScoreDeduction = time.time() drawScore(score) pygame.display.update() FPSCLOCK.tick(FPS) def getSwappingGems(board, firstXY, secondXY): # If the gems at the (X, Y) coordinates of the two gems are adjacent, # then their 'direction' keys are set to the appropriate direction # value to be swapped with each other. # Otherwise, (None, None) is returned. firstGem = {'imageNum': board[firstXY['x']][firstXY['y']], 'x': firstXY['x'], 'y': firstXY['y']} secondGem = {'imageNum': board[secondXY['x']][secondXY['y']], 'x': secondXY['x'], 'y': secondXY['y']} highlightedGem = None if firstGem['x'] == secondGem['x'] + 1 and firstGem['y'] == secondGem['y']: firstGem['direction'] = LEFT secondGem['direction'] = RIGHT elif firstGem['x'] == secondGem['x'] - 1 and firstGem['y'] == secondGem['y']: firstGem['direction'] = RIGHT secondGem['direction'] = LEFT elif firstGem['y'] == secondGem['y'] + 1 and firstGem['x'] == secondGem['x']: firstGem['direction'] = UP secondGem['direction'] = DOWN elif firstGem['y'] == secondGem['y'] - 1 and firstGem['x'] == secondGem['x']: firstGem['direction'] = DOWN secondGem['direction'] = UP else: # These gems are not adjacent and can't be swapped. return None, None return firstGem, secondGem def getBlankBoard(): # Create and return a blank board data structure. board = [] for x in range(BOARDWIDTH): board.append([EMPTY_SPACE] * BOARDHEIGHT) return board def canMakeMove(board): # Return True if the board is in a state where a matching # move can be made on it. Otherwise return False. # The patterns in oneOffPatterns represent gems that are configured # in a way where it only takes one move to make a triplet. oneOffPatterns = (((0,1), (1,0), (2,0)), ((0,1), (1,1), (2,0)), ((0,0), (1,1), (2,0)), ((0,1), (1,0), (2,1)), ((0,0), (1,0), (2,1)), ((0,0), (1,1), (2,1)), ((0,0), (0,2), (0,3)), ((0,0), (0,1), (0,3))) # The x and y variables iterate over each space on the board. # If we use + to represent the currently iterated space on the # board, then this pattern: ((0,1), (1,0), (2,0))refers to identical # gems being set up like this: # # +A # B # C # # That is, gem A is offset from the + by (0,1), gem B is offset # by (1,0), and gem C is offset by (2,0). In this case, gem A can # be swapped to the left to form a vertical three-in-a-row triplet. # # There are eight possible ways for the gems to be one move # away from forming a triple, hence oneOffPattern has 8 patterns. for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): for pat in oneOffPatterns: # check each possible pattern of "match in next move" to # see if a possible move can be made. if (getGemAt(board, x+pat[0][0], y+pat[0][1]) == \ getGemAt(board, x+pat[1][0], y+pat[1][1]) == \ getGemAt(board, x+pat[2][0], y+pat[2][1]) != None) or \ (getGemAt(board, x+pat[0][1], y+pat[0][0]) == \ getGemAt(board, x+pat[1][1], y+pat[1][0]) == \ getGemAt(board, x+pat[2][1], y+pat[2][0]) != None): return True # return True the first time you find a pattern return False def drawMovingGem(gem, progress): # Draw a gem sliding in the direction that its 'direction' key # indicates. The progress parameter is a number from 0 (just # starting) to 100 (slide complete). movex = 0 movey = 0 progress *= 0.01 if gem['direction'] == UP: movey = -int(progress * GEMIMAGESIZE) elif gem['direction'] == DOWN: movey = int(progress * GEMIMAGESIZE) elif gem['direction'] == RIGHT: movex = int(progress * GEMIMAGESIZE) elif gem['direction'] == LEFT: movex = -int(progress * GEMIMAGESIZE) basex = gem['x'] basey = gem['y'] if basey == ROWABOVEBOARD: basey = -1 pixelx = XMARGIN + (basex * GEMIMAGESIZE) pixely = YMARGIN + (basey * GEMIMAGESIZE) r = pygame.Rect( (pixelx + movex, pixely + movey, GEMIMAGESIZE, GEMIMAGESIZE) ) DISPLAYSURF.blit(GEMIMAGES[gem['imageNum']], r) def pullDownAllGems(board): # pulls down gems on the board to the bottom to fill in any gaps for x in range(BOARDWIDTH): gemsInColumn = [] for y in range(BOARDHEIGHT): if board[x][y] != EMPTY_SPACE: gemsInColumn.append(board[x][y]) board[x] = ([EMPTY_SPACE] * (BOARDHEIGHT - len(gemsInColumn))) + gemsInColumn def getGemAt(board, x, y): if x < 0 or y < 0 or x >= BOARDWIDTH or y >= BOARDHEIGHT: return None else: return board[x][y] def getDropSlots(board): # Creates a "drop slot" for each column and fills the slot with a # number of gems that that column is lacking. This function assumes # that the gems have been gravity dropped already. boardCopy = copy.deepcopy(board) pullDownAllGems(boardCopy) dropSlots = [] for i in range(BOARDWIDTH): dropSlots.append([]) # count the number of empty spaces in each column on the board for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT-1, -1, -1): # start from bottom, going up if boardCopy[x][y] == EMPTY_SPACE: possibleGems = list(range(len(GEMIMAGES))) for offsetX, offsetY in ((0, -1), (1, 0), (0, 1), (-1, 0)): # Narrow down the possible gems we should put in the # blank space so we don't end up putting an two of # the same gems next to each other when they drop. neighborGem = getGemAt(boardCopy, x + offsetX, y + offsetY) if neighborGem != None and neighborGem in possibleGems: possibleGems.remove(neighborGem) newGem = random.choice(possibleGems) boardCopy[x][y] = newGem dropSlots[x].append(newGem) return dropSlots def findMatchingGems(board): gemsToRemove = [] # a list of lists of gems in matching triplets that should be removed boardCopy = copy.deepcopy(board) # loop through each space, checking for 3 adjacent identical gems for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): # look for horizontal matches if getGemAt(boardCopy, x, y) == getGemAt(boardCopy, x + 1, y) == getGemAt(boardCopy, x + 2, y) and getGemAt(boardCopy, x, y) != EMPTY_SPACE: targetGem = boardCopy[x][y] offset = 0 removeSet = [] while getGemAt(boardCopy, x + offset, y) == targetGem: # keep checking, in case there's more than 3 gems in a row removeSet.append((x + offset, y)) boardCopy[x + offset][y] = EMPTY_SPACE offset += 1 gemsToRemove.append(removeSet) # look for vertical matches if getGemAt(boardCopy, x, y) == getGemAt(boardCopy, x, y + 1) == getGemAt(boardCopy, x, y + 2) and getGemAt(boardCopy, x, y) != EMPTY_SPACE: targetGem = boardCopy[x][y] offset = 0 removeSet = [] while getGemAt(boardCopy, x, y + offset) == targetGem: # keep checking if there's more than 3 gems in a row removeSet.append((x, y + offset)) boardCopy[x][y + offset] = EMPTY_SPACE offset += 1 gemsToRemove.append(removeSet) return gemsToRemove def highlightSpace(x, y): pygame.draw.rect(DISPLAYSURF, HIGHLIGHTCOLOR, BOARDRECTS[x][y], 4) def getDroppingGems(board): # Find all the gems that have an empty space below them boardCopy = copy.deepcopy(board) droppingGems = [] for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT - 2, -1, -1): if boardCopy[x][y + 1] == EMPTY_SPACE and boardCopy[x][y] != EMPTY_SPACE: # This space drops if not empty but the space below it is droppingGems.append( {'imageNum': boardCopy[x][y], 'x': x, 'y': y, 'direction': DOWN} ) boardCopy[x][y] = EMPTY_SPACE return droppingGems def animateMovingGems(board, gems, pointsText, score): # pointsText is a dictionary with keys 'x', 'y', and 'points' progress = 0 # progress at 0 represents beginning, 100 means finished. while progress < 100: # animation loop DISPLAYSURF.fill(BGCOLOR) drawBoard(board) for gem in gems: # Draw each gem. drawMovingGem(gem, progress) drawScore(score) for pointText in pointsText: pointsSurf = BASICFONT.render(str(pointText['points']), 1, SCORECOLOR) pointsRect = pointsSurf.get_rect() pointsRect.center = (pointText['x'], pointText['y']) DISPLAYSURF.blit(pointsSurf, pointsRect) pygame.display.update() FPSCLOCK.tick(FPS) progress += MOVERATE # progress the animation a little bit more for the next frame def moveGems(board, movingGems): # movingGems is a list of dicts with keys x, y, direction, imageNum for gem in movingGems: if gem['y'] != ROWABOVEBOARD: board[gem['x']][gem['y']] = EMPTY_SPACE movex = 0 movey = 0 if gem['direction'] == LEFT: movex = -1 elif gem['direction'] == RIGHT: movex = 1 elif gem['direction'] == DOWN: movey = 1 elif gem['direction'] == UP: movey = -1 board[gem['x'] + movex][gem['y'] + movey] = gem['imageNum'] else: # gem is located above the board (where new gems come from) board[gem['x']][0] = gem['imageNum'] # move to top row def fillBoardAndAnimate(board, points, score): dropSlots = getDropSlots(board) while dropSlots != [[]] * BOARDWIDTH: # do the dropping animation as long as there are more gems to drop movingGems = getDroppingGems(board) for x in range(len(dropSlots)): if len(dropSlots[x]) != 0: # cause the lowest gem in each slot to begin moving in the DOWN direction movingGems.append({'imageNum': dropSlots[x][0], 'x': x, 'y': ROWABOVEBOARD, 'direction': DOWN}) boardCopy = getBoardCopyMinusGems(board, movingGems) animateMovingGems(boardCopy, movingGems, points, score) moveGems(board, movingGems) # Make the next row of gems from the drop slots # the lowest by deleting the previous lowest gems. for x in range(len(dropSlots)): if len(dropSlots[x]) == 0: continue board[x][0] = dropSlots[x][0] del dropSlots[x][0] def checkForGemClick(pos): # See if the mouse click was on the board for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): if BOARDRECTS[x][y].collidepoint(pos[0], pos[1]): return {'x': x, 'y': y} return None # Click was not on the board. def drawBoard(board): for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): pygame.draw.rect(DISPLAYSURF, GRIDCOLOR, BOARDRECTS[x][y], 1) gemToDraw = board[x][y] if gemToDraw != EMPTY_SPACE: DISPLAYSURF.blit(GEMIMAGES[gemToDraw], BOARDRECTS[x][y]) def getBoardCopyMinusGems(board, gems): # Creates and returns a copy of the passed board data structure, # with the gems in the "gems" list removed from it. # # Gems is a list of dicts, with keys x, y, direction, imageNum boardCopy = copy.deepcopy(board) # Remove some of the gems from this board data structure copy. for gem in gems: if gem['y'] != ROWABOVEBOARD: boardCopy[gem['x']][gem['y']] = EMPTY_SPACE return boardCopy def drawScore(score): scoreImg = BASICFONT.render(str(score), 1, SCORECOLOR) scoreRect = scoreImg.get_rect() scoreRect.bottomleft = (10, WINDOWHEIGHT - 6) DISPLAYSURF.blit(scoreImg, scoreRect) if __name__ == '__main__': main()
摘要
希望这些游戏程序给了你自己关于你想制作什么游戏以及如何编写它们的想法。即使你没有自己的想法,尝试编写你玩过的其他游戏的克隆版本也是很好的练习。
以下是一些可以教你更多关于 Python 编程的网站:
pygame.org
- 官方 Pygame 网站上有数百个游戏的源代码,这些游戏利用了 Pygame 库。通过下载和阅读其他人的源代码,你可以学到很多东西。python.org/doc/
- 更多 Python 教程和所有 Python 模块和函数的文档。pygame.org/docs/
- Pygame 模块和函数的完整文档reddit.com/r/learnpython
和reddit.com/r/learnprogramming
有很多用户可以帮助你找到学习编程的资源。inventwithpython.com/pygame
- 本书的网站,包括这些程序的所有源代码和额外信息。该网站还包含 Pygame 程序中使用的图像和声音文件。inventwithpython.com
- 《用 Python 发明你自己的计算机游戏》一书的网站,涵盖基本的 Python 编程。invpy.com/wiki
- 一个涵盖个别 Python 编程概念的维基,如果你需要了解特定内容,可以查阅。invpy.com/traces
- 一个帮助你逐步跟踪本书中程序执行的网络应用。invpy.com/videos
- 与本书中程序配套的视频。gamedevlessons.com
- 一个关于如何设计和编程视频游戏的有用网站。- [email protected] - 我的电子邮件地址。随时给我发电子邮件,询问关于本书或 Python 编程的问题。
或者你可以通过搜索全球网络了解更多关于 Python 的信息。前往搜索网站google.com
,搜索“Python 编程”或“Python 教程”以找到更多关于 Python 编程的网站。
现在开始发明你自己的游戏。祝你好运!