Memory puzzle
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 to 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 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() wil 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 True
The 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 False
Why 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 the exact same. However, there are two good reasons to put them 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 "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 mcodeorypuzzle.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 'mcodeorypuzzle' if it is being imported. This is why the main() function is not run when we executed the import mcodeorypuzzle 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 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.
1 import random, pygame, sys 2 from pygame.locals import * 3 def hhh(): 4 global a, b 5 pygame.init() 6 a = pygame.time.Clock() 7 b = pygame.display.set_mode((640, 480)) 8 j = 0 9 k = 0 10 pygame.display.set_caption('Memory Game') 11 i = c() 12 hh = d(False) 13 h = None 14 b.fill((60, 60, 100)) 15 g(i) 16 while True: 17 e = False 18 b.fill((60, 60, 100)) 19 f(i, hh) 20 for eee in pygame.event.get(): 21 if eee.type == QUIT or (eee.type == KEYUP and eee.key == K_ESCAPE): 22 pygame.quit() 23 sys.exit() 24 elif eee.type == MOUSEMOTION: 25 j, k = eee.pos 26 elif eee.type == MOUSEBUTTONUP: 27 j, k = eee.pos 28 e = True 29 bb, ee = m(j, k) 30 if bb != None and ee != None: 31 if not hh[bb][ee]: 32 n(bb, ee) 33 if not hh[bb][ee] and e: 34 o(i, [(bb, ee)]) 35 hh[bb][ee] = True 36 if h == None: 37 h = (bb, ee) 38 else: 39 q, fff = s(i, h[0], h[1]) 40 r, ggg = s(i, bb, ee) 41 if q != r or fff != ggg: 42 pygame.time.wait(1000) 43 p(i, [(h[0], h[1]), (bb, ee)]) 44 hh[h[0]][h[1]] = False 45 hh[bb][ee] = False 46 elif ii(hh): 47 jj(i) 48 pygame.time.wait(2000) 49 i = c() 50 hh = d(False) 51 f(i, hh) 52 pygame.display.update() 53 pygame.time.wait(1000) 54 g(i) 55 h = None 56 pygame.display.update() 57 a.tick(30) 58 def d(ccc): 59 hh = [] 60 for i in range(10): 61 hh.append([ccc] * 7) 62 return hh 63 def c(): 64 rr = [] 65 for tt in ((255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), (255, 128, 0), (255, 0, 255), (0, 255, 255)): 66 for ss in ('a', 'b', 'c', 'd', 'e'): 67 rr.append( (ss, tt) ) 68 random.shuffle(rr) 69 rr = rr[:35] * 2 70 random.shuffle(rr) 71 bbb = [] 72 for x in range(10): 73 v = [] 74 for y in range(7): 75 v.append(rr[0]) 76 del rr[0] 77 bbb.append(v) 78 return bbb 79 def t(vv, uu): 80 ww = [] 81 for i in range(0, len(uu), vv): 82 ww.append(uu[i:i + vv]) 83 return ww 84 def aa(bb, ee): 85 return (bb * 50 + 70, ee * 50 + 65) 86 def m(x, y): 87 for bb in range(10): 88 for ee in range(7): 89 oo, ddd = aa(bb, ee) 90 aaa = pygame.Rect(oo, ddd, 40, 40) 91 if aaa.collidepoint(x, y): 92 return (bb, ee) 93 return (None, None) 94 def w(ss, tt, bb, ee): 95 oo, ddd = aa(bb, ee) 96 if ss == 'a': 97 pygame.draw.circle(b, tt, (oo + 20, ddd + 20), 15) 98 pygame.draw.circle(b, (60, 60, 100), (oo + 20, ddd + 20), 5) 99 elif ss == 'b': 100 pygame.draw.rect(b, tt, (oo + 10, ddd + 10, 20, 20)) 101 elif ss == 'c': 102 pygame.draw.polygon(b, tt, ((oo + 20, ddd), (oo + 40 - 1, ddd + 20), (oo + 20, ddd + 40 - 1), (oo, ddd + 20))) 103 elif ss == 'd': 104 for i in range(0, 40, 4): 105 pygame.draw.line(b, tt, (oo, ddd + i), (oo + i, ddd)) 106 pygame.draw.line(b, tt, (oo + i, ddd + 39), (oo + 39, ddd + i)) 107 elif ss == 'e': 108 pygame.draw.ellipse(b, tt, (oo, ddd + 10, 40, 20)) 109 def s(bbb, bb, ee): 110 return bbb[bb][ee][0], bbb[bb][ee][1] 111 def dd(bbb, boxes, gg): 112 for box in boxes: 113 oo, ddd = aa(box[0], box[1]) 114 pygame.draw.rect(b, (60, 60, 100), (oo, ddd, 40, 40)) 115 ss, tt = s(bbb, box[0], box[1]) 116 w(ss, tt, box[0], box[1]) 117 if gg > 0: 118 pygame.draw.rect(b, (255, 255, 255), (oo, ddd, gg, 40)) 119 pygame.display.update() 120 a.tick(30) 121 def o(bbb, cc): 122 for gg in range(40, (-8) - 1, -8): 123 dd(bbb, cc, gg) 124 def p(bbb, ff): 125 for gg in range(0, 48, 8): 126 dd(bbb, ff, gg) 127 def f(bbb, pp): 128 for bb in range(10): 129 for ee in range(7): 130 oo, ddd = aa(bb, ee) 131 if not pp[bb][ee]: 132 pygame.draw.rect(b, (255, 255, 255), (oo, ddd, 40, 40)) 133 else: 134 ss, tt = s(bbb, bb, ee) 135 w(ss, tt, bb, ee) 136 def n(bb, ee): 137 oo, ddd = aa(bb, ee) 138 pygame.draw.rect(b, (0, 0, 255), (oo - 5, ddd - 5, 50, 50), 4) 139 def g(bbb): 140 mm = d(False) 141 boxes = [] 142 for x in range(10): 143 for y in range(7): 144 boxes.append( (x, y) ) 145 random.shuffle(boxes) 146 kk = t(8, boxes) 147 f(bbb, mm) 148 for nn in kk: 149 o(bbb, nn) 150 p(bbb, nn) 151 def jj(bbb): 152 mm = d(True) 153 tt1 = (100, 100, 100) 154 tt2 = (60, 60, 100) 155 for i in range(13): 156 tt1, tt2 = tt2, tt1 157 b.fill(tt1) 158 f(bbb, mm) 159 pygame.display.update() 160 pygame.time.wait(300) 161 def ii(hh): 162 for i in hh: 163 if False in i: 164 return False 165 return True 166 if __name__ == '__main__': 167 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 syntactic 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 something 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.