در این قسمت آموزش ساخت بازی حافظه (یادمان) را یاد خواهیم گرفت. تصویر این بازی در زیر آمده است.
در بازی یادمان یا حافظه شکل های گوناگونی وجود دارند که دو به دو باید با هم جفت شوند، پس از هر شکل دو عدد وجود دارد (مثلا از دایره قرمز دو تا، از مربع آبی دو تا و ....). این شکل ها با جعبه های سفیدی پوشانده شده اند. در آغاز بازی یک بار جای همه شکل ها به سرعت (مثلا به مدت 10 ثانیه) به بازیکن نمایش داده می شود. بازیکن در این مدت کوتاه باید جای همه ی آن ها را به خاطر بسپارد. پس از آن دوباره شکل ها پوشیده می شوند. بازیکن می تواند بر روی جعبه های سفید کلیک کند تا ببیند چه شکلی در پشت آن ها قرار دارد. اگر در دو انتخاب نخست، شکل ها با هم یکی باشند، جعبه ها برای همیشه به کنار می روند. بازیکن هنگامی برنده می شود که همه جعبه ها به کنار بروند یا همه شکل ها با هم جفت شوند.
در بازی یادمان باید بتوانیم هر مختصات ممکن X و Y را داشته باشیم. برای اطمینان از این که هر ترکیب ممکن را بدست آورده ایم، از حلقه های تو در تو استفاده خواهیم کرد. توجه داشته باشید که حلقه داخلی (حلقه ای که درون حلقه ی دیگر است) پیش از رفتن به تکرار بعدی حلقه بیرونی، همه تکرارهای خود را انجام می دهد و سپس حلقه بیرونی وارد دور بعدی خود می شود.
کد بازی یادمان را می توانید از نشانی http://invpy.com/memorypuzzle.py بارگیری کنید. احتمالا با نگاه کردن به کد برنامه ایده هایی را در مورد چگونگی ساخت آن به دست آورده اید ولی شتاب نکنید. بیایید با هم نخستین بازی در این کتاب را خوب بنگریم و سپس آن را بسازیم.
# Memory Puzzle # 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 = 30 # frames per second, the general speed of the program WINDOWWIDTH = 640 # size of window's width in pixels WINDOWHEIGHT = 480 # size of windows' height in pixels REVEALSPEED = 8 # speed boxes' sliding reveals and covers BOXSIZE = 40 # size of box height & width in pixels GAPSIZE = 10 # size of gap between boxes in pixels BOARDWIDTH = 10 # number of columns of icons BOARDHEIGHT = 7 # number of rows of icons assert (BOARDWIDTH * BOARDHEIGHT) % 2 == 0, 'Board needs to have an even number of boxes for pairs of matches.' XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * (BOXSIZE + GAPSIZE))) / 2) YMARGIN = int((WINDOWHEIGHT - (BOARDHEIGHT * (BOXSIZE + GAPSIZE))) / 2) # R G B GRAY = (100, 100, 100) NAVYBLUE = (60, 60, 100) WHITE = (255, 255, 255) RED = (255, 0, 0) GREEN = (0, 255, 0) BLUE = (0, 0, 255) YELLOW = (255, 255, 0) ORANGE = (255, 128, 0) PURPLE = (255, 0, 255) CYAN = (0, 255, 255) BGCOLOR = NAVYBLUE LIGHTBGCOLOR = GRAY BOXCOLOR = WHITE HIGHLIGHTCOLOR = BLUE DONUT = 'donut' SQUARE = 'square' DIAMOND = 'diamond' LINES = 'lines' OVAL = 'oval' ALLCOLORS = (RED, GREEN, BLUE, YELLOW, ORANGE, PURPLE, CYAN) ALLSHAPES = (DONUT, SQUARE, DIAMOND, LINES, OVAL) assert len(ALLCOLORS) * len(ALLSHAPES) * 2 >= BOARDWIDTH * \ BOARDHEIGHT, "Board is too big for the number of shapes/colors defined." 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') mainBoard = getRandomizedBoard() revealedBoxes = generateRevealedBoxesData(False) firstSelection = None # stores the (x, y) of the first box clicked. DISPLAYSURF.fill(BGCOLOR) startGameAnimation(mainBoard) while True: # main game loop mouseClicked = False DISPLAYSURF.fill(BGCOLOR) # drawing the window drawBoard(mainBoard, revealedBoxes) 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 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) if not revealedBoxes[boxx][boxy] and mouseClicked: revealBoxesAnimation(mainBoard, [(boxx, boxy)]) revealedBoxes[boxx][boxy] = True # set the box as "revealed" 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) 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 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 # Redraw the screen and wait a clock tick. pygame.display.update() FPSCLOCK.tick(FPS) def generateRevealedBoxesData(val): revealedBoxes = [] for i in range(BOARDWIDTH): revealedBoxes.append([val] * BOARDHEIGHT) return revealedBoxes 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)) 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) # 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 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 def leftTopCoordsOfBox(boxx, boxy): # Convert board coordinates to pixel coordinates left = boxx * (BOXSIZE + GAPSIZE) + XMARGIN top = boxy * (BOXSIZE + GAPSIZE) + YMARGIN return (left, top) 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) 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) # 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)) 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] 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) 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) 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) def drawHighlightBox(boxx, boxy): left, top = leftTopCoordsOfBox(boxx, boxy) pygame.draw.rect(DISPLAYSURF, HIGHLIGHTCOLOR, (left - 5, top - 5, BOXSIZE + 10, BOXSIZE + 10), 4) 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) drawBoard(board, coveredBoxes) for boxGroup in boxGroups: revealBoxesAnimation(board, boxGroup) coverBoxesAnimation(board, boxGroup) 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) 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 if __name__ == '__main__': main()
# Memory Puzzle # 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 *
در بالای برنامه اطلاعاتی درمورد نام بازی، نویسنده آن و این که کاربر از کجا می تواند اطلاعات بیشتری در مورد آن پیدا کند وجود دارد. این برنامه از توابع موجود در ماژول های pygame استفاده می کند، بنابراین این ماژول ها را با دستور import وارد برنامه می کنیم تا بتوانیم از آن ها استفاده کنیم. هیچ تابعی در ماژول pygame.locals وجود ندارد، اما چندین ثابت در آن هست که برای نوشتن برنامه به آن ها نیاز دازیم مانند MOUSEMOTION ،KEYUP یاQUIT.
FPS = 30 # frames per second, the general speed of the program WINDOWWIDTH = 640 # size of window's width in pixels WINDOWHEIGHT = 480 # size of windows' height in pixels REVEALSPEED = 8 # speed boxes' sliding reveals and covers BOXSIZE = 40 # size of box height & width in pixels GAPSIZE = 10 # size of gap between boxes in pixels
برنامه های موجود در این سری آموزشی از ثابت های زیادی استفاده می کنند. شاید نفهمید که چرا آن ها تا این اندازه مفید هستند. به عنوان نمونه، به جای استفاده از متغیر BOXSIZE می توانیم عدد صحیح 40 را مستقیما در برنامه به کار ببریم. اما دو دلیل برای استفاده از متغیرهای ثابت وجود دارد که در زیر به آن ها نگاهی می اندازیم:
XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * (BOXSIZE + GAPSIZE))) / 2)
اما اگر کد بالا از ثابت ها استفاده نمی کرد، به صورت زیر به نظر می رسید:
XMARGIN = int((640 – (10 * (40 + 10))) / 2)
پی بردن به این که برنامه نویس با نوشتن این خط کد دقیقا چه منظوری دارد غیرممکن است. این اعداد مبهم و غیرقابل توضیح اغلب اعداد جادویی نامیده می شوند. هر گاه که دیدید در حال استفاده از اعداد جادویی هستید خودتان را آسوده و آن ها را با یک ثابت جایگزین کنید. از نظر مفسر پایتون، دو خط بالا یکسان هستند. اما از نظر یک برنامه نویس که به برنامه نگاهی می اندازد و می کوشد که چگونگی کارکرد آن را دریابد، روش دوم به هیچ وجه معنایی ندارد! پس ثابت ها به خوانایی کد بسیاری کمک می کنند.
BOARDWIDTH = 10 # number of columns of icons BOARDHEIGHT = 7 # number of rows of icons assert (BOARDWIDTH * BOARDHEIGHT) % 2 == 0, 'Board needs to have an even number of boxes for pairs of matches.' XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * (BOXSIZE + GAPSIZE))) / 2) YMARGIN = int((WINDOWHEIGHT - (BOARDHEIGHT * (BOXSIZE + GAPSIZE))) / 2)
عبارت assert در بالا تضمین می کند عرض و ارتفاعی که برای صفحه انتخاب می کنیم یک عدد زوج خواهد بود.
هر عبارت assert سه بخش دارد:
در واقع assert این را می گوید که : "برنامه نویس ادعا می کند این عبارت باید صحیح باشد چون اگر نباشد باعث بروز خطا در برنامه می شود." این یک روش خوب برای بررسی خردمندانه برنامه است تا مطمئن شویم در صورتی که اجرای برنامه یک assert را رد می کند حداقل می توان مطمئن بود کد همان طور که انتظار می رود کار می کند.به عبارت دیگر اگر بخواهیم در هنگام پدید آمدن خطا برنامه ی ما متوقف نشود روش خردمندانه این است که از assert ها استفاده کنیم.
اگر حاصل ضرب ارتفاع در عرض صفحه به دو تقسیم شود و باقیمانده 0 شود پس عدد به دست آمده زوج است (عملگر ٪ میزان باقی مانده را ارزیابی می کند). باقیمانده تقسیم اعداد زوج بر دو همیشه صفر خواهد بود و باقیمانده تقسیم اعداد فرد بر دو همیشه یک است . این یک روش خوب برای تعیین زوج یا فرد بودن یک عدد است :
isEven = someNumber % 2 == 0 عدد زوج است isOdd = someNumber % 2 != 0 عدد فرد است
در مثال بالا، اگر someNumber زوج باشد، isEven دارای مقدار True خواهد شد و اگر فرد باشد، isOdd دارای مقدار True خواهد شد.
درهم شکستن برنامه چیز بدی است. درهم شکستن برنامه هنگامی رخ می دهد که برنامه شما خطایی داشته باشد و نتواند به کار خود ادامه دهد. اما مواردی وجود دارد که خراب شدن اختیاری و زود هنگام برنامه می تواند از بروز اشکال های بدتر در آینده جلوگیری کند. اگر مقادیری که برای BOARDWIDTH و BOARDHEIGHT انتخاب می کنیم عددی فرد باشند آن گاه حاصل ضرب آن ها عددی فرد خواهد شد که در نتیجه آن تعداد جعبه ها هم فرد می شود (مانند عرض 3 و ارتفاع 5 باشد) اگر تعداد جعبه ها فرد باشد و از آن جایی که از هر شکل دو تا وجود دارد که باید با هم جفت شوند پس همیشه یکی از این جعبه ها باقی خواهد ماند. این کار باعث ایجاد اشکال در برنامه می شود و باید وقت زیادی را صرف کنیم تا بفهمیم که ریشه ی واقعی این اشکال در همان آغاز برنامه است. برای دیدن این حقیقت به چشم خود، عبارت assert را comment کنید تا اجرا نشود، و سپس ثابت های BOARDWIDTH و BOARDHEIGHT را با اعداد فرد مقدار دهی کنید(مانند 5 و 7). هنگامی که برنامه را اجرا می کنید، بلافاصله خطایی رخ می دهد که ریشه اصلی آن در تابع getRandomizedBoard است.
باید زمان زیادی را صرف جستجو در تابع getRandomizedBoard بکنیم تا اشتباه آن را پیدا کنیم در حالی که ریشه واقعی اشکال در ثابت های BOARDWIDTH و BOARDHEIGHT در ابتدای برنامه است. استفاده از assert اطمینان می دهد که چنین اتفاقی هرگز رخ نمی دهد.
# R G B GRAY = (100, 100, 100) NAVYBLUE = (60, 60, 100) WHITE = (255, 255, 255) RED = (255, 0, 0) GREEN = (0, 255, 0) BLUE = (0, 0, 255) YELLOW = (255, 255, 0) ORANGE = (255, 128, 0) PURPLE = (255, 0, 255) CYAN = (0, 255, 255) BGCOLOR = NAVYBLUE LIGHTBGCOLOR = GRAY BOXCOLOR = WHITE HIGHLIGHTCOLOR = BLUE
به یاد داشته باشید که رنگ ها در Pygame با یک تاپل سه تایی از اعداد صحیح 0 تا 255 نشان داده می شوند. این سه عدد میزان رنگ قرمز، سبز و آبی را نشان می دهند به همین دلیل به این تاپل از مقادیر RGB (red green blue) می گویند. توجه داشته باشید که فاصله درج شده در تاپل ها در خطوط 22 تا 31 به گونه ای است که این اعداد صحیح در زیر حروف RGB قرار بگیرند. در پایتون تورفتگی (یعنی فضای موجود در ابتدای خط) باید بسیار دقیق باشد، اما فاصله در بقیه خط چندان مهم نیست. با فاصله دادن اعداد صحیح در تاپل، می توانیم به وضوح ببینیم که مقادیر RGB با یکدیگر چگونه مقایسه می شوند(اطلاعات بیشتر در مورد فاصله و تورفتگی در آدرس اینترنتی http://invpy.com/whitespace است).
DONUT = 'donut' SQUARE = 'square' DIAMOND = 'diamond' LINES = 'lines' OVAL = 'oval'
در بالا رشته ها را از نوع ثابت تعریف کرده ایم. از این ثابت ها در ساختمان داده ها برای صفحه استفاده می شود و هم چنین برای ردیابی این که فضاهای موجود در صفحه مربوط به چه شکل هایی هستند به کار می رود. استفاده از ثابت به جای مقدار رشته ای روش خوبی است. اگر هنگام نوشتن خط 187 اشتباه تایپی مانند زیر داشته باشیم :
if shape == DUNUT:
متغیر shape با یکی از رشته های 'donut'، 'square'، 'diamond'، 'lines' یا 'oval' مقداردهی می شود و سپس با ثابت DONUT مقایسه می شود. اگر هنگام نوشتن خط 187 اشتباه تایپی داشته باشیم، همانند کد زیر:
if shape == DUNOT:
برنامه متوقف می شود و پیغام خطایی می دهد که هیچ متغیری به نام DUNOT وجود ندارد. از آن جا که در خط 187 خطا داریم، هنگامی که آن خط را بررسی می کنیم، می توان فهمید که این اشکال توسط یک typo (اشتباه تایپی) ایجاد شده است. اما اگر به جای ثابت ها از رشته ها استفاده می کردیم و همان اشتباه تایپی را می ساختیم، خط 187 مانند زیر می شد:
if shape == 'dunot':
کد بالا یک کد درست است، بنابراین هنگام اجرای آن در ابتدا خراب نمی شود و خطایی ایجاد نمی کند. با این حال، این کار باعث می شود که در آینده در برنامه ما اشکالات عجیب و غریب پدیدار شود. از آن جا که کد بلافاصله جایی که مشکل ایجاد می شود خراب نمی شود، پیدا کردن سرچشمه آن بسیار سخت تر خواهد بود.
ALLCOLORS = (RED, GREEN, BLUE, YELLOW, ORANGE, PURPLE, CYAN) ALLSHAPES = (DONUT, SQUARE, DIAMOND, LINES, OVAL) assert len(ALLCOLORS) * len(ALLSHAPES) * 2 >= BOARDWIDTH * \ BOARDHEIGHT, "Board is too big for the number of shapes/colors defined."
برای این که برنامه ما بتواند شکل هایی از هر ترکیب ممکن رنگ و شکل ایجاد کند، باید تاپلی درست کنیم که تمام این مقادیر را در خود جای دهد(پس دو تاپل برای این کار نیاز داریم یک تاپل برای همه ی رنگ ها و یک تاپل ها برای همه ی شکل ها) .assert دوم در این برنامه برای مطمئن کردن ما است از این که ترکیب های کافی از رنگ و شکل داریم (این ترکیب ها متناسب با اندازه صفحه است). اگر به میزان لازم جفت رنگ و شکل وجود نداشته باشد، برنامه در خط 46 دچار خطا می شود و می دانیم که یا باید رنگ ها و شکل های بیشتری اضافه کنیم، یا عرض و ارتفاع صفحه را کم تر کنیم. با 7 رنگ و 5 شکل که در برنامه آن را مشخص کردیم می توانیم 35 (5*7) شکل گوناگون درست کنیم و از آن جایی که ما یک جفت از هر شکل داریم، صفحه ای خواهیم داشت که 70 (2*5*7) شکل دارد.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.