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

Making Wormy Game with Python - Part 1

10 آبان 1400
Making-Wormy-Game-with-Python---Part-1

پیش گفتار

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

کد بازی مار

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

# Wormy (a Nibbles clone)
# By Al Sweigart al@inventwithpython.com
# http://inventwithpython.com/pygame
# Released under a "Simplified BSD" license

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

FPS = 10
WINDOWWIDTH = 640
WINDOWHEIGHT = 480
CELLSIZE = 20
assert WINDOWWIDTH % CELLSIZE == 0, "Window width must be a multiple of cell size."
assert WINDOWHEIGHT % CELLSIZE == 0, "Window height must be a multiple of cell size."
CELLWIDTH = int(WINDOWWIDTH / CELLSIZE)
CELLHEIGHT = int(WINDOWHEIGHT / CELLSIZE)

#             R    G    B
WHITE = (255, 255, 255)
BLACK = (0,   0,   0)
RED = (255,   0,   0)
GREEN = (0, 255,   0)
DARKGREEN = (0, 155,   0)
DARKGRAY = (40,  40,  40)
BGCOLOR = BLACK

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

HEAD = 0  # syntactic sugar: index of the worm's head


def main():
    global FPSCLOCK, DISPLAYSURF, BASICFONT

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

    showStartScreen()
    while True:
        runGame()
        showGameOverScreen()


def runGame():
    # Set a random start point.
    startx = random.randint(5, CELLWIDTH - 6)
    starty = random.randint(5, CELLHEIGHT - 6)
    wormCoords = [{'x': startx,     'y': starty},
                  {'x': startx - 1, 'y': starty},
                  {'x': startx - 2, 'y': starty}]
    direction = RIGHT

    # Start the apple in a random place.
    apple = getRandomLocation()

    while True:  # main game loop
        for event in pygame.event.get():  # event handling loop
            if event.type == QUIT:
                terminate()
            elif event.type == KEYDOWN:
                if (event.key == K_LEFT or event.key == K_a) and direction != RIGHT:
                    direction = LEFT
                elif (event.key == K_RIGHT or event.key == K_d) and direction != LEFT:
                    direction = RIGHT
                elif (event.key == K_UP or event.key == K_w) and direction != DOWN:
                    direction = UP
                elif (event.key == K_DOWN or event.key == K_s) and direction != UP:
                    direction = DOWN
                elif event.key == K_ESCAPE:
                    terminate()

        # check if the worm has hit itself or the edge
        if wormCoords[HEAD]['x'] == -1 or wormCoords[HEAD]['x'] == CELLWIDTH or wormCoords[HEAD]['y'] == -1 or wormCoords[HEAD]['y'] == CELLHEIGHT:
            return  # game over
        for wormBody in wormCoords[1:]:
            if wormBody['x'] == wormCoords[HEAD]['x'] and wormBody['y'] == wormCoords[HEAD]['y']:
                return  # game over

        # check if worm has eaten an apply
        if wormCoords[HEAD]['x'] == apple['x'] and wormCoords[HEAD]['y'] == apple['y']:
            # don't remove worm's tail segment
            apple = getRandomLocation()  # set a new apple somewhere
        else:
            del wormCoords[-1]  # remove worm's tail segment

        # move the worm by adding a segment in the direction it is moving
        if direction == UP:
            newHead = {'x': wormCoords[HEAD]['x'],
                       'y': wormCoords[HEAD]['y'] - 1}
        elif direction == DOWN:
            newHead = {'x': wormCoords[HEAD]['x'],
                       'y': wormCoords[HEAD]['y'] + 1}
        elif direction == LEFT:
            newHead = {'x': wormCoords[HEAD]
                       ['x'] - 1, 'y': wormCoords[HEAD]['y']}
        elif direction == RIGHT:
            newHead = {'x': wormCoords[HEAD]
                       ['x'] + 1, 'y': wormCoords[HEAD]['y']}
        wormCoords.insert(0, newHead)
        DISPLAYSURF.fill(BGCOLOR)
        drawGrid()
        drawWorm(wormCoords)
        drawApple(apple)
        drawScore(len(wormCoords) - 3)
        pygame.display.update()
        FPSCLOCK.tick(FPS)


def drawPressKeyMsg():
    pressKeySurf = BASICFONT.render('Press a key to play.', True, DARKGRAY)
    pressKeyRect = pressKeySurf.get_rect()
    pressKeyRect.topleft = (WINDOWWIDTH - 200, WINDOWHEIGHT - 30)
    DISPLAYSURF.blit(pressKeySurf, pressKeyRect)


