Chapter 8 – Squirrel Eat Squirrel


How to Play Squirrel Eat Squirrel
Squirrel Eat Squirrel is loosely based on the game “Katamari Damacy”. The player controls a small squirrel that must hop around the screen eating smaller squirrels and avoiding larger squirrels. Each time the player’s squirrel eats a squirrel that is smaller than it, it grows larger. If the player’s squirrel gets hit by a squirrel larger than it, it loses a life point. The player wins when the squirrel becomes a monstrously large squirrel called the Omega Squirrel. The player loses if their squirrel gets hit three times.
I’m not really sure where I got the idea for a video game where squirrels eat each other. I’m a little strange sometimes.
The Design of Squirrel Eat Squirrel
There are three types of data structures in this game, which are represented as dictionary values. The types are player squirrels, enemy squirrels, and grass objects. There is only one player squirrel object at a time in the game.
Note: Technically, “object” means something specific in Object-Oriented Programming. Python does have OOP features, but they aren’t covered in this book. Technically the Pygame objects such as “Rect object” or “Surface object” are objects. But I’m going to use the term “object” in this book to refer to “things that exist in the game world”. But really, the player squirrel, enemy squirrels, and grass “objects” are just dictionary values.
All the objects have the following keys in their dictionary
value: 'x', 'y', and 'rect'. The 'x' and 'y' keys’ values give the coordinates of the top left of
the object in game world coordinates. These are different from pixel
coordinates (which is what the 'rect' key’s value
tracks). The difference between game world and pixel coordinates will be
explained when you learn about the concept of cameras.
In addition, the player squirrel, enemy squirrel, and grass objects have other keys which are explained in a large comment at the start of the source code.
Source Code to Squirrel Eat Squirrel
This source code can be downloaded from http://invpy.com/squirrel.py. 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/squirrel to see the differences between your code and the code in the book.
You will also need to download the following image files:
- http://invpy.com/gameicon.png
- http://invpy.com/squirrel.png
- http://invpy.com/grass1.png
- http://invpy.com/grass2.png
- http://invpy.com/grass3.png
- http://invpy.com/grass4.png
1. # Squirrel Eat Squirrel (a 2D Katamari Damacy clone)
2. # By Al Sweigart [email protected]
3. # http://inventwithpython.com/pygame
4. # Released under a "Simplified BSD" license
5.
6. import random, sys, time, math, pygame
7. from pygame.locals import *
8.
9. FPS = 30 # frames per second to update the screen
10. WINWIDTH = 640 # width of the program's window, in pixels
11. WINHEIGHT = 480 # height in pixels
12. HALF_WINWIDTH = int(WINWIDTH / 2)
13. HALF_WINHEIGHT = int(WINHEIGHT / 2)
14.
15. GRASSCOLOR = (24, 255, 0)
16. WHITE = (255, 255, 255)
17. RED = (255, 0, 0)
18.
19. CAMERASLACK = 90 # how far from the center the squirrel moves before moving the camera
20. MOVERATE = 9 # how fast the player moves
21. BOUNCERATE = 6 # how fast the player bounces (large is slower)
22. BOUNCEHEIGHT = 30 # how high the player bounces
23. STARTSIZE = 25 # how big the player starts off
24. WINSIZE = 300 # how big the player needs to be to win
25. INVULNTIME = 2 # how long the player is invulnerable after being hit in seconds
26. GAMEOVERTIME = 4 # how long the "game over" text stays on the screen in seconds
27. MAXHEALTH = 3 # how much health the player starts with
28.
29. NUMGRASS = 80 # number of grass objects in the active area
30. NUMSQUIRRELS = 30 # number of squirrels in the active area
31. SQUIRRELMINSPEED = 3 # slowest squirrel speed
32. SQUIRRELMAXSPEED = 7 # fastest squirrel speed
33. DIRCHANGEFREQ = 2 # % chance of direction change per frame
34. LEFT = 'left'
35. RIGHT = 'right'
36.
37. """
38. This program has three data structures to represent the player, enemy squirrels, and grass background objects. The data structures are dictionaries with the following keys:
39.
40. Keys used by all three data structures:
41. 'x' - the left edge coordinate of the object in the game world (not a pixel coordinate on the screen)
42. 'y' - the top edge coordinate of the object in the game world (not a pixel coordinate on the screen)
43. 'rect' - the pygame.Rect object representing where on the screen the object is located.
44. Player data structure keys:
45. 'surface' - the pygame.Surface object that stores the image of the squirrel which will be drawn to the screen.
46. 'facing' - either set to LEFT or RIGHT, stores which direction the player is facing.
47. 'size' - the width and height of the player in pixels. (The width & height are always the same.)
48. 'bounce' - represents at what point in a bounce the player is in. 0 means standing (no bounce), up to BOUNCERATE (the completion of the bounce)
49. 'health' - an integer showing how many more times the player can be hit by a larger squirrel before dying.
50. Enemy Squirrel data structure keys:
51. 'surface' - the pygame.Surface object that stores the image of the squirrel which will be drawn to the screen.
52. 'movex' - how many pixels per frame the squirrel moves horizontally. A negative integer is moving to the left, a positive to the right.
53. 'movey' - how many pixels per frame the squirrel moves vertically. A negative integer is moving up, a positive moving down.
54. 'width' - the width of the squirrel's image, in pixels
55. 'height' - the height of the squirrel's image, in pixels
56. 'bounce' - represents at what point in a bounce the player is in. 0 means standing (no bounce), up to BOUNCERATE (the completion of the bounce)
57. 'bouncerate' - how quickly the squirrel bounces. A lower number means a quicker bounce.
58. 'bounceheight' - how high (in pixels) the squirrel bounces
59. Grass data structure keys:
60. 'grassImage' - an integer that refers to the index of the pygame.Surface object in GRASSIMAGES used for this grass object
61. """
62.
63. def main():
64. global FPSCLOCK, DISPLAYSURF, BASICFONT, L_SQUIR_IMG, R_SQUIR_IMG, GRASSIMAGES
65.
66. pygame.init()
67. FPSCLOCK = pygame.time.Clock()
68. pygame.display.set_icon(pygame.image.load('gameicon.png'))
69. DISPLAYSURF = pygame.display.set_mode((WINWIDTH, WINHEIGHT))
70. pygame.display.set_caption('Squirrel Eat Squirrel')
71. BASICFONT = pygame.font.Font('freesansbold.ttf', 32)
72.
73. # load the image files
74. L_SQUIR_IMG = pygame.image.load('squirrel.png')
75. R_SQUIR_IMG = pygame.transform.flip(L_SQUIR_IMG, True, False)
76. GRASSIMAGES = []
77. for i in range(1, 5):
78. GRASSIMAGES.append(pygame.image.load('grass%s.png' % i))
79.
80. while True:
81. runGame()
82.
83.
84. def runGame():
85. # set up variables for the start of a new game
86. invulnerableMode = False # if the player is invulnerable
87. invulnerableStartTime = 0 # time the player became invulnerable
88. gameOverMode = False # if the player has lost
89. gameOverStartTime = 0 # time the player lost
90. winMode = False # if the player has won
91.
92. # create the surfaces to hold game text
93. gameOverSurf = BASICFONT.render('Game Over', True, WHITE)
94. gameOverRect = gameOverSurf.get_rect()
95. gameOverRect.center = (HALF_WINWIDTH, HALF_WINHEIGHT)
96.
97. winSurf = BASICFONT.render('You have achieved OMEGA SQUIRREL!', True, WHITE)
98. winRect = winSurf.get_rect()
99. winRect.center = (HALF_WINWIDTH, HALF_WINHEIGHT)
100.
101. winSurf2 = BASICFONT.render('(Press "r" to restart.)', True, WHITE)
102. winRect2 = winSurf2.get_rect()
103. winRect2.center = (HALF_WINWIDTH, HALF_WINHEIGHT + 30)
104.
105. # camerax and cameray are the top left of where the camera view is
106. camerax = 0
107. cameray = 0
108.
109. grassObjs = [] # stores all the grass objects in the game
110. squirrelObjs = [] # stores all the non-player squirrel objects
111. # stores the player object:
112. playerObj = {'surface': pygame.transform.scale(L_SQUIR_IMG, (STARTSIZE, STARTSIZE)),
113. 'facing': LEFT,
114. 'size': STARTSIZE,
115. 'x': HALF_WINWIDTH,
116. 'y': HALF_WINHEIGHT,
117. 'bounce':0,
118. 'health': MAXHEALTH}
119.
120. moveLeft = False
121. moveRight = False
122. moveUp = False
123. moveDown = False
124.
125. # start off with some random grass images on the screen
126. for i in range(10):
127. grassObjs.append(makeNewGrass(camerax, cameray))
128. grassObjs[i]['x'] = random.randint(0, WINWIDTH)
129. grassObjs[i]['y'] = random.randint(0, WINHEIGHT)
130.
131. while True: # main game loop
132. # Check if we should turn off invulnerability
133. if invulnerableMode and time.time() - invulnerableStartTime > INVULNTIME:
134. invulnerableMode = False
135.
136. # move all the squirrels
137. for sObj in squirrelObjs:
138. # move the squirrel, and adjust for their bounce
139. sObj['x'] += sObj['movex']
140. sObj['y'] += sObj['movey']
141. sObj['bounce'] += 1
142. if sObj['bounce'] > sObj['bouncerate']:
143. sObj['bounce'] = 0 # reset bounce amount
144.
145. # random chance they change direction
146. if random.randint(0, 99) < DIRCHANGEFREQ:
147. sObj['movex'] = getRandomVelocity()
148. sObj['movey'] = getRandomVelocity()
149. if sObj['movex'] > 0: # faces right
150. sObj['surface'] = pygame.transform.scale(R_SQUIR_IMG, (sObj['width'], sObj['height']))
151. else: # faces left
152. sObj['surface'] = pygame.transform.scale(L_SQUIR_IMG, (sObj['width'], sObj['height']))
153.
154.
155. # go through all the objects and see if any need to be deleted.
156. for i in range(len(grassObjs) - 1, -1, -1):
157. if isOutsideActiveArea(camerax, cameray, grassObjs[i]):
158. del grassObjs[i]
159. for i in range(len(squirrelObjs) - 1, -1, -1):
160. if isOutsideActiveArea(camerax, cameray, squirrelObjs[i]):
161. del squirrelObjs[i]
162.
163. # add more grass & squirrels if we don't have enough.
164. while len(grassObjs) < NUMGRASS:
165. grassObjs.append(makeNewGrass(camerax, cameray))
166. while len(squirrelObjs) < NUMSQUIRRELS:
167. squirrelObjs.append(makeNewSquirrel(camerax, cameray))
168.
169. # adjust camerax and cameray if beyond the "camera slack"
170. playerCenterx = playerObj['x'] + int(playerObj['size'] / 2)
171. playerCentery = playerObj['y'] + int(playerObj['size'] / 2)
172. if (camerax + HALF_WINWIDTH) - playerCenterx > CAMERASLACK:
173. camerax = playerCenterx + CAMERASLACK - HALF_WINWIDTH
174. elif playerCenterx - (camerax + HALF_WINWIDTH) > CAMERASLACK:
175. camerax = playerCenterx - CAMERASLACK - HALF_WINWIDTH
176. if (cameray + HALF_WINHEIGHT) - playerCentery > CAMERASLACK:
177. cameray = playerCentery + CAMERASLACK - HALF_WINHEIGHT
178. elif playerCentery - (cameray + HALF_WINHEIGHT) > CAMERASLACK:
179. cameray = playerCentery - CAMERASLACK - HALF_WINHEIGHT
180.
181. # draw the green background
182. DISPLAYSURF.fill(GRASSCOLOR)
183.
184. # draw all the grass objects on the screen
185. for gObj in grassObjs:
186. gRect = pygame.Rect( (gObj['x'] - camerax,
187. gObj['y'] - cameray,
188. gObj['width'],
189. gObj['height']) )
190. DISPLAYSURF.blit(GRASSIMAGES[gObj['grassImage']], gRect)
191.
192.
193. # draw the other squirrels
194. for sObj in squirrelObjs:
195. sObj['rect'] = pygame.Rect( (sObj['x'] - camerax,
196. sObj['y'] - cameray - getBounceAmount(sObj['bounce'], sObj['bouncerate'], sObj['bounceheight']),
197. sObj['width'],
198. sObj['height']) )
199. DISPLAYSURF.blit(sObj['surface'], sObj['rect'])
200.
201.
202. # draw the player squirrel
203. flashIsOn = round(time.time(), 1) * 10 % 2 == 1
204. if not gameOverMode and not (invulnerableMode and flashIsOn):
205. playerObj['rect'] = pygame.Rect( (playerObj['x'] - camerax,
206. playerObj['y'] - cameray - getBounceAmount(playerObj['bounce'], BOUNCERATE, BOUNCEHEIGHT),
207. playerObj['size'],
208. playerObj['size']) )
209. DISPLAYSURF.blit(playerObj['surface'], playerObj['rect'])
210.
211.
212. # draw the health meter
213. drawHealthMeter(playerObj['health'])
214.
215. for event in pygame.event.get(): # event handling loop
216. if event.type == QUIT:
217. terminate()
218.
219. elif event.type == KEYDOWN:
220. if event.key in (K_UP, K_w):
221. moveDown = False
222. moveUp = True
223. elif event.key in (K_DOWN, K_s):
224. moveUp = False
225. moveDown = True
226. elif event.key in (K_LEFT, K_a):
227. moveRight = False
228. moveLeft = True
229. if playerObj['facing'] != LEFT: # change player image
230. playerObj['surface'] = pygame.transform.scale(L_SQUIR_IMG, (playerObj['size'], playerObj['size']))
231. playerObj['facing'] = LEFT
232. elif event.key in (K_RIGHT, K_d):
233. moveLeft = False
234. moveRight = True
235. if playerObj['facing'] != RIGHT: # change player image
236. playerObj['surface'] = pygame.transform.scale(R_SQUIR_IMG, (playerObj['size'], playerObj['size']))
237. playerObj['facing'] = RIGHT
238. elif winMode and event.key == K_r:
239. return
240.
241. elif event.type == KEYUP:
242. # stop moving the player's squirrel
243. if event.key in (K_LEFT, K_a):
244. moveLeft = False
245. elif event.key in (K_RIGHT, K_d):
246. moveRight = False
247. elif event.key in (K_UP, K_w):
248. moveUp = False
249. elif event.key in (K_DOWN, K_s):
250. moveDown = False
251.
252. elif event.key == K_ESCAPE:
253. terminate()
254.
255. if not gameOverMode:
256. # actually move the player
257. if moveLeft:
258. playerObj['x'] -= MOVERATE
259. if moveRight:
260. playerObj['x'] += MOVERATE
261. if moveUp:
262. playerObj['y'] -= MOVERATE
263. if moveDown:
264. playerObj['y'] += MOVERATE
265.
266. if (moveLeft or moveRight or moveUp or moveDown) or playerObj['bounce'] != 0:
267. playerObj['bounce'] += 1
268.
269. if playerObj['bounce'] > BOUNCERATE:
270. playerObj['bounce'] = 0 # reset bounce amount
271.
272. # check if the player has collided with any squirrels
273. for i in range(len(squirrelObjs)-1, -1, -1):
274. sqObj = squirrelObjs[i]
275. if 'rect' in sqObj and playerObj['rect'].colliderect(sqObj['rect']):
276. # a player/squirrel collision has occurred
277.
278. if sqObj['width'] * sqObj['height'] <= playerObj['size']**2:
279. # player is larger and eats the squirrel
280. playerObj['size'] += int( (sqObj['width'] * sqObj['height'])**0.2 ) + 1
281. del squirrelObjs[i]
282.
283. if playerObj['facing'] == LEFT:
284. playerObj['surface'] = pygame.transform.scale(L_SQUIR_IMG, (playerObj['size'], playerObj['size']))
285. if playerObj['facing'] == RIGHT:
286. playerObj['surface'] = pygame.transform.scale(R_SQUIR_IMG, (playerObj['size'], playerObj['size']))
287.
288. if playerObj['size'] > WINSIZE:
289. winMode = True # turn on "win mode"
290.
291. elif not invulnerableMode:
292. # player is smaller and takes damage
293. invulnerableMode = True
294. invulnerableStartTime = time.time()
295. playerObj['health'] -= 1
296. if playerObj['health'] == 0:
297. gameOverMode = True # turn on "game over mode"
298. gameOverStartTime = time.time()
299. else:
300. # game is over, show "game over" text
301. DISPLAYSURF.blit(gameOverSurf, gameOverRect)
302. if time.time() - gameOverStartTime > GAMEOVERTIME:
303. return # end the current game
304.
305. # check if the player has won.
306. if winMode:
307. DISPLAYSURF.blit(winSurf, winRect)
308. DISPLAYSURF.blit(winSurf2, winRect2)
309.
310. pygame.display.update()
311. FPSCLOCK.tick(FPS)
312.
313.
314.
315.
316. def drawHealthMeter(currentHealth):
317. for i in range(currentHealth): # draw red health bars
318. pygame.draw.rect(DISPLAYSURF, RED, (15, 5 + (10 * MAXHEALTH) - i * 10, 20, 10))
319. for i in range(MAXHEALTH): # draw the white outlines
320. pygame.draw.rect(DISPLAYSURF, WHITE, (15, 5 + (10 * MAXHEALTH) - i * 10, 20, 10), 1)
321.
322.
323. def terminate():
324. pygame.quit()
325. sys.exit()
326.
327.
328. def getBounceAmount(currentBounce, bounceRate, bounceHeight):
329. # Returns the number of pixels to offset based on the bounce.
330. # Larger bounceRate means a slower bounce.
331. # Larger bounceHeight means a higher bounce.
332. # currentBounce will always be less than bounceRate
333. return int(math.sin( (math.pi / float(bounceRate)) * currentBounce ) * bounceHeight)
334.
335. def getRandomVelocity():
336. speed = random.randint(SQUIRRELMINSPEED, SQUIRRELMAXSPEED)
337. if random.randint(0, 1) == 0:
338. return speed
339. else:
340. return -speed
341.
342.
343. def getRandomOffCameraPos(camerax, cameray, objWidth, objHeight):
344. # create a Rect of the camera view
345. cameraRect = pygame.Rect(camerax, cameray, WINWIDTH, WINHEIGHT)
346. while True:
347. x = random.randint(camerax - WINWIDTH, camerax + (2 * WINWIDTH))
348. y = random.randint(cameray - WINHEIGHT, cameray + (2 * WINHEIGHT))
349. # create a Rect object with the random coordinates and use colliderect()
350. # to make sure the right edge isn't in the camera view.
351. objRect = pygame.Rect(x, y, objWidth, objHeight)
352. if not objRect.colliderect(cameraRect):
353. return x, y
354.
355.
356. def makeNewSquirrel(camerax, cameray):
357. sq = {}
358. generalSize = random.randint(5, 25)
359. multiplier = random.randint(1, 3)
360. sq['width'] = (generalSize + random.randint(0, 10)) * multiplier
361. sq['height'] = (generalSize + random.randint(0, 10)) * multiplier
362. sq['x'], sq['y'] = getRandomOffCameraPos(camerax, cameray, sq['width'], sq['height'])
363. sq['movex'] = getRandomVelocity()
364. sq['movey'] = getRandomVelocity()
365. if sq['movex'] < 0: # squirrel is facing left
366. sq['surface'] = pygame.transform.scale(L_SQUIR_IMG, (sq['width'], sq['height']))
367. else: # squirrel is facing right
368. sq['surface'] = pygame.transform.scale(R_SQUIR_IMG, (sq['width'], sq['height']))
369. sq['bounce'] = 0
370. sq['bouncerate'] = random.randint(10, 18)
371. sq['bounceheight'] = random.randint(10, 50)
372. return sq
373.
374.
375. def makeNewGrass(camerax, cameray):
376. gr = {}
377. gr['grassImage'] = random.randint(0, len(GRASSIMAGES) - 1)
378. gr['width'] = GRASSIMAGES[0].get_width()
379. gr['height'] = GRASSIMAGES[0].get_height()
380. gr['x'], gr['y'] = getRandomOffCameraPos(camerax, cameray, gr['width'], gr['height'])
381. gr['rect'] = pygame.Rect( (gr['x'], gr['y'], gr['width'], gr['height']) )
382. return gr
383.
384.
385. def isOutsideActiveArea(camerax, cameray, obj):
386. # Return False if camerax and cameray are more than
387. # a half-window length beyond the edge of the window.
388. boundsLeftEdge = camerax - WINWIDTH
389. boundsTopEdge = cameray - WINHEIGHT
390. boundsRect = pygame.Rect(boundsLeftEdge, boundsTopEdge, WINWIDTH * 3, WINHEIGHT * 3)
391. objRect = pygame.Rect(obj['x'], obj['y'], obj['width'], obj['height'])
392. return not boundsRect.colliderect(objRect)
393.
394.
395. if __name__ == '__main__':
396. main()The Usual Setup Code
1. # Squirrel Eat Squirrel (a 2D Katamari Damacy clone)
2. # By Al Sweigart [email protected]
3. # http://inventwithpython.com/pygame
4. # Released under a "Simplified BSD" license
5.
6. import random, sys, time, math, pygame
7. from pygame.locals import *
8.
9. FPS = 30 # frames per second to update the screen
10. WINWIDTH = 640 # width of the program's window, in pixels
11. WINHEIGHT = 480 # height in pixels
12. HALF_WINWIDTH = int(WINWIDTH / 2)
13. HALF_WINHEIGHT = int(WINHEIGHT / 2)
14.
15. GRASSCOLOR = (24, 255, 0)
16. WHITE = (255, 255, 255)
17. RED = (255, 0, 0)The start of the program assigns several constant variables.
This program needs half the width and half the height of the window often enough that it is worth storing those numbers in the HALF_WINWIDTH and HALF_WINHEIGHT variables store these numbers.
19. CAMERASLACK = 90 # how far from the center the squirrel moves before moving the cameraThe “camera slack” is described later. Basically, it means that the camera will begin following the player squirrel when it moves 90 pixels away from the center of the window.
20. MOVERATE = 9 # how fast the player moves
21. BOUNCERATE = 6 # how fast the player bounces (large is slower)
22. BOUNCEHEIGHT = 30 # how high the player bounces
23. STARTSIZE = 25 # how big the player starts off
24. WINSIZE = 300 # how big the player needs to be to win
25. INVULNTIME = 2 # how long the player is invulnerable after being hit in seconds
26. GAMEOVERTIME = 4 # how long the "game over" text stays on the screen in seconds
27. MAXHEALTH = 3 # how much health the player starts with
28.
29. NUMGRASS = 80 # number of grass objects in the active area
30. NUMSQUIRRELS = 30 # number of squirrels in the active area
31. SQUIRRELMINSPEED = 3 # slowest squirrel speed
32. SQUIRRELMAXSPEED = 7 # fastest squirrel speed
33. DIRCHANGEFREQ = 2 # % chance of direction change per frame
34. LEFT = 'left'
35. RIGHT = 'right'The comments next to these constants explain what each constant variable is used for.
Describing the Data Structures
37. """
38. This program has three data structures to represent the player, enemy squirrels, and grass background objects. The data structures are dictionaries with the following keys:
39.
40. Keys used by all three data structures:
41. 'x' - the left edge coordinate of the object in the game world (not a pixel coordinate on the screen)
42. 'y' - the top edge coordinate of the object in the game world (not a pixel coordinate on the screen)
43. 'rect' - the pygame.Rect object representing where on the screen the object is located.
44. Player data structure keys:
45. 'surface' - the pygame.Surface object that stores the image of the squirrel which will be drawn to the screen.
46. 'facing' - either set to LEFT or RIGHT, stores which direction the player is facing.
47. 'size' - the width and height of the player in pixels. (The width & height are always the same.)
48. 'bounce' - represents at what point in a bounce the player is in. 0 means standing (no bounce), up to BOUNCERATE (the completion of the bounce)
49. 'health' - an integer showing how many more times the player can be hit by a larger squirrel before dying.
50. Enemy Squirrel data structure keys:
51. 'surface' - the pygame.Surface object that stores the image of the squirrel which will be drawn to the screen.
52. 'movex' - how many pixels per frame the squirrel moves horizontally. A negative integer is moving to the left, a positive to the right.
53. 'movey' - how many pixels per frame the squirrel moves vertically. A negative integer is moving up, a positive moving down.
54. 'width' - the width of the squirrel's image, in pixels
55. 'height' - the height of the squirrel's image, in pixels
56. 'bounce' - represents at what point in a bounce the player is in. 0 means standing (no bounce), up to BOUNCERATE (the completion of the bounce)
57. 'bouncerate' - how quickly the squirrel bounces. A lower number means a quicker bounce.
58. 'bounceheight' - how high (in pixels) the squirrel bounces
59. Grass data structure keys:
60. 'grassImage' - an integer that refers to the index of the pygame.Surface object in GRASSIMAGES used for this grass object
61. """The comments from lines 37 to 61 are in one large, multi-line string. They describe the keys in the player squirrel, enemy squirrel, and grass objects. In Python, a multi-line string value by itself works as a multi-line comment.
The main() Function
63. def main():
64. global FPSCLOCK, DISPLAYSURF, BASICFONT, L_SQUIR_IMG, R_SQUIR_IMG, GRASSIMAGES
65.
66. pygame.init()
67. FPSCLOCK = pygame.time.Clock()
68. pygame.display.set_icon(pygame.image.load('gameicon.png'))
69. DISPLAYSURF = pygame.display.set_mode((WINWIDTH, WINHEIGHT))
70. pygame.display.set_caption('Squirrel Eat Squirrel')
71. BASICFONT = pygame.font.Font('freesansbold.ttf', 32)The first several lines of the main()
function are the same setup code that we’ve seen in our previous game programs.
The pygame.display.set_icon() is a Pygame function
that sets the icon in the window’s title bar (just like pygame.display.set_caption()
sets the caption text in the title bar). The single argument to pygame.display.set_icon() is a Surface object of a small
image. The ideal image size is 32 x 32 pixels, although you can use other sized
images. The image will just be compressed into a smaller size to be used as the
window’s icon.
The pygame.transform.flip()
Function
73. # load the image files
74. L_SQUIR_IMG = pygame.image.load('squirrel.png')
75. R_SQUIR_IMG = pygame.transform.flip(L_SQUIR_IMG, True, False)
76. GRASSIMAGES = []
77. for i in range(1, 5):
78. GRASSIMAGES.append(pygame.image.load('grass%s.png' % i))The image for the player and enemy squirrels is loaded from squirrel.png
on line 74. Make sure that this PNG file is in the same folder as squirrel.py,
otherwise you will get the error pygame.error: Couldn't open
squirrel.png.
The image in squirrel.png (which you can download
from http://invpy.com/squirrel.png) is of a
squirrel facing to the left. We also need a Surface object that contains a
picture of the squirrel facing to the right. Instead of creating a second PNG
image file, we can call the pygame.transform.flip()
function. This function has three parameters: the Surface object with the image
to flip, a Boolean value to do a horizontal flip, and a Boolean value to do a
vertical flip. By passing True for the second
parameter and False for the third parameter, the
Surface object that returns has the image of the squirrel facing to the right.
The original Surface object in L_SQUIR_IMG that we
passed in is unchanged.
Here are examples of images being horizontally and vertically flipped:




80. while True:
81. runGame()After the setup in main() is
complete, the game begins with runGame() being
called.
A More Detailed Game State than Usual
84. def runGame():
85. # set up variables for the start of a new game
86. invulnerableMode = False # if the player is invulnerable
87. invulnerableStartTime = 0 # time the player became invulnerable
88. gameOverMode = False # if the player has lost
89. gameOverStartTime = 0 # time the player lost
90. winMode = False # if the player has wonThe Squirrel Eat Squirrel game has quite a few variables that track the game state. These variables will be explained in more detail later when they are used in the code.
The Usual Text Creation Code
92. # create the surfaces to hold game text
93. gameOverSurf = BASICFONT.render('Game Over', True, WHITE)
94. gameOverRect = gameOverSurf.get_rect()
95. gameOverRect.center = (HALF_WINWIDTH, HALF_WINHEIGHT)
96.
97. winSurf = BASICFONT.render('You have achieved OMEGA SQUIRREL!', True, WHITE)
98. winRect = winSurf.get_rect()
99. winRect.center = (HALF_WINWIDTH, HALF_WINHEIGHT)
100.
101. winSurf2 = BASICFONT.render('(Press "r" to restart.)', True, WHITE)
102. winRect2 = winSurf2.get_rect()
103. winRect2.center = (HALF_WINWIDTH, HALF_WINHEIGHT + 30)These variables contain Surface objects with the “Game Over”, “You have achieved OMEGA SQUIRREL!”, and “(Press "r" to restart.)” text that appears on the screen after the game ends (with either the player losing or winning).
Cameras
105. # camerax and cameray are the top left of where the camera view is
106. camerax = 0
107. cameray = 0The camerax and cameray variables track the game coordinates of the
“camera”. Imagine the game world as an infinite 2D space. This could, of
course, never fit on any screen. We can only draw a portion of the infinite 2D
space on the screen. We call the area of this portion a camera,
because it is as though our screen is just the area of the game world in front of what a camera would see. Here’s a picture of the game world (an infinite green
field) and the area that the camera can view:

As you can see, the game world XY coordinates keep getting bigger and smaller forever. The game world origin is where the (0, 0) game world coordinates are. You can see that the three squirrels are located (in game world coordinates) at (-384, -84), (384, 306), and (585, -234).
But we can only display a 640 x 480 pixel area on the screen
(though this can change if we pass different numbers to the pygame.display.set_mode() function), so we need to track
where the camera’s origin is located in game world coordinates. In the picture above,
the camera is placed at (-486, -330) in game world coordinates.
The picture below shows the same field and squirrels, except everything is given in camera coordinates:

The area that the camera can see (called the camera view) has its top left corner (that is, its origin) at the game world coordinates (-486, -330). Since what the camera sees is displayed on the player’s screen, the “camera” coordinates are the same as the “pixel” coordinates. To find out the pixel coordinates of the squirrels (that is, where on the screen they appear), take the game coordinates of the squirrel and subtract the game coordinates of the camera’s origin.
So the squirrel on the left has game world coordinates of (-384, -84) but appears at (102, 246) on the screen in pixel coordinates. (For the X coordinate, -384 - -486 = 102 and for the Y coordinate, -84 - -330 = 246.)
When we do the same calculation to find the pixel coordinates of the other two squirrels, we find that they exist outside of the range of the screen. This is why they don’t appear in the camera’s view.
The “Active Area”
The “active area” is just a name I came up with to describe the area of the game world that the camera views plus an area around it the size of the camera area:

Calculating if something is in the active area or not is
explained in the isOutsideActiveArea() function’s
explanation later in this chapter. When we create new enemy squirrel or grass
objects, we don’t want them to be created inside the view of the camera, since
it’ll appear that they just pop out of nowhere.
But we also don’t want to create them too far away from the camera, because then they may never wander into the camera’s view. Inside the active area but outside the camera is where squirrel and grass objects can safely be created.
Also, when squirrel and grass objects are beyond the border of the active area then they are far enough away to delete so that they don’t take up memory anymore. Objects that far away aren’t needed since it is much less likely that they’ll come back into view of the camera.
If you have ever played Super Mario World on the Super Nintendo, there is a good YouTube video explaining how Super Mario World’s camera system works. You can find this video at http://invpy.com/mariocamera.
Keeping Track of the Location of Things in the Game World
109. grassObjs = [] # stores all the grass objects in the game
110. squirrelObjs = [] # stores all the non-player squirrel objects
111. # stores the player object:
112. playerObj = {'surface': pygame.transform.scale(L_SQUIR_IMG, (STARTSIZE, STARTSIZE)),
113. 'facing': LEFT,
114. 'size': STARTSIZE,
115. 'x': HALF_WINWIDTH,
116. 'y': HALF_WINHEIGHT,
117. 'bounce':0,
118. 'health': MAXHEALTH}
119.
120. moveLeft = False
121. moveRight = False
122. moveUp = False
123. moveDown = FalseThe grassObjs variable holds a
list of all the grass objects in the game. As new grass objects are created,
they are added to this list. As grass objects are deleted, they are removed
from this list. The same goes for the squirrelObjs
variable and the enemy squirrel objects.
The playerObj variable is not a
list, but just the dictionary value itself.
The move variables on lines 120 to 123 track which of the arrow keys (or WASD keys) are being held down, just like in a few of the previous game programs.
Starting Off with Some Grass
125. # start off with some random grass images on the screen
126. for i in range(10):
127. grassObjs.append(makeNewGrass(camerax, cameray))
128. grassObjs[i]['x'] = random.randint(0, WINWIDTH)
129. grassObjs[i]['y'] = random.randint(0, WINHEIGHT)The active area should start off with a few grass objects
visible on the screen. The makeNewGrass() function
will create and return a grass object that is randomly located somewhere in the
active area but outside the camera view. This is what we normally want when we
call makeNewGrass(), but since we want to make sure
the first few grass objects are on the screen, the X and Y coordinates are
overwritten.
The Game Loop
131. while True: # main game loopThe game loop, like the game loops in the previous game programs, will do event handling, updating the game state, and drawing everything to the screen.
Checking to Disable Invulnerability
132. # Check if we should turn off invulnerability
133. if invulnerableMode and time.time() - invulnerableStartTime > INVULNTIME:
134. invulnerableMode = FalseWhen the player gets hit by an enemy squirrel and does not
die, we make the player invulnerable for a couple seconds (since the INVULNTIME constant is set to 2).
During this time, the player’s squirrel flashes and the player won’t take any damage from other squirrels. If the “invulnerability mode” time is over, line 134 will
set invulnerableMode to False.
Moving the Enemy Squirrels
136. # move all the squirrels
137. for sObj in squirrelObjs:
138. # move the squirrel, and adjust for their bounce
139. sObj['x'] += sObj['movex']
140. sObj['y'] += sObj['movey']The enemy squirrels all move according to the values in
their 'movex' and 'movey'
keys. If these values are positive, the squirrels move right or down. If these
values are negative, they move left or up. The larger the value, the farther
they move on each iteration through the game loop (which means they move
faster).
The for loop on line 137 will
apply this moving code to each of the enemy squirrel objects in the squirrelObjs list. First, lines 139 and 140 will adjust
their 'x' and 'y' keys’
values.
141. sObj['bounce'] += 1
142. if sObj['bounce'] > sObj['bouncerate']:
143. sObj['bounce'] = 0 # reset bounce amountThe value in sObj['bounce'] is
incremented on each iteration of the game loop for each squirrel. When this
value is 0, the squirrel is at the very beginning of
its bounce. When this value is equal to the value in sObj['bouncerate']
the value is at its end. (This is why a smaller sObj['bouncerate']
value makes for a faster bounce. If sObj['bouncerate']
is 3, then it only takes three iterations through
the game loop for the squirrel to do a full bounce. If sObj['bouncerate']
were 10, then it would take ten iterations.)
When sObj['bounce'] gets larger
than sObj['bouncerate'], then it needs to be reset
to 0. This is what lines 142 and 143 do.
145. # random chance they change direction
146. if random.randint(0, 99) < DIRCHANGEFREQ:
147. sObj['movex'] = getRandomVelocity()
148. sObj['movey'] = getRandomVelocity()
149. if sObj['movex'] > 0: # faces right
150. sObj['surface'] = pygame.transform.scale(R_SQUIR_IMG, (sObj['width'], sObj['height']))
151. else: # faces left
152. sObj['surface'] = pygame.transform.scale(L_SQUIR_IMG, (sObj['width'], sObj['height']))There is a 2% chance on each iteration through the game loop
that the squirrel will randomly change speed and direction. On line 146 the random.randint(0, 99) call randomly selects an integer out
of 100 possible integers. If this number is less than DIRCHANGEFREQ
(which we set to 2 on line 33) then a new value will
be set for sObj['movex'] and sObj['movey'].
Because this means the squirrel might have changed
direction, the Surface object in sObj['surface']
should be replaced by a new one that is properly facing left or right and
scaled to the squirrel’s size. This is what lines 149 to 152 determine. Note
that line 150 gets a Surface object scaled from R_SQUIR_IMG
and line 152 gets one scaled from L_SQUIR_IMG.
Removing the Far Away Grass and Squirrel Objects
155. # go through all the objects and see if any need to be deleted.
156. for i in range(len(grassObjs) - 1, -1, -1):
157. if isOutsideActiveArea(camerax, cameray, grassObjs[i]):
158. del grassObjs[i]
159. for i in range(len(squirrelObjs) - 1, -1, -1):
160. if isOutsideActiveArea(camerax, cameray, squirrelObjs[i]):
161. del squirrelObjs[i]During each iteration of the game loop, the code will check
all of the grass and enemy squirrel objects to see if they are outside the
“active area”. The isOutsideActiveArea() function
takes the current coordinates of the camera (which are stored in camerax and cameray) and the
grass/enemy squirrel object, and returns True if the
object is not located in the active area.
If this is the case, this object is deleted on line 158 (for grass objects) or line 161 (for squirrel objects). This is how squirrel and grass objects get deleted when the player moves far enough away from them (or when the enemy squirrels move away far enough from the player). This ensures that there are always some squirrels and grass objects near the player.
When Deleting Items in a List, Iterate Over the List in Reverse
Deleting squirrel and grass objects is done with the del statement. However, notice that the for loops on lines 156 and 159 pass arguments to the range() function so that the numbering starts at the index
of the last item and then counts down with a step of -1
(unlike counting up with a step of 1 as it normally
does) until it reaches the number -1. We are iterating backwards
over the list’s indexes compared to how it is normally done. This is done
because we are iterating over the list that we are also deleting items from.
To see why this reverse order is needed, say we had the following list value:
animals = ['cat', 'mouse', 'dog', 'horse']Say we wanted to write code to delete any instances of the string 'dog' from this list. We might think to write
out code like this:
for i in range(len(animals)):
if animals[i] == 'dog':
del animals[i]But if we ran this code, we would get an IndexError that looks like this:
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
IndexError: list index out of rangeTo see why this error happens, let’s walk through the code.
First, the animals list would be set to ['cat', 'mouse', 'dog', 'horse'] and len(animals)
would return 4. This means that the call to range(4) would cause the for
loop to iterate with the values 0, 1, 2, and 3.
When the for loop iterates with i set to 2, the if statement’s condition will be True
and the del animals[i] statement will delete animals[2]. This means that afterwards the animals list
will be ['cat', 'mouse', 'horse']. The indexes of
all the items after 'dog' are all shifted down by
one because the 'dog' value was removed.
But on the next iteration through the for
loop, i is set to 3. But animals[3] is out of bounds because the valid indexes of the animals list are no longer 0 to 3 but 0 to 2.
The original call to range() was for a list with 4
items in it. The list changed in length, but the for
loop is set up for the original length.
However, if we iterate from the last index of the list to 0, we don’t run into this problem. The following program
deletes the 'dog' string from the animals list without causing an IndexError:
animals = ['cat', 'mouse', 'dog', 'horse']
for i in range(len(animals) - 1, -1, -1):
if animals[i] == 'dog':
del animals[i]The reason this code doesn’t cause an error is because the for loop iterates over 3, 2, 1, and 0.
On the first iteration, the code checks if animals[3]
is equal to 'dog'. It isn’t (animals[3]
is 'horse') so the code moves on to the next
iteration. Then animals[2] is checked if it equals 'dog'. It does, so animals[2]
is deleted.
After animals[2] is deleted, the animals list is set to ['cat',
'mouse', 'horse']. On the next iteration, i
is set to 1. There is a value at animals[1] (the 'mouse' value),
so no error is caused. It doesn’t matter that all the items in the list after 'dog' have shifted down by one, because since we started
at the end of the list and are going towards the front, all of those items have
already been checked.
Similarly, we can delete grass and squirrel objects from the
grassObjs and squirrelObjs
lists without error because the for loop on lines
156 and 159 iterate in reverse order.
Adding New Grass and Squirrel Objects
163. # add more grass & squirrels if we don't have enough.
164. while len(grassObjs) < NUMGRASS:
165. grassObjs.append(makeNewGrass(camerax, cameray))
166. while len(squirrelObjs) < NUMSQUIRRELS:
167. squirrelObjs.append(makeNewSquirrel(camerax, cameray))Remember that the NUMGRASS
constant was set to 80 and the NUMSQUIRRELS
constant was set to 30 at the beginning of the
program? These variables are set so that we can be sure there are always plenty
of grass and squirrel objects in the active area at all times. If the length of
the grassObjs or squirrelObjs
drops below NUMGRASS or NUMSQUIRRELS
respectively, then new grass and squirrel objects are created. The makeNewGrass() and makeNewSquirrel()
functions that create these objects are explained later in this chapter.
Camera Slack, and Moving the Camera View
169. # adjust camerax and cameray if beyond the "camera slack"
170. playerCenterx = playerObj['x'] + int(playerObj['size'] / 2)
171. playerCentery = playerObj['y'] + int(playerObj['size'] / 2)
172. if (camerax + HALF_WINWIDTH) - playerCenterx > CAMERASLACK:
173. camerax = playerCenterx + CAMERASLACK - HALF_WINWIDTH
174. elif playerCenterx - (camerax + HALF_WINWIDTH) > CAMERASLACK:
175. camerax = playerCenterx - CAMERASLACK - HALF_WINWIDTH
176. if (cameray + HALF_WINHEIGHT) - playerCentery > CAMERASLACK:
177. cameray = playerCentery + CAMERASLACK - HALF_WINHEIGHT
178. elif playerCentery - (cameray + HALF_WINHEIGHT) > CAMERASLACK:
179. cameray = playerCentery - CAMERASLACK - HALF_WINHEIGHTThe camera’s position (which is stored as integers in the camerax and cameray variables) needs
to be updated when the player moves over. I’ve called the number of pixels the
player can move before the camera gets updated the “camera slack”. Line 19 set
the CAMERASLACK constant to 90,
which our program will take to mean that the player squirrel can move 90 pixels
from the center before the camera position gets updated to follow the squirrel.
In order to understand the equations used in the if statements on lines 172, 174, 176, and 178, you should
note that (camerax + HALF_WINWIDTH) and (cameray + HALF_WINHEIGHT) are the XY game world coordinates
currently at the center of the screen. The playerCenterx
and playerCentery is set to the middle of the player’s
squirrel’s position, also in game world coordinates.
For line 172, if the center X coordinate minus the player’s
center X coordinate is greater than the CAMERASLACK
value, that means the player is more pixels to the left of the center of the
camera than the camera slack should allow. The camerax
value needs to be updated so that the player squirrel is just at the edge of
the camera slack. This is why line 173 sets camerax
to playerCenterx + CAMERASLACK - HALF_WINWIDTH. Note
that the camerax variable is changed, not the playerObj['x'] value. We want to move the camera, not the
player.
The other three if statements
follow similar logic for the right, up, and down sides.
Drawing the Background, Grass, Squirrels, and Health Meter
181. # draw the green background
182. DISPLAYSURF.fill(GRASSCOLOR)Line 182 begins the code that starts drawing the contents of the display Surface object. First, line 182 draws a green color for the background. This will paint over all of the previous contents of the Surface so that we can start drawing the frame from scratch.
184. # draw all the grass objects on the screen
185. for gObj in grassObjs:
186. gRect = pygame.Rect( (gObj['x'] - camerax,
187. gObj['y'] - cameray,
188. gObj['width'],
189. gObj['height']) )
190. DISPLAYSURF.blit(GRASSIMAGES[gObj['grassImage']], gRect)The for loop on line 185 goes
through all the grass objects in the grassObjs list
and creates a Rect object from the x, y, width, and height information stored
in it. This Rect object is stored in a variable named gRect.
On line 190, gRect is used in the blit() method call to draw the grass image on the display
Surface. Note that gObj['grassImage'] only contains
an integer that is an index to GRASSIMAGES. GRASSIMAGES is a list of Surface objects that contain all
the grass images. Surface objects take up much more memory than just a single
integer, and all the grass objects with similar gObj['grassImage']
values look identical. So it makes sense to only have each grass image stored
once in GRASSIMAGES and simply store integers in the
grass objects themselves.
193. # draw the other squirrels
194. for sObj in squirrelObjs:
195. sObj['rect'] = pygame.Rect( (sObj['x'] - camerax,
196. sObj['y'] - cameray - getBounceAmount(sObj['bounce'], sObj['bouncerate'], sObj['bounceheight']),
197. sObj['width'],
198. sObj['height']) )
199. DISPLAYSURF.blit(sObj['surface'], sObj['rect'])The for loop that draws all the
enemy squirrel game objects is similar to the previous for
loop, except that the Rect object it creates is saved in the 'rect' key’s value of the squirrel dictionary. The reason
the code does this is because we will use this Rect object later to check if
the enemy squirrels have collided with the player squirrel.
Note that the top parameter for the Rect constructor is not
just sObj['y'] - cameray but sObj['y']
- cameray - getBounceAmount(sObj['bounce'], sObj['bouncerate'],
sObj['bounceheight']). The getBounceAmount()
function will return the number of pixels that the top value should be raised.
Also, there is no common list of Surface objects of the squirrel
images, like there was with grass game objects and GRASSIMAGES.
Each enemy squirrel game object has its own Surface object stored in the 'surface' key. This is because the squirrel images can be scaled
to different sizes.
202. # draw the player squirrel
203. flashIsOn = round(time.time(), 1) * 10 % 2 == 1After drawing the grass and enemy squirrels, the code will draw the player’s squirrel. However, there is one case where we would skip drawing the player’s squirrel. When the player collides with a larger enemy squirrel, the player takes damage and flashes for a little bit to indicate that the player is temporarily invulnerable. This flashing effect is done by drawing the player squirrel on some iterations through the game loop but not on others.
The player squirrel will be drawn on game loop iterations
for a tenth of a second, and then not drawn on the game loop iterations for a tenth of a second. This repeats over and over again as long as the player is
invulnerable (which, in the code, means that the invulnerableMode
variable is set to True). Our code will make the
flashing last for two seconds, since 2 was stored in
the INVULNTIME constant variable on line 25.
To determine if the flash is on or not, line 202 grabs the
current time from time.time(). Let’s use the example
where this function call returns 1323926893.622.
This value is passed to round(), which rounds it to
one digit past the decimal point (since 1 is passed
as round()’s second parameter). This means round() will return the value 1323926893.6.
This value is then multiplied by 10,
to become 13239268936.0. This value is a whole
number (even though it is still a float), so we can do the “mod two” trick
first discussed in the Memory Puzzle chapter to see if it is even or odd. 13239268936 % 2
evaluates to 0, which means that flashIsOn will be set to False,
since 0 == 1 is False.
In fact, time.time() will keep
returning values that will end up putting False into
flashIsOn until about 1323926893.65,
which is where round() starts rounding up to the
next tenth of a second. This is why the flashIsOn
variable will constantly have False for one tenth of
a second, and then True for the next one tenth of a
second (no matter how many iterations happen in that tenth of a second).
204. if not gameOverMode and not (invulnerableMode and flashIsOn):
205. playerObj['rect'] = pygame.Rect( (playerObj['x'] - camerax,
206. playerObj['y'] - cameray - getBounceAmount(playerObj['bounce'], BOUNCERATE, BOUNCEHEIGHT),
207. playerObj['size'],
208. playerObj['size']) )
209. DISPLAYSURF.blit(playerObj['surface'], playerObj['rect'])There are two things that must be True
before we draw the player’s squirrel. The game must currently be going on
(which happens while gameOverMode is False), and the player must not be in the “off” half of
the invulnerability flash (which happens whenever invulnerableMode
and flashIsOn aren’t both True).
Note that the squirrel is still drawn while the player is invulnerable, as long
as flashIsOn is False.
That is what produces the flashing effect.
The code for drawing the player’s squirrel is almost identical to the code for drawing the enemy squirrels.
212. # draw the health meter
213. drawHealthMeter(playerObj['health'])The drawHealthMeter() function
draws the indicator at the top left corner of the screen that tells the player
how many times the player squirrel can be hit before dying. This function will
be explained later in this chapter.
The Event Handling Loop
215. for event in pygame.event.get(): # event handling loop
216. if event.type == QUIT:
217. terminate()The first thing that is checked in the event handling loop
is if the QUIT event has been generated. If so, then
the program should be terminated.
219. elif event.type == KEYDOWN:
220. if event.key in (K_UP, K_w):
221. moveDown = False
222. moveUp = True
223. elif event.key in (K_DOWN, K_s):
224. moveUp = False
225. moveDown = TrueIf the up or down arrow keys have been pressed (or their
WASD equivalents), then the move variable (moveRight,
moveDown, etc.) for that direction should be set to True and the move variable for the opposite direction
should be set to False.
226. elif event.key in (K_LEFT, K_a):
227. moveRight = False
228. moveLeft = True
229. if playerObj['facing'] != LEFT: # change player image
230. playerObj['surface'] = pygame.transform.scale(L_SQUIR_IMG, (playerObj['size'], playerObj['size']))
231. playerObj['facing'] = LEFT
232. elif event.key in (K_RIGHT, K_d):
233. moveLeft = False
234. moveRight = True
235. if playerObj['facing'] != RIGHT: # change player image
236. playerObj['surface'] = pygame.transform.scale(R_SQUIR_IMG, (playerObj['size'], playerObj['size']))
237. playerObj['facing'] = RIGHTThe moveLeft and moveRight variables should also be set when the left or
right arrow keys are pressed. Also, the value in playerObj['facing']
should be updated to either LEFT or RIGHT. If the player squirrel is now facing a new
direction, the playerObj['surface'] value should be
replaced with a correctly scaled image of the squirrel facing the new
direction.
Line 229 is run if the left arrow key was pressed and checks
that the player squirrel wasn’t already facing left. If it wasn’t, then a new
scaled Surface object of the player squirrel image is stored in playerObj['surface'].
The code in line 232’s elif statement handles the
opposite case.
238. elif winMode and event.key == K_r:
239. returnIf the player has won the game by growing large enough (in
which case, winMode will be set to True) and the R key has been pressed, then runGame() should return. This will end the current game,
and a new game will start the next time that runGame()
gets called.
241. elif event.type == KEYUP:
242. # stop moving the player's squirrel
243. if event.key in (K_LEFT, K_a):
244. moveLeft = False
245. elif event.key in (K_RIGHT, K_d):
246. moveRight = False
247. elif event.key in (K_UP, K_w):
248. moveUp = False
249. elif event.key in (K_DOWN, K_s):
250. moveDown = FalseIf the player lets up on any of the arrow or WASD keys, then
the code should set the move variable for that direction to False. This will stop the squirrel from moving in that
direction any more.
252. elif event.key == K_ESCAPE:
253. terminate()If the key that was pressed was the Esc key, then terminate the program.
Moving the Player, and Accounting for Bounce
255. if not gameOverMode:
256. # actually move the player
257. if moveLeft:
258. playerObj['x'] -= MOVERATE
259. if moveRight:
260. playerObj['x'] += MOVERATE
261. if moveUp:
262. playerObj['y'] -= MOVERATE
263. if moveDown:
264. playerObj['y'] += MOVERATEThe code inside the if statement
on line 255 will move the player’s squirrel around only if the game is not
over. (This is why pressing on the arrow keys after the player’s squirrel dies
will have no effect.) Depending on which of the move variables is set to True, the playerObj dictionary
should have its playerObj['x'] and playerObj['y'] values changed by MOVERATE.
(This is why a larger value in MOVERATE makes the
squirrel move faster.)
266. if (moveLeft or moveRight or moveUp or moveDown) or playerObj['bounce'] != 0:
267. playerObj['bounce'] += 1
268.
269. if playerObj['bounce'] > BOUNCERATE:
270. playerObj['bounce'] = 0 # reset bounce amountThe value in playerObj['bounce']
keeps track of where the player is in the bounce. This variable stores
an integer value from 0 to BOUNCERATE.
Just like the bounce value for the enemy squirrels, a playerObj['bounce']
value of 0 means the player squirrel is at the start
of a bounce and a value of BOUNCERATE means the
player squirrel is at the end of the bounce.
The player squirrel will bounce whenever the player is
moving, or if the player has stopped moving but the squirrel hasn’t finished
its current bounce. This condition is captured in the if
statement on line 266. If any of the move variables is set to True or the current playerObj['bounce']
is not 0 (which means the player is currently in a
bounce), then the variable should be incremented on line 267.
Because the playerObj['bounce']
variable should only be in the range of 0 to BOUNCERATE, if incrementing it makes it larger than BOUNCERATE, it should be reset back to 0.
Collision Detection: Eat or Be Eaten
272. # check if the player has collided with any squirrels
273. for i in range(len(squirrelObjs)-1, -1, -1):
274. sqObj = squirrelObjs[i]The for loop on line 273 will run code on each of the enemy squirrel game objects in squirrelObjs.
Notice that the parameters to range() on line 273
start at the last index of squirrelObjs and
decrement. This is because the code inside this for
loop may end up deleting some of these enemy squirrel game objects (if the
player’s squirrel ends up eating them), so it is important to iterate from the
end down to the front. The reason why was explained previously in the “When
Deleting Items in a List, Iterate Over the List in Reverse” section.
275. if 'rect' in sqObj and playerObj['rect'].colliderect(sqObj['rect']):
276. # a player/squirrel collision has occurred
277.
278. if sqObj['width'] * sqObj['height'] <= playerObj['size']**2:
279. # player is larger and eats the squirrel
280. playerObj['size'] += int( (sqObj['width'] * sqObj['height'])**0.2 ) + 1
281. del squirrelObjs[i]If the player’s squirrel is the same size as or larger than the enemy squirrel it has collided with, then the player’s squirrel will eat
that squirrel and grow. The number that is added to the 'size'
key in the player object (that is, the growth) is calculated based on the enemy
squirrel’s size on line 280. Here’s a graph showing the growth from different
sized squirrels. Notice that larger squirrels cause more growth:

So, according to the chart, eating a squirrel that has a width and height of 45 (that is, an area of 2025 pixels) would cause the player to grow 5 pixels wider and taller.
Line 281 deletes the eaten squirrel object from the squirrelObjs list so that it will no longer appear on the
screen or have its position updated.
283. if playerObj['facing'] == LEFT:
284. playerObj['surface'] = pygame.transform.scale(L_SQUIR_IMG, (playerObj['size'], playerObj['size']))
285. if playerObj['facing'] == RIGHT:
286. playerObj['surface'] = pygame.transform.scale(R_SQUIR_IMG, (playerObj['size'], playerObj['size']))The player’s squirrel image needs to be updated now that the
squirrel is larger. This can be done by passing the original squirrel image in L_SQUIR_IMG or R_SQUIR_IMG to
the pygame.transform.scale() function, which will
return an enlarged version of the image. Whether playerObj['facing'] is equal to LEFT or RIGHT determines which original squirrel image we
pass to the function.
288. if playerObj['size'] > WINSIZE:
289. winMode = True # turn on "win mode"The way the player wins the game is by getting the squirrel
to have a size larger than the integer stored in the WINSIZE
constant variable. If this is true, then the winMode
variable is set to True. Code in the other parts of
this function will handle displaying the congratulations text and checking for
the player to press the R key to restart the game.
291. elif not invulnerableMode:
292. # player is smaller and takes damage
293. invulnerableMode = True
294. invulnerableStartTime = time.time()
295. playerObj['health'] -= 1
296. if playerObj['health'] == 0:
297. gameOverMode = True # turn on "game over mode"
298. gameOverStartTime = time.time()If the player’s area was not equal to or larger than the area
of the enemy squirrel, and invulnerableMode was not
set to True, then the player will take damage from
colliding with this larger squirrel.
To prevent the player from being damaged several times by
the same squirrel immediately, we will briefly make the player invulnerable to
further squirrel attacks by setting invulnerableMode
to True on line 293. Line 294 will set invulnerableStartTime to the current time (which is
returned by time.time()) so that lines 133 and 134
can know when to set invulnerableMode to False.
Line 295 decrements the player’s health by 1. Because there is a chance that the player’s health is
now at 0, line 296 checks for this and, if so, sets gameOverMode to True and gameOverStartTime to the current time.
The Game Over Screen
299. else:
300. # game is over, show "game over" text
301. DISPLAYSURF.blit(gameOverSurf, gameOverRect)
302. if time.time() - gameOverStartTime > GAMEOVERTIME:
303. return # end the current gameWhen the player has died, the “Game Over” text (which is on
the Surface object in the gameOverSurf variable)
will be shown on the screen for the number of seconds that is in the GAMEOVERTIME constant. Once this amount of time has
elapsed, then the runGame() function will return.
This lets the enemy squirrels continue to be animated and moving around for a few seconds after the player dies and before the next game starts. The “game over screen” in Squirrel Eat Squirrel does not wait until the player presses a key before a new game starts.
Winning
305. # check if the player has won.
306. if winMode:
307. DISPLAYSURF.blit(winSurf, winRect)
308. DISPLAYSURF.blit(winSurf2, winRect2)
309.
310. pygame.display.update()
311. FPSCLOCK.tick(FPS)The winMode variable is set to True on line 289 if the player has reached a certain size
(which is dictated by the WINSIZE constant). All
that happens when the player has won is that the “You have achieved OMEGA
SQUIRREL!” text (which is on the Surface object stored in the winSurf variable) and the “(Press “r” to restart.)” text
(which is on the Surface object stored in the winSurf2
variable) appears on the screen. The game continues until the user presses the
R key, at which point the program execution will return from runGame(). The event handling code for the R key is done
on lines 238 and 239.
Drawing a Graphical Health Meter
316. def drawHealthMeter(currentHealth):
317. for i in range(currentHealth): # draw red health bars
318. pygame.draw.rect(DISPLAYSURF, RED, (15, 5 + (10 * MAXHEALTH) - i * 10, 20, 10))
319. for i in range(MAXHEALTH): # draw the white outlines
320. pygame.draw.rect(DISPLAYSURF, WHITE, (15, 5 + (10 * MAXHEALTH) - i * 10, 20, 10), 1)To draw the health meter, first the for
loop on line 317 draws the filled-in red rectangle for the amount of health the
player has. Then the for loop on line 319 draws an unfilled
white rectangle for all of the possible health the player could have (which is
the integer value stored in the MAXHEALTH constant).
Note that the pygame.display.update() function is
not called in drawHealthMeter().
The Same Old terminate()
Function
323. def terminate():
324. pygame.quit()
325. sys.exit()The terminate() function works
the same as in the previous game programs.
The Mathematics of the Sine Function
328. def getBounceAmount(currentBounce, bounceRate, bounceHeight):
329. # Returns the number of pixels to offset based on the bounce.
330. # Larger bounceRate means a slower bounce.
331. # Larger bounceHeight means a higher bounce.
332. # currentBounce will always be less than bounceRate
333. return int(math.sin( (math.pi / float(bounceRate)) * currentBounce ) * bounceHeight)
334.There is a mathematical function (which is similar to
functions in programming in that they both “return” or “evaluate” to a number
based on their parameters) called sine
(pronounced like “sign” and often abbreviated as “sin”). You may have learned
about it in math class, but if you haven’t it will be explained here. Python
has this mathematic function as a Python function in the math
module. You can pass an int or float value to math.sin(),
and it will return a float value that is called the “sine value”.
In the interactive shell, let’s see what math.sin() returns for some values:
>>> import math
>>> math.sin(1)
0.8414709848078965
>>> math.sin(2)
0.90929742682568171
>>> math.sin(3)
0.14112000805986721
>>> math.sin(4)
-0.7568024953079282
>>> math.sin(5)
-0.95892427466313845It seems really hard to predict what value math.sin() is going to return based on what value we pass
it (which might make you wonder what math.sin() is
useful for). But if we graph the sine values of the integers 1 through 10 on a graph, we
would get this:

