ساخت بازی لغزنده در پایتون (بخش اول)

Slide Puzzle Game in Python - Part 1

09 آبان 1400
Slide-Puzzle-Game-in-Python---Part-1

پیش گفتار

بازی که در زیر آموزش داده شده است، بازی لغزنده یا Slide Puzzle است. این بازی یکی دیگر از بازی هایی است که در این سری آموزشی یاد می گیریم. بیایید یادگیری را شروع کنیم.

بازی لغزنده (Slide Puzzle)

صفحه بازی یک مربع 4x4 با پانزده خانه (از شماره 1 تا 15 که از چپ به راست هستند) و یک خانه خالی تشکیل شده است. خانه های شماره دار به صورت نامرتب در صفحه قرار می گیرند و بازیکن تا زمانی که خانه ها به ترتیب اصلی خود برنگشته اند، باید خانه ها را حرکت دهد.

کد بازی لغزنده

کد این بازی را می توانید از نشانی اینترنتی http://invpy.com/slidepuzzle.py بارگیری کنید. کد کامل بازی در زیر نیز آمده است.

# Slide Puzzle
# By Al Sweigart al@inventwithpython.com
# http://inventwithpython.com/pygame
# Released under a "Simplified BSD" license

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

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

#                 R    G    B
BLACK = (0,   0,   0)
WHITE = (255, 255, 255)
BRIGHTBLUE = (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)
YMARGIN = int(
    (WINDOWHEIGHT - (TILESIZE * BOARDHEIGHT + (BOARDHEIGHT - 1))) / 2)

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


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

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

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

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

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

        drawBoard(mainBoard, msg)

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

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

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

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

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


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


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


def getStartingBoard():
    # Return a board data structure with tiles in the solved state.
    # For example, if BOARDWIDTH and BOARDHEIGHT are both 3, this function
    # returns [[1, 4, 7], [2, 5, 8], [3, 6, BLANK]]
    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] = BLANK
    return board


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


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

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


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


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

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

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


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


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


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


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


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

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

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

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


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

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

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

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

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


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


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

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


if __name__ == '__main__':
    main()

تنظیم اولیه

بیش تر کدهای برنامه همانند بازی پیشین است، به ویژه ثابت هایی که در آغاز کد تنظیم کردیم.

# Slide Puzzle
# By Al Sweigart al@inventwithpython.com
# http://inventwithpython.com/pygame
# Released under a "Simplified BSD" license

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

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

#                 R    G    B
BLACK = (0,   0,   0)
WHITE = (255, 255, 255)
BRIGHTBLUE = (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)
YMARGIN = int(
    (WINDOWHEIGHT - (TILESIZE * BOARDHEIGHT + (BOARDHEIGHT - 1))) / 2)

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

کدی که در بالا آورده شده است وارد کردن کتابخانه ها و وابستگی های لازم است. همچنین در آن چندین متغییر و ثابت نیز تعریف شده است. اگر دقت کنید این کد بسیار شبیه به کد بازی یادمان است.

تنظیم دکمه ها

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

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

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

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

همانند فصل پیشین، تابع هایی که در تابع main  استفاده می شوند در آخر این قسمت توضیح داده می شوند. اکنون، باید بدانید که این تابع ها چه کاری انجام می دهند و چه مقدار هایی را برمی گردانند. لازم نیست بدانید که چگونه این کارها را انجام می دهد. نخستین قسمت از تابع main برای ساخت پنجره، شی Clock و شی Font است. تابع makeText در بعد در برنامه تعریف شده است، اما اکنون باید بدانید که این تابع یک شی pygame.Surface و pygame.Rect را بازمی گرداند که برای ساختن دکمه های قابل کلیک استفاده می شود. بازی لغزنده سه  دکمه دارد:

  1. دکمه Reset که همه حرکت های بازیکن را بی اثر می کند و بازی را به حالت نخستین خود برمی گرداند.
  2. دکمه New که یک بازی جدید را می سازد.
  3. دکمه Solve که بازی را بدون دخالت بازیکن حل می کند.