def checkForKeyPress():
    if len(pygame.event.get(QUIT)) > 0:
        terminate()

    keyUpEvents = pygame.event.get(KEYUP)
    if len(keyUpEvents) == 0:
        return None
    if keyUpEvents[0].key == K_ESCAPE:
        terminate()
    return keyUpEvents[0].key


def showStartScreen():
    titleFont = pygame.font.Font('freesansbold.ttf', 100)
    titleSurf1 = titleFont.render('Wormy!', True, WHITE, DARKGREEN)
    titleSurf2 = titleFont.render('Wormy!', True, GREEN)

    degrees1 = 0
    degrees2 = 0
    while True:
        DISPLAYSURF.fill(BGCOLOR)
        rotatedSurf1 = pygame.transform.rotate(titleSurf1, degrees1)
        rotatedRect1 = rotatedSurf1.get_rect()
        rotatedRect1.center = (WINDOWWIDTH / 2, WINDOWHEIGHT / 2)
        DISPLAYSURF.blit(rotatedSurf1, rotatedRect1)

        rotatedSurf2 = pygame.transform.rotate(titleSurf2, degrees2)
        rotatedRect2 = rotatedSurf2.get_rect()
        rotatedRect2.center = (WINDOWWIDTH / 2, WINDOWHEIGHT / 2)
        DISPLAYSURF.blit(rotatedSurf2, rotatedRect2)

        drawPressKeyMsg()

        if checkForKeyPress():
            pygame.event.get()  # clear event queue
            return
        pygame.display.update()
        FPSCLOCK.tick(FPS)
        degrees1 += 3  # rotate by 3 degrees each frame
        degrees2 += 7  # rotate by 7 degrees each frame


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


def getRandomLocation():
    return {'x': random.randint(0, CELLWIDTH - 1), 'y': random.randint(0, CELLHEIGHT - 1)}


def showGameOverScreen():
    gameOverFont = pygame.font.Font('freesansbold.ttf', 150)
    gameSurf = gameOverFont.render('Game', True, WHITE)
    overSurf = gameOverFont.render('Over', True, WHITE)
    gameRect = gameSurf.get_rect()
    overRect = overSurf.get_rect()
    gameRect.midtop = (WINDOWWIDTH / 2, 10)
    overRect.midtop = (WINDOWWIDTH / 2, gameRect.height + 10 + 25)

    DISPLAYSURF.blit(gameSurf, gameRect)
    DISPLAYSURF.blit(overSurf, overRect)
    drawPressKeyMsg()
    pygame.display.update()
    pygame.time.wait(500)
    checkForKeyPress()  # clear out any key presses in the event queue

    while True:
        if checkForKeyPress():
            pygame.event.get()  # clear event queue
            return


def drawScore(score):
    scoreSurf = BASICFONT.render('Score: %s' % (score), True, WHITE)
    scoreRect = scoreSurf.get_rect()
    scoreRect.topleft = (WINDOWWIDTH - 120, 10)
    DISPLAYSURF.blit(scoreSurf, scoreRect)


def drawWorm(wormCoords):
    for coord in wormCoords:
        x = coord['x'] * CELLSIZE
        y = coord['y'] * CELLSIZE
        wormSegmentRect = pygame.Rect(x, y, CELLSIZE, CELLSIZE)
        pygame.draw.rect(DISPLAYSURF, DARKGREEN, wormSegmentRect)
        wormInnerSegmentRect = pygame.Rect(
            x + 4, y + 4, CELLSIZE - 8, CELLSIZE - 8)
        pygame.draw.rect(DISPLAYSURF, GREEN, wormInnerSegmentRect)


def drawApple(coord):
    x = coord['x'] * CELLSIZE
    y = coord['y'] * CELLSIZE
    appleRect = pygame.Rect(x, y, CELLSIZE, CELLSIZE)
    pygame.draw.rect(DISPLAYSURF, RED, appleRect)


def drawGrid():
    for x in range(0, WINDOWWIDTH, CELLSIZE):  # draw vertical lines
        pygame.draw.line(DISPLAYSURF, DARKGRAY, (x, 0), (x, WINDOWHEIGHT))
    for y in range(0, WINDOWHEIGHT, CELLSIZE):  # draw horizontal lines
        pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, y), (WINDOWWIDTH, y))