You can kind of see a wavy pattern in the values returned by
math.sin(). If you figure out the sine values for
more numbers besides integers (for example, 1.5 and 2.5 and so on) and then connect the dots with lines, you
can see this wavy pattern more easily:

In fact, if you kept adding more and more data points to this graph, you would see that the sine wave looks like this:

Notice that math.sin(0) returns 0, then gradually increases until math.sin(3.14
/ 2) returns 1, then it begins to decrease
until math.sin(3.14) returns 0.
The number 3.14 is a special number in mathematics
called pi (pronounced the same as delicious
“pie”). This value is also stored in the constant variable pi
in the math module (which is why line 333 uses the
variable, math.pi), which is technically the float value
3.1415926535897931. Since we want a wavy-looking
bounce for our squirrel, we’ll only pay attention to the return values of math.sin() for the arguments 0
to 3.14:

Let’s take a look at the return value of getBounceAmount() and figure out what it does exactly.
333. return int(math.sin( (math.pi / float(bounceRate)) * currentBounce ) * bounceHeight)Remember that on line 21 we set the BOUNCERATE
constant to 6. This means that our code will only
increment playerObj['bounce'] from 0 to 6 and that we want to
split up the range of floating-point values from 0
to math.pi (about 3.1415)
into 6 parts, which we can do with simple division: math.pi / 6 = 0.5236. Each of the 6
equal parts of the math.pi length on the graph for
the “sine wave bounce” is 0.5236.
You can see that when playerObj['bounce']
is at 3 (halfway between 0
and 6), the value passed to the math.sin()
call is math.pi / 6 * 3, which is 1.5707 (halfway between 0 and 3.1415). Then math.sin(1.5707)
will return 1.0, which is the highest part of the
sine wave (and the highest part of the sine wave happens halfway through the wave).
As playerObj['bounce'] gets its
value incremented, the getBounceAmount() function
will return values that have the same bounce shape that the sine wave has from 0 to 3.14. If you want to make the bounce higher, then increase the BOUNCEHEIGHT constant. If you want to make the bounce slower, then increase the BOUNCERATE constant.
The sine function is a concept from trigonometry. If you’d like to learn more about the sine wave, the Wikipedia page has detailed information: http://en.wikipedia.org/wiki/Sine
Backwards Compatibility with Python Version 2
The reason we call float() to
convert bounceRate to a floating point number is
simply so that this program will work in Python version 2. In Python version 3,
the division operator will evaluate to a floating point value even if both of
the operands are integers, like this:
>>> # Python version 3
...
>>> 10 / 5
2.0
>>> 10 / 4
2.5
>>>However, in Python version 2, the /
division operator will only evaluate to a floating point value if one of the
operands is also a floating point value. If both operands are integers, then
Python 2’s division operator will evaluate to an integer value (rounding down
if needed), like this:
>>> # Python version 2
...
>>> 10 / 5
2
>>> 10 / 4
2
>>> 10 / 4.0
2.5
>>> 10.0 / 4
2.5
>>> 10.0 / 4.0
2.5But if we always convert one of the values to a floating
point value with the float() function, then the
division operator will evaluate to a float value no matter which version of
Python runs this source code. Making these changes so that our code works with
older versions of software is called backwards
compatibility. It is important to maintain backwards compatibility,
because not everyone will always be running the latest version of software and
you want to ensure that the code you write works with as many computers as
possible.
You can’t always make your Python 3 code backwards compatible with Python 2, but if it’s possible then you should do it. Otherwise, people who try to run your games with Python 2 will get error messages and think that your program is buggy.
A list of some differences between Python 2 and Python 3 can be found at http://inventwithpython.com/appendixa.html.
The getRandomVelocity()
Function
335. def getRandomVelocity():
336. speed = random.randint(SQUIRRELMINSPEED, SQUIRRELMAXSPEED)
337. if random.randint(0, 1) == 0:
338. return speed
339. else:
340. return -speedThe getRandomVelocity() function
is used to randomly determine how fast an enemy squirrel will move. The range
of this velocity is set in the SQUIRRELMINSPEED and SQUIRRELMAXSPEED constants, but on top of that, the speed
is either negative (indicating the squirrel goes to the left or up) or positive
(indicating the squirrel goes to the right or down). There is a fifty-fifty
chance for the random speed to be positive or negative.
Finding a Place to Add New Squirrels and Grass
343. def getRandomOffCameraPos(camerax, cameray, objWidth, objHeight):
344. # create a Rect of the camera view
345. cameraRect = pygame.Rect(camerax, cameray, WINWIDTH, WINHEIGHT)
346. while True:
347. x = random.randint(camerax - WINWIDTH, camerax + (2 * WINWIDTH))
348. y = random.randint(cameray - WINHEIGHT, cameray + (2 * WINHEIGHT))
349. # create a Rect object with the random coordinates and use colliderect()
350. # to make sure the right edge isn't in the camera view.
351. objRect = pygame.Rect(x, y, objWidth, objHeight)
352. if not objRect.colliderect(cameraRect):
353. return x, yWhen a new squirrel or grass object is created in the game
world, we want it to be within the active area (so that it is near the player’s
squirrel) but not within the view of the camera (so that it doesn’t just
suddenly pop into existence on the screen). To do this, we create a Rect object
that represents the area of the camera (using camerax,
cameray, WINWIDTH, and WINHEIGHT constants).
Next, we randomly generate numbers for the XY coordinates
that would be within the active area. The active area’s left and top edge are WINWIDTH and WINHEIGHT pixels
to the left and up of camerax and cameray. So the active area’s left and top edge are at camerax - WINWIDTH and cameray -
WINHEIGHT. The active area’s width and height are also three times the
size of the WINWIDTH and WINHEIGHT,
as you can see in this image (where WINWIDTH is set
to 640 pixels and WINHEIGHT set to 480 pixels):

