Опубликован: 06.08.2013 | Доступ: свободный | Студентов: 783 / 11 | Длительность: 27:51:00
Лекция 3:

Memory puzzle

< Лекция 2 || Лекция 3: 12345 || Лекция 4 >

The Game Loop

66.     while True: # main game loop
67.         mouseClicked = False
68.
69.         DISPLAYSURF.fill(BGCOLOR) # drawing the window
70.         drawBoard(mainBoard, revealedBoxes)

The game loop is an infinite loop that starts on line 66 that keeps iterating for as long as the game is in progress. Remember that the game loop handles events, updates the game state, and draws the game state to the screen.

The game state for the Memory Puzzle program is stored in the following variables:

  • mainBoard
  • revealedBoxes
  • firstSelection
  • mouseClicked
  • mousex
  • mousey

On each iteration of the game loop in the Memory Puzzle program, the mouseClicked variable stores a Boolean value that is True if the player has clicked the mouse during this iteration through the game loop (This is part of keeping track of the game state).

On line 69, the surface is painted over with the background color to erase anything that was previously drawn on it. The program then calls drawBoard() to draw the current state of the board based on the board and "revealed boxes" data structures that we pass it (These lines of code are part of drawing and updating the screen).

Remember that our drawing functions only draw on the in-memory display Surface object. This Surface object will not actually appear on the screen until we call pygame.display.update(), which is done at the end of the game loop on line 121.

The Event Handling Loop

72.         for event in pygame.event.get(): # event handling loop
73.             if event.type == QUIT or (event.type == KEYUP and event.key ==
K_ESCAPE):
74.                 pygame.quit()
75.                 sys.exit()
76.             elif event.type == MOUSEMOTION:
77.                 mousex, mousey = event.pos
78.             elif event.type == MOUSEBUTTONUP:
79.                 mousex, mousey = event.pos
80.                 mouseClicked = True

The for loop on line 72 executes code for every event that has happened since the last iteration of the game loop. This loop is called the event handling loop (which is different from the game loop, although the event handling loop is inside of the game loop) and iterates over the list of pygame.Event objects returned by the pygame.event.get() call.

If the event object was a either a QUIT event or a KEYUP event for the Esc key, then the program should terminate. Otherwise, in the event of a MOUSEMOTION event (that is, the mouse cursor has moved) or MOUSEBUTTONUP event (that is, a mouse button was pressed earlier and now the button was let up), the position of the mouse cursor should be stored in the mousex and mousey variables. If this was a MOUSEBUTTONUP event, mouseClicked should also be set to True.

Once we have handled all of the events, the values stored in mousex, mousey, and mouseClicked will tell us any input that player has given us. Now we should update the game state and draw the results to the screen.

Checking Which Box The Mouse Cursor is Over

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

The getBoxAtPixel() function will return a tuple of two integers. The integers represent the XY board coordinates of the box that the mouse coordinates are over. How getBoxAtPixel() does this is explained later. All we have to know for now is that if the mousex and mousey coordinates were over a box, a tuple of the XY board coordinates are returned by the function and stored in boxx and boxy. If the mouse cursor was not over any box (for example, if it was off to the side of the board or in a gap in between boxes) then the tuple (None, None) is returned by the function and boxx and boxy will both have None stored in them.

We are only interested in the case where boxx and boxy do not have None in them, so the next several lines of code are in the block following the if statement on line 83 that checks for this case. If execution has come inside this block, we know the user has the mouse cursor over a box (and maybe has also clicked the mouse, depending on the value stored in mouseClicked).

The if statement on line 85 checks if the box is covered up or not by reading the value stored in revealedBoxes[boxx][boxy]. If it is False, then we know the box is covered. Whenever the mouse is over a covered up box, we want to draw a blue highlight around the box to inform the player that they can click on it. This highlighting is not done for boxes that are already uncovered. The highlight drawing is handled by our drawHighlightBox() function, which is explained later.

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

On line 87, we check if the mouse cursor is not only over a covered up box but if the mouse has also been clicked. In that case, we want to play the "reveal" animation for that box by calling our revealBoxesAnimation() function (which is, as with all the other functions main() calls, explained later in this chapter). You should note that calling this function only draws the animation of the box being uncovered. It isn’t until line 89 when we set revealedBoxes[boxx][boxy] = True that the data structure that tracks the game state is updated.

