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

Memory Game - Part 3

07 آبان 1400
Memory-Game---Part-3

پیش گفتار

در این قسمت بازی یادمان را به پایان می بریم.در قسمت بعدی بازی slide game یا لغزنده را خواهیم داشت.

سیستم های مختصات گوناگون

def leftTopCoordsOfBox(boxx, boxy):
    # Convert board coordinates to pixel coordinates
    left = boxx * (BOXSIZE + GAPSIZE) + XMARGIN
    top = boxy * (BOXSIZE + GAPSIZE) + YMARGIN
    return (left, top)

در بخش های پیشین با سیستم مختصات دکارتی و پیکسلی آشنا شدید. (اگر می خواهید دانش خود را در این زمینه گسترش دهید، به نشانی اینترنتی  http://invpy.com/coordinates سر بزنید).در این بازی از دو سیستم مختصات استفاده می کنیم.یکی از سیستم های مختصات که در یادمان استفاده می شود مختصات پیکسلی است. اما هم چنین از سیستم مختصات دیگری برای جعبه ها استفاده خواهیم کرد. این کار به خاطر این است که استفاده از (3, 2) که به جعبه چهارم از سمت چپ و جعبه سوم از بالا اشاره می کند (به یاد داشته باشید که اعداد از 0 شروع می شوند نه از 1) از مختصات پیکسلی (220، 165) آسان تر است. با این حال، ما به راهی برای تبدیل این دو سیستم به یکدیگر نیاز داریم.به یاد داشته باشید که پنجره 640 پیکسل طول و 480 پیکسل عرض دارد، بنابراین (639, 479)  گوشه پایین سمت راست است زیرا پیکسل گوشه بالا سمت چپ (0، 0) است و نه (1، 1).

 

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

تبدیل از مختصات پیکسلی به مختصات جعبه ای

def getBoxAtPixel(x, y):
    for boxx in range(BOARDWIDTH):
        for boxy in range(BOARDHEIGHT):
            left, top = leftTopCoordsOfBox(boxx, boxy)
            boxRect = pygame.Rect(left, top, BOXSIZE, BOXSIZE)
            if boxRect.collidepoint(x, y):
                return (boxx, boxy)
    return (None, None)

ما هم چنین به یک تابع نیاز داریم تا مختصات پیکسلی را به مختصات جعبه ای تبدیل کند.این تابع از کلیک موس و رویدادهای حرکت موس استفاده می کند. اشیا Rect دارای یک متد collidepoint هستند که مختصات X و Y را می گیرد.اگر X و Y در داخل شی Rect قرار گیرند یعنی با هم برخورد کنند، True را برمی گرداند.

برای این که دریابیم موس بر روی کدام جعبه است، از متد collidepoint  استفاده می کنیم و مختصات هر جعبه را به متد collidepoint می فرستیم و سپس آن را برای یک شی Rect فراخوانی می کنیم. هنگامی که collidepoint مقدار True را برمی گرداند، آن گاه جعبه ای که روی آن کلیک شده یا موس روی آن حرکت کرده را پیدا کرده ایم و سپس مختصات جعبه را باز می گردانیم. اگر هیچ یک از فراخوانی ها برای جعبه ها True را برنگرداند،تابع getBoxAtPixel مقدار (None, None)را برمی گرداند.

کشیدن شکل و شکر دستور (Syntactic Sugar)

def drawIcon(shape, color, boxx, boxy):
    quarter = int(BOXSIZE * 0.25)  # syntactic sugar
    half = int(BOXSIZE * 0.5)  # syntactic sugar

    # get pixel coords from board coords
    left, top = leftTopCoordsOfBox(boxx, boxy)

تابع drawIcon یک شکل را (با شکل و رنگ مشخص شده) که مختصات آن در پارامترهای boxx و boxx آورده است، می کشد.برای هر شکل ممکن مجموعه متفاوتی از فراخوانی توابع ترسیم Pygame را خواهیم داشت، بنابراین باید از عبارات if و elif استفاده کنیم تا بتوانیم بین شکال های گوناگون تفاوت قائل شویم. (این عبارات در خط های 187 تا 198 هستند).مختصات X و Y گوشه سمت چپ و بالای جعبه را می توان با فراخوانی تابع leftTopCoordsOfBox به دست آورد.طول و عرض جعبه در ثابت BOXSIZE مقدار دهی شده است. بسیاری از توابع ترسیم از midpoint(نقطه میانی) و quarter-point (چهار نقطه اطراف شکل)استفاده می کنند. ما می توانیم این مقدارها را محاسبه کرده و آن را در متغیرهای quarter و half ذخیره کنیم. به راحتی می توانیم به جای کد int(BOXSIZE * 0.25) متغیر quarter را داشته باشیم.با این کار خوانایی کد بیش تر می شود.

چنین متغیرهایی نمونه ای از شکر دستور هستند. syntactic sugar یعنی از کدی استفاده کنیم که می توانست به روشی دیگر نوشته شود. متغیرهای ثابت یک نوع syntactic sugar هستند.محاسبه دقیق یک مقدار و ذخیره آن در یک متغیر نوع دیگری از شکر دستور است.به عنوان نمونه، در تابع getRandomizedBoard، می توانستیم به راحتی کد خطوط 140 و 141 را در یک خط قرار دهیم. اما خواندن آن به عنوان دو خط جداگانه آسان تر است. به متغیرهای اضافی quarter و half نیازی نداریم اما داشتن آن ها خواندن کد را آسان تر می کند. کدی که خواندن آن آسان تر است، اشکال زدایی و ویرایش آن نیز آسان تر خواهد کرد.

# Draw the shapes
if shape == DONUT:
    pygame.draw.circle(DISPLAYSURF, color,
                       (left + half, top + half), half - 5)
    pygame.draw.circle(DISPLAYSURF, BGCOLOR,
                       (left + half, top + half), quarter - 5)
elif shape == SQUARE:
    pygame.draw.rect(DISPLAYSURF, color, (left + quarter,
                                          top + quarter, BOXSIZE - half, BOXSIZE - half))
elif shape == DIAMOND:
    pygame.draw.polygon(DISPLAYSURF, color, ((left + half, top), (left + BOXSIZE - 1,
                                                                  top + half), (left + half, top + BOXSIZE - 1), (left, top + half)))
elif shape == LINES:
    for i in range(0, BOXSIZE, 4):
        pygame.draw.line(DISPLAYSURF, color,
                         (left, top + i), (left + i, top))
        pygame.draw.line(DISPLAYSURF, color, (left + i,
                                              top + BOXSIZE - 1), (left + BOXSIZE - 1, top + i))
elif shape == OVAL:
    pygame.draw.ellipse(DISPLAYSURF, color,
                        (left, top + quarter, BOXSIZE, half))

هر یک از شکل های donut، square، diamond، lines و oval به فراخوانی تابع های رسم نیاز دارند که کد آن ها در بالا آمده است.

به دست آوردن شکل و رنگ با شکر دستور

def getShapeAndColor(board, boxx, boxy):
    # shape value for x, y spot is stored in board[x][y][0]
    # color value for x, y spot is stored in board[x][y][1]
    return board[boxx][boxy][0], board[boxx][boxy][1]

تابع  getShapeAndColor فقط یک خط دارد.این تابع سه پارامتر board، boxx و boxy را می گیرد و رنگ و نوع شکل را برمی گرداند.

کشیدن پوشش جعبه

def drawBoxCovers(board, boxes, coverage):
    # Draws boxes being covered/revealed. "boxes" is a list
    # of two-item lists, which have the x & y spot of the box.
    for box in boxes:
        left, top = leftTopCoordsOfBox(box[0], box[1])
        pygame.draw.rect(DISPLAYSURF, BGCOLOR, (left, top, BOXSIZE, BOXSIZE))
        shape, color = getShapeAndColor(board, box[0], box[1])
        drawIcon(shape, color, box[0], box[1])
        if coverage > 0:  # only draw the cover if there is an coverage
            pygame.draw.rect(DISPLAYSURF, BOXCOLOR,
                             (left, top, coverage, BOXSIZE))
    pygame.display.update()
    FPSCLOCK.tick(FPS)

تابع drawBoxCovers سه پارامتر دارد:

  1. ساختمان داده board
  2. لیستی از تاپل های (X, Y) برای جعبه هایی که باید پوشیده شوند
  3.  میزان پوشیدگی برای جعبه ها.

از آن جا که می خواهیم از همان کد ترسیم برای هر جعبه استفاده کنیم، از یک حلقه for استفاده می کنیم.در داخل این حلقه for، سه کار باید انجام شود : رنگ پس زمینه را کشیده شود (برای پوشاندن هر چیزی که از پیش در آن جا بوده است )، شکل را بکشد، سپس به اندازه ای که نیاز است جعبه با رنگ سفید پوشیده شود. تابع leftTopCoordsOfBox مختصات پیکسل گوشه بالا سمت چپ جعبه را برمی گرداند. عبارت if در خط 216 بررسی می کند اگر coverage کمتر از 0 باشد، تابع pygame.draw.rect فراخوانی نشود. هنگامی که coverage صفر باشد، شکل زیر آن پوشیده نمی شود. اگر coverage بیست باشد، یک جعبه سفید با اندازه 20 پیکسل شکل را می پوشاند. بیشترین اندازه ای که coverage دارد، عدد BOXSIZE است. در این حالت شکل ها به طور کامل پوشیده می شوند. تابع drawBoxCovers در حلقه جداگانه ای فراخوانی می شود. به همین دلیل، برای نمایش پویانمایی باید توابع pygame.display.update و FPSCLOCK.tick(FPS) را دوباره فراخوانی کنیم.

کار با پویانمایی (انیمیشن) آشکارسازی و پنهان سازی

def revealBoxesAnimation(board, boxesToReveal):
    # Do the "box reveal" animation.
    for coverage in range(BOXSIZE, (-REVEALSPEED) - 1, -REVEALSPEED):
        drawBoxCovers(board, boxesToReveal, coverage)


def coverBoxesAnimation(board, boxesToCover):
    # Do the "box cover" animation.
    for coverage in range(0, BOXSIZE + REVEALSPEED, REVEALSPEED):
        drawBoxCovers(board, boxesToCover, coverage)

حتما می دانید که پویانمایی نمایش پشت سر هم و کوتاه تصاویر مختلف است. حرکات سریع تصاویر این گمان را در بیننده ایجاد می کند، که چیزهایی روی صفحه نمایش در حال حرکت هستند. توابع revealBoxesAnimation و coverBoxesAnimation فقط باید با استفاده از جعبه سفید یک شکل با مقدار متغیر coverage بکشند. ما می توانیم یک تابع یکتا به نام drawBoxCovers بنویسیم که می تواند این کار را انجام دهد، و سپس با استفاده از تابع drawBoxCovers برای هر فریم انیمیشن فراخوانی شود. همان طور که در بخش پیشین دیدیم، drawBoxCovers خود pygame.display.update و FPSCLOCK.tick(FPS) را فراخوانی می کند. برای انجام این کار،ما یک حلقه for ایجاد می کنیم تا کاهش دهد (در مورد revealBoxesAnimation) یا افزایش دهد (در مورد coverBoxesAnimation) پارامتر coverage را. مقداری که متغیر converage کاهش یا افزایش می دهد عدد ثابت REVEALSPEED است. در خط 12 این ثابت را روی 8 قرار می دهیم، به این معنی که در هر فرخوانی تابع drawBoxCovers، جعبه سفید در هر تکرار 8 پیکسل کاهش/افزایش می یابد. اگر این تعداد را افزایش دهیم، در هر فراخوانی پیکسل های بیشتری کشیده می شود، به این معنی که جعبه سفید در اندازه سریعتر کاهش/افزایش می یابد. اگر آن را بر روی 1 تنظیم کنیم، به نظر می رسد که جعبه سفید فقط در هر تکرار با 1 پیکسل کاهش یا افزایش می یابد و باعث می شود اجرای انیمیشن آشکارسازی یا پوشش بیشتر طول بکشد. مانند بالا رفتن از پله ها به آن فکر کنید. اگر در هر گام یک پله به جلو روید، در این صورت یک زمان معمولی برای بالا رفتن از کل راه پله لازم است. اما اگر در هر گام دو پله بالا بروید (و پله ها دقیقا مثل گذشته باشند)، می توانستید دو برابر سریعتر از کل پله ها بالا بروید. اگر می توانستید از در هر گام 8 پله بالا بروید، آن گاه 8 برابر این کار را انجام می دادید.

کشیدن همه صفحه

def drawBoard(board, revealed):
    # Draws all of the boxes in their covered or revealed state.
    for boxx in range(BOARDWIDTH):
        for boxy in range(BOARDHEIGHT):
            left, top = leftTopCoordsOfBox(boxx, boxy)
            if not revealed[boxx][boxy]:
                # Draw a covered box.
                pygame.draw.rect(DISPLAYSURF, BOXCOLOR,
                                 (left, top, BOXSIZE, BOXSIZE))
            else:
                # Draw the (revealed) icon.
                shape, color = getShapeAndColor(board, boxx, boxy)
                drawIcon(shape, color, boxx, boxy)

تابع drawBoard برای هر یک از جعبه های موجود در صفحه،drawIcon را فراخوانی می کند. حلقه های تو در تو در خطوط 236 و 237 برای هر مختصات ممکن X و Y برای جعبه ها حلقه را  تکرار می کنند، یا شکل را در آن مکان ترسیم می کنند یا به جای آن یک مربع سفید ترسیم می کنند (برای نشان دادن جعبه پوشیده شده).

کشیدن سایه روشن

def drawHighlightBox(boxx, boxy):
    left, top = leftTopCoordsOfBox(boxx, boxy)
    pygame.draw.rect(DISPLAYSURF, HIGHLIGHTCOLOR, (left - 5,top - 5, BOXSIZE + 10, BOXSIZE + 10), 4)

برای کمک به بازیکن برای این که تشخیص دهد که می تواند روی یک جعبه پوشانده شده کلیک کند تا آن را فاش کند، ما یک خط آبی رنگ در اطراف یک جعبه برای برجسته کردن آن ایجاد خواهیم کرد. این خط آبی با فراخوانی تابع pygame.draw.rect() ترسیم می شود تا مستطیلی با عرض 4 پیکسل بسازد.

پویا نمایی "start game"

def startGameAnimation(board):
    # Randomly reveal the boxes 8 at a time.
    coveredBoxes = generateRevealedBoxesData(False)
    boxes = []
    for x in range(BOARDWIDTH):
        for y in range(BOARDHEIGHT):
            boxes.append((x, y))
    random.shuffle(boxes)
    boxGroups = splitIntoGroupsOf(8, boxes)

پویانمایی که در ابتدای بازی پخش می شود، به بازیکن سرنخ می دهد که شکل ها در کجا قرار دارند. برای ساختن این پویانمایی، باید گروه های جعبه را یکی پس از دیگری آشکار کنیم و بپوشانیم. برای انجام این کار، ابتدا لیستی از هر فضای ممکن در صفحه ایجاد می کنیم. حلقه های تو در تو در خطوط 257 و 258 تاپل های(X, Y)  را به یک لیست در متغیر boxes می افزاید.

8 جعبه نخست را در این لیست آشکار و پوشانده، سپس 8 مورد بعدی، سپس 8 مورد بعدی بعد از آن و غیره. اما از آن جایی که ترتیب هر بار تاپل های(X, Y)  در boxes یکسان است، پس همان ترتیب جعبه ها نمایش داده می شود. (خط 260 را کامنت کنید و سپس برای دیدن این اثر به برنامه مراجعه کنید.) برای تغییر جعبه ها هر بار که بازی شروع می شود، تابع random.shuffle فراخوانی می کنیم تا به طور تصادفی ترتیب تاپل ها را در لیست boxes تغییر دهیم. سپس وقتی 8 جعبه اول در این لیست را آشکار و پوشانده (و پس از آن هر گروه از 8 جعبه)، به صورت تصادفی از 8 جعبه خواهد بود.برای به دست آوردن لیست های 8 جعبه، با استفاده از تابع splitIntoGroupsOf، با فرستادن 8 و لیست جعبه یعنی boxes آن را فراخوانی می کنیم. لیست دوبعدی که تابع را برمی گرداند در متغیری به نام boxGroups ذخیره می شود.

نشان دادن و پوشاندن جعبه ها به طور گروهی

drawBoard(board, coveredBoxes)
for boxGroup in boxGroups:
    revealBoxesAnimation(board, boxGroup)
    coverBoxesAnimation(board, boxGroup)

در آغاز صفحه را می کشیم. از آن جایی که هر مقدار در coveredBoxes روی False تنظیم شده است، فراخوانی تابع drawBoard  ترسیم را در حالی به پایان می برد که فقط جعبه های سفید پوشیده شده باشند. توابع revealBoxesAnimation و coverBoxesAnimation فضای این جعبه های سفید را خواهند کشید.حلقه for در هر یک از لیست های داخلی لیست های boxGroups حلقه می زند. این ها را به تابع revealBoxesAnimation می فرستیم.با این کار انیمیشن جعبه های سفید را که به بیرون کشیده می شوند، برای نشان دادن شکل ها که در زیر هستند نمایش می دهد. سپس فراخوانی coverBoxesAnimation جعبه های سفید را نشان می دهد که برای پوشاندن شکل ها گسترش می یابند. سپس حلقه for به تکرار بعدی می رود تا مجموعه بعدی 8 جعبه را به نمایش درآورد.

پویا نمایی Game Won

def gameWonAnimation(board):
    # flash the background color when the player has won
    coveredBoxes = generateRevealedBoxesData(True)
    color1 = LIGHTBGCOLOR
    color2 = BGCOLOR

    for i in range(13):
        color1, color2 = color2, color1  # swap colors
        DISPLAYSURF.fill(color1)
        drawBoard(board, coveredBoxes)
        pygame.display.update()
        pygame.time.wait(300)

وقتی بازیکن با تطبیق هر جفت روی صفحه، همه جعبه ها را کنار زد، می خواهیم با چشمک زدن در رنگ پس زمینه، به او تبریک بگوییم. حلقه for باعث تغییر رنگ در متغیر color1 برای رنگ پس زمینه می شود و سپس صفحه را روی آن می کشیم. با این حال، در هر تکرار حلقه برای مقادیر، مقادیر color1 و color2 با یکدیگر تعویض می شوند. از این طریق این برنامه دو رنگ پس زمینه متفاوت خواهد داشت.به خاطر داشته باشید که این تابع باید pygame.display.update را فراخوانی کند تا سطح DISPLAYSURF روی صفحه ظاهر شود.

نشان دادن برنده شدن بازیکن

def hasWon(revealedBoxes):
    # Returns True if all the boxes have been revealed, otherwise False
    for i in revealedBoxes:
        if False in i:
            return False  # return False if any boxes are covered.
    return True

بازیکن هنگامی برنده می شود که همه جفت شکل ها با هم جور شوند. از آن جایی که ساختمان داده  revealed مقادیر بولین شکل ها را دارد و اگر شکل ها مطابقت داشته باشند True می شود، می توانیم به سادگی در هر فضای موجود در revealedBoxes که به دنبال یک مقدار False است حلقه بزنیم.

اگر حتی یک مقدار False در revealedBoxes وجود داشته باشد، می دانیم که هنوز شکل های جور نشده در صفحه وجود دارند. توجه داشته باشید که revealedBoxes یک لیست دوبعدی است، حلقه for در خط 285 لیست داخلی را به عنوان مقادیر i تعیین می کند. اما ما می توانیم از عملگر in برای جستجوی مقدار False در کل لیست داخلی استفاده کنیم. به این ترتیب نیازی نیست که کد اضافی بنویسیم و از دو حلقه ی تودرتو مثل این استفاده کنید :

for x in revealedBoxes:
    for y in revealedBoxes[x]: 
        if False == revealedBoxes[x][y]: 
            return False 

چرا زحمت داشتن تابع main را به خود بدهیم؟

if __name__ == '__main__':
    main()

به نظر می رسد که استفاده از تابع main بیهوده است، زیرا می توانید به جای آن، کد را در دامنه سراسری قرار دهید و کد دقیقا همان کار را انجام دهد. با این وجود، دو دلیل خوب برای قرار دادن کدها در یک تابع main وجود دارد.نخست، به شما امکان می دهد متغیرهای محلی داشته باشید در غیر این صورت متغیرهای محلی در تابع main باید تبدیل به متغیرهای سراسری شوند. محدود کردن تعداد متغیرهای سراسری روشی مناسب برای ساده نگه داشتن کد و رفع اشکال در آن است. (به بخش "چرا متغیرهای سراسری به هستند مراجعه کنید.)

کد بازی را می توانید از نشانی دانلود کنید.

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

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

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