玩蛇系列之Pygame教程(九)-- SlidePuzzle滑动拼图

** SlidePuzzle滑动拼图 **

先上个图(●'◡'●)

ps:本人比较笨,3 * 3 的都要解半天,各位可以自行增加难度

游戏中比较核心的三个方法:

方法一,就是生成有规律的二维数组,例如:

在2 x 2下: [ [ 1, 3 ] , [ 2, None ] ]
在3 x 3下: [ [ 1, 4, 7 ] , [ 2, 5, 8 ] , [ 3, 6, None ] ]
在4 x 4下: [ [ 1, 5, 9, 13 ] , [ 2, 6, 10, 14 ] , [ 3, 7, 11, 15 ] , [ 4, 8, 12, None ] ]

# 生成刚开始的board序列
def getStartingBoard():
    # 返回一个board的序列
    # 举个例子,如果board的列数和行数都是3,则返回以下数据
    # [[1, 4, 7], [2, 5, 8], [3, 6, None]]
    
    counter = 1
    board = []
    for x in range(BOARDWIDTH):
        column = []
        for y in range(BOARDHEIGHT):
            column.append(counter)
            counter += BOARDWIDTH
        board.append(column)
        counter -= BOARDWIDTH * (BOARDHEIGHT - 1) + BOARDWIDTH - 1
            
    board[BOARDWIDTH-1][BOARDHEIGHT-1] = None
    
    return board

方法二,打乱上面生成的二维数组,并返回打乱的结果和储存打乱过程的每一步的一个序列

# 生成新的拼图(就是随机打乱已经排序好的board,numSlides为打乱的步数)    
def generateNewPuzzle(numSlides):
    
    sequence = [] # 储存打乱的步数的序列
    
    board = getStartingBoard() # 获得开始board的序列
    
    drawBoard(board, '') # 绘制board
    
    pygame.display.update() # 更新屏幕
    
    pygame.time.wait(500) # 暂停500毫秒 for effect 
    
    lastMove = None
    for i in range(numSlides):
        move = getRandomMove(board, lastMove)# 随机的移动一步    
        slideAnimation(board, move, 'Generating new puzzle...', int(TILESIZE / 3)) # 滑动的动画        
        makeMove(board, move) # 交换空白方块的位置
        sequence.append(move) # 将这个步骤添加到sequence序列中
        lastMove = move
        
    return (board, sequence)

方法三,根据所有操作的的序列,恢复游戏

# 根据所有操作的的序列,恢复游戏
def resetAnimation(board, allMoves):
    
    revAllMoves = allMoves[:] # 复制一份所有操作的的序列
    
    revAllMoves.reverse() # 倒序排列

    for move in revAllMoves: # 取出每一步,做相反的操作(就能恢复为原来的最初的序列)
        if move == UP: 
            oppositeMove = DOWN
        elif move == DOWN:
            oppositeMove = UP
        elif move == RIGHT:
            oppositeMove = LEFT
        elif move == LEFT:
            oppositeMove = RIGHT
            
        slideAnimation(board, oppositeMove, '', int(TILESIZE / 2)) # 滑动的动画     
        
        makeMove(board, oppositeMove) # 交换空白方块的位置   
    
理一下思路:

1,绘制一个640*480的窗口

2,根据行数和列数生成一个有规律的二维数组 SOLVEDBOARD

3,打乱上面生成的二维数组,返回打乱的结果 mainBoard 和一个储存打乱过程的每一步的序列solutionSeq

4,定义一个记录玩家操作的的序列 allMoves

5,绘制主Board,边框,左上角的文字提示,以及右下角的三个按钮

6,游戏主循环,判断mainBoard是否和SOLVEDBOARD相等,相等就说明拼图已经还原

7,鼠标、键盘事件处理

8,交换空白方块与其上下左右方块的位置,记录每次操作到 allMoves 中

9,点击恢复按钮:调用resetAnimation方法,allMoves 作为参数,并清空 allMoves

10,点中的是解决方案按钮:调用resetAnimation方法,solutionSeq + allMoves 作为参数,并清空 allMoves