If you comment out line 89 and then run the program, you’ll notice that after clicking on a box the reveal animation is played, but then the box immediately appears covered up again. This is because revealedBoxes[boxx][boxy] is still set to False, so on the next iteration of the game loop, the board is drawn with this box covered up. Not having line 89 would cause quite an odd bug in our program.

Handling the First Clicked Box

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

Before the execution entered the game loop, the firstSelection variable was set to None. Our program will interpret this to mean that no boxes have been clicked, so if line 90’s condition is True, that means this is the first of the two possibly matching boxes that was clicked. We want to play the reveal animation for the box and then keep that box uncovered. We also set the firstSelection variable to a tuple of the box coordinates for the box that was clicked.

If this is the second box the player has clicked on, we want to play the reveal animation for that box but then check if the two icons under the boxes are matching. The getShapeAndColor() function (explained later) will retrieve the shape and color values of the icons (These values will be one of the values in the ALLCOLORS and ALLSHAPES tuples).

Handling a Mismatched Pair of Icons

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

The if statement on line 97 checks if either the shapes or colors of the two icons don’t match. If this is the case, then we want to pause the game for 1000 milliseconds (which is the same as 1 second) by calling pygame.time.wait(1000) so that the player has a chance to see that the two icons don’t match. Then the "cover up" animation plays for both boxes. We also want to update the game state to mark these boxes as not revealed (that is, covered up).

Handling If the Player Won

103.                     elif hasWon(revealedBoxes): # check if all pairs found
104.                         gameWonAnimation(mainBoard)
105.                         pygame.time.wait(2000)
106.
107.                         # Reset the board
108.                         mainBoard = getRandomizedBoard()
109.                         revealedBoxes = generateRevealedBoxesData(False)
110.
111.                         # Show the fully unrevealed board for a second.
112.                         drawBoard(mainBoard, revealedBoxes)
113.                         pygame.display.update()
114.                         pygame.time.wait(1000)
115.
116.                         # Replay the start game animation.
117.                         startGameAnimation(mainBoard)
118.                     firstSelection = None # reset firstSelection variable

Otherwise, if line 97’s condition was False, then the two icons must be a match. The program doesn’t really have to do anything else to the boxes at that point: it can just leave both boxes in the revealed state. However, the program should check if this was the last pair of icons on the board to be matched. This is done inside our hasWon() function, which returns True if the board is in a winning state (that is, all of the boxes are revealed).

If that is the case, we want to play the "game won" animation by calling gameWonAnimation(), then pause slightly to let the player revel in their victory, and then reset the data structures in mainBoard and revealedBoxes to start a new game.

Line 117 plays the "start game" animation again. After that, the program execution will just loop through the game loop as usual, and the player can continue playing until they quit the program.

No matter if the two boxes were matching or not, after the second box was clicked line 118 will set the firstSelection variable back to None so that the next box the player clicks on will be interpreted as the first clicked box of a pair of possibly matching icons.

Drawing the Game State to the Screen

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

At this point, the game state has been updated depending on the player’s input, and the latest game state has been drawn to the DISPLAYSURF display Surface object. We’ve reached the end of the game loop, so we call pygame.display.update() to draw the DISPLAYSURF Surface object to the computer screen.

Line 9 set the FPS constant to the integer value 30, meaning we want the game to run (at most) at 30 frames per second. If we want the program to run faster, we can increase this number. If we want the program to run slower, we can decrease this number. It can even be set to a float value like 0.5, which will run the program at half a frame per second, that is, one frame per two seconds.

In order to run at 30 frames per second, each frame must be drawn in 1/30 th of a second. This means that pygame.display.update() and all the code in the game loop must execute in under 33.3 milliseconds. Any modern computer can do this easily with plenty of time left over. To prevent the program from running too fast, we call the tick() method of the pygame.Clock object in FPSCLOCK to have to it pause the program for the rest of the 33.3 milliseconds.

Since this is done at the very end of the game loop, it ensures that each iteration of the game loop takes (at least) 33.3 milliseconds. If for some reason the pygame.display.update() call and the code in the game loop takes longer than 33.3 milliseconds, then the tick() method will not wait at all and immediately return.

I’ve kept saying that the other functions would be explained later in the chapter. Now that we’ve gone over the main() function and you have an idea for how the general program works, let’s go into the details of all the other functions that are called from main().

Creating the "Revealed Boxes" Data Structure

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

The generateRevealedBoxesData() function needs to create a list of lists of Boolean values. The Boolean value will just be the one that is passed to the function as the val parameter. We start the data structure as an empty list in the revealedBoxes variable.

