Chapter 3 – Memory Puzzle

How to Play Memory Puzzle
In the Memory Puzzle game, several icons are covered up by white boxes. There are two of each icon. The player can click on two boxes to see what icon is behind them. If the icons match, then those boxes remain uncovered. The player wins when all the boxes on the board are uncovered. To give the player a hint, the boxes are quickly uncovered once at the beginning of the game.
Nested for Loops
One concept that you will see in Memory Puzzle (and most of
the games in this book) is the use of a for loop
inside of another for loop. These are called nested for loops. Nested for loops are
handy for going through every possible combination of two lists. Type the
following into the interactive shell:
>>> for x in [0, 1, 2, 3, 4]:
... for y in ['a', 'b', 'c']:
... print(x, y)
...
0 a
0 b
0 c
1 a
1 b
1 c
2 a
2 b
2 c
3 a
3 b
3 c
4 a
4 b
4 c
>>>There are several times in the Memory Puzzle code that we
need to iterate through every possible X and Y coordinate on the board. We’ll
use nested for loops to make sure that we get every
combination. Note that the inner for loop (the for loop inside the other for
loop) will go through all of its iterations before going to the next iteration
of the outer for loop. If we reverse the order of
the for loops, the same values will be printed but
they will be printed in a different order. Type the following code into the
interactive shell, and compare the order it prints values to the order in the
previous nested for loop example:
>>> for y in ['a', 'b', 'c']:
... for x in [0, 1, 2, 3, 4]:
... print(x, y)
...
0 a
1 a
2 a
3 a
4 a
0 b
1 b
2 b
3 b
4 b
0 c
1 c
2 c
3 c
4 c
>>>Source Code of Memory Puzzle
This source code can be downloaded from http://invpy.com/memorypuzzle.py.
Go ahead and first type in the entire program into IDLE’s file editor, save it as memorypuzzle.py, and run it. If you get any error messages, look at the line number that is mentioned in the error message and check your code for any typos. You can also copy and paste your code into the web form at http://invpy.com/diff/memorypuzzle to see the differences between your code and the code in the book.
You’ll probably pick up a few ideas about how the program works just by typing it in once. And when you’re done typing it in, you can then play the game for yourself.
1. # Memory Puzzle
2. # By Al Sweigart [email protected]
3. # http://inventwithpython.com/pygame
4. # Released under a "Simplified BSD" license
5.
6. import random, pygame, sys
7. from pygame.locals import *
8.
9. FPS = 30 # frames per second, the general speed of the program
10. WINDOWWIDTH = 640 # size of window's width in pixels
11. WINDOWHEIGHT = 480 # size of windows' height in pixels
12. REVEALSPEED = 8 # speed boxes' sliding reveals and covers
13. BOXSIZE = 40 # size of box height & width in pixels
14. GAPSIZE = 10 # size of gap between boxes in pixels
15. BOARDWIDTH = 10 # number of columns of icons
16. BOARDHEIGHT = 7 # number of rows of icons
17. assert (BOARDWIDTH * BOARDHEIGHT) % 2 == 0, 'Board needs to have an even number of boxes for pairs of matches.'
18. XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * (BOXSIZE + GAPSIZE))) / 2)
19. YMARGIN = int((WINDOWHEIGHT - (BOARDHEIGHT * (BOXSIZE + GAPSIZE))) / 2)
20.
21. # R G B
22. GRAY = (100, 100, 100)
23. NAVYBLUE = ( 60, 60, 100)
24. WHITE = (255, 255, 255)
25. RED = (255, 0, 0)
26. GREEN = ( 0, 255, 0)
27. BLUE = ( 0, 0, 255)
28. YELLOW = (255, 255, 0)
29. ORANGE = (255, 128, 0)
30. PURPLE = (255, 0, 255)
31. CYAN = ( 0, 255, 255)
32.
33. BGCOLOR = NAVYBLUE
34. LIGHTBGCOLOR = GRAY
35. BOXCOLOR = WHITE
36. HIGHLIGHTCOLOR = BLUE
37.
38. DONUT = 'donut'
39. SQUARE = 'square'
40. DIAMOND = 'diamond'
41. LINES = 'lines'
42. OVAL = 'oval'
43.
44. ALLCOLORS = (RED, GREEN, BLUE, YELLOW, ORANGE, PURPLE, CYAN)
45. ALLSHAPES = (DONUT, SQUARE, DIAMOND, LINES, OVAL)
46. assert len(ALLCOLORS) * len(ALLSHAPES) * 2 >= BOARDWIDTH * BOARDHEIGHT, "Board is too big for the number of shapes/colors defined."
47.
48. def main():
49. global FPSCLOCK, DISPLAYSURF
50. pygame.init()
51. FPSCLOCK = pygame.time.Clock()
52. DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
53.
54. mousex = 0 # used to store x coordinate of mouse event
55. mousey = 0 # used to store y coordinate of mouse event
56. pygame.display.set_caption('Memory Game')
57.
58. mainBoard = getRandomizedBoard()
59. revealedBoxes = generateRevealedBoxesData(False)
60.
61. firstSelection = None # stores the (x, y) of the first box clicked.
62.
63. DISPLAYSURF.fill(BGCOLOR)
64. startGameAnimation(mainBoard)
65.
66. while True: # main game loop
67. mouseClicked = False
68.
69. DISPLAYSURF.fill(BGCOLOR) # drawing the window
70. drawBoard(mainBoard, revealedBoxes)
71.
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
81.
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)
87. if not revealedBoxes[boxx][boxy] and mouseClicked:
88. revealBoxesAnimation(mainBoard, [(boxx, boxy)])
89. revealedBoxes[boxx][boxy] = True # set the box as "revealed"
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)
96.
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
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
119.
120. # Redraw the screen and wait a clock tick.
121. pygame.display.update()
122. FPSCLOCK.tick(FPS)
123.
124.
125. def generateRevealedBoxesData(val):
126. revealedBoxes = []
127. for i in range(BOARDWIDTH):
128. revealedBoxes.append([val] * BOARDHEIGHT)
129. return revealedBoxes
130.
131.
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) )
138.
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)
143.
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
153.
154.
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
162.
163.
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)
169.
170.
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)
179.
180.
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
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))
200.
201.
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]
206.
207.
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)
220.
221.
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)
232.
233.
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)
246.
247.
248. def drawHighlightBox(boxx, boxy):
249. left, top = leftTopCoordsOfBox(boxx, boxy)
250. pygame.draw.rect(DISPLAYSURF, HIGHLIGHTCOLOR, (left - 5, top - 5, BOXSIZE + 10, BOXSIZE + 10), 4)
251.
252.
253. def startGameAnimation(board):
254. # Randomly reveal the boxes 8 at a time.
255. coveredBoxes = generateRevealedBoxesData(False)
256. boxes = []
257. for x in range(BOARDWIDTH):
258. for y in range(BOARDHEIGHT):
259. boxes.append( (x, y) )
260. random.shuffle(boxes)
261. boxGroups = splitIntoGroupsOf(8, boxes)
262.
263. drawBoard(board, coveredBoxes)
264. for boxGroup in boxGroups:
265. revealBoxesAnimation(board, boxGroup)
266. coverBoxesAnimation(board, boxGroup)
267.
268.
269. def gameWonAnimation(board):
270. # flash the background color when the player has won
271. coveredBoxes = generateRevealedBoxesData(True)
272. color1 = LIGHTBGCOLOR
273. color2 = BGCOLOR
274.
275. for i in range(13):
276. color1, color2 = color2, color1 # swap colors
277. DISPLAYSURF.fill(color1)
278. drawBoard(board, coveredBoxes)
279. pygame.display.update()
280. pygame.time.wait(300)
281.
282.
283. def hasWon(revealedBoxes):
284. # Returns True if all the boxes have been revealed, otherwise False
285. for i in revealedBoxes:
286. if False in i:
287. return False # return False if any boxes are covered.
288. return True
289.
290.
291. if __name__ == '__main__':
292. main()Credits and Imports
1. # Memory Puzzle
2. # By Al Sweigart [email protected]
3. # http://inventwithpython.com/pygame
4. # Released under a "Simplified BSD" license
5.
6. import random, pygame, sys
7. from pygame.locals import *At the top of the program are comments about what the game is, who made it, and where the user could find more information. There’s also a note that the source code is freely copyable under a “Simplified BSD” license. The Simplified BSD license is more appropriate for software than the Creative Common license (which this book is released under), but they basically mean the same thing: People are free to copy and share this game. More info about licenses can be found at http://invpy.com/licenses.
This program makes use of many functions in other modules,
so it imports those modules on line 6. Line 7 is also an import
statement in the from (module name) import * format,
which means you do not have to type the module name in front of it. There are
no functions in the pygame.locals module, but there
are several constant variables in it that we want to use such as MOUSEMOTION, KEYUP, or QUIT. Using this style of import
statement, we only have to type MOUSEMOTION rather
than pygame.locals.MOUSEMOTION.
Magic Numbers are Bad
9. FPS = 30 # frames per second, the general speed of the program
10. WINDOWWIDTH = 640 # size of window's width in pixels
11. WINDOWHEIGHT = 480 # size of windows' height in pixels
12. REVEALSPEED = 8 # speed boxes' sliding reveals and covers
13. BOXSIZE = 40 # size of box height & width in pixels
14. GAPSIZE = 10 # size of gap between boxes in pixelsThe game programs in this book use a lot of constant
variables. You might not realize why they’re so handy. For example, instead of
using the BOXSIZE variable in our code we could just
type the integer 40 directly in the code. But there
are two reasons to use constant variables.
First, if we ever wanted to change the size of each box
later, we would have to go through the entire program and find and replace each
time we typed 40. By just using the BOXSIZE constant, we only have to change line 13 and the
rest of the program is already up to date. This is much better, especially
since we might use the integer value 40 for
something else besides the size of the white boxes, and changing that 40 accidentally would cause bugs in our program.
Second, it makes the code more readable. Go down to the next
section and look at line 18. This sets up a calculation for the XMARGIN constant, which is how many pixels are on the side
of the entire board. It is a complicated looking expression, but you can
carefully piece out what it means. Line 18 looks like this:
XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * (BOXSIZE + GAPSIZE))) / 2)But if line 18 didn’t use constant variables, it would look like this:
XMARGIN = int((640 - (10 * (40 + 10))) / 2)Now it becomes impossible to remember what exactly the programmer intended to mean. These unexplained numbers in the source code are often called magic numbers. Whenever you find yourself entering magic numbers, you should consider replacing them with a constant variable instead. To the Python interpreter, both of the previous lines are exactly the same. But to a human programmer who is reading the source code and trying to understand how it works, the second version of line 18 doesn’t make much sense at all! Constants really help the readability of source code.
Of course, you can go too far replacing numbers with constant variables. Look at the following code:
ZERO = 0
ONE = 1
TWO = 99999999
TWOANDTHREEQUARTERS = 2.75Don’t write code like that. That’s just silly.
Sanity Checks with assert
Statements
15. BOARDWIDTH = 10 # number of columns of icons
16. BOARDHEIGHT = 7 # number of rows of icons
17. assert (BOARDWIDTH * BOARDHEIGHT) % 2 == 0, 'Board needs to have an even number of boxes for pairs of matches.'
18. XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * (BOXSIZE + GAPSIZE))) / 2)
19. YMARGIN = int((WINDOWHEIGHT - (BOARDHEIGHT * (BOXSIZE + GAPSIZE))) / 2)The assert statement on line 17
ensures that the board width and height we’ve selected will result in an even
number of boxes (since we will have pairs of icons in this game). There are
three parts to an assert statement: the assert keyword; an expression which, if False, results in crashing the program; and, after a comma,
a string that appears if the program crashes because of the assertion.
The assert statement with an
expression basically says, “The programmer asserts that this expression must be
True, otherwise crash the program.” This is a good
way of adding a sanity check to your program to make sure that if the execution
ever passes an assertion we can at least know that that code is working as
expected.
Telling If a Number is Even or Odd
If the product of the board width and height is divided by
two and has a remainder of 0 (the % modulus operator evaluates what the remainder is) then
the number is even. Even numbers divided by two will always have a remainder of
zero. Odd numbers divided by two will always have a remainder of one. This is a
good trick to remember if you need your code to tell if a number is even or
odd:
>>> isEven = someNumber % 2 == 0
>>> isOdd = someNumber % 2 != 0In the above case, if the integer in someNumber
was even, then isEven will be True.
If it was odd, then isOdd will be True.
Crash Early and Crash Often!
Having your program crash is a bad thing. It happens when your program has some mistake in the code and cannot continue. But there are some cases where crashing a program early can avoid worse bugs later.
If the values we chose for BOARDWIDTH and BOARDHEIGHT on lines 15 and 16 result in a board with an odd number of boxes (such as if the width were 3 and
the height were 5), then there would always be one left over icon that would
not have a pair to be matched with. This would cause a bug later on in the
program, and it could take a lot of debugging work to figure out that the real
source of the bug is at the very beginning of the program. In fact, just for
fun, try commenting out the assertion so it doesn’t run, and then setting the BOARDWIDTH and BOARDHEIGHT
constants both to odd numbers. When you run the program, it will immediately show an error happening on line 149 in memorypuzzle.py, which is in the getRandomizedBoard() function!
Traceback (most recent call last):
File "C:\book2svn\src\memorypuzzle.py", line 292, in <module>
main()
File "C:\book2svn\src\memorypuzzle.py", line 58, in main
mainBoard = getRandomizedBoard()
File "C:\book2svn\src\memorypuzzle.py", line 149, in getRandomizedBoard
columns.append(icons[0])
IndexError: list index out of rangeWe could spend a lot of time looking at getRandomizedBoard()
trying to figure out what’s wrong with it before realizing that getRandomizedBoard() is perfectly fine: the real source of
the bug was on line 15 and 16 where we set the BOARDWIDTH
and BOARDHEIGHT constants.
The assertion makes sure that this never happens. If our code is going to crash, we want it to crash as soon as it detects something is terribly wrong, because otherwise the bug may not become apparent until much later in the program. Crash early!
You want to add assert statements
whenever there is some condition in your program that must always, always,
always be True. Crash often! You don’t have to go
overboard and put assert statements everywhere, but
crashing often with asserts goes a long way in detecting the true source of a
bug. Crash early and crash often!
(In your code that is. Not, say, when riding a pony.)
Making the Source Code Look Pretty
21. # R G B
22. GRAY = (100, 100, 100)
23. NAVYBLUE = ( 60, 60, 100)
24. WHITE = (255, 255, 255)
25. RED = (255, 0, 0)
26. GREEN = ( 0, 255, 0)
27. BLUE = ( 0, 0, 255)
28. YELLOW = (255, 255, 0)
29. ORANGE = (255, 128, 0)
30. PURPLE = (255, 0, 255)
31. CYAN = ( 0, 255, 255)
32.
33. BGCOLOR = NAVYBLUE
34. LIGHTBGCOLOR = GRAY
35. BOXCOLOR = WHITE
36. HIGHLIGHTCOLOR = BLUERemember that colors in Pygame are represented by a tuple of
three integers from 0 to 255.
These three integers represent the amount of red, green, and blue in the color
which is why these tuples are called RGB values. Notice that the spacing of the tuples on lines 22 to 31 is such that the R, G, and B integers line up. In
Python the indentation (that is, the space at the beginning of the line) needs to be exact, but the spacing in the rest of the line is not so strict. By
spacing the integers in the tuple out, we can clearly see how the RGB values
compare to each other. (More info on spacing and indentation is at http://invpy.com/whitespace.)
It is a nice thing to make your code more readable this way, but don’t bother spending too much time doing it. Code doesn’t have to be pretty to work. At a certain point, you’ll just be spending more time typing spaces than you would have saved by having readable tuple values.
Using Constant Variables Instead of Strings
38. DONUT = 'donut'
39. SQUARE = 'square'
40. DIAMOND = 'diamond'
41. LINES = 'lines'
42. OVAL = 'oval'The program also sets up constant variables for some strings. These constants will be used in the data structure for the board, tracking which spaces on the board have which icons. Using a constant variable instead of the string value is a good idea. Look at the following code, which comes from line 187:
if shape == DONUT:The shape variable will be set to one of the strings 'donut', 'square', 'diamond', 'lines', or 'oval' and then compared to the DONUT
constant. If we made a typo when writing line 187, for example, something like
this:
if shape == DUNOT:Then Python would crash, giving an error message saying that
there is no variable named DUNOT. This is good.
Since the program has crashed on line 187, when we check that line it will be
easy to see that the bug was caused by a typo. However, if we were using
strings instead of constant variables and made the same typo, line 187 would
look like this:
if shape == 'dunot':This is perfectly acceptable Python code, so it won’t crash at first when you run it. However, this will lead to weird bugs later on in our program. Because the code does not immediately crash where the problem is caused, it can be much harder to find it.
Making Sure We Have Enough Icons
44. ALLCOLORS = (RED, GREEN, BLUE, YELLOW, ORANGE, PURPLE, CYAN)
45. ALLSHAPES = (DONUT, SQUARE, DIAMOND, LINES, OVAL)
46. assert len(ALLCOLORS) * len(ALLSHAPES) * 2 >= BOARDWIDTH * BOARDHEIGHT, "Board is too big for the number of shapes/colors defined."In order for our game program to be able to create icons of every possible color and shape combination, we need to make a tuple that holds all of these values. There is also another assertion on line 46 to make sure that there are enough color/shape combinations for the size of the board we have. If there isn’t, then the program will crash on line 46 and we will know that we either have to add more colors and shapes, or make the board width and height smaller. With 7 colors and 5 shapes, we can make 35 (that is, 7 x 5) different icons. And because we’ll have a pair of each icon, that means we can have a board with up to 70 (that is, 35 x 2, or 7 x 5 x 2) spaces.
Tuples vs. Lists, Immutable vs. Mutable
You might have noticed that the ALLCOLORS
and ALLSHAPES variables are tuples instead of lists.
When do we want to use tuples and when do we want to use lists? And what’s the
difference between them anyway?
Tuples and lists are the same in every way except two: tuples use parentheses instead of square brackets, and the items in tuples cannot be modified (but the items in lists can be modified). We often call lists mutable (meaning they can be changed) and tuples immutable (meaning they cannot be changed).
For an example of trying to change values in lists and tuples, look at the following code:
>>> listVal = [1, 1, 2, 3, 5, 8]
>>> tupleVal = (1, 1, 2, 3, 5, 8)
>>> listVal[4] = 'hello!'
>>> listVal
[1, 1, 2, 3, 'hello!', 8]
>>> tupleVal[4] = 'hello!'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> tupleVal
(1, 1, 2, 3, 5, 8)
>>> tupleVal[4]
5Notice that when we try to change the item at index 2 in the tuple, Python gives us an error message saying
that tuple objects do not support “item assignment”.
There is a silly benefit and an important benefit to a tuple’s immutability. The silly benefit is that code that uses tuples is slightly faster than code that uses lists. (Python is able to make some optimizations knowing that the values in a tuple will never change.) But having your code run a few nanoseconds faster is not important.
The important benefit to using tuples is similar to the benefit of using constant variables: it’s a sign that the value in the tuple will never change, so anyone reading the code later will be able to say, “I can expect that this tuple will always be the same. Otherwise the programmer would have used a list.” This also lets a future programmer reading your code say, “If I see a list value, I know that it could be modified at some point in this program. Otherwise, the programmer who wrote this code would have used a tuple.”
You can still assign a new tuple value to a variable:
>>> tupleVal = (1, 2, 3)
>>> tupleVal = (1, 2, 3, 4)The reason this code works is because the code isn’t
changing the (1, 2, 3) tuple on the second line. It
is assigning an entirely new tuple (1, 2, 3, 4) to
the tupleVal, and overwriting the old tuple value.
You cannot, however, use the square brackets to modify an item in the tuple.
Strings are also an immutable data type. You can use the square brackets to read a single character in a string, but you cannot change a single character in a string:
>>> strVal = 'Hello'
>>> strVal[1]
'e'
>>> strVal[1] = 'X'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignmentOne Item Tuples Need a Trailing Comma
Also, one minor detail about tuples: if you ever need to write code about a tuple that has one value in it, then it needs to have a trailing comma in it, such as this:
oneValueTuple = (42, )If you forget this comma (and it is very easy to forget), then Python won’t be able to tell the difference between this and a set of parentheses that just change the order of operations. For example, look at the following two lines of code:
variableA = (5 * 6)
variableB = (5 * 6, )The value that is stored in variableA
is just the integer 30. However, the expression for variableB’s assignment statement is the single-item tuple value (30,). Empty tuple values do not need a comma in them; they can just be a set of parentheses by themselves: ().
Converting Between Lists and Tuples
You can convert between list and tuple values just like you
can convert between string and integer values. Just pass a tuple value to the list() function and it will return a list form of that tuple
value. Or, pass a list value to the tuple() function
and it will return a tuple form of that list value. Try typing the following
into the interactive shell:
>>> spam = (1, 2, 3, 4)
>>> spam = list(spam)
>>> spam
[1, 2, 3, 4]
>>> spam = tuple(spam)
>>> spam
(1, 2, 3, 4)
>>>The global
statement, and Why Global Variables are Evil
48. def main():
49. global FPSCLOCK, DISPLAYSURF
50. pygame.init()
51. FPSCLOCK = pygame.time.Clock()
52. DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
53.
54. mousex = 0 # used to store x coordinate of mouse event
55. mousey = 0 # used to store y coordinate of mouse event
56. pygame.display.set_caption('Memory Game')This is the start of the main()
function, which is where (oddly enough) the main part of the game code is. The
functions called in the main() function will be
explained later in this chapter.
Line 49 is a global statement.
The global statement is the global
keyword followed by a comma-delimited list of variable names. These variable
names are then marked as global variables. Inside the main()
function, those names are not for local variables that might just happen to
have the same name as global variables. They are the global variables.
Any values assigned to them in the main() function
will persist outside the main() function. We are
marking the FPSCLOCK and DISPLAYSURF
variables as global because they are used in several other functions in the
program. (More info is at http://invpy.com/scope.)
There are four simple rules to determine if a variable is local or global:
- If there is a global statement for a variable at the beginning of the function, then the variable is global.
- If the name of a variable in a function has the same name as a global variable and the function never assigns the variable a value, then that variable is the global variable.
- If the name of a variable in a function has the same name as a global variable and the function does assign the variable a value, then that variable is a local variable.
- If there isn't a global variable with the same name as the variable in the function, then that variable is obviously a local variable.
You generally want to avoid using global variables inside functions. A function is supposed to be like a mini-program inside your program with specific inputs (the parameters) and an output (the return value). But a function that reads and writes to global variables has additional inputs and output. Since the global variable could have been modified in many places before the function was called, it can be tricky to track down a bug involving a bad value set in the global variable.
Having a function as a separate mini-program that doesn’t use global variables makes it easier to find bugs in your code, since the parameters of the function are clearly known. It also makes changing the code in a function easier, since if the new function works with the same parameters and gives the same return value, it will automatically work with the rest of the program just like the old function.
Basically, using global variables might make it easier to write your program, but it generally makes it harder to debug.
In the games in this book, global variables are mostly used
for variables that would be global constants that never change, but need the pygame.init() function called first. Since this happens in
the main() function, they are set in the main() function and must be global for other functions to
see them. But the global variables are used as constants and don’t change, so
they are less likely to cause confusing bugs.
If you don’t understand this, don’t worry. Just write your code so that you pass in values to functions rather than have the functions read global variables as a general rule.
Data Structures and 2D Lists
58. mainBoard = getRandomizedBoard()
59. revealedBoxes = generateRevealedBoxesData(False)The getRandomizedBoard() function
returns a data structure that represents the state of the board. The generateRevealedBoxesData() function returns a data structure that represents which boxes are covered. The return values of these functions are two-dimensional (2D) lists, or lists of lists. A
list of lists of lists of values would be a 3D list. Another word for two or
more dimensional lists is a multidimensional
list.
If we have a list value stored in a variable named spam, we could access a value in that list with the square
brackets, such as spam[2] to retrieve the third
value in the list. If the value at spam[2] is itself
a list, then we could use another set of square brackets to retrieve a value in
that list. This would look like, for example, spam[2][4],
which would retrieve the fifth value in the list that is the third value in spam. Using this notation of lists of lists makes it
easy to map a 2D board to a 2D list value. Since the mainBoard
variable will store icons in it, if we wanted to get the icon on the board at
the position (4, 5) then we could just use the expression mainBoard[4][5].
Since the icons themselves are stored as two-item tuples with the shape and
color, the complete data structure is a list of lists of two-item tuples. Whew!
Here’s a small example. Say the board looked like this:

The corresponding data structure would be:
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)]](If your book is in black and white, you can see a color
version of the above picture at http://invpy.com/memoryboard.)
You’ll notice that mainBoard[x][y] will correspond
to the icon at the (x, y) coordinate on the board.
Meanwhile, the “revealed boxes” data structure is also a 2D
list, except instead of two-item tuples like the board data structure, it has
Boolean values: True if the box at that x, y
coordinate is revealed, and False if it is covered
up. Passing False to the generateRevealedBoxesData()
function sets all of the Boolean values to False.
(This function is explained in detail later.)
These two data structures are used to keep track of the state of the game board.
Setting Up the Start of a Game
61. firstSelection = None # stores the (x, y) of the first box clicked.
62.
63. DISPLAYSURF.fill(BGCOLOR)
64. startGameAnimation(mainBoard)Line 61 sets up a variable called firstSelection
with the value None. (None
is the value that represents a lack of a value. It is the only value of the
data type, NoneType. More info at http://invpy.com/None)
When the player clicks on an icon on the board, the program needs to track if
this was the first icon of the pair that was clicked on or the second icon. If firstSelection is None, the
click was on the first icon and we store the XY coordinates in the firstSelection variable as a tuple of two integers (one
for the X value, the other for Y). On the second click the value will be this
tuple and not None, which is how the program tracks
that it is the second icon click. Line 63 fills the entire surface with the
background color. This will also paint over anything that used to be on the
surface, which gives us a clean slate to start drawing graphics on.
If you’ve played the Memory Puzzle game, you’ll notice that
at the beginning of the game, all of the boxes are quickly covered and
uncovered randomly to give the player a sneak peek at which icons are under
which boxes. This all happens in the startGameAnimation()
function, which is explained later in this chapter.
It’s important to give the player this sneak peek (but not long enough of a peek to let the player easily memorize the icon locations), because otherwise they would have no clue where any icons are. Blindly clicking on the icons isn’t as much fun as having a little hint to go on.
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:
mainBoardrevealedBoxesfirstSelectionmouseClickedmousexmousey
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 = TrueThe 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 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 the 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 is 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] = FalseThe 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 variableOtherwise, 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 sets 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/30th 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 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 of 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 revealedBoxesThe 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 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 boardNow 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 resultThe 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 fewer if there are fewer 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 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.
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)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)).

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 coordsThe 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 shapes requires a different set of 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 code like shape, color = getShapeAndColor() does. But if you looked at 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 coverage 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).
Drawing the Highlight
248. def drawHighlightBox(boxx, boxy):
249. left, top = leftTopCoordsOfBox(boxx, boxy)
250. pygame.draw.rect(DISPLAYSURF, HIGHLIGHTCOLOR, (left - 5, top - 5, BOXSIZE + 10, BOXSIZE + 10), 4)To help the player recognize that they can click on a
covered box to reveal it, we will make a blue outline appear around a box to
highlight it. This outline is drawn with a call to pygame.draw.rect()
to make a rectangle with a width of 4 pixels.
The “Start Game” Animation
253. def startGameAnimation(board):
254. # Randomly reveal the boxes 8 at a time.
255. coveredBoxes = generateRevealedBoxesData(False)
256. boxes = []
257. for x in range(BOARDWIDTH):
258. for y in range(BOARDHEIGHT):
259. boxes.append( (x, y) )
260. random.shuffle(boxes)
261. boxGroups = splitIntoGroupsOf(8, boxes)The animation that plays at the beginning of the game gives
the player a quick hint as to where all the icons are located. In order to make
this animation, we have to reveal and cover up groups of boxes one group after
another. To do this, first we’ll create a list of every possible space on the
board. The nested for loops on lines 257 and 258 will
add (X, Y) tuples to a list in the boxes variable.
We will reveal and cover up the first 8 boxes in this list, then the next 8, then the next 8 after that, and so on. However, since the order of the (X, Y) tuples in boxes would be the same each time, then the same order of boxes would be displayed. (Try commenting out line 260 and then running the program a few times to see this effect.)
To change up the boxes each time a game starts, we will call
the random.shuffle() function to randomly shuffle
the order of the tuples in the boxes list. Then when we reveal and cover up the
first 8 boxes in this list (and each group of 8 boxes afterwards), it will be a random group of 8 boxes.
To get the lists of 8 boxes, we call our splitIntoGroupsOf() function, passing 8
and the list in boxes. The list of lists that the
function returns will be stored in a variable named boxGroups.
Revealing and Covering the Groups of Boxes
263. drawBoard(board, coveredBoxes)
264. for boxGroup in boxGroups:
265. revealBoxesAnimation(board, boxGroup)
266. coverBoxesAnimation(board, boxGroup)First, we draw the board. Since every value in coveredBoxes is set to False,
this call to drawBoard() will end up drawing only
covered up white boxes. The revealBoxesAnimation()
and coverBoxesAnimation() functions will draw over the
spaces of these white boxes.
The for loop will go through each
of the inner lists in the boxGroups lists. We pass
these to revealBoxesAnimation(), which will perform
the animation of the white boxes being pulled away to reveal the icon
underneath. Then the call to coverBoxesAnimation()
will animate the white boxes expanding to cover up the icons. Then the for loop goes to the next iteration to animate the next
set of 8 boxes.
The “Game Won” Animation
269. def gameWonAnimation(board):
270. # flash the background color when the player has won
271. coveredBoxes = generateRevealedBoxesData(True)
272. color1 = LIGHTBGCOLOR
273. color2 = BGCOLOR
274.
275. for i in range(13):
276. color1, color2 = color2, color1 # swap colors
277. DISPLAYSURF.fill(color1)
278. drawBoard(board, coveredBoxes)
279. pygame.display.update()
280. pygame.time.wait(300)When the player has uncovered all of the boxes by matching
every pair on the board, we want to congratulate them by flashing the
background color. The for loop will draw the color
in the color1 variable for the background color and
then draw the board over it. However, on each iteration of the for loop, the values in color1
and color2 will be swapped with each other on line
276. This way the program will alternate between drawing two different
background colors.
Remember that this function needs to call pygame.display.update() to actually make the DISPLAYSURF surface appear on the screen.
Telling if the Player Has Won
283. def hasWon(revealedBoxes):
284. # Returns True if all the boxes have been revealed, otherwise False
285. for i in revealedBoxes:
286. if False in i:
287. return False # return False if any boxes are covered.
288. return TrueThe player has won the game when all of the icon pairs have
been matched. Since the “revealed” data structure gets values in it set to True as icons have been matched, we can simply loop
through every space in revealedBoxes looking for a False value. If even one False
value is in revealedBoxes, then we know there are
still unmatched icons on the board.
Note that because revealedBoxes
is a list of lists, the for loop on line 285 will
set the inner list as the values of i. But we can
use the in operator to search for a False value in
the entire inner list. This way we don’t need to write an additional line of
code and have two nested for loops like this:
for x in revealedBoxes:
for y in revealedBoxes[x]:
if False == revealedBoxes[x][y]:
return FalseWhy Bother Having a main()
Function?
291. if __name__ == '__main__':
292. main()It may seem pointless to have a main()
function, since you could just put that code in the global scope at the bottom
of the program instead, and the code would run exactly the same. However, there are two good reasons to put it inside of a main() function.
First, this lets you have local variables whereas otherwise
the local variables in the main() function would
have to become global variables. Limiting the number of global variables is a
good way to keep the code simple and easier to debug. (See the “The global statement, and Why Global Variables are Evil”
section in this chapter.)
Second, this also lets you import the program so that you
can call and test individual functions. If the memorypuzzle.py file is
in the C:\Python32 folder, then you can import it from the interactive shell.
Type the following to test out the splitIntoGroupsOf()
and getBoxAtPixel() functions to make sure they
return the correct return values:
>>> import memorypuzzle
>>> memorypuzzle.splitIntoGroupsOf(3, [0,1,2,3,4,5,6,7,8,9])
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
>>> memorypuzzle.getBoxAtPixel(0, 0)
(None, None)
>>> memorypuzzle.getBoxAtPixel(150, 150)
(1, 1)When a module is imported, all of the code in it is run. If
we didn’t have the main() function, and had its code
in the global scope, then the game would have automatically started as soon as
we imported it, which really wouldn’t let us call individual functions in it.
That’s why the code is in a separate function that we have
named main(). Then we check the built-in Python
variable __name__ to see if we should call the main() function or not. This variable is automatically set
by the Python interpreter to the string '__main__'
if the program itself is being run and 'memorypuzzle'
if it is being imported. This is why the main()
function is not run when we executed the import memorypuzzle
statement in the interactive shell.
This is a handy technique for being able to import the program you are working on from the interactive shell and make sure individual functions are returning the correct values by testing them one call at a time.
Why Bother With Readability?
A lot of the suggestions in this chapter haven’t been about how to write programs that computers can run so much as how to write programs that programmers can read. You might not understand why this is important. After all, as long as the code works, who cares if it is hard or easy for human programmers to read?
However, the important thing to realize about software is that it is rarely ever left alone. When you are creating your own games, you will rarely be “done” with the program. You will always get new ideas for game features you want to add, or find new bugs with the program. Because of this, it is important that your program is readable so that you can look at the code and understand it. And understanding the code is the first step to changing it to add more code or fix bugs.
As an example, here is an obfuscated version of the Memory Puzzle program that was made entirely unreadable. If you type it in (or download it from http://invpy.com/memorypuzzle_obfuscated.py) and run it you will find it runs exactly the same as the code at the beginning of this chapter. But if there was a bug with this code, it would be impossible to read the code and understand what’s going on, much less fix the bug.
The computer doesn’t mind code as unreadable as this. It’s all the same to it.
import random, pygame, sys
from pygame.locals import *
def hhh():
global a, b
pygame.init()
a = pygame.time.Clock()
b = pygame.display.set_mode((640, 480))
j = 0
k = 0
pygame.display.set_caption('Memory Game')
i = c()
hh = d(False)
h = None
b.fill((60, 60, 100))
g(i)
while True:
e = False
b.fill((60, 60, 100))
f(i, hh)
for eee in pygame.event.get():
if eee.type == QUIT or (eee.type == KEYUP and eee.key == K_ESCAPE):
pygame.quit()
sys.exit()
elif eee.type == MOUSEMOTION:
j, k = eee.pos
elif eee.type == MOUSEBUTTONUP:
j, k = eee.pos
e = True
bb, ee = m(j, k)
if bb != None and ee != None:
if not hh[bb][ee]:
n(bb, ee)
if not hh[bb][ee] and e:
o(i, [(bb, ee)])
hh[bb][ee] = True
if h == None:
h = (bb, ee)
else:
q, fff = s(i, h[0], h[1])
r, ggg = s(i, bb, ee)
if q != r or fff != ggg:
pygame.time.wait(1000)
p(i, [(h[0], h[1]), (bb, ee)])
hh[h[0]][h[1]] = False
hh[bb][ee] = False
elif ii(hh):
jj(i)
pygame.time.wait(2000)
i = c()
hh = d(False)
f(i, hh)
pygame.display.update()
pygame.time.wait(1000)
g(i)
h = None
pygame.display.update()
a.tick(30)
def d(ccc):
hh = []
for i in range(10):
hh.append([ccc] * 7)
return hh
def c():
rr = []
for tt in ((255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), (255, 128, 0), (255, 0, 255), (0, 255, 255)):
for ss in ('a', 'b', 'c', 'd', 'e'):
rr.append( (ss, tt) )
random.shuffle(rr)
rr = rr[:35] * 2
random.shuffle(rr)
bbb = []
for x in range(10):
v = []
for y in range(7):
v.append(rr[0])
del rr[0]
bbb.append(v)
return bbb
def t(vv, uu):
ww = []
for i in range(0, len(uu), vv):
ww.append(uu[i:i + vv])
return ww
def aa(bb, ee):
return (bb * 50 + 70, ee * 50 + 65)
def m(x, y):
for bb in range(10):
for ee in range(7):
oo, ddd = aa(bb, ee)
aaa = pygame.Rect(oo, ddd, 40, 40)
if aaa.collidepoint(x, y):
return (bb, ee)
return (None, None)
def w(ss, tt, bb, ee):
oo, ddd = aa(bb, ee)
if ss == 'a':
pygame.draw.circle(b, tt, (oo + 20, ddd + 20), 15)
pygame.draw.circle(b, (60, 60, 100), (oo + 20, ddd + 20), 5)
elif ss == 'b':
pygame.draw.rect(b, tt, (oo + 10, ddd + 10, 20, 20))
elif ss == 'c':
pygame.draw.polygon(b, tt, ((oo + 20, ddd), (oo + 40 - 1, ddd + 20), (oo + 20, ddd + 40 - 1), (oo, ddd + 20)))
elif ss == 'd':
for i in range(0, 40, 4):
pygame.draw.line(b, tt, (oo, ddd + i), (oo + i, ddd))
pygame.draw.line(b, tt, (oo + i, ddd + 39), (oo + 39, ddd + i))
elif ss == 'e':
pygame.draw.ellipse(b, tt, (oo, ddd + 10, 40, 20))
def s(bbb, bb, ee):
return bbb[bb][ee][0], bbb[bb][ee][1]
def dd(bbb, boxes, gg):
for box in boxes:
oo, ddd = aa(box[0], box[1])
pygame.draw.rect(b, (60, 60, 100), (oo, ddd, 40, 40))
ss, tt = s(bbb, box[0], box[1])
w(ss, tt, box[0], box[1])
if gg > 0:
pygame.draw.rect(b, (255, 255, 255), (oo, ddd, gg, 40))
pygame.display.update()
a.tick(30)
def o(bbb, cc):
for gg in range(40, (-8) - 1, -8):
dd(bbb, cc, gg)
def p(bbb, ff):
for gg in range(0, 48, 8):
dd(bbb, ff, gg)
def f(bbb, pp):
for bb in range(10):
for ee in range(7):
oo, ddd = aa(bb, ee)
if not pp[bb][ee]:
pygame.draw.rect(b, (255, 255, 255), (oo, ddd, 40, 40))
else:
ss, tt = s(bbb, bb, ee)
w(ss, tt, bb, ee)
def n(bb, ee):
oo, ddd = aa(bb, ee)
pygame.draw.rect(b, (0, 0, 255), (oo - 5, ddd - 5, 50, 50), 4)
def g(bbb):
mm = d(False)
boxes = []
for x in range(10):
for y in range(7):
boxes.append( (x, y) )
random.shuffle(boxes)
kk = t(8, boxes)
f(bbb, mm)
for nn in kk:
o(bbb, nn)
p(bbb, nn)
def jj(bbb):
mm = d(True)
tt1 = (100, 100, 100)
tt2 = (60, 60, 100)
for i in range(13):
tt1, tt2 = tt2, tt1
b.fill(tt1)
f(bbb, mm)
pygame.display.update()
pygame.time.wait(300)
def ii(hh):
for i in hh:
if False in i:
return False
return True
if __name__ == '__main__':
hhh()Never write code like this. If you program like this while facing the mirror in a bathroom with the lights turned off, the ghost of Ada Lovelace will come out of the mirror and throw you into the jaws of a Jacquard loom.
Summary, and a Hacking Suggestion
This chapter covers the entire explanation of how the Memory
Puzzle program works. Read over the chapter and the source code again to
understand it better. Many of the other game programs in this book make use of
the same programming concepts (like nested for loops,
syntactic sugar, and different coordinate systems in the same program) so they
won’t be explained again to keep this book short.
One idea to try out to understand how the code works is to intentionally break it by commenting out random lines. Doing this to some of the lines will probably cause a syntax error that will prevent the script from running at all. But commenting out other lines will result in weird bugs and other cool effects. Try doing this and then figure out why a program has the bugs it does.
This is also the first step in being able to add your own secret cheats or hacks to the program. By breaking the program from what it normally does, you can learn how to change it to do some neat effect (like secretly giving you hints on how to solve the puzzle). Feel free to experiment. You can always save a copy of the unchanged source code in a different file if you want to play the regular game again.
In fact, if you’d like some practice fixing bugs, there are several versions of this game’s source code that have small bugs in them. You can download these buggy versions from http://invpy.com/buggy/memorypuzzle. Try running the program to figure out what the bug is, and why the program is acting that way.