11,点中的是新游戏按钮:调用generateNewPuzzle方法生成新的 mainBoard 和 solutionSeq ,并清空 allMoves

在第8步中,用到一个** DISPLAYSURF.copy() **方法
copy()方法会返回一个新的Surface,上面绘制着和原来一样的内容,在调用完copy方法后,如果我们在这个Surface上面绘制image内容,他将不会改变这个原来那个Surface

完整代码:
# -*- coding: UTF-8 -*-
'''
Created on 2016年11月28日

@author: 小峰峰
'''

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



BOARDWIDTH = 4 # board的列数
BOARDHEIGHT = 4 # board的行数

TILESIZE = 80 # 方块的大小

WINDOWWIDTH = 640 # 窗口宽度
WINDOWHEIGHT = 480 # 窗口高度


FPS = 30 # 帧率

BLANK = None

# 定义几个颜色 (R G B)
BLACK = ( 0, 0, 0)
WHITE = (255, 255, 255)
BRIGHTBLUE = ( 0, 50, 255)
DARKTURQUOISE = ( 3, 54, 73)
GREEN = ( 0, 204, 0)


BGCOLOR = DARKTURQUOISE # 背景色
TILECOLOR = GREEN # 方块颜色
TEXTCOLOR = WHITE # 文字颜色
BORDERCOLOR = BRIGHTBLUE # 边框颜色
BASICFONTSIZE = 20 # 字体大小

BUTTONCOLOR = WHITE # 按钮颜色
BUTTONTEXTCOLOR = BLACK # 按钮文字颜色
MESSAGECOLOR = WHITE # 消息颜色

XMARGIN = int((WINDOWWIDTH - (TILESIZE * BOARDWIDTH + (BOARDWIDTH - 1))) / 2) # X轴边距
YMARGIN = int((WINDOWHEIGHT - (TILESIZE * BOARDHEIGHT + (BOARDHEIGHT - 1))) / 2) # Y轴边距


# 定义"上下左右"操作
UP = 'up'
DOWN = 'down'
LEFT = 'left'
RIGHT = 'right'