In order to make the data structure have the revealedBoxes[x][y] structure, we need to make sure that the inner lists represent the vertical columns of the board and not the horizontal rows. Otherwise, the data structure will have a revealedBoxes[y][x] structure.

The for loop will create the columns and then append them to revealedBoxes. The columns are created using list replication, so that the column list has as many val values as the BOARDHEIGHT dictates.

Creating the Board Data Structure: Step 1 – Get All Possible Icons

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

The board data structure is just a list of lists of tuples, where each tuple has a two values: one for the icon’s shape and one for the icon’s color. But creating this data structure is a little complicated. We need to be sure to have exactly as many icons for the number of boxes on the board and also be sure there are two and only two icons of each type.

The first step to do this is to create a list with every possible combination of shape and color. Recall that we have a list of each color and shape in ALLCOLORS and ALLSHAPES, so nested for loops on lines 135 and 136 will go through every possible shape for every possible color. These are each added to the list in the icons variable on line 137.

Step 2 – Shuffling and Truncating the List of All Icons

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

But remember, there may be more possible combinations than spaces on the board. We need to calculate the number of spaces on the board by multiplying BOARDWIDTH by BOARDHEIGHT. Then we divide that number by 2 because we will have pairs of icons. On a board with 70 spaces, we’d only need 35 different icons, since there will be two of each icon. This number will be stored in numIconsUsed.

Line 141 uses list slicing to grab the first numIconsUsed number of icons in the list. (If you’ve forgotten how list slicing works, check out http://invpy.com/slicing .) This list has been shuffled on line 139, so it won’t always be the same icons each game. Then this list is replicated by using the * operator so that there are two of each of the icons. This new doubled up list will overwrite the old list in the icons variable. Since the first half of this new list is identical to the last half, we call the shuffle() method again to randomly mix up the order of the icons.

Step 3 – Placing the Icons on the Board

144.     # Create the board data structure, with randomly placed icons.
145.     board = []
146.     for x in range(BOARDWIDTH):
147.         column = []
148.         for y in range(BOARDHEIGHT):
149.             column.append(icons[0])
150.             del icons[0] # remove the icons as we assign them
151.         board.append(column)
152.     return board

Now we need to create a list of lists data structure for the board. We can do this with nested for loops just like the generateRevealedBoxesData() function did. For each column on the board, we will create a list of randomly selected icons. As we add icons to the column, on line 149 we will then delete them from the front of the icons list on line 150. This way, as the icons list gets shorter and shorter, icons[0] will have a different icon to add to the columns.

To picture this better, type the following code into the interactive shell. Notice how the del statement changes the myList list.

>>> myList = ['cat', 'dog', 'mouse', 'lizard']
>>> del myList[0]
>>> myList
['dog', 'mouse', 'lizard']
>>> del myList[0]
>>> myList
['mouse', 'lizard']
>>> del myList[0]
>>> myList
['lizard']
>>> del myList[0]
>>> myList
[]
>>>

Because we are deleting the item at the front of the list, the other items shift forward so that the next item in the list becomes the new "first" item. This is the same way line 150 works.

Splitting a List into a List of Lists

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

The splitIntoGroupsOf() function (which will be called by the startGameAnimation() function) splits a list into a list of lists, where the inner lists have groupSize number of items in them (The last list could have less if there are less than groupSize items left over).

The call to range() on line 159 uses the three-parameter form of range() (If you are unfamiliar with this form, take a look at http://invpy.com/range ). Let’s use an example. If the length of the list is 20 and the groupSize parameter is 8, then range(0,len(theList), groupSize) evaluates to range(0, 20, 8). This will give the i variable the values 0, 8, and 16 for the three iterations of the for loop.

The list slicing on line 160 with theList[i:i + groupSize] creates the lists that are added to the result list. On each iteration where i is 0, 8, and 16 (and groupSize is 8), this list slicing expression would be theList[0:8], then theList[8:16] on the second iteration, and then theList[16:24] on the third iteration.

Note that even though the largest index of theList would be 19 in our example, theList[16:24] won’t raise an IndexError error even though 24 is larger than 19. It will just create a list slice with the remaining items in the list. List slicing doesn’t destroy or change the original list stored in theList. It just copies a portion of it to evaluate to a new list value. This new list value is the list that is appended to the list in the result variable on line 160. So when we return result at the end of this function, we are returning a list of lists.

< Лекция 2 || Лекция 3: 12345 || Лекция 4 >
Андрей Дубинин
Андрей Дубинин
Россия, Калининград, МГУТУ, 2001