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

Memory puzzle

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

Different Coordinate Systems

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

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.

You should be familiar with Cartesian Coordinate systems. (If you'd like a refresher on this topic, read http://invpy.com/coordinates .) In most of our games we will be using multiple Cartesian Coordinate systems. One system of coordinates that is used in the Memory Puzzle game is for the pixel or screen coordinates. But we will also be using another coordinate system for the boxes. This is because it will be easier to use (3, 2) to refer to the 4th box from the left and 3rd from the top (remember that the numbers start with 0, not 1) instead of using the pixel coordinate of the box's top left corner, (220, 165). However, we need a way to translate between these two coordinate systems.

Here's a picture of the game and the two different coordinate systems. Remember that the window is 640 pixels wide and 480 pixels tall, so (639, 479) is the bottom right corner (because the top left corner's pixel is (0, 0), and not (1, 1)).


Рис. 3.3.

The leftTopCoordsOfBox() function will take box coordinates and return pixel coordinates. Because a box takes up multiple pixels on the screen, we will always return the single pixel at the top left corner of the box. This value will be returned as a two-integer tuple. The leftTopCoordsOfBox() function will often be used when we need pixel coordinates for drawing these boxes.

Converting from Pixel Coordinates to Box Coordinates

171. def getBoxAtPixel(x, y):
172.     for boxx in range(BOARDWIDTH):
173.         for boxy in range(BOARDHEIGHT):
174.             left, top = leftTopCoordsOfBox(boxx, boxy)
175.             boxRect = pygame.Rect(left, top, BOXSIZE, BOXSIZE)
176.             if boxRect.collidepoint(x, y):
177.                 return (boxx, boxy)
178.     return (None, None)

We will also need a function to convert from pixel coordinates (which the mouse clicks and mouse movement events use) to box coordinates (so we can find out over which box the mouse event happened). Rect objects have a collidepoint() method that you can pass X and Y coordinates too and it will return True if the coordinates are inside (that is, collide with) the Rect object's area.

In order to find which box the mouse coordinates are over, we will go through each box's coordinates and call the collidepoint() method on a Rect object with those coordinates. When collidepoint() returns True, we know we have found the box that was clicked on or moved over and will return the box coordinates. If none of them return True, then the getBoxAtPixel() function will return the value (None, None). This tuple is returned instead of simply returning None because the caller of getBoxAtPixel() is expecting a tuple of two values to be returned.

Drawing the Icon, and Syntactic Sugar

181. def drawIcon(shape, color, boxx, boxy):
182.     quarter = int(BOXSIZE * 0.25) # syntactic sugar
183.     half =    int(BOXSIZE * 0.5)  # syntactic sugar
184.
185.     left, top = leftTopCoordsOfBox(boxx, boxy) # get pixel coords from
board coords

The drawIcon() function will draw an icon (with the specified shape and color) at the space whose coordinates are given in the boxx and boxy parameters. Each possible shape has a different set of Pygame drawing function calls for it, so we must have a large set of if and elif statements to differentiate between them. (These statements are on lines 187 to 198.)

The X and Y coordinates of the left and top edge of the box can be obtained by calling the leftTopCoordsOfBox() function. The width and height of the box are both set in the BOXSIZE constant. However, many of the shape drawing function calls use the midpoint and quarter-point of the box as well. We can calculate this and store it in the variables quarter and half. We could just as easily have the code int(BOXSIZE * 0.25) instead of the variable quarter, but this way the code becomes easier to read since it is more obvious what quarter means rather than int(BOXSIZE * 0.25).