def main():
    
    # 全局变量
    global FPSCLOCK, DISPLAYSURF, BASICFONT, RESET_SURF, RESET_RECT, NEW_SURF, NEW_RECT, SOLVE_SURF, SOLVE_RECT 

    pygame.init() # 初始化pygame
    
    FPSCLOCK = pygame.time.Clock() # 获得pygame时钟
    
    DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) # 设置窗口大小
    
    pygame.display.set_caption('Slide Puzzle') # 设置标题
    
    BASICFONT = pygame.font.Font('PAPYRUS.ttf', BASICFONTSIZE) # 设置字体,和字体大小
    
    
    # 操作按钮的属性
    RESET_SURF, RESET_RECT = makeText('  Reset  ', TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 120, WINDOWHEIGHT - 105) # 恢复
    NEW_SURF, NEW_RECT = makeText('New Game', TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 120, WINDOWHEIGHT - 70) # 新游戏
    SOLVE_SURF, SOLVE_RECT = makeText('  Solve  ', TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 120, WINDOWHEIGHT - 35) # 解决方案
    
    
    mainBoard, solutionSeq = generateNewPuzzle(80)# 打乱生成的拼图(返回Board序列,和对应的解决序列)
    
    SOLVEDBOARD = getStartingBoard() # 获得生成刚开始的board序列,作为是否解决的参照
    
    allMoves = [] # 记录玩家操作的的序列
    
    
    
    while True: # 游戏主循环
        
        slideTo = None # 滑动的方向
        
        msg = '' # 左上角显示的消息内容
        
        if mainBoard == SOLVEDBOARD: # 如果Board序列等于SOLVEDBOARD序列
            
            msg = 'Solved!' # 显示消息已经修复好拼图了
        
            
        drawBoard(mainBoard, msg)
        
        
        for event in pygame.event.get(): # 事件处理
            
            if event.type == MOUSEBUTTONUP: # 如果是鼠标点击事件
                
                spotx, spoty = getSpotClicked(mainBoard, event.pos[0], event.pos[1]) # 根据鼠标点击的像素坐标,获得方块的坐标
                
                
                if (spotx, spoty) == (None, None): # 如果没有点方块
                    
                    # 检查是否点中右下角的几个按钮
                    
                    if RESET_RECT.collidepoint(event.pos): # 点中的是恢复按钮
                        resetAnimation(mainBoard, allMoves) # 将玩家操作的的序列作为参数,恢复游戏
                        allMoves = [] # 清空 allMoves
                        
                    elif NEW_RECT.collidepoint(event.pos): # 点中的是新游戏 
                        mainBoard, solutionSeq = generateNewPuzzle(80) # 重新生成游戏
                        allMoves = [] # 清空 allMoves
                        
                    elif SOLVE_RECT.collidepoint(event.pos): # 点中的是解决方案
                        resetAnimation(mainBoard, solutionSeq + allMoves) # 将对应的解决序列和玩家操作的的序列作为参数,恢复游戏
                        allMoves = [] # 清空 allMoves
                
                else:
                    # 检查点中的方块是否与空白的方块相邻
                    
                    blankx, blanky = getBlankPosition(mainBoard) # 获得空白方块的坐标
                    
                    if spotx == blankx + 1 and spoty == blanky: # 如果点中的方块在空白方块的右边,就往左滑
                        slideTo = LEFT
                    elif spotx == blankx - 1 and spoty == blanky:# 如果点中的方块在空白方块的左边,就往右滑
                        slideTo = RIGHT
                    elif spotx == blankx and spoty == blanky + 1:# 如果点中的方块在空白方块的下边,就往上滑
                        slideTo = UP
                    elif spotx == blankx and spoty == blanky - 1:# 如果点中的方块在空白方块的上边,就往下滑
                        slideTo = DOWN
                        
                        
            elif event.type == KEYUP: # 如果是按键操作
                
                # 检查是否可以移动,如果可以就滑动方块
                
                if event.key in (K_LEFT, K_a) and isValidMove(mainBoard, LEFT):
                    slideTo = LEFT
                elif event.key in (K_RIGHT, K_d) and isValidMove(mainBoard, RIGHT):
                    slideTo = RIGHT
                elif event.key in (K_UP, K_w) and isValidMove(mainBoard, UP):
                    slideTo = UP
                elif event.key in (K_DOWN, K_s) and isValidMove(mainBoard, DOWN):
                    slideTo = DOWN
                    
                elif event.key == K_ESCAPE: # 如果是ESC键,退出
                    pygame.quit()
                    sys.exit() 
                
            elif event.type == QUIT: # 如果是退出操作,退出
                    pygame.quit() 
                    sys.exit()
                    
                           
        
        if slideTo:
            # 根据slideTo的值来操作滑动方块
            slideAnimation(mainBoard, slideTo, 'Click tile or press arrow keys to slide.', 8) # 滑动的操作
            makeMove(mainBoard, slideTo) # 交换空白方块与其上下左右方块的位置
            allMoves.append(slideTo) # 记录下这次滑动的操作 
            
            
        pygame.display.update() # 更新屏幕
        FPSCLOCK.tick(FPS) # 设置帧率            

    

# 生成刚开始的board序列
def getStartingBoard():
    # 返回一个board的序列
    # 举个例子,如果board的列数和行数都是3,则返回以下数据
    # [[1, 4, 7], [2, 5, 8], [3, 6, None]]
    
    counter = 1
    board = []
    for x in range(BOARDWIDTH):
        column = []
        for y in range(BOARDHEIGHT):
            column.append(counter)
            counter += BOARDWIDTH
        board.append(column)
        counter -= BOARDWIDTH * (BOARDHEIGHT - 1) + BOARDWIDTH - 1
            
    board[BOARDWIDTH-1][BOARDHEIGHT-1] = None
    
    print board
    return board

# 获得空白方块的位置
def getBlankPosition(board):
    for x in range(BOARDWIDTH):
        for y in range(BOARDHEIGHT):
            if board[x][y] == None: # 穷举board中所有的方块,判断为None的就是空白方块
                return (x, y)
            