if __name__ == '__main__':
    main()

شبکه بندی

اگر بازی را انجام دهید، متوجه خواهید شد که سیب و بدن مار همیشه در یک شبکه از خط ها قرار می گیرند. مربع های موجود در این شبکه را یک سلول می نامیم.سلول ها دارای سیستم مختصات دکارتی خود هستند، که (0، 0) سلول بالایی چپ و (31, 23) سلول پایین سمت راست است.

تنظیمات اولیه

# Wormy (a Nibbles clone)
# By Al Sweigart al@inventwithpython.com
# http://inventwithpython.com/pygame
# Released under a "Simplified BSD" license

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

FPS = 10
WINDOWWIDTH = 640
WINDOWHEIGHT = 480
CELLSIZE = 20
assert WINDOWWIDTH % CELLSIZE == 0, "Window width must be a multiple of cell size."
assert WINDOWHEIGHT % CELLSIZE == 0, "Window height must be a multiple of cell size."
CELLWIDTH = int(WINDOWWIDTH / CELLSIZE)
CELLHEIGHT = int(WINDOWHEIGHT / CELLSIZE)

آغاز برنامه برای تنظیم برخی از ثابت ها است. طول و عرض سلول ها در CELLSIZE ذخیره می شود. assert ها اطمینان حاصل می کنند که سلول ها کاملا در پنجره قرار می گیرند. به عنوان نمونه، اگر  CELLSIZE، دارای مقدار 10 باشد و ثابت های WINDOWWIDTH یا WINDOWHEIGHT، دارای مقدار 15  باشند، آن گاه فقط 1.5 سلول در صفحه می توانند جای بگیرند. assert ها اطمینان حاصل می کنند که فقط تعداد صحیحی از سلول ها در پنجره جای می گیرند.

#             R    G    B
WHITE = (255, 255, 255)
BLACK = (0,   0,   0)
RED = (255,   0,   0)
GREEN = (0, 255,   0)
DARKGREEN = (0, 155,   0)
DARKGRAY = (40,  40,  40)
BGCOLOR = BLACK

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

HEAD = 0  # syntactic sugar: index of the worm's head

تابع main

def main():
    global FPSCLOCK, DISPLAYSURF, BASICFONT

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

    showStartScreen()
    while True:
        runGame()
        showGameOverScreen()

در بازی wormy، قسمت اصلی کد را در تابعی به نام runGame قرار داده ایم. دلیل این کار این است که می خواهیم صفحه آغاز بازی یا "start screen" را تنها در شروع بازی نشان دهیم (این صفحه یک پویانمایی با نوشته Wormy است و این کار را با فراخوانی تابع showStartScreen انجام می دهیم). سپس می خواهیم تابع runGame را فراخوانی کنیم تا بازی Wormy را آغاز کند. این تابع هنگامی برمی گردد که مار به یک دیوار یا خودش بخورد و باعث پایان بازی شود. در این مرحله با فراخوانی showGameOverScreen بازی تمام است یا game over را روی صفحه نمایش نشان خواهیم داد. هنگامی که فراخوانی تابع بازگردد، کنترل حلقه به آغاز برمی گردد و دوباره runGame را فراخوانی می کند. حلقه for در خط 44 تا پایان برنامه ادامه می یابد.

تابع runGame

def runGame():
    # Set a random start point.
    startx = random.randint(5, CELLWIDTH - 6)
    starty = random.randint(5, CELLHEIGHT - 6)
    wormCoords = [{'x': startx,     'y': starty},
                  {'x': startx - 1, 'y': starty},
                  {'x': startx - 2, 'y': starty}]
    direction = RIGHT

    # Start the apple in a random place.
    apple = getRandomLocation()

در آغاز بازی، می خواهیم مار در یک موقعیت تصادفی شروع به حرکت کند (اما خیلی به لبه های صفحه نزدیک نباشد) بنابراین یک مختصات تصادفی را در startx و starty ذخیره می کنیم. (به یاد داشته باشید که CELLWIDTH و CELLHEIGHT تعداد سلول هایی که در دو طرف پنجره قرار می گیرند، نه عرض و طول  پیکسل ها). بدن مار در یک دیکشنری ذخیره می شود. برای هر قسمت از بدن مار یک مقدار از دیکشنری وجود خواهد داشت. دیکشنری کلیدهای 'x'  و 'y' را برای مختصات XY بخشی از بدن مار درنظر می گیرد. سر مار در startx  و starty خواهد بود. دو بخش دیگر بدن دو سلول در سمت چپ سر خواهند بود. سر مار همیشه در wormCoords[0] خواهد بود. برای خوانایی بیش تر این کد، ثابت HEAD را در خط 32 با 0 مقداردهی می کنیم تا بتوانیم به جای wormCoords[0] از wormCoords[HEAD] استفاده کنیم.