برای این بازی به دو ساختمان داده board نیاز داریم. ساختمان داده نخست نمایان گر وضعیت کنونی بازی است. ساختمان داده دیگر خانه های خودش را در حالت "حل شده" یا "solved" خواهد داشت، به این معنی که همه خانه ها به ترتیب شماره مرتب شده اند. هنگامی که صفحه کنونی بازی دقیقا برابر با صفحه حل شده باشد، آن گاه بازیکن برنده شده است. (ساختمان داده دوم را تغییر نمی دهیم و فقط از آن برای مقایسه حالت کنونی بازی با ساختمان داده نخست استفاده می کنیم.)

تابع generateNewPuzzle ساختمان داده board را می سازد که در آغاز مرتب است و سپس 80 حرکت لغزشی تصادفی روی آن انجام می شود (زیرا ما عدد صحیح 80 را به تابع فرستاده ایم.اگر بخواهیم صفحه از این هم، آشفته تر شود، باید عدد بزرگتری را به تابع بفرستیم.) این کار باعث می شود که صفحه ای نامرتب ساخته شود (که در یک متغیر به نام mainBoard ذخیره می شود). تابع generateNewPuzzle همچنین آرایه ای از همه حرکت های تصادفی را که در متغیری به نام solutionSeq ذخیره می شود را برمی گرداند.

زیرک بودن یا احمقانه کد نوشتن ؟!

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

بازی لغزنده واقعا می تواند مشکل باشد. می توانیم کامپیوتر را برای انجام آن برنامه ریزی کنیم، اما رایانه برای انجام این کار به یک الگوریتم نیاز دارد و کشف این الگوریتم بر عهده ما است.پیدا گردن این الگوریتم کاری بسیار سخت خواهد بود و هوشمندی زیادی را می خواهد.

خوشبختانه، روش ساده تری وجود دارد. می توانیم کاری  کنیم که کامپیوتر همه حرکت های تصادفی را هنگام ساختن ساختمان داده board به یاد داشته باشد و سپس با استفاده از حرکت مخالف بتواند بازی را انجام دهد. از آن جا که board در ابتدا در حالت حل شده شروع به کار می کند، به عقب برگرداندن همه حرکت ها آن را به حالت حل شده باز می گرداند.به عنوان نمونه، در زیر خانه شماره 15 را از سمت راست به چپ حرکت می دهیم، و به شکل سمت راست می رسیم:

اگر پس از به سمت راست بردن خانه 15، آن را دوباره به چپ حرکت دهیم، صفحه به حالت اصلی برمی گردد. بنابراین برای بازگشت به حالت اولیه پس از چندین حرکت، فقط باید حرکت های مخالف را به ترتیب وارونه انجام دهیم. اگر دو حرکت به سمت راست و سپس یک حرکت به سمت به  پایین داشته باشیم، آن گاه برای رسیدن به حالت نخست باید یک حرکت رو به بالا و سپس دو حرکت به سمت چپ داشته باشیم تا آن سه حرکت نخست را خنثی کنیم. این کار بسیار ساده تر از نوشتن یک تابع است که می تواند با بررسی وضعیت فعلی آن ها، این معما را حل کند.

حلقه اصلی بازی

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

        drawBoard(mainBoard, msg)

در حلقه اصلی بازی، متغیر slideTo پیگیری می کند که بازیکن از چه جهتی می خواهد خانه را حرکت بدهد (در آغاز حلقه با None مقداردهی می شود و بعد تنظیم می شود) و متغیر msg برای این است که چه رشته ای در بالا پنجره نشان داده شود. برنامه یک بررسی سریع را انجام می دهد تا ببیند آیا ساختمان داده board دارای همان مقدار ساختمان داده SOLVEDBOARD است یا نه. اگر چنین باشد، متغیر msg به رشته '!Solved'  تغییر می یابد. تا زمانی که drawBoard فراخوانی نشده است چیزی روی صفحه نمایش داده نمی شود و pygame.display.update  برای کشیدن شی DISPLAYSURF در صفحه کامپیوتر که در انتهای حلقه بازی است فراخوانی می شود.

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

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

پیش از ورود به حلقه رویداد، برنامه با فراخوانی تابع checkForQuit بررسی می کند آیا رویداد QUIT ایجاد شده است یا نه (و در صورت وجود، برنامه پایان می یابد). این که چرا یک تابع جداگانه checkForQuit برای رسیدگی به رویدادهای QUIT داریم در بعد توضیح داده خواهد شد. حلقه for کد مدیریت رویداد را برای هر رویداد اجرا می کند.