This means the right and bottom edges will be at camerax + (2 * WINWIDTH) and cameray +
(2 * WINHEIGHT). Line 352 will check if the random XY coordinates would
collide with the camera view’s Rect object. If not, then those coordinates are
returned. If so, then the while loop on line 346
will keep generating new coordinates until it finds acceptable ones.
Creating Enemy Squirrel Data Structures
356. def makeNewSquirrel(camerax, cameray):
357. sq = {}
358. generalSize = random.randint(5, 25)
359. multiplier = random.randint(1, 3)
360. sq['width'] = (generalSize + random.randint(0, 10)) * multiplier
361. sq['height'] = (generalSize + random.randint(0, 10)) * multiplier
362. sq['x'], sq['y'] = getRandomOffCameraPos(camerax, cameray, sq['width'], sq['height'])
363. sq['movex'] = getRandomVelocity()
364. sq['movey'] = getRandomVelocity()Creating enemy squirrel game objects is similar to making
the grass game objects. The data for each enemy squirrel is also stored in a
dictionary. The width and height are set to random sizes on lines 360 and 361. The
generalSize variable is used so that the width and
height of each squirrel aren’t too different from each other. Otherwise, using
completely random numbers for width and height could give us very tall and
skinny squirrels or very short and wide squirrels. The width and height of the
squirrel are this general size with a random number from 0
to 10 added to it (for slight variation), and then
multiplied by the multiplier variable.
The original XY coordinate position of the squirrel will be a random location that the camera cannot see, to prevent the squirrels from just “popping” into existence on the screen.
The speed and direction are also randomly selected by the getRandomVelocity() function.
Flipping the Squirrel Image
365. if sq['movex'] < 0: # squirrel is facing left
366. sq['surface'] = pygame.transform.scale(L_SQUIR_IMG, (sq['width'], sq['height']))
367. else: # squirrel is facing right
368. sq['surface'] = pygame.transform.scale(R_SQUIR_IMG, (sq['width'], sq['height']))
369. sq['bounce'] = 0
370. sq['bouncerate'] = random.randint(10, 18)
371. sq['bounceheight'] = random.randint(10, 50)
372. return sqThe L_SQUIR_IMG and R_SQUIR_IMG constants contain Surface objects with
left-facing and right-facing squirrel images on them. New Surface objects will
be made using the pygame.transform.scale() function
to match the squirrel’s width and height (stored in sq['width']
and sq['height'] respectively).
After that, the three bounce-related values are randomly
generated (except for sq['bounce'] which is 0 because the squirrel always starts at the beginning of
the bounce) and the dictionary is returned on line 372.
Creating Grass Data Structures
375. def makeNewGrass(camerax, cameray):
376. gr = {}
377. gr['grassImage'] = random.randint(0, len(GRASSIMAGES) - 1)
378. gr['width'] = GRASSIMAGES[0].get_width()
379. gr['height'] = GRASSIMAGES[0].get_height()
380. gr['x'], gr['y'] = getRandomOffCameraPos(camerax, cameray, gr['width'], gr['height'])
381. gr['rect'] = pygame.Rect( (gr['x'], gr['y'], gr['width'], gr['height']) )
382. return grThe grass game objects are dictionaries with the usual 'x', 'y', 'width',
'height', and 'rect' keys
but also a 'grassImage' key which is a number from 0 to one less than the length of the GRASSIMAGES
list. This number will determine what image the grass game object has. For
example, if the value of the grass object’s 'grassImage'
key is 3, then it will use the Surface object stored
at GRASSIMAGES[3] for its image.
Checking if Outside the Active Area
385. def isOutsideActiveArea(camerax, cameray, obj):
386. # Return False if camerax and cameray are more than
387. # a half-window length beyond the edge of the window.
388. boundsLeftEdge = camerax - WINWIDTH
389. boundsTopEdge = cameray - WINHEIGHT
390. boundsRect = pygame.Rect(boundsLeftEdge, boundsTopEdge, WINWIDTH * 3, WINHEIGHT * 3)
391. objRect = pygame.Rect(obj['x'], obj['y'], obj['width'], obj['height'])
392. return not boundsRect.colliderect(objRect)The isOutsideActiveArea() will
return True if the object you pass it is outside of
the “active area” that is dictated by the camerax
and cameray parameters. Remember that the active
area is an area around the camera view the size of the camera view (which has a
width and height set by WINWIDTH and WINHEIGHT), like this:

