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

Memory Game - Part 2

06 آبان 1400
Memory-Game---Part-2

پیش گفتار

در این بخش ادامه بازی یادمان را از سر خواهیم گرفت و با نکته های بیش تری در بازی سازی آشنا خواهیم شد.

تاپل در مقایسه با آرایه (لیست)، تغییرناپذیر در مقابل تغییرپذیر

شاید متوجه شده باشید که ثابت های ALLCOLORS و ALLSHAPES به جای لیست، تاپل هستند. شاید از خود بپرسید که چه هنگام باید از تاپل ها و چه هنگام باید از لیست استفاده کنم؟ و چه تفاوتی میان آن ها وجود دارد ؟

در پاسخ باید بگویم که تاپل ها و لیست ها از هر نظر یکسان هستند به جز دو مورد:

  1. تاپل ها به جای براکت ها از پرانتز استفاده می کنند(یعنی به جای [] از () بهره می گیرند)
  2. آیتم های موجود در تاپل ها تغییرناپذیر هستند (ولی آیتم های موجود در لیست ها می توانند تغییر کنند).

ما اغلب لیست ها را تغییرپذیر می نامیم (به معنای این که می توانند تغییر می کنند) و تاپل ها را تغییر ناپذیر می نامیم (به این معنی که نمی توان آن ها را تغییر داد).

به کد زیر نگاه کنید :

 

آرایه در مقایسه با لیست

 

 

 

 

 

 

 

 

با نگاه به کد بالا در می یابیم که اگر بخواهیم آیتم سوم تاپل را تغییر دهیم، با خطایی به صورت زیر رو به رو می شویم:

tuple' object does not support item assignment'

 

ویژگی تغییر ناپذیری تاپل دو فایده دارد.نخست این که کدی که از تاپل ها استفاده می کند کمی سریع تر از کدی است که از لیست ها استفاده می کند. اما اجرا شدن سریع تر کد شما به ازای چند نانو ثانیه چندان مهم نیست.

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

کلمه کلیدی global و چرا استفاده از متغیر های global بد است

def main():
    global FPSCLOCK, DISPLAYSURF
    pygame.init()
    FPSCLOCK = pygame.time.Clock()
    DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))

    mousex = 0  # used to store x coordinate of mouse event
    mousey = 0  # used to store y coordinate of mouse event
    pygame.display.set_caption('Memory Game')