اگر نوع رویداد MOUSEBUTTONUP باشد (یعنی بازیکن در جایی روی پنجره، کلید موس را کلیک کرده باشد)، آن گاه باید مختصات موس را به تابع getSpotClicked بفرستیم که مختصات صفحه یا board را از روی نقطه ای که در آن موس رها شده است برمی گرداند. رویداد event.pos[0] مختصات X و رویداد event.pos[1] مختصات Y است.

اگر رها کردن دکمه موس روی یکی از فضاهای موجود در صفحه رخ ندهد (اما هنوز ممکن است جایی روی پنجره این اتفاق افتاده باشد، به خاطر این که یک رویداد MOUSEBUTTONUP ایجاد شده است)، آن گاه تابع  getSpotClicked ، مقدار None را بر می گرداند. در این صورت، می خواهیم یک بررسی اضافی انجام دهیم تا ببینیم بازیکن روی دکمه های Reset، New یا Solve  (که در صفحه قرار ندارند) کلیک کرده است یا نه.

جای و مختصات دکمه ها در پنجره، در اشیا pygame.Rect ذخیره می شوند.این اشیا در متغیرهای RESET_RECT، NEW_RECT و SOLVE_RECT ذخیره می شوند. می توانیم مختصات موس را از شی Event به متد collidepoint بفرستیم. اگر مختصات موس در ناحیه شی Rect قرار داشته باشد True و در غیر این صورت False را بر می گرداند.

حرکت دادن خانه ها با موس

else:
    # check if the clicked tile was next to the blank spot
    blankx, blanky = getBlankPosition(mainBoard)
    if spotx == blankx + 1 and spoty == blanky:
        slideTo = LEFT
    elif spotx == blankx - 1 and spoty == blanky:
        slideTo = RIGHT
    elif spotx == blankx and spoty == blanky + 1:
        slideTo = UP
    elif spotx == blankx and spoty == blanky - 1:
        slideTo = DOWN

اگرgetSpotClicked مقدار (None,None) را برنگرداند، آن گاه یک تاپل از دو مقدار صحیح که نشانگر مختصات X و Y نقطه ای که کلیک شده است را بر می گرداند. سپس عبارات if و elif در خطوط 89 تا 96 بررسی می کنند که آیا نقطه ای که بر روی آن کلیک شده خانه ای خالی در کنار خود دارد یا نه (در غیر این صورت خانه جایی برای لغزیدن ندارد). تابع getBlankPosition ساختمان داده board را می گیرد و مختصات X و Y صفحه یا board نقاط خالی را بر می گرداند، که در متغیرهای blankx و blanky ذخیره می شوند. اگر نقطه ای که کاربر روی آن کلیک کرده بود در کنار فضای خالی بود، متغیر slideTo را با مقدارخانه ای که باید بلغزد، تنظیم می کنیم.

حرکت دادن خانه ها با صفحه کلید

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

همچنین می توان با فشار دادن کلیدهای صفحه کلید، خانه ها را به حرکت درآوریم. عبارات if و elif به کاربر اجازه می دهد متغیر slideTo را با فشار دادن کلیدهای پیکانی یا کلیدهای WASD تنظیم کند (بعد توضیح داده شده است). همچنین هر عبارت if و elif تابع isValidMove را فراخوانی می کند تا اطمینان پیدا کند که خانه می تواند در آن جهت حرکت کند. (مجبور نیستیم با کلیک موس این فراخوانی را انجام دهیم زیرا بررسی های فضای خالی همسایه همین کار را انجام می دهد.)

حرفه ای تر کد نوشتن

عبارت event.key در (K_LEFT, K_a)  یک ترفند پایتون برای ساده تر کردن کد است.یعنی اگر یکی از دو عبارت (K_LEFT, K_a)  برابر با True بودند آن گاه event.key را برابر با True کن. دو عبارت زیر دقیقا به همین روش ارزیابی می شوند:

event.key in (K_LEFT, K_a)

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

WASD و کلیدهای پیکانی

کلیدهای WASD (که به طور خلاصه وازدی نامیده می شوند) معمولا در بازی های رایانه ای مورد استفاده قرار می گیرند تا همان کاری که کلیدهای پیکانی انجام می دهند را انجام بدهند، با این تفاوت که بازیکن با دست چپ (از آن جا که کلیدهای WASD در سمت چپ صفحه کلید هستند) از آن ها استفاده می کند. W برای بالا، A برای سمت چپ، S برای پایین و D برای سمت راست است. به راحتی می توانید این را به خاطر بسپارید زیرا کلیدهای WASD همان طرح کلیدهای پیکان را دارند :

 