We can create a Rect object that represents the active area
by passing camerax - WINWIDTH for the left edge
value and cameray - WINHEIGHT for the top edge
value, and then WINWIDTH * 3 and WINHEIGHT * 3 for the width and height. Once we have the
active area represented as a Rect object, we can use the colliderect()
method to determine if the object in the obj parameter collides with (that is, is inside of) the active area Rect object.
Since the player squirrel, enemy squirrel and grass objects
all have 'x', 'y', 'width' and 'height' keys, the isOutsideActiveArea() code can work with any of those types of game object.
395. if __name__ == '__main__':
396. main()Finally, after all the functions have been defined, the
program will run the main() function and start the
game.
Summary
Squirrel Eat Squirrel was our first game to have multiple enemies moving around the game world at once. The key to having several enemies was using a dictionary value with identical keys for each enemy squirrel, so that the same code could be run on each of them during an iteration through the game loop.
The concept of the camera was also introduced. Cameras weren’t needed for our previous games because the entire game world fit onto one screen. However, when you make your own games that involve a player moving around a large game world, you will need code to handle converting between the game world’s coordinate system and the screen’s pixel coordinate system.
Finally, the mathematical sine function was introduced to give realistic squirrel hops (no matter how tall or long each hop was). You don’t need to know a lot of math to do programming. In most cases, just knowing addition, multiplication, and negative numbers is fine. However, if you study mathematics, you’ll often find several uses for math to make your games cooler.
For additional programming practice, you can download buggy versions of Squirrel Eat Squirrel from http://invpy.com/buggy/squirrel and try to figure out how to fix the bugs.