Wormy
How to Play Wormy
Wormy is a Nibbles clone. The player starts out controlling a short worm that is constantly moving around the screen. The player cannot stop or slow down the worm, but they can control which direction it turns. A red apple appears randomly on the screen, and the player must move the worm so that it eats the apple. Each time the worm eats an apple, the worm grows longer by one segment and a new apply randomly appears on the screen. The game is over if the worm crashes into itself or the edges of the screen.
Source Code to Wormy
This source code can be downloaded from http://invpy.com/wormy.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/wormy to see if the differences between your code and the code in the book.
The Grid
If you play the game a little, you’ll notice that the apple and the segments of the worm’s body always fit along a grid of lines. We will call each of the squares in this grid a cell (it’s not always what a space in a grid is called, it’s just a name I came up with). The cells have their own Cartesian coordinate system, with (0, 0) being the top left cell and (31, 23) being the bottom right cell.
The Setup Code
1. # Wormy (a Nibbles clone) 2. # By Al Sweigart al@inventwithpython.com 3. # http://inventwithpython.com/pygame 4. # Creative Commons BY-NC-SA 3.0 US 5. 6. import random, pygame, sys 7. from pygame.locals import * 8. 9. FPS = 15 10. WINDOWWIDTH = 640 11. WINDOWHEIGHT = 480 12. CELLSIZE = 20 13. assert WINDOWWIDTH % CELLSIZE == 0, "Window width must be a multiple of cell size." 14. assert WINDOWHEIGHT % CELLSIZE == 0, "Window height must be a multiple of cell size." 15. CELLWIDTH = int(WINDOWWIDTH / CELLSIZE) 16. CELLHEIGHT = int(WINDOWHEIGHT / CELLSIZE)
The code at the start of the program just sets up some constant variables used in the game. The width and height of the cells are stored in CELLSIZE. The assert statements on lines 13 and 14 ensure that the cells fit perfectly in the window. For example, if the CELLSIZE was 10 and the WINDOWWIDTH or WINDOWHEIGHT constants were set to 15, then only 1.5 cells could fit. The assert statements make sure that only a whole integer number of cells fits in the window.
18. # R G B 19. WHITE = (255, 255, 255) 20. BLACK = ( 0, 0, 0) 21. RED = (255, 0, 0) 22. GREEN = ( 0, 255, 0) 23. DARKGREEN = ( 0, 155, 0) 24. DARKGRAY = ( 40, 40, 40) 25. BGCOLOR = BLACK 26. 27. UP = 'up' 28. DOWN = 'down' 29. LEFT = 'left' 30. RIGHT = 'right' 31. 32. HEAD = 0 # syntactic sugar: index of the worm's head
Some more constants are set on lines 19 to 32. The HEAD constant will be explained later in this chapter.
The main() Function
34. def main(): 35. global FPSCLOCK, DISPLAYSURF, BASICFONT 36. 37. pygame.init() 38. FPSCLOCK = pygame.time.Clock() 39. DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) 40. BASICFONT = pygame.font.Font('freesansbold.ttf', 18) 41. pygame.display.set_caption('Wormy') 42. 43. showStartScreen() 44. while True: 45. runGame() 46. showGameOverScreen()
In the Wormy game program, we’ve put the main part of the code in a function called runGame(). This is because we only want to show the "start screen" (the animation with the rotating "Wormy" text) once when the program starts (by calling the showStartScreen() function). Then we want to call runGame(), which will start a game of Wormy. This function will return when the player’s worm collides into a wall or into itself and causes a game over.
At that point we will show the game over screen by calling showGameOverScreen(). When that function call returns, the loop goes back to the start and calls runGame() again. The while loop on line 44 will loop forever until the program terminates.
A Separate runGame() Function
49. def runGame(): 50. # Set a random start point. 51. startx = random.randint(5, CELLWIDTH - 6) 52. starty = random.randint(5, CELLHEIGHT - 6) 53. wormCoords = [{'x': startx, 'y': starty}, 54. {'x': startx - 1, 'y': starty}, 55. {'x': startx - 2, 'y': starty}] 56. direction = RIGHT 57. 58. # Start the apple in a random place. 59. apple = getRandomLocation()
At the beginning of a game, we want the worm to start in a random position (but not too close to the edges of the board) so we store a random coordinate in startx and starty. (Remember that CELLWIDTH and CELLHEIGHT is the number of cells wide and high the window is, not the number of pixels wide and high).
The body of the worm will be stored in a list of dictionary values. There will be one dictionary value per body segment of the worm. The dictionary will have keys 'x' and 'y' for the XY coordinates of that body segment. The head of the body to be at startx and starty. The other two body segments will be one and two cells to the left of the head.
The head of the worm will always be the body part atwormCoords[0]. To make this code more readable, we’ve set the HEAD constant to 0 on line 32, so that we can use wormCoords[HEAD] instead of wormCoords[0].
The Event Handling Loop
61. while True: # main game loop 62. for event in pygame.event.get(): # event handling loop 63. if event.type == QUIT: 64. terminate() 65. elif event.type == KEYDOWN: 66. if (event.key == K_LEFT or event.key == K_a) and direction != RIGHT: 67. direction = LEFT 68. elif (event.key == K_RIGHT or event.key == K_d) and direction != LEFT: 69. direction = RIGHT 70. elif (event.key == K_UP or event.key == K_w) and direction != DOWN: 71. direction = UP 72. elif (event.key == K_DOWN or event.key == K_s) and direction != UP: 73. direction = DOWN 74. elif event.key == K_ESCAPE: 75. terminate()
Line 61 is the start of the main game loop and line 62 is the start of the event handling loop. If the event is a QUIT event, then we call terminate() (which we’ve defined the same as the terminate() function in the previous game programs).
Otherwise, if the event is a KEYDOWN event, then we check if the key that was pressed down is an arrow key or a WASD key. We want an additional check so that the worm does not turn in on itself. For example, if the worm is moving left, then if the player accidentally presses the right arrow key, the worm would immediate start going right and crash into itself.
That is why we have this check for the current value of the direction variable. That way, if the player accidentally presses an arrow key that would cause them to immediately crash the worm, we just ignore that key press.
Collision Detection
77. # check if the worm has hit itself or the edge 78. if wormCoords[HEAD]['x'] == -1 or wormCoords[HEAD]['x'] == CELLWIDTH or wormCoords[HEAD]['y'] == -1 or wormCoords[HEAD]['y'] == CELLHEIGHT: 79. return # game over 80. for wormBody in wormCoords[1:]: 81. if wormBody['x'] == wormCoords[HEAD]['x'] and wormBody['y'] == wormCoords[HEAD]['y']: 82. return # game over
The worm has crashed when the head has moved off the edge of the grid or when the head moves onto a cell that is already occupied by another body segment.
We can check if the head has moved off the edge of the grid by seeing if either the X coordinate of the head (which is stored in wormCoords[HEAD]['x']) is -1 (which is past the left edge of the grid) or equal to CELLWIDTH (which is past the right edge, since the rightmost X cell coordinate is one less than CELLWIDTH).
The head has also moved off the grid if the Y coordinate of the head (which is stored in wormCoords[HEAD]['y']) is either -1 (which is past the top edge) or CELLHEIGHT (which is past the bottom edge).
All we have to do to end the current game is to return out ofrunGame(). When runGame() returns to the function call in main(), the next line after the runGame() call (line 46) is the call to showGameOverScreen() which makes the large "Game Over" text appear. This is why we have the return statement on line 79.
Line 80 loops through every body segment in wormCoords after the head (which is at index 0. This is why the for loop iterates over wormCoords[1:] instead of just wormCoords). If both the 'x' and 'y' values of the body segment are the same as the 'x' and 'y' of the head, then we also end the game by returning out of the runGame() function.
Detecting Collisions with the Apple
84. # check if worm has eaten an apply 85. if wormCoords[HEAD]['x'] == apple['x'] and wormCoords[HEAD]['y'] == apple['y']: 86. # don't remove worm's tail segment 87. apple = getRandomLocation() # set a new apple somewhere 88. else: 89. del wormCoords[-1] # remove worm's tail segment
We do a similar collision detection check between the head of the worm and the apple’s XY coordinates. If they match, we set the coordinates of the apple to a random new location (which we get from the return value of getRandomLocation()).
If the head has not collided with an apple, then we delete the last body segment in the wormCoords list. Remember that negative integers for indexes count from the end of the list. So while 0 is the index of the first item in the list and 1 is for the second item, -1 is for the last item in the list and -2 is for the second to last item.
The code on lines 91 to 100 (described next in the "Moving the Worm" section) will add a new body segment (for the head) in the direction that the worm is going. This will make the worm one segment longer. By not deleting the last body segment when the worm eats an apple, the overall length of the worm increases by one. But when line 89 deletes the last body segment, the size remains the same because a new head segment is added right afterwards.
Moving the Worm
91. # move the worm by adding a segment in the direction it is moving 92. if direction == UP: 93. newHead = {'x': wormCoords[HEAD]['x'], 'y': wormCoords[HEAD]['y'] - 1} 94. elif direction == DOWN: 95. newHead = {'x': wormCoords[HEAD]['x'], 'y': wormCoords[HEAD]['y'] + 1} 96. elif direction == LEFT: 97. newHead = {'x': wormCoords[HEAD]['x'] - 1, 'y': wormCoords[HEAD]['y']} 98. elif direction == RIGHT: 99. newHead = {'x': wormCoords[HEAD]['x'] + 1, 'y': wormCoords[HEAD]['y']} 100. wormCoords.insert(0, newHead)
To move the worm, we add a new body segment to the beginning of the wormCoords list. Because the body segment is being added to the beginning of the list, it will become the new head. The coordinates of the new head will be right next to the old head’s coordinates. Whether 1 is added or subtracted from either the X or Y coordinate depends on the direction the worm was going.
This new head segment is added to wormCoords with the insert() list method on line 100.
The insert() List Method
Unlike the append() list method that can only add items to the end of a list, the insert() list method can add items anywhere inside the list. The first parameter for insert() is the index where the item should go (all the items originally at this index and after have their indexes increase by one). If the argument passed for the first parameter is larger than the length of the list, the item is simply added to the end of the list (just like what append() does). The second parameter for insert() is the item value to be added. Type the following into the interactive shell to see how insert() works:
>>> spam = ['cat', 'dog', 'bat'] >>> spam.insert(0, 'frog') >>> spam ['frog', 'cat', 'dog', 'bat'] >>> spam.insert(10, 42) >>> spam ['frog', 'cat', 'dog', 'bat', 42] >>> spam.insert(2, 'horse') >>> spam ['frog', 'cat', 'horse', 'dog', 'bat', 42] >>>
Drawing the Screen
101. DISPLAYSURF.fill(BGCOLOR) 102. drawGrid() 103. drawWorm(wormCoords) 104. drawApple(apple) 105. drawScore(len(wormCoords) - 3) 106. pygame.display.update() 107. FPSCLOCK.tick(FPS)
The code for drawing the screen in the runGame() function is fairly simple. Line 101 fills in the entire display Surface with the background color. Lines 102 to 105 draw the grid, worm, apple, and score to the display Surface. Then the call to pygame.display.update() draws the display Surface to the actual computer screen.
Drawing "Press a key" Text to the Screen
109. def drawPressKeyMsg(): 110. pressKeySurf = BASICFONT.render('Press a key to play.', True, DARKGRAY) 111. pressKeyRect = pressKeySurf.get_rect() 112. pressKeyRect.topleft = (WINDOWWIDTH - 200, WINDOWHEIGHT - 30) 113. DISPLAYSURF.blit(pressKeySurf, pressKeyRect)
While the start screen animation is playing or the game over screen is being shown, there will be some small text in the bottom right corner that says "Press a key to play". Rather than have the code typed out in both the showStartScreen() and the showGameOverScreen(), we put it in a this separate function and simply call the function from showStartScreen() and showGameOverScreen().
The checkForKeyPress() Function
116. def checkForKeyPress(): 117. if len(pygame.event.get(QUIT)) > 0: 118. terminate() 119. 120. keyUpEvents = pygame.event.get(KEYUP) 121. if len(keyUpEvents) == 0: 122. return None 123. if keyUpEvents[0].key == K_ESCAPE: 124. terminate() 125. return keyUpEvents[0].key
This function first checks if there are any QUIT events in the event queue. The call to pygame.event.get() on line 117 returns a list of all the QUIT events in the event queue (because we pass QUIT as an argument). If there are not QUIT events in the event queue, then the list that pygame.event.get() returns will be the empty list: []
The len() call on line 117 will return 0 if pygame.event.get() returned an empty list. If there are more than zero items in the list returned by pygame.event.get() (and remember, any items in this list will only be QUIT events because we passed QUIT as the argument to pygame.event.get()), then the terminate() function gets called on line 118 and the program terminates.
After that, the call to pygame.event.get() gets a list of any KEYUP events in the event queue. If the key event is for the Esc key, then the program terminates in that case as well. Otherwise, the first key event object in the list that was returned by pygame.event.get() is returned from this checkForKeyPress() function.