Such variables are an example of syntactic sugar. Syntactic sugar is when we add code that could have been written in another way (probably with less actual code and variables), but does make the source code easier to read. Constant variables are one form of syntactic sugar. Pre- calculating a value and storing it in a variable is another type of syntactic sugar. (For example, in the getRandomizedBoard() function, we could have easily made the code on lines 140 and line 141 into a single line of code. But it's easier to read as two separate lines.) We don't need to have the extra quarter and half variables, but having them makes the code easier to read. Code that is easy to read is easy to debug and upgrade in the future.

186.     # Draw the shapes
187.     if shape == DONUT:
188.         pygame.draw.circle(DISPLAYSURF, color, (left + half, top + half),
half - 5)
189.         pygame.draw.circle(DISPLAYSURF, BGCOLOR, (left + half, top +
half), quarter - 5)
190.     elif shape == SQUARE:
191.         pygame.draw.rect(DISPLAYSURF, color, (left + quarter, top +
quarter, BOXSIZE - half, BOXSIZE - half))
192.     elif shape == DIAMOND:
193.         pygame.draw.polygon(DISPLAYSURF, color, ((left + half, top), (left
+ BOXSIZE - 1, top + half), (left + half, top + BOXSIZE - 1), (left, top +
half)))
194.     elif shape == LINES:
195.         for i in range(0, BOXSIZE, 4):
196.             pygame.draw.line(DISPLAYSURF, color, (left, top + i), (left +
i, top))
197.             pygame.draw.line(DISPLAYSURF, color, (left + i, top + BOXSIZE
- 1), (left + BOXSIZE - 1, top + i))
198.     elif shape == OVAL:
199.         pygame.draw.ellipse(DISPLAYSURF, color, (left, top + quarter,
BOXSIZE, half))

Each of the donut, square, diamond, lines, and oval functions require different drawing primitive function calls to make.

Syntactic Sugar with Getting a Board Space's Icon's Shape and Color

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

The getShapeAndColor() function only has one line. You might wonder why we would want a function instead of just typing in that one line of code whenever we need it. This is done for the same reason we use constant variables: it improves the readability of the code.

It's easy to figure out what a code like shape, color = getShapeAndColor() does. But if you looked a code like shape, color = board[boxx][boxy][0], board[boxx][boxy][1], it would be a bit more difficult to figure out.

Drawing the Box Cover

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

The drawBoxCovers() function has three parameters: the board data structure, a list of (X, Y) tuples for each box that should have the cover drawn, and then the amount of coverage to draw for the boxes.

Since we want to use the same drawing code for each box in the boxes parameter, we will use a for loop on line 211 so we execute the same code on each box in the boxes list. Inside this for loop, the code should do three things: draw the background color (to paint over anything that was there before), draw the icon, then draw however much of the white box over the icon that is needed. The leftTopCoordsOfBox() function will return the pixel coordinates of the top left corner of the box. The if statement on line 216 makes sure that if the number in coverage happens to be less than 0, we won't call the pygame.draw.rect() function.

When the coverage parameter is 0, there is no coverage at all. When the coverage is set to 20, there is a 20 pixel wide white box covering the icon. The largest size we'll want the coverage set to is the number in BOXSIZE, where the entire icon is completely covered.

drawBoxCovers() is going to be called from a separate loop than the game loop. Because of this, it needs to have its own calls to pygame.display.update() and FPSCLOCK.tick(FPS) to display the animation (This does mean that while inside this loop, there is no code being run to handle any events being generated. That's fine, since the cover and reveal animations only take a second or so to play).

Handling the Revealing and Covering Animation

222. def revealBoxesAnimation(board, boxesToReveal):
223.     # Do the "box reveal" animation.
224.     for coverage in range(BOXSIZE, (-REVEALSPEED) - 1, - REVEALSPEED):
225.         drawBoxCovers(board, boxesToReveal, coverage)
226.
227.
228. def coverBoxesAnimation(board, boxesToCover):
229.     # Do the "box cover" animation.
230.     for coverage in range(0, BOXSIZE + REVEALSPEED, REVEALSPEED):
231.         drawBoxCovers(board, boxesToCover, coverage)

Remember that an animation is simply just displaying different images for brief moments of time, and together they make it seem like things are moving on the screen. The revealBoxesAnimation() and coverBoxesAnimation() only need to draw an icon with a varying amount of coverage by the white box. We can write a single function called drawBoxCovers() which can do this, and then have our animation function call drawBoxCovers() for each frame of animation. As we saw in the last section, drawBoxCovers() makes a call to pygame.display.update() and FPSCLOCK.tick(FPS) itself.

To do this, we'll set up a for loop to make decreasing (in the case of revealBoxesAnimation()) or increasing (in the case of coverBoxesAnimation()) numbers for the converage parameter. The amount that the coverage variable will decrease/increase by is the number in the REVEALSPEED constant. On line 12 we set this constant to 8, meaning that on each call to drawBoxCovers(), the white box will decrease/increase by 8 pixels on each iteration. If we increase this number, then more pixels will be drawn on each call, meaning that the white box will decrease/increase in size faster. If we set it to 1, then the white box will only appear to decrease or increase by 1 pixel on each iteration, making the entire reveal or cover animation take longer.

Think of it like climbing stairs. If on each step you take, you climbed one stair, then it would take a normal amount of time to climb the entire staircase. But if you climbed two stairs at a time on each step (and the steps took just as long as before), you could climb the entire staircase twice as fast. If you could climb the staircase 8 stairs at a time, then you would climb the entire staircase 8 times as fast.

Drawing the Entire Board

234. def drawBoard(board, revealed>):
235.     # Draws all of the boxes in their covered or revealed state.
236.     for boxx in range(BOARDWIDTH):
237.         for boxy in range(BOARDHEIGHT):
238.             left, top = leftTopCoordsOfBox(boxx, boxy)
239.             if not revealed[boxx][boxy]:
240.                 # Draw a covered box.
241.                 pygame.draw.rect(DISPLAYSURF, BOXCOLOR, (left, top,
BOXSIZE, BOXSIZE))
242.             else:
243.                 # Draw the (revealed) icon.
244.                 shape, color = getShapeAndColor(board, boxx, boxy)
245.                 drawIcon(shape, color, boxx, boxy)

The drawBoard() function makes a call to drawIcon() for each of the boxes on the board. The nested for loops on lines 236 and 237 will loop through every possible X and Y coordinate for the boxes, and will either draw the icon at that location or draw a white square instead (to represent a covered up box).

< Лекция 2 || Лекция 3: 12345 || Лекция 4 >
Константин Шилов
Константин Шилов
Россия
Павел Лафицкий
Павел Лафицкий
Украина