حلقه مدیریت رویداد

while True:  # main game loop
        for event in pygame.event.get():  # event handling loop
            if event.type == QUIT:
                terminate()
            elif event.type == KEYDOWN:
                if (event.key == K_LEFT or event.key == K_a) and direction != RIGHT:
                    direction = LEFT
                elif (event.key == K_RIGHT or event.key == K_d) and direction != LEFT:
                    direction = RIGHT
                elif (event.key == K_UP or event.key == K_w) and direction != DOWN:
                    direction = UP
                elif (event.key == K_DOWN or event.key == K_s) and direction != UP:
                    direction = DOWN
                elif event.key == K_ESCAPE:
                    terminate()

خط 61 آغاز حلقه اصلی بازی و خط 62 آغاز حلقه رویداد است. اگر رویداد QUIT باشد، آن گاه terminate را فراخوانی می کنیم (که همانند تابع terminate در بازی های پیشین تعریف می شود). در غیر این صورت، اگر رویداد KEYDOWN باشد، بررسی می کنیم که آیا کلید فشرده شده کلید پیکانی یا کلید WASD است یا خیر. یک بررسی اضافی می خواهیم تا بررسی کند که آیا کرم به خودش بر می خورد یا نه. به عنوان نمونه، اگر کرم به سمت چپ حرکت کند و در همان حال بازیکن به طور تصادفی کلید راست را فشار دهد، کرم بلافاصله شروع به رفتن به سمت راست می کند و کرم با خود برخورد می کند.به همین دلیل است که این بررسی را برای مقدار فعلی متغیر direction  داریم. به این ترتیب، اگر بازیکن به طور ناگهانی با فشار دادن کلید باعث شود که کرم برخورد داشته باشد، فشار داده شدن کلید را نادیده می گیریم.

پیدا کردن برخورد

# check if the worm has hit itself or the edge
if wormCoords[HEAD]['x'] == -1 or wormCoords[HEAD]['x'] == CELLWIDTH or wormCoords[HEAD]['y'] == -1 or wormCoords[HEAD]['y'] == CELLHEIGHT:
    return  # game over
for wormBody in wormCoords[1:]:
    if wormBody['x'] == wormCoords[HEAD]['x'] and wormBody['y'] == wormCoords[HEAD]['y']:
        return  # game over

هنگامی که سر از شبکه خارج شود یا بر روی سلولی که از پیش توسط بخش دیگری از بدن اشغال شده است حرکت کند، آن گاه مار برخورد داشته است. با دیدن این که مختصات X سر (که در wormCoords [HEAD] ['x'] ذخیره شده است) می توانیم بررسی کنیم که آیا سر از لبه شبکه رد شده است (که از لبه سمت چپ شبکه گذشته است) یا برابر با CELLWIDTH  است (که از لبه سمت راست گذشته است، زیرا بیش ترین مختصات سلول X یکی کمتر از CELLWIDTH است).

اگر مختصات Y سر (که در wormCoords [HEAD] ['y'] ذخیره شده است) 1- باشد سر از لبه شبکه رد شده است (که از لبه بالایی گذشته است) یا CELLHEIGHT (که از انتهای آن گذشته است). تنها کاری که باید برای پایان دادن به بازی کنونی انجام دهیم، بازگشت از runGame است. هنگامی که runGame() به فراخوانی تابع در main بازگردد، خطی که پس از runGame است فراخوانی می شود که خود این خط تابع  showGameOverScreen را فراخوانی می کند و باعث پدیدار شدن متن بزرگ Game Over در صفحه می شود. به همین دلیل از return در خط 79 استفاده می کنیم. خط 80 در هر بخش بدن در wormCoords که پس از head قرار دارد حلقه می زند.خط 80 در هر بخش بدن حلقه می زند. سر در اندیس 0 قرار دارد به همین دلیل حلقه for به جای wormCoords روی wormCoords [1:] تکرار می شود. اگر مقادیر  'x' و  'y' بدن برابر با 'x' و  'y' سر باشند، پس با بازگشت از تابع runGame به بازی پایان می دهیم.

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

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

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