# 交换空白方块与其上下左右方块的位置            
def makeMove(board, move):
    
    blankx, blanky = getBlankPosition(board) # 获得空白方块的位置
    
    # 根据slideTo交换位置
    if move == UP:
        board[blankx][blanky], board[blankx][blanky + 1] = board[blankx][blanky + 1], board[blankx][blanky]
    elif move == DOWN:
        board[blankx][blanky], board[blankx][blanky - 1] = board[blankx][blanky - 1], board[blankx][blanky]
    elif move == LEFT:
        board[blankx][blanky], board[blankx + 1][blanky] = board[blankx + 1][blanky], board[blankx][blanky]
    elif move == RIGHT:
        board[blankx][blanky], board[blankx - 1][blanky] = board[blankx - 1][blanky], board[blankx][blanky]            
            
            
# 该方法用在玩家用按键操作时检查方块是否可以移动 
def isValidMove(board, move):
    
    blankx, blanky = getBlankPosition(board) # 获得空白方块的位置
    
    # 如果时向上滑动,就要确保空白方块不在最下面的一行,否则就无法继续和下面的方块交换
    # 同理,如果是向下,就确保空白方块不在最上面
    # 同理,如果是向左,就确保空白方块不在最右面
    # 同理,如果是向右,就确保空白方块不在最左面
    return (move == UP and blanky != len(board[0]) - 1) or \
        (move == DOWN and blanky != 0) or \
        (move == LEFT and blankx != len(board) - 1) or \
        (move == RIGHT and blankx != 0)            
            

# 随机的移动一步            
def getRandomMove(board, lastMove=None):
    # 定义合法的移动集合
    validMoves = [UP, DOWN, LEFT, RIGHT]

    # 剔除掉不可用的方向
    if lastMove == UP or not isValidMove(board, DOWN):
        validMoves.remove(DOWN)
    if lastMove == DOWN or not isValidMove(board, UP):
        validMoves.remove(UP)
    if lastMove == LEFT or not isValidMove(board, RIGHT):
        validMoves.remove(RIGHT)
    if lastMove == RIGHT or not isValidMove(board, LEFT):
        validMoves.remove(LEFT)

    # 从剩下可用的validMoves里随机选取一个返回
    return random.choice(validMoves)


# 获得方块左上角的像素坐标
def getLeftTopOfTile(tileX, tileY):
    left = XMARGIN + (tileX * TILESIZE) + (tileX - 1)
    top = YMARGIN + (tileY * TILESIZE) + (tileY - 1)
    return (left, top)

# 根据像素坐标获得方块在board里的坐标
def getSpotClicked(board, x, y):
    for tileX in range(len(board)):
        for tileY in range(len(board[0])):
            
            # 获得每一个方块的左上角的像素坐标
            left, top = getLeftTopOfTile(tileX, tileY)
            # 获得方块的Rect
            tileRect = pygame.Rect(left, top, TILESIZE, TILESIZE)
            # 判断这个Rect是否包含点 (x,y)
            if tileRect.collidepoint(x, y):
                return (tileX, tileY)
    return (None, None)


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

# 创建text的对应的Surf和Rect   
def makeText(text, color, bgcolor, top, left):
    textSurf = BASICFONT.render(text, True, color, bgcolor)
    textRect = textSurf.get_rect()
    textRect.topleft = (top, left)
    return (textSurf, textRect)            


# 绘制整个Board            
def drawBoard(board, message):
    
    DISPLAYSURF.fill(BGCOLOR) # 绘制背景 
    
    if message: # 如果有消息,就绘制消息
        textSurf, textRect = makeText(message, MESSAGECOLOR, BGCOLOR, 5, 5)
        DISPLAYSURF.blit(textSurf, textRect) 

    #绘制每一个方块 
    for tilex in range(len(board)):
        for tiley in range(len(board[0])):
            if board[tilex][tiley]:
                drawTile(tilex, tiley, board[tilex][tiley])

    # 绘制Board的边框
    left, top = getLeftTopOfTile(0, 0)
    width = BOARDWIDTH * TILESIZE
    height = BOARDHEIGHT * TILESIZE
    
    pygame.draw.rect(DISPLAYSURF, BORDERCOLOR, (left - 5, top - 5, width + 11, height + 11), 4)

    # 绘制右下角的三个按钮
    DISPLAYSURF.blit(RESET_SURF, RESET_RECT)
    DISPLAYSURF.blit(NEW_SURF, NEW_RECT)
    DISPLAYSURF.blit(SOLVE_SURF, SOLVE_RECT) 
    