کد بالا آغاز تابع main است،  که اصلی ترین قسمت برنامه در آن قرار دارد. همه توابعی که در این قسمت فراخوانی می شوند در بعد توضیح داده می شوند. خط اول تابع main یک عبارت global است. global یک کلمه کلیدی است و به دنبال آن نام متغیرهایی که با کاما از هم جدا شده اند آمده است. این متغیرها به عنوان متغیرهای سراسری یا global مشخص می شوند. متغیرهای global در سرتاسر برنامه قابل استفاده هستند. FPSCLOCK و DISPLAYSURF را به عنوان global تعریف می کنیم زیرا در چند تابع دیگر نیز از آن ها استفاده می شود. (اطلاعات بیشتر در http://invpy.com/scope است.)

چهار قانون ساده برای تعیین  متغیر محلی (local) و سراسری (global) است وجود دارد:

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

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

ساختمان داده ها و آرایه های دو بعدی (2D)

mainBoard = getRandomizedBoard()
revealedBoxes = generateRevealedBoxesData(False)

تابع getRandomizedBoard  ساختمان داده ای است که وضعیت صفحه را نشان می دهد. تابع generateRevealedBoxesData نشان دهنده ترتیب پوشانده شدن جعبه ها است. مقدار برگشتی این دو تابع یک آرایه(لیست) دو بعدی است. اگر یک آرایه را در یک متغیر به نام spam ذخیره کنیم، می توانیم با براکت به مقدار درون آن دست یابی پیدا بکنیم مانند spam[2] که به مقدار سوم در این لیست اشاره می کند. اگر مقدار موجود در  spam[2] خود یک آرایه باشد، می توانیم از یک جفت براکت دیگر برای بازیابی این مقدار در آن لیست استفاده کنیم.  استفاده از این روش نماد گذاری آرایه های دوبعدی باعث می شود تا بتوان یک صفحه دو بعدی را به یک آرایه دو بعدی به آسانی نگاشت کنیم. از آن جایی که متغیر mainBoard شکل هایی را در خود ذخیره می کند، اگر می خواستیم شکل را در موقعیت (5، 4)  در صفحه قرار دهیم، می توانیم از کد mainBoard[4][5] استفاده کنیم. از آن جایی که خود شکل ها به عنوان تاپل های دوتایی یعنی با شکل و رنگ ذخیره می شوند، ساختمان داده کامل یک آرایه دو بعدی از تاپل های دوتایی خواهد بود.این یک نمونه کوچک است. گمان کنید صفحه مانند زیر باشد:

آن گاه ساختمان داده مربوطه به شکل زیر خواهد بود:

mainBoard = [[(DONUT, BLUE), (LINES, BLUE), (SQUARE, ORANGE)], [(SQUARE, GREEN), (DONUT, BLUE), (DIAMOND, YELLOW)], [(SQUARE, GREEN), (OVAL, YELLOW), (SQUARE, ORANGE)], [(DIAMOND, YELLOW), (LINES, BLUE), (OVAL, YELLOW)]]

تا اینجا باید متوجه شده باشید که mainBoard[x][y] معادل با مختصه (x, y) در صفحه است. در همین حال، ساختمان داده revealedbox بر خلاف ساختمان داده board یک آرایه دو بعدی است و مقدار بولین دارد. اگر جعبه موجود در مختصات x،y  به کنار رفته باشد، مقدار True و اگر جعبه پوشیده باشد مقدار False را دارد. فرستادن False به تابع generateRevealedBoxesData باعث می شود که همه مقادیر آن False شوند. (این تابع در بعد توضیح داده خواهد شد). از دو ساختمان داده mainBoard و revealedbox برای ردیابی وضعیت صفحه استفاده می شود.

انیمیشن (پویانمایی) Start Game

firstSelection = None  # stores the (x, y) of the first box clicked
DISPLAYSURF.fill(BGCOLOR)
startGameAnimation(mainBoard)

خط بالا یک متغیر به نام firstSelection با مقدار None را تعریف می کند. (None مقداری است که نشان دهنده نبودن مقدار باشد.) هنگامی که بازیکن روی یک جعبه کلیک می کند، برنامه باید بررسی کند آیا شکل کلیک شده نخستین شکل بوده است یا نه. اگر firstSelection دارای مقدار None باشد و شکلی که کلیک می شود شکل اول باشد مختصات XY شکل را در متغیر firstSelection به عنوان یک تاپل دو تایی ذخیره می کنیم. با کلیک بر روی جعبه دوم، یک تاپل خواهیم داشت و دیگر مقدار None در کار نخواهد بود. خط بعد تمام سطح را با رنگ پس زمینه رنگ می کند. این خط هم  چنین بر روی هر چیزی که از قبل روی سطح بوده است را رنگ می زند و به ما یک صفحه تمیز و یک دست می دهد تا شروع به ترسیم کنیم.

اگر بازی یادمان را از پیش انجام داده باشید، متوجه خواهید شد که در آغاز بازی، همه جعبه ها به سرعت پنهان می شوند و به طور ناگهانی و در یک چشم بر هم زدنی آشکار می شوند و به بازیکن سرنخی در مورد جایگاه شکل ها می دهند. همه این رخداد ها در تابع startGameAnimation مدیریت می شود که در آینده توضیح داده می شوند.

مهم است که به بازیکن این سرنخ کوتاه را بدهید اما نباید مدت نمایش آن زیاد طول بکشد تا بازیکن به راحتی بتواند مکان شکل ها را به خاطر بسپارد. کورکورانه کلیک کردن شکل ها نیز کار جالبی به نظر نمی رسد و از زیبایی و هیجان انگیز بودن بازی می کاهد و حتی دیگر نمی توانیم نام این بازی را یادمان یا یادآوری بگذاریم.

حلقه بازی

while True:  # main game loop
       mouseClicked = False

       DISPLAYSURF.fill(BGCOLOR)  # drawing the window
       drawBoard(mainBoard, revealedBoxes)

حلقه بازی یک حلقه بی نهایت است و تا زمانی که بازی در حال انجام است، تکرار می شود. به یاد داشته باشید که حلقه بازی رویداد ها را مدیریت می کند، وضعیت بازی را به روزرسانی می کند و وضعیت بازی را روی صفحه رسم می کند.

وضعیت بازی برای برنامه یادمان در متغیرهای زیر ذخیره می شود:

  • mainboard
  • revealedBoxes
  • firstSelection
  • mouseClicked
  • mousex
  • mousey

در هر تکرار از حلقه بازی، متغیر mouseClicked مقداری از نوع بولین را در خود ذخیره می کند. اگر بازیکن در حین اجرا شدن حلقه بازی روی موس کلیک کند مقدار بولین True می شود (این بخشی از پی گیری وضعیت بازی است.) در خط بعدی، صفحه نمایش دوباره با رنگ پس زمینه رنگ می شود تا هر چیزی را که از پیش روی آن کشیده شده است پاک کند. سپس برای ترسیم وضعیت فعلی تابع drawBoard را فراخوانی می کنیم. تابع drawBoard دارای دو پارامتر board و ساختمان داده revealedBoxes است. (این خطوط بخشی از کار ترسیم و به روزرسانی صفحه هستند.) به یاد داشته باشید که توابع ترسیم فقط روی شی نمایش سطح کار ترسیم را انجام می دهند. تا زمانی که تابع pygame.display.update  را فراخوانی نکنیم، شی سطح روی صفحه نمایش داده نمی شود. این کار را در پایان حلقه بازی انجام می دهیم.

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

for event in pygame.event.get():  # event handling loop
    if event.type == QUIT or (event.type == KEYUP and event.key == K_ESCAPE):
        pygame.quit()
        sys.exit()
    elif event.type == MOUSEMOTION:
        mousex, mousey = event.pos
    elif event.type == MOUSEBUTTONUP:
        mousex, mousey = event.pos
        mouseClicked = True

 حلقه for در خط 72 برای هر رویدادی که در آخرین تکرار حلقه بازی رخ داده است، کد را اجرا می کند. این حلقه، حلقه مدیریت رویداد نام دارد و روی لیست اشیا pygame.event که توسط pygame.event.get باز گردانده می شود حلقه می زند. اگر شی رویداد، QUIT یا یک رویداد KEYUP برای کلید Esc باشد، آن گاه برنامه متوقف می شود. در غیر این صورت، در صورت بروز یک رویداد MOUSEMOTION (یعنی مکان نما موس حرکت کرده بکند) یا رویداد MOUSEBUTTONUP (یعنی اگر موس از پیش کلیک شده باشد ولی اکنون رها شده است) باید موقعیت مکان نما موس در متغیرهای mousex و mousey ذخیره شود. اگر رویداد یک رویداد MOUSEBUTTONUP بود، باید mouseClicked نیز بر روی True تنظیم شود. هنگامی که همه رویدادها را کنترل و مدیریت کردیم، مقادیر ذخیره شده در mousex، mousey و  mouseClicked، نشان دهنده ورودی های بازیکن خواهند بود. حال باید وضعیت بازی را به روز کنیم و نتایج را در صفحه نمایش دهیم.

از کجا بفهمیم که موس روی کدام جعبه است؟

boxx, boxy = getBoxAtPixel(mousex, mousey)
if boxx != None and boxy != None:
    # The mouse is currently over a box.
    if not revealedBoxes[boxx][boxy]:
        drawHighlightBox(boxx, boxy)

تابع getBoxAtPixel یک تاپل دوتایی از اعداد صحیح را برمی گرداند.این دو عدد صحیح نشانگر مختصات صفحه XY جعبه است که مختصات موس در بالای قرار می گیرد به عبارتی دیگر برای جعبه یک مختصات دیگر در نظر می گیریم و بررسی می کنیم که آیا مختصات موس در مختصات XY جعبه قرار می گیرد یا نه. این که تابع getBoxAtPixel چگونه این کار را انجام می دهد در آینده توضیح داده می شود. تنها چیزی که اکنون باید بدانیم این است که اگر مختصات mousex و mousey موس بالای یک جعبه بودند، یک تاپل  از مختصات صفحه XY توسط این تابع بازگردانده می شوند و در boxx و boxy ذخیره می شوند.

اگر مکان نما موس، بالای هیچ کدام از جعبه ها نباشد (به عنوان نمونه، اگر از کنار صفحه یا در فاصله بین جعبه ها باشد)، آن گاه تاپل (None,None)، توسط تابع بازگردانده می شود و در boxx و boxy مقدار None ذخیره می شود. ما مواردی را می خواهیم که boxx و boxy دارای مقدار None نباشند، بنابراین خط های بعدی که پس از if در خط 83 آمده اند این مورد را بررسی می کنند. اگر اجرای این بخش در داخل این بلوک از کد قرار داشته باشد، می دانیم که مکان نمای کاربر بالای جعبه قرار دارد (و شاید موس را نیز کلیک کرده است.فهمیدن آن به مقدار ذخیره شده در mouseClicked بستگی دارد) عبارت if در خط 85 با خواندن مقدار ذخیره شده در revealedBoxes[boxx][boxy] بررسی می کند که آیا جعبه پوشانده شده است یا نه. اگر این مقدار False باشد، می دانیم جعبه پوشانده شده است. هر گاه موس بالای یک جعبه پوشیده شده باشد می خواهیم یک سایه روشن آبی در اطراف آن نمایش بدهیم تا بازیکن را آگاه کنیم که می تواند روی آن کلیک کند. این سایه روشن برای جعبه هایی که از پیش آشکار شده اند نخواهد بود. طراحی سایه روشن توسط تابع drawHighlightBox انجام می شود که در آینده توضیح داده می شود.

if not revealedBoxes[boxx][boxy] and mouseClicked:
   revealBoxesAnimation(mainBoard, [(boxx, boxy)])
   revealedBoxes[boxx][boxy] = True  # set the box as "revealed"

در خط بالا، بررسی می کنیم که مکان نما موس نه تنها بالای جعبه پوشانده شده است، بلکه این را هم بررسی می کنیم که موس کلیک کرده است یا نه.در این حالت، ما می خواهیم پویا نمایی آشکار سازی جعبه را با استفاده از تابع revealBoxesAnimation به کاربر نشان دهیم که در آینده توضیح داده می شود.توجه داشته باشید که فراخوانی این تابع فقط باعث اجرا شدن پویانمایی پدیدار شدن جعبه می شود. تا خط 89 این رویداد رخ نمی دهد یعنی تا هنگامی که هنوز revealedBoxes[boxx][boxy] را با True مقداردهی نکرده ایم. اگر خط 89 را کامنت کنید یعنی از اجرای آن جلوگیری کنید و سپس برنامه را اجرا کنید، متوجه خواهید شد که پس از کلیک روی جعبه، پویانمایی آشکارسازی جعبه نمایش داده می شود، اما پس از آن بی هیچ درنگی دوباره جعبه پوشانده می شود. دلیل این رخداد آن است که revealedBoxes[boxx][boxy] روی False تنظیم شده است، بنابراین در تکرار بعدی حلقه بازی، صفحه با این جعبه پوشیده شده کشیده خواهد شد.اجرا نشدن خط 89 باعث بروز اشکالات عجیب و غریب در برنامه ما خواهد شد.

رسیدگی به نخستین جعبه کلیک شده

if firstSelection == None:  # the current box was the first box clicked
    firstSelection = (boxx, boxy)
else:  # the current box was the second box clicked
    # Check if there is a match between the two icons.
    icon1shape, icon1color = getShapeAndColor(mainBoard, firstSelection[0], firstSelection[1])
    icon2shape, icon2color = getShapeAndColor(mainBoard, boxx, boxy)

پیش از آن که اجرای برنامه به حلقه بازی برسد، متغیر firstSelection از پیش روی None تنظیم شده است. برنامه ما این مقدار را این گونه تفسیر می کند که هیچ جعبه ای کلیک نشده است، بنابراین اگر شرط خط 90 درست یا True باشد، این بدان معنی است که جعبه کلیک شده نخستین جعبه است.ما می خواهیم پویانمایی آشکار سازی را برای جعبه نمایش دهیم و سپس آن جعبه را آشکار شده نگه داریم. همچنین متغیر firstSelection را برای جعبه ای که کلیک شده است، با یک تاپل از مختصات جعبه مقداردهی کنیم.اگر این جعبه، جعبه دومی باشد که بازیکن روی آن کلیک کرده است، می خواهیم پویانمایی آشکار سازی را برای آن جعبه نمایش دهیم و سپس بررسی کنیم که آیا دو شکل در زیر جعبه ها با هم همسانی دارند یا خیر. تابع getShapeAndColor که در آینده توضیح داده می شوند نوع و رنگ شکل ها را بازیابی می کند.(این مقادیر بازیابی شده یکی از مقادیر در تاپل های ALLCOLORS و ALLSHAPES خواهند بود)

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

if icon1shape != icon2shape or icon1color != icon2color:
    # Icons don't match. Re-cover up both selections.
    pygame.time.wait(1000)  # 1000 milliseconds = 1 sec
    coverBoxesAnimation(mainBoard, [(firstSelection[0], firstSelection[1]), (boxx, boxy)])
    revealedBoxes[firstSelection[0]][firstSelection[1]] = False
    revealedBoxes[boxx][boxy] = False

عبارت if در خط 97 بررسی می کند که شکل ها یا رنگ های دو شکل  یکسان هستند یا نه. در صورت یکسان نبودن، می خواهیم با فراخوانی  تابع pygame.time.wait(1000)  بازی را برای 1000 میلی ثانیه (که برابر با 1 ثانیه است) متوقف کنیم تا بازیکن بتواند این فرصت را پیدا کند که این دو شکل را که با هم یکی نیستند ببیند و جای آن ها را به خاطر بسپارد. سپس پونمایی پنهان سازی را برای هر دو جعبه پخش کنیم. ما همچنین می خواهیم وضعیت بازی را به روز کنیم تا این جعبه ها به عنوان آشکار شده علامت گذاری نشوند (زیرا آن ها هنوز در زیر جعبه ها پنهان هستند).

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

elif hasWon(revealedBoxes):  # check if all pairs found
    gameWonAnimation(mainBoard)
    pygame.time.wait(2000)

    # Reset the board
    mainBoard = getRandomizedBoard()
    revealedBoxes = generateRevealedBoxesData(False)

    # Show the fully unrevealed board for a second.
    drawBoard(mainBoard, revealedBoxes)
    pygame.display.update()
    pygame.time.wait(1000)

    # Replay the start game animation.
    startGameAnimation(mainBoard)
firstSelection = None  # reset firstSelection variable

در غیر این صورت، اگر شرط خط 97 نادرست بود، باید این دو شکل مطابقت داشته باشند. برنامه واقعا در این مرحله مجبور به انجام کار دیگری نیست. این فقط می تواند هر دو جعبه را در حالت آشکار باقی بگذارد. با این حال، این برنامه باید بررسی کند که آیا این آخرین جفت شکل در صفحه است که مطابقت داشته یا خیر. این کار در داخل تابع hasWon ما انجام می شود که اگر صفحه در حالت برنده شدن باشد True را برمی گرداند (یعنی حالتی که در آن همه جعبه ها نشان داده می شوند و دیگر پنهان نیستند). در این صورت، می خواهیم پویا نمایی "game won" یا "بازی برنده شد" را با فراخوانی تابع gameWonAnimation  پخش کنیم، سپس کمی درنگ کنیم تا بازیکن از پیروزی خود لذت ببرد و سپس ساختمان داده ها را در mainboard و  revealedBoxes بازنشانی یا پاک کند تا به این ترتیب بتوان یک بازی جدید را شروع کرد.خط 117 دوباره پویا نمایی "start game" یا آغاز بازی را پخش می کند. پس از آن، اجرای برنامه به طور معمول از طریق حلقه بازی ادامه می یابد و بازیکن می تواند بازی خود را تا زمان ترک برنامه ادامه دهد.مهم نیست که این دو جعبه مطابقت داشته باشند یا خیر، پس از کلیک بر روی جعبه دوم، خط 118 متغیر firstSelection را بر روی None تنظیم می کند تا جعبه بعدی که کلیک می شود، به عنوان نخستین جعبه تعبیر شود.

کشیدن وضعیت بازی در صفحه

# Redraw the screen and wait a clock tick.
pygame.display.update()
FPSCLOCK.tick(FPS)

در این مرحله، وضعیت بازی بسته به ورودی بازیکن و آخرین حالت بازی رسم شده روی شی سطح نمایش DISPLAYSURF، به روز رسانی می شود. ما به پایان حلقه بازی رسیدیم، بنابراین تابع pygame.display.update را فراخوانی می کنیم تا شی سطح  DISPLAYSURF را روی صفحه رایانه بکشیم. خط 9 مقدار FPS را بر روی عدد صحیح 30 قرار می دهد، به این معنی که می خواهیم بازی با (حداکثر) سرعت 30 فریم در ثانیه اجرا شود. اگر بخواهیم برنامه سریعتر اجرا شود، می توانیم این FPS را افزایش دهیم. اگر بخواهیم برنامه کندتر اجرا شود، می توانیم این مقدار را کاهش دهیم. حتی می توان آن را با یک مقدار اعشاری مانند 0.5 تنظیم کرد که برنامه را با نیم فریم در ثانیه اجرا می کند، یعنی یک فریم در هر دو ثانیه. برای اجرای 30 فریم در ثانیه، هر فریم باید در 30/1 ثانیه کشیده شود. این بدان معنی است که تابع pygame.display.update و تمام کدهای موجود در حلقه بازی باید در کمتر از 33.3 میلی ثانیه اجرا شوند. هر رایانه مدرن می تواند این کار را با زمان زیادی که باقی مانده است به راحتی انجام دهد.برای جلوگیری از اجرای سریع برنامه، متد tick شی pygame.Clock در FPSCLOCK را فراخوانی می کنیم تا مجبور شود برنامه را برای بقیه 33.3 میلی ثانیه متوقف کند.از آن جا که این کار در پایان حلقه بازی انجام می شود، به ما این اطمینان را می دهد که هر تکرار از حلقه بازی (حداقل) 33.3 میلی ثانیه طول می کشد. اگر بنا به دلایلی فراخوانی pygame.display.update و اجرای کد موجود در حلقه بازی بیش از 33.3 میلی ثانیه طول بکشد، متد tick اصلا درنگ نخواهد کرد و بلافاصله برمی گردد.

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

ایجاد ساختمان داده RevealedBoxes

def generateRevealedBoxesData(val):
    revealedBoxes = []
    for i in range(BOARDWIDTH):
        revealedBoxes.append([val] * BOARDHEIGHT)
    return revealedBoxes

تابع generateRevealedBoxesData باید لیستی دو بعدی از مقادیر بولی بسازد. مقدار بولی مقداری خواهد بود که به عنوان پارامتر val به این تابع فرستاده داده می شود. در آغاز ساختمان داده را به عنوان یک لیست خالی در متغیر revealedBoxes ذخیره می کنیم. برای این که ساختمان داده را برای داشتن ساختاری مانند revealedBoxes[x][y] توانمند کنیم، باید مطمئن شویم که لیست داخلی نشان دهنده ستون ها است و نه سطرهای آن، در غیر این صورت، ساختمان داده دارای ساختار revealedBoxes[y][x]  خواهد بود.حلقه for ستون ها را می سازد و سپس آن ها را به revealedBoxes پیوست می کند. ستون ها با استفاده از همانند سازی لیست ساخته می شوند، به گونه ای که لیست ستون ها همان اندازه مقدارهای val را خواهند داشت که BOARDHEIGHT دیکته می کند.

ایجاد ساختمان داده Board:

گام  1 : به دست آوردن همه شکل های ممکن با هر رنگ و نوع

def getRandomizedBoard():
    # Get a list of every possible shape in every possible color.
    icons = []
    for color in ALLCOLORS:
        for shape in ALLSHAPES:
            icons.append((shape, color))

ساختمان داده صفحه یا Board لیستی دوبعدی از تاپل ها است که در آن هر تاپل دو مقدار دارد : مقدار نخست برای نوع شکل و مقدار دوم رنگ آن است. اما ساخت این ساختمان داده کمی پیچیده است. ما باید دقیقا به اندازه تعداد جعبه های موجود در صفحه، شکل داشته باشیم و همچنین باید مطمئن باشیم که از هر نوع شکل با یک  رنگ خاص فقط دو تا داریم. نخستین گام برای این کار ایجاد لیست با هر ترکیب احتمالی شکل و رنگ است. به خاطر بیاورید که ما در لیست های ALLCOLORS و ALLSHAPES لیستی از هر رنگ و شکل را داریم، بنابراین استفاده از حلقه های تودرتو در خط های 135 و 136 باعث می شود که هر شکل ممکن با هر رنگ ممکن را داشته باشیم. این ها هر کدام در متغیر icons در خط 137 به لیست افزوده شده اند.

گام 2 : ترکیب کردن و بریدن لیست shapes

random.shuffle(icons)  # randomize the order of the icons list
# calculate how many icons are needed
numIconsUsed = int(BOARDWIDTH * BOARDHEIGHT / 2)
icons = icons[:numIconsUsed] * 2  # make two of each
random.shuffle(icons)

ممکن است ترکیبات احتمالی که از شکل و رنگ داریم، از فضایی که در صفحه موجود است بیش تر باشد. برای به دست آوردن تعداد فضاهای موجود در صفحه، باید BOARDWIDTH را در BOARDHEIGHT ضرب کنیم سپس آن عدد را به 2 تقسیم می کنیم زیرا از هر شکل یک جفت داریم پس در یک صفحه با فضای 70، ما فقط 35 شکل مختلف نیاز داریم. این عدد در متغیر numIconsUsed ذخیره می شود. خط 141 از برش لیست برای به دست آوردن تعداد نخستین numIconsUsed شکل های موجود در لیست استفاده می کند. (اگر فراموش کرده اید که چگونه برش لیست کار می کند، به نشانی http://invpy.com/slicing   سری بزنید.) این لیست در خط 139 تغییر می باید، بنابراین در هر بازی همان شکل های پیشین را نخواهیم داشت.سپس این لیست با استفاده از عملگر * دوبرابر می شود، به طوری که از هر یک از این شکل ها دو تا خواهیم داشت. این لیست جدید، لیست قدیمی را در متغیر icons بازنویسی می کند. از آن جا که نیمه اول این لیست جدید با نیمه اول لیست قدیمی یکسان است، ما دوباره متد shuffle را فراخوانی می کنیم تا به طور تصادفی ترتیب شکل ها را تغییر بدهیم.

گام 3 : جادادن شکل ها روی صفحه

# Create the board data structure, with randomly placed icons.
board = []
for x in range(BOARDWIDTH):
    column = []
    for y in range(BOARDHEIGHT):
        column.append(icons[0])
        del icons[0]  # remove the icons as we assign them
    board.append(column)
return board

حال باید ساختمان داده لیست دو بعدی را برای صفحه بسازیم. این کار را می توانیم با حلقه های تودرتو که در تابع generateRevealedBoxesData است انجام دهیم.برای هر ستون روی صفحه، لیستی از شکل های تصادفی انتخاب شده را ایجاد می کنیم. همان طور که در خط 149 شکل ها را به ستون ها  می افزاییم، در خط 150  آن ها را از قسمت جلوی لیست icons حذف می کنیم. به این ترتیب، هرچه لیست icons کوتاه تر و کوتاه تر می شود، icons[0] یک شکل دیگر برای اضافه کردن به ستون ها دارد. برای درک بهتر کد زیر را در پوسته اندرکنشی بنویسید. توجه کنید که چگونه del لیست myList را تغییر می دهد.

از آن جا که ما در حال حذف آیتم نخست لیست هستیم، بقیه آیتم ها به جلو حرکت می کنند به این ترتیب که آیتم بعدی در لیست به آیتم اول تبدیل می شود.خط 150 به همین روش کار می کند.

تبدیل لیست به یک لیست دوبعدی

def splitIntoGroupsOf(groupSize, theList):
    # splits a list into a list of lists, where the inner lists have at
    # most groupSize number of items.
    result = []
    for i in range(0, len(theList), groupSize):
        result.append(theList[i:i + groupSize])
    return result

تابع splitIntoGroupsOf (که توسط تابعstartGameAnimation فراخوانی می شود) یک لیست را به یک لیست دو بعدی تبدیل می کند، که لیست داخلی حداکثر به اندازه groupSize آیتم خواهد داشت. (لیست بیرونی می تواند کمتر آیتم داشته باشد اگر تعداد کمتری از آیتم های  groupSize باقی مانده باشد) فراخوانی range() در خط 159 از شکل سه پارامتری تابع range() استفاده می کند. (اگر با این شکل نا آشنا هستید، به نشانی  http://invpy.com/range نگاهی بیندازید).

بگذارید از یک مثال استفاده کنیم. اگر طول لیست 20 باشد و پارامتر groupSize، 8 باشد، آن گاه range(0, len(theList), groupSize) مقداری که ارزیابی می کند range(0, 20, 8) است. این تابع به متغیر i مقادیر 0، 8 و 16 را برای سه تکرار حلقه for می دهد. عدد سوم نشان دهنده گام حلقه است. برش لیست در خط 160 با theList [i: i + groupSize] لیست هایی را ایجاد می کند که به لیست result افزوده می شوند. در هر تکرار که در آن i  0، 8 و 16 است (و groupSize برابر  8  است)، این عبارت برش دهنده لیست شامل theList[0:8]، سپس theList[8:16]  در تکرار دوم، و سپس theList[16:24] در تکرار سوم خواهد بود.

توجه داشته باشید با این که بزرگترین ایندکس لیست در نمونک ما 19 است، لیست [16:24] خطای IndexError را ایجاد نمی کند، اگرچه 24 بزرگتر از 19 است. فقط یک قطعه لیست با موارد باقیمانده را ایجاد می کند. برش لیست، لیست اصلی ذخیره شده در theList را تغییر نمی دهد. فقط بخشی از آن را کپی می کند تا به یک لیست جدید مقداردهی شود. این مقدار جدید لیست، لیستی است که در متغیر result در خط 160 به لیست پیوست می شود. بنابراین وقتی resulte را در انتهای این تابع برمی گردانیم، لیستی دوبعدی را برمی گردانیم.

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

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

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