حرکت دادن خانه ها

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

اکنون که همه رویدادها به دست آمده اند، باید متغیرهای حالت بازی یا state بازی را به روز کنیم و سپس آن را روی صفحه نمایش دهیم. اگر slideTo مقداری داشته باشد (با کد رویداد موس یا با رویداد صفحه کلید) می توانیم slideAnimation را برای انجام پویانمایی لغزیدن فراخوانی کنیم. پارامترهای آن 1- ساختمان داده board یا صفحه،2- جهت لغزیدن، 3- پیامی برای نمایش در هنگام لغزیدن خانه ها و 4- سرعت لغزش هستند.

پس از برگشت، باید ساختمان داده واقعی board را بروزرسانی کنیم (که توسط تابع  makeMove انجام می شود) و سپس حرکت ها را به آرایه allMoves که در بردارنده ی همه حرکت ها تا زمان حاضر است بیافزاییم. این کار به این صورت انجام می شود که اگر بازیکن روی دکمه Reset کلیک کند، می دانیم چگونه می توان همه حرکت های بازیکن را خنثی سازی کرد.

پایان دادن به بازی

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

این کد تابع pygame.quit  و  sys.exit را فراخوانی می کند. این تابع به نوعی از شکر نگارش استفاده می کند و به جای این که دو تابع را به طور جداگانه فراخوانی کنیم، آن ها را تنها با یک بار فراخوانی کردن مورد استفاده قرار می دهیم.

یافتن یک رویداد ویژه و افزودن آن به صف رویداد

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

تابع  checkForQuit رویدادهای QUIT و فشرده شدن کلید Esc را بررسی می کند و سپس تابع  terminate را فراخوانی می کند. اما یافتن این رویدادها کمی پیچیده هستند و نیاز به روشن شدن دارند. pygame ساختمان داده آرایه خود را دارد و اشیا رویداد یا Event objects را به همان صورت که ساخته می شوند، به آرایه پیوست می کند. این ساختمان داده را صف رویداد می نامند. هنگامی که تابع pygame.event.get بدون پارامتر فراخوانی می شود، کل آرایه بازگردانده می شود. با این حال، می توان فقط ثابت هایی مانند QUIT را به pygame.event.get فرستاد، تا فقط رویدادهای QUIT در صورت وجود برگرداننده شوند. بقیه رویدادها برای فراخوانی های بعدی pygame.event.get در صف رویداد خواهند ماند.

باید یادآوری شود که صف رویداد Pygame فقط 127 رویداد را ذخیره می کند. اگر برنامه شما pygame.event.get را به اندازه کافی فراخوانی نکند و صف پر  شود، آن گاه هیچ کدام از رویدادهای جدیدی که ساخته می شوند به صف رویداد افزوده نمی شوند. در صورت وقوع رویداد های QUIT در صف رویداد، برنامه پایان می یابد.

خط 125 تمام رویدادهای KEYUP را از صف رویداد خارج می کند و بررسی می کند آیا هیچ کدام از آن ها مربوط به کلید Esc است یا نه. اگر یکی از رویدادهای صف رویداد KEYUP باشد، برنامه پایان می یابد. با این وجود، ممکن است برای کلیدهای دیگری غیر از کلید Esc، رویدادهای KEYUP وجود داشته باشد. در این حالت، ما باید رویداد KEYUP را دوباره در صف رویداد Pygame قرار دهیم. می توانیم این کار را با تابع pygame.event.post انجام دهیم، که شی رویداد فرستاده شده به آن را به انتهای صف رویداد Pygame می افزاید. به این ترتیب، هنگامی که pygame.event.get فراخوانی می شود رویداد های KEYUP که Esc نیستند هنوز هم وجود خواهند داشت. در غیر این صورت فراخوانی checkForQuit همه رویدادهای KEYUP را به حساب می آورد و هرگز از این رویدادها استفاده نمی شود.تابع pygame.event.post نیز در صورتی مفید است که برنامه بخواهد اشیا رویداد را به صف رویداد Pygame بیافزاید.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری بازی‌سازی با پایتون توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.