# 滑动的动画    
def slideAnimation(board, direction, message, animationSpeed):

    blankx, blanky = getBlankPosition(board)# 获得空白方块的坐标
    
    # 根据方向操作,计算 movex、movey(也就是与之交换的方块的坐标)
    if direction == UP:
        movex = blankx
        movey = blanky + 1
    elif direction == DOWN:
        movex = blankx    
        movey = blanky - 1
    elif direction == LEFT:
        movex = blankx + 1
        movey = blanky
    elif direction == RIGHT:
        movex = blankx - 1
        movey = blanky
        

    drawBoard(board, message) # 显示消息
    
    baseSurf = DISPLAYSURF.copy() # 拷贝一份原来的surface
    
    # 在baseSurf上绘制空白的方块,覆盖在移动的方块上面 
    moveLeft, moveTop = getLeftTopOfTile(movex, movey)
    pygame.draw.rect(baseSurf, BGCOLOR, (moveLeft, moveTop, TILESIZE, TILESIZE))
    
    pygame.display.update()
    FPSCLOCK.tick(FPS)

    # 显示移动的动画
    # range(1,5,2) #代表从1到5,间隔2(不包含5)
    for i in range(0, TILESIZE, animationSpeed):
         
         
        DISPLAYSURF.blit(baseSurf, (0, 0))
        
        if direction == UP:
            drawTile(movex, movey, board[movex][movey], 0, -i)
        if direction == DOWN:
            drawTile(movex, movey, board[movex][movey], 0, i)
        if direction == LEFT:
            drawTile(movex, movey, board[movex][movey], -i, 0)
        if direction == RIGHT:
            drawTile(movex, movey, board[movex][movey], i, 0)
 
        pygame.display.update()
        FPSCLOCK.tick(FPS)
    
    
    
# 生成新的拼图(就是随机打乱已经排序好的board,numSlides为打乱的步数)    
def generateNewPuzzle(numSlides):
    
    sequence = [] # 储存打乱的步数的序列
    
    board = getStartingBoard() # 获得开始board的序列
    
    drawBoard(board, '') # 绘制board
    
    pygame.display.update() # 更新屏幕
    
    pygame.time.wait(500) # 暂停500毫秒 for effect 
    
    lastMove = None
    for i in range(numSlides):
        move = getRandomMove(board, lastMove)# 随机的移动一步    
        slideAnimation(board, move, 'Generating new puzzle...', int(TILESIZE / 3)) # 滑动的动画        
        makeMove(board, move) # 交换空白方块的位置
        sequence.append(move) # 将这个步骤添加到sequence序列中
        lastMove = move
        
    return (board, sequence)
    
# 根据所有操作的的序列,恢复游戏
def resetAnimation(board, allMoves):
    
    revAllMoves = allMoves[:] # 复制一份所有操作的的序列
    
    revAllMoves.reverse() # 倒序排列

    for move in revAllMoves: # 取出每一步,做相反的操作(就能恢复为原来的最初的序列)
        if move == UP: 
            oppositeMove = DOWN
        elif move == DOWN:
            oppositeMove = UP
        elif move == RIGHT:
            oppositeMove = LEFT
        elif move == LEFT:
            oppositeMove = RIGHT
            
        slideAnimation(board, oppositeMove, '', int(TILESIZE / 2)) # 滑动的动画     
        
        makeMove(board, oppositeMove) # 交换空白方块的位置   
    
    
    
if __name__ == '__main__':
    main()    
    
运行游戏:
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容