实现思路:
游戏的基本规则是玩家通过交换相邻的宝石,形成三个或以上的同色宝石连线,以消除宝石并获得分数。以下是对代码实现思路的详细说明:
-
初始化设置:
- 设置游戏的窗口大小、帧率、宝石大小、宝石数量等基本参数。
- 初始化pygame库,设置窗口,加载字体,加载宝石图像和匹配声音。
-
主函数 (
main
):- 初始化游戏环境,包括加载图像、声音、创建棋盘矩形等。
- 调用
runGame
函数开始游戏。
-
游戏循环 (
runGame
):- 初始化棋盘、分数、选择的宝石等变量。
- 进入主游戏循环,处理事件(如鼠标点击、键盘按键)。
- 根据事件更新游戏状态,如选择宝石、交换宝石、检查匹配、更新分数等。
- 绘制棋盘、宝石、分数和游戏结束信息。
-
交换宝石 (
getSwappingGems
):- 确定两个相邻宝石是否可以交换,并设置交换方向。
-
棋盘初始化 (
getBlankBoard
):- 创建一个空的棋盘数据结构,所有位置初始为
EMPTY_SPACE
。
- 创建一个空的棋盘数据结构,所有位置初始为
-
检查可移动性 (
canMakeMove
):- 检查棋盘上是否存在可以形成三连的宝石配置,以判断游戏是否结束。
-
动画效果 (
animateMovingGems
):- 用于显示宝石移动和分数变化的动画效果。
-
下落宝石 (
pullDownAllGems
和getDroppingGems
):- 将棋盘上的宝石向下拉以填补空位,并找到需要下落的宝石。
-
匹配宝石 (
findMatchingGems
):- 检查并找出所有符合条件的三连宝石,并准备消除。
-
高亮显示 (
highlightSpace
):- 用于高亮显示玩家当前选中的宝石。
-
填充棋盘 (
fillBoardAndAnimate
):- 在棋盘底部生成新的宝石,并显示下落动画。
-
检查点击 (
checkForGemClick
):- 检测鼠标点击的位置是否在棋盘的某个宝石上。
-
绘制棋盘 (
drawBoard
):- 在屏幕上绘制棋盘和宝石。
-
绘制分数 (
drawScore
):- 在屏幕上绘制当前分数
import random, time, pygame, sys, copy from pygame.locals import * FPS = 30 # 每秒更新屏幕的帧数 WINDOWWIDTH = 600 # 程序窗口的宽度,单位为像素 WINDOWHEIGHT = 600 # 窗口的高度,单位为像素 BOARDWIDTH = 8 # 棋盘的列数 BOARDHEIGHT = 8 # 棋盘的行数 GEMIMAGESIZE = 64 # 每个方格的宽度和高度,单位为像素 # NUMGEMIMAGES 表示宝石类型的数量。你需要有名为 gem0.png, gem1.png, 等,直到 gem(N-1).png 的 .png 图像文件。 NUMGEMIMAGES = 7 assert NUMGEMIMAGES >= 5 # 游戏至少需要5种宝石才能运行 # NUMMATCHSOUNDS 表示匹配时可以选择的不同声音的数量。.wav 文件名为 match0.wav, match1.wav, 等。 NUMMATCHSOUNDS = 6 MOVERATE = 25 # 1到100,数值越大意味着动画速度越快 DEDUCTSPEED = 0.8 # 每 DEDUCTSPEED 秒减少1分。 # 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 # 选中的宝石边框颜色 BGCOLOR = LIGHTBLUE # 屏幕的背景颜色 GRIDCOLOR = BLUE # 游戏棋盘的颜色 GAMEOVERCOLOR = RED # "游戏结束"文本的颜色 GAMEOVERBGCOLOR = BLACK # "游戏结束"文本的背景颜色 SCORECOLOR = BROWN # 玩家分数文本的颜色 # 棋盘两侧到窗口边缘的空间量在多个地方使用,因此在这里计算一次并存储在变量中。 XMARGIN = int((WINDOWWIDTH - GEMIMAGESIZE * BOARDWIDTH) / 2) YMARGIN = int((WINDOWHEIGHT - GEMIMAGESIZE * BOARDHEIGHT) / 2) # 方向值的常量 UP = 'up' DOWN = 'down' LEFT = 'left' RIGHT = 'right' EMPTY_SPACE = -1 # 任意非正数值 ROWABOVEBOARD = 'row above board' # 任意非整数值 def main(): global FPSCLOCK, DISPLAYSURF, GEMIMAGES, GAMESOUNDS, BASICFONT, BOARDRECTS # 初始设置。 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) # 加载图像 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) # 加载声音。 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)) # 为每个棋盘空间创建 pygame.Rect 对象,以便进行棋盘坐标到像素坐标的转换。 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(): # 进行单个游戏。游戏结束时,此函数返回。 # 初始化棋盘 gameBoard = getBlankBoard() score = 0 fillBoardAndAnimate(gameBoard, [], score) # 掉落初始宝石。 # 初始化新游戏开始时的变量 firstSelectedGem = None lastMouseDownX = None lastMouseDownY = None gameIsOver = False lastScoreDeduction = time.time() clickContinueTextSurf = None while True: # 主游戏循环 clickedSpace = None for event in pygame.event.get(): # 事件处理循环 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 # 开始新游戏 elif event.type == MOUSEBUTTONUP: if gameIsOver: return # 游戏结束后,点击开始新游戏 if event.pos == (lastMouseDownX, lastMouseDownY): # 此事件是鼠标点击,不是鼠标拖动的结束。 clickedSpace = checkForGemClick(event.pos) else: # 这是鼠标拖动的结束 firstSelectedGem = checkForGemClick((lastMouseDownX, lastMouseDownY)) clickedSpace = checkForGemClick(event.pos) if not firstSelectedGem or not clickedSpace: # 如果不是有效的拖动,取消选择两个 firstSelectedGem = None clickedSpace = None elif event.type == MOUSEBUTTONDOWN: # 这是鼠标点击或鼠标拖动的开始 lastMouseDownX, lastMouseDownY = event.pos if clickedSpace and not firstSelectedGem: # 这是第一个点击的宝石。 firstSelectedGem = clickedSpace elif clickedSpace and firstSelectedGem: # 两个宝石已被点击并选中。交换宝石。 firstSwappingGem, secondSwappingGem = getSwappingGems(gameBoard, firstSelectedGem, clickedSpace) if firstSwappingGem == None and secondSwappingGem == None: # 如果两者都是 None,那么宝石不是相邻的 firstSelectedGem = None # 取消选择第一个宝石 continue # 在屏幕上显示交换动画。 boardCopy = getBoardCopyMinusGems(gameBoard, (firstSwappingGem, secondSwappingGem)) animateMovingGems(boardCopy, [firstSwappingGem, secondSwappingGem], [], score) # 在棋盘数据结构中交换宝石。 gameBoard[firstSwappingGem['x']][firstSwappingGem['y']] = secondSwappingGem['imageNum'] gameBoard[secondSwappingGem['x']][secondSwappingGem['y']] = firstSwappingGem['imageNum'] # 查看这是否是一个匹配的移动。 matchedGems = findMatchingGems(gameBoard) if matchedGems == []: # 不是一个匹配的移动;交换宝石回来 GAMESOUNDS['bad swap'].play() animateMovingGems(boardCopy, [firstSwappingGem, secondSwappingGem], [], score) gameBoard[firstSwappingGem['x']][firstSwappingGem['y']] = firstSwappingGem['imageNum'] gameBoard[secondSwappingGem['x']][secondSwappingGem['y']] = secondSwappingGem['imageNum'] else: # 这是一个匹配的移动。 scoreAdd = 0 while matchedGems != []: # 移除匹配的宝石,然后向下拉棋盘。 # points 是一个列表,其中包含了告诉 fillBoardAndAnimate() 在屏幕上显示文本以展示玩家获得多少分数的信息。points 是一个列表,因为如果玩家获得多个匹配,则应该显示多个分数文本。 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 # 掉落新的宝石。 fillBoardAndAnimate(gameBoard, points, score) # 检查是否有任何新的匹配。 matchedGems = findMatchingGems(gameBoard) firstSelectedGem = None if not canMakeMove(gameBoard): gameIsOver = True # 绘制棋盘。 DISPLAYSURF.fill(BGCOLOR) drawBoard(gameBoard) if firstSelectedGem != None: highlightSpace(firstSelectedGem['x'], firstSelectedGem['y']) if gameIsOver: if clickContinueTextSurf is None: # 只渲染文本一次。在未来的迭代中,只需使用已经存在于 clickContinueTextSurf 中的 Surface 对象 clickContinueTextSurf = BASICFONT.render('score: %s (click go on)' % (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 -= 1 lastScoreDeduction = time.time() drawScore(score) pygame.display.update() FPSCLOCK.tick(FPS) def getSwappingGems(board, firstXY, secondXY): # 如果两个宝石的 (X, Y) 坐标处的宝石相邻, # 则它们的 'direction' 键被设置为与彼此交换的适当方向 # 值。 # 否则,返回 (None, None)。 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: # 这些宝石不相邻,不能交换。 return None, None return firstGem, secondGem def getBlankBoard(): # 创建并返回一个空的棋盘数据结构。 board = [] for x in range(BOARDWIDTH): board.append([EMPTY_SPACE] * BOARDHEIGHT) return board def canMakeMove(board): # 如果棋盘处于可以进行匹配移动的状态,则返回 True。否则返回 False。 # oneOffPatterns 中的模式代表配置好的宝石,只需一步即可形成三连。 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))) # x 和 y 变量遍历棋盘上的每个空间。 # 如果我们用 + 表示当前遍历的棋盘上的空间,那么这个模式:((0,1), (1,0), (2,0)) 指的是相同的宝石这样排列: # # +A # B # C # # 也就是说,宝石 A 相对于 + 的偏移是 (0,1),宝石 B 的偏移是 (1,0),宝石 C 的偏移是 (2,0)。在这种情况下,宝石 A 可以向左交换,形成一个垂直的三连。 # # 宝石有八种可能的方式只需一步就能形成三连,因此 oneOffPattern 有 8 种模式。 for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): for pat in oneOffPatterns: # 检查每种可能的“下一步匹配”模式,看是否可以进行移动。 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]) is not 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]) is not None): return True # 第一次找到模式时返回 True return False def drawMovingGem(gem, progress): # 绘制一个宝石,它沿着其 'direction' 键指示的方向滑动。progress 参数是一个从 0(刚开始)到 100(滑动完成)的数字。 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): # 将棋盘上的宝石向下拉到底部,以填补任何空隙 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): # 为每列创建一个“下落槽”,并用该列缺少的宝石填充槽。此函数假设宝石已经受到重力影响下落。 boardCopy = copy.deepcopy(board) pullDownAllGems(boardCopy) dropSlots = [] for i in range(BOARDWIDTH): dropSlots.append([]) # 计算棋盘上每列中空格的数量 for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT - 1, -1, -1): # 从底部开始,向上移动 if boardCopy[x][y] == EMPTY_SPACE: possibleGems = list(range(len(GEMIMAGES))) for offsetX, offsetY in ((0, -1), (1, 0), (0, 1), (-1, 0)): # 缩小应该放入空白空间的宝石的可能性,以免在宝石下落时出现两个相同的宝石相邻。 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 = [] # 应该移除的匹配三连宝石的列表列表 boardCopy = copy.deepcopy(board) # 遍历每个空间,检查是否有 3 个相邻的相同宝石 for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): # 寻找水平匹配 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: # 继续检查是否有超过 3 个宝石在同一行 removeSet.append((x + offset, y)) boardCopy[x + offset][y] = EMPTY_SPACE offset += 1 gemsToRemove.append(removeSet) # 寻找垂直匹配 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: # 继续检查,以防有超过 3 个宝石在同一行 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): # 找到所有在其下方有空格的宝石 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: # 如果该空间不为空,但其下方的空间为空,则该空间下落 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 是一个字典,包含键 'x', 'y', 和 'points' progress = 0 # progress 为 0 表示开始,100 表示完成。 while progress < 100: # 动画循环 DISPLAYSURF.fill(BGCOLOR) drawBoard(board) for gem in gems: # 绘制每个宝石。 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 # 为下一帧的动画进度增加一点 def moveGems(board, movingGems): # movingGems 是一个包含键 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: # 宝石位于棋盘上方(新宝石出现的地方) board[gem['x']][0] = gem['imageNum'] # 移动到顶行 def fillBoardAndAnimate(board, points, score): dropSlots = getDropSlots(board) while dropSlots != [[]] * BOARDWIDTH: # 只要还有更多的宝石要下落,就进行下落动画 movingGems = getDroppingGems(board) for x in range(len(dropSlots)): if len(dropSlots[x]) != 0: # 使每个槽中最低的宝石开始向下移动 movingGems.append({'imageNum': dropSlots[x][0], 'x': x, 'y': ROWABOVEBOARD, 'direction': DOWN}) boardCopy = getBoardCopyMinusGems(board, movingGems) animateMovingGems(boardCopy, movingGems, points, score) moveGems(board, movingGems) # 通过删除前一行的最低宝石,使下落槽中的下一行宝石成为最低的 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): # 检查鼠标点击是否在棋盘上 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 # 点击不在棋盘上。 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): # 创建并返回传入的棋盘数据结构的副本, # 其中“gems”列表中的宝石已被移除。 # # Gems 是一个包含键 x, y, direction, imageNum 的字典列表 boardCopy = copy.deepcopy(board) # 从这个棋盘数据结构副本中移除一些宝石。 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()
- 在屏幕上绘制当前分数