.. Copyright Gareth McCaughan and Jeffrey Elkner. All rights reserved. CONDITIONS: A "Transparent" form of a document means a machine-readable form, represented in a format whose specification is available to the general public, whose contents can be viewed and edited directly and straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup has been designed to thwart or discourage subsequent modification by readers is not Transparent. A form that is not Transparent is called "Opaque". Examples of Transparent formats include LaTeX source and plain text. Examples of Opaque formats include PDF and Postscript. Paper copies of a document are considered to be Opaque. Redistribution and use of this document in Transparent and Opaque forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of this document in Transparent form must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions of this document in Opaque form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution, and reproduce the above copyright notice in the Opaque document itself. - Neither the name of Scripture Union, nor LiveWires nor the names of its contributors may be used to endorse or promote products derived from this document without specific prior written permission. DISCLAIMER: THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS, CONTRIBUTORS OR SCRIPTURE UNION BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS DOCUMENT, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Chomp ===== Introduction ------------ When you've finished working through this worksheet, you'll have a version of another classic arcade game. What you need to know --------------------- * The basics of Python from `Sheet 1 <1-intro.html>`__ (*Introducing Python*) and `Sheet 2 <2-tables.html>`__ (*Turning the Tables*). * Functions from `Sheet 3 <3-pretty.html>`__ (*Pretty Pictures*); you might want to look at `Sheet F `__ (*Functions*) too. * Lists from `Sheet A `__ (*Lists in Python*). * Classes and Objects from `Sheet O `__ (*Objects and Classes*). You will need to look over these sheets before you start. If you get stuck, you can always ask for help. What is Chomp? ~~~~~~~~~~~~~~ First, we'll describe the game of Chomp. No doubt some of the details described here are different from the original `Pac-Man `_ game, but the result is still an enjoyable game. The game takes place in a maze filled with little bits of food. Our champion, Chomp, is yellow, round and very hungry. The object of the game is to guide Chomp round the walls of the maze to eat all the bits of food which are lying around. Easy! you may think, but Chomp's task is made harder by the brightly colored ghosts which haunt the maze, drifting aimlessly about, and Chomp will faint from fright if he ever bumps into one of those. When this happens, Chomp and all the ghosts return to their starting positions. If this happens too many times --- three times in fact --- the fright will be too much for Chomp and he will die. The only form of defence Chomp has against the ghosts is a capsule, which gives Chomp power to send the ghosts back to where they came from. When a capsule is consumed, all the ghosts turn white, and if Chomp captures a ghost which is white, he earns points, and the ghost then returns to its original starting position and color. After this has happened, Chomp is again powerless against that ghost. After a while, however, the power of the capsule begins to wear off and the ghosts will start showing their original color, switching rapidly between their color and white as they do so. While this is happening, Chomp can still safely capture the ghosts. Then, when the power of the capsule wears off completely, all the ghosts return to their original colors, and again, Chomp is powerless against them. When Chomp completes the task of eating all the food which has been left in the maze, he quickly moves on to another level which is in some way harder than the last. Perhaps the ghosts move faster, or there are more of them, or they start trying to find Chomp in the maze to give him the fright of his life. Not only does Chomp have to avoid the ghosts, but he must also move quickly because there is only a limited time available for him to collect the food. When Chomp completes a level he is rewarded with points for the time which is left remaining. The traditional maze also had a tunnel passing from one side of the screen to the other. Anything which entered the tunnel on one side of the screen would appear on the other side. How the grid works ~~~~~~~~~~~~~~~~~~ The grid is made up of imaginary vertical and horizontal lines, which we'll call **grid lines**, that make up squares. The places where vertical grid lines cross horizontal grid lines, we'll call **grid points**. Walls will lie along grid lines, and Chomp and the ghosts will travel along grid lines. The food and capsules will sit on grid points. Each grid point is identified by a pair of numbers: Its row number and its column number. It will also have its coordinates on the screen. we'll call the first pair of numbers **grid coordinates** and the second pair **screen coordinates**. Things on the screen -------------------- The kinds of objects which take part in the game are: Maze, Chomp, Food, Walls, Ghosts and Capsules. Some of these, Chomp and Ghosts, can move, but not through walls - we'll call these **movables**. Others, Walls, Food and Capsules, never move. We're going to define a class for each of these kinds of object. You may like to skim-read this section and refer back to it later. The ``Maze`` class ~~~~~~~~~~~~~~~~~~ The ``Maze`` contains all the other objects in a grid of squares. It's main job is to keep track of where all the objects are. It will deal with two types of object: those that do and those that don't. ``Movable`` objects will ask the ``Maze`` about walls which might be in the way. Chomp will keep telling the ``Maze`` where he is, and the ``Maze`` will check for collisions with other objects. The ``Immovable`` class ~~~~~~~~~~~~~~~~~~~~~~~ This class is a superclass for objects which are stationary. When one of these objects is created it is expected to draw itself on the screen. The ``Maze`` object will keep a note of which object is at each location. The ``Wall`` class ~~~~~~~~~~~~~~~~~~ Walls are pretty boring. They don't move. They don't do anything. They just sit there on the screen, stopping movables from passing. They are a kind of ``Immovable`` because they don't move. The walls lie along grid lines, but it turns out to be easier if we have a ``Wall`` object at each grid point which the wall passes through. If a ``Wall`` object is next to another ``Wall`` object, we will draw a line on the screen between them. The ``Food`` class ~~~~~~~~~~~~~~~~~~ These are a kind of ``Immovable``. When eaten, they tell the ``Maze`` object that there is one less bit of ``Food`` to collect. The ``Capsule`` class ~~~~~~~~~~~~~~~~~~~~~ These are also a kind of ``Immovable``. When eaten, they tell the ``Maze`` that all the ``Ghost`` objects should turn white. The ``Maze`` then tells each ``Ghost`` to turn white. The ``Movable`` class ~~~~~~~~~~~~~~~~~~~~~ These can move about the maze, but they need to check that there aren't any walls in the way. We're only going to allow movables to travel along grid lines. The ``Maze`` will keep telling the movables to move, but what happens then depends on which kind of ``Movable`` it is. The ``Chomp`` class ~~~~~~~~~~~~~~~~~~~ Our hero! ``Chomp`` is a kind of ``Movable``, which moves when the player holds a key down. ``Chomp`` will keep telling the ``Maze`` where he is, and the ``Maze`` will check for things he might have bumped into. The ``Ghost`` class ~~~~~~~~~~~~~~~~~~~ A ``Ghost`` is also a kind of ``Movable``. They don't interact with anything except ``Wall`` objects and ``Chomp``. Each ``Ghost`` will keep travelling in the same direction until it reaches a grid point. Then it will decide on a new direction to travel in. The first working program ------------------------- It's always satisfying to get something working early on, even if it can't be played. We'll start with a simplified version of the game which is missing most of the features. Having done that, we can add things to it until it does everything we want it to. We'll eventually go through the following steps: * Make a Maze with just Walls, * Add Chomp, * Add Food, * Add the Ghosts, * Add the Capsules. After each of these steps we'll have a working program and at the end, we'll have a playable game. There will still be things we can do to improve it after that. An outline of the program ~~~~~~~~~~~~~~~~~~~~~~~~~ The main program is simple. .. sourcecode:: python from gasp import * # As usual # ... Class definitions go here the_maze = Maze() # Make Maze object while not the_maze.finished(): # Keep playing until we're done the_maze.play() the_maze.done() # We're finished Don't type the lines which start ``# ...``. They are just there to indicate that something else belongs there. The basic outline is that we make the ``Maze``, play until we're finished and then stop. Of course this code won't work yet because we haven't defined the ``Maze`` class, but we'll never need to change it because all we need is the right definition of the ``Maze`` class. All we will do is put some definitions after the first line. colors and sizes ~~~~~~~~~~~~~~~~~ It is often useful to have definitions of things you might want to change at the top of your program. When you decide you'd like a green Chomp and tiny graphics, you can just make a couple of changes at the top of the program and it's done. We'll start with a few things and add to them later. Since these are meant to be global constants, the Python convention is to name them with all capital letters. .. sourcecode:: python GRID_SIZE = 30 # This sets size of everything MARGIN = GRID_SIZE # How much space to leave round edge BACKGROUND_COLOR = color.BLACK # Colors we use WALL_COLOR = '#99E5E5' # gasp uses HTML colors The ``Maze`` layout ~~~~~~~~~~~~~~~~~~~ One of the first challenges is to tell the computer what the Maze looks like in a way that is easy for both the computer and you to understand. One good solution to this is to have a list of strings with one string for each row (horizontal grid line) of the maze, and one character in the string for each grid point along the row. Different characters will have different meanings; we'll have G for Ghost and so on. We'll put all of the objects into the layout now, so that we don't have to change it. We'll then write the program so that anything it doesn't recognise is just ignored. We think it's best if we show you. .. sourcecode:: python # The shape of the maze. Each character # represents a different type of object # % - Wall # . - Food # o - Capsule # G - Ghost # P - Chomp # Other characters are ignored the_layout = [ "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", # There are 31 '%'s in this line "%.....%.................%.....%", "%o%%%.%.%%%.%%%%%%%.%%%.%.%%%o%", "%.%.....%......%......%.....%.%", "%...%%%.%.%%%%.%.%%%%.%.%%%...%", "%%%.%...%.%.........%.%...%.%%%", "%...%.%%%.%.%%% %%%.%.%%%.%...%", "%.%%%.......%GG GG%.......%%%.%", "%...%.%%%.%.%%%%%%%.%.%%%.%...%", "%%%.%...%.%.........%.%...%.%%%", "%...%%%.%.%%%%.%.%%%%.%.%%%...%", "%.%.....%......%......%.....%.%", "%o%%%.%.%%%.%%%%%%%.%%%.%.%%%o%", "%.....%........P........%.....%", "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"] You'll need a layout like this in your program. If you like, you can make changes to it later, but stick with this one for now. Put it immediately above the main body of the program. The first few lines (which begin with '#') are *comment* lines which means that Python ignores them when it sees them. They are just there for people to read. It will be useful to have a reminder in the program for what the different characters mean. Writing comments in your programs is a very good thing because it will help you to understand your program when you come back to it and it is almost impossible to have too many. Looking at the section of program above, you can see what the maze is going to look like. The computer has an easy job as well. To find out what character is on row ``Y`` in column ``X``, we just say ``the_layout[max_y - Y][X]``. If you were expecting ``the_layout[Y][X]``, consider the top line: This will have the highest row number, ``max_y``, but it is entry 0 in the list. Every time we add 1 to the row number, we must subtract 1 from the entry number, so the row number and entry number always add up to ``max_y``. The ``Maze`` class ~~~~~~~~~~~~~~~~~~ The Maze class will keep ``Movable`` objects separate from those which don't move. It will keep those which move in a list and it will go through the list every time it wants to find one. This wouldn't be very efficient for the objects which don't move because the list would be very long. There are over 200 bits of food scattered around the maze and going round each one to ask "Are you touching Chomp?" would take a long time. If you pointed at each dot in the layout and said, "Are you touching Chomp?", you'd get bored pretty quickly. For the ``Movable`` objects it's a different matter. There are only 5 of them in total and going round each of them in turn won't take long at all. To keep track of the stationary objects we will have a list for each row in the maze, with an entry for each grid point, which will contain the object at that point. We will keep these lists in yet another list. Let me say that another way. There is a list for the ``Maze`` which contains a list for each row in the ``Maze`` which contains an object for each grid point in the row. The aim here is to be able to easily answer the question: What is at the grid point in row ``Y`` and column ``X``? . To answer this question, we first find the list for the row, and then find the object in that list for the column. In Python this looks like ``map[Y][X]``. There are several things the Maze class will need to do. The first is to build the map from the layout we defined earlier. In the process, it will bring up a window on the screen and create objects that will draw into it. Each character in the layout will have an object associated with it. .. sourcecode:: python class Maze: def __init__(self): self.have_window = False # We haven't made window yet self.game_over = False # Game isn't over yet self.set_layout(the_layout) # Make all objects set_speed(20) # Set loop rate to 20 loops per second We have 2 variables in the ``Maze`` object. The first tells us if we've called ``begin_graphics`` yet. The second tells us when to stop playing the game. The final thing we do is to call the ``set_layout`` method which we've not yet defined. Having a ``set_layout`` method will be useful if we decide to have another level with a different maze layout. Now we'll define the ``set_layout`` method. It works out the size of the layout, makes a map of the maze and adds all the objects from the layout definition into the map. .. sourcecode:: python class Maze: # This should already be in your program # ... # Other definitions will be here def set_layout(self, layout): height = len(layout) # Length of list width = len(layout[0]) # Length of first string self.make_window(width, height) self.make_map(width, height) # Start new map max_y = height - 1 for x in range(width): # Go through whole layout for y in range(height): char = layout[max_y - y][x] # See discussion 1 page ago self.make_object((x, y), char) # Create object The first line works out the height of the layout. The ``len`` function is a function which is supplied by Python. If the ``len`` function is given a list it will return the number of items in the list. Our layout list has an item --- string in this case --- for each row of the maze. The second line works out the width of the layout. If the ``len`` function is given a string, it will return the number of characters in the string. We count the number of characters in the first line of the layout to give the width of the maze. Working out the size of the maze in this way means that we can have different layouts of different sizes. The Maze class is also responsible for creating the window and managing the size of things. The ``make_window`` method makes sure the window is large enough to contain the maze. .. sourcecode:: python class Maze: # This should already be in your program # ... # Other definitions will be here def make_window(self, width, height): grid_width = (width-1) * GRID_SIZE # Work out size of window grid_height = (height-1) * GRID_SIZE screen_width = 2*MARGIN + grid_width screen_height = 2*MARGIN + grid_height begin_graphics(screen_width, # Create window screen_height, BACKGROUND_COLOR, "Chomp") Working out the size of the window needs a little explanation. The number of grid points across the maze is ``width``, but the grid points don't really take up any space. The space is taken up by the gap between the grid points and the number of these is one less than the number of points. If there were three points, there would only be two gaps. One between points 1 and 2, and one between points 2 and 3. The values of ``grid_width`` and ``grid_height`` use ``width-1`` and ``height-1`` because that is the number of gaps between grid points that there are. There is a MARGIN between the edge of the screen and the first grid point on all 4 sides of the screen. We'll be using a method for converting from grid coordinates to screen coordinates. The game will operate in grid coordinates, but whenever something needs drawing on the screen we'll need to know the screen coordinates. .. sourcecode:: python class Maze: # This should already be in your program # ... # Other definitions will be here def to_screen(self, point): (x, y) = point x = x*GRID_SIZE + MARGIN # Work out coordinates of point y = y*GRID_SIZE + MARGIN # on screen return (x, y) We've now got 2 more methods to define. The first is ``make_map`` which will create an empty map of the maze, but one thing we haven't yet considered is: What do we put in the map for an empty grid point? One of the best things to put in an empty map entry is an object that represents an empty grid point. This may seem quite odd at first, to have something to represent nothing, but it makes writing the program a lot easier. You don't have to keep checking to see if the grid point you're dealing with is empty before doing something with object. Each grid point has an object, so you can safely use it. Empty points will have an object in the ``Nothing`` class. [Actually the number ``0`` and the word nothing both represent nothing. We're just doing the same thing here.] A useful thing to know is that if you find you've got a bunch of objects in a list somewhere, they've probably got something in common. Otherwise why did you put them in the same list? If two different kinds of object have something in common, they should often have the same superclass. This is particularly the case if they are in the same list, because you can make sure that all the objects in the list have a particular method by putting it in the superclass. You'll see how we do this later with the ``is_a_wall`` method. The superclass in this case is the ``Immovable`` class. .. sourcecode:: python class Immovable: pass # We have nothing to put in this class yet class Nothing(Immovable): pass class Maze: # This should already be in your program # ... # Other definitions will be here def make_map(self, width, height): self.width = width # Store size of layout self.height = height self.map = [] # Start with empty list for y in range(height): new_row = [] # Make new row list for x in range(width): new_row.append(Nothing()) # Add entry to list self.map.append(new_row) # Put row in map We will now make a start on the ``make_object`` method. This will check the character from the layout and create an object of the right class if it knows how. For the moment, we'll only check for the wall character which is % . If it is a wall, it creates a Wall object and puts it into the map. .. sourcecode:: python class Maze: # This should already be in your program # ... # Other definitions will be here def make_object(self, point, character): (x, y) = point if character == '%': # Is it a wall? self.map[y][x] = Wall(self, point) We're nearly finished with the ``Maze`` class. What we need now are definitions of ``finished``, ``play`` and ``done`` methods. We arrange for the maze to appear on the screen until the window is closed. .. sourcecode:: python class Maze: # This should already be in your program # ... # Other definitions will be here def finished(self): return self.game_over # Stop if game is over def play(self): update_when('next_tick') # Just pass time at loop rate def done(self): end_graphics() # We've finished self.map = [] # Forget all objects The ``Wall`` class ~~~~~~~~~~~~~~~~~~ Now we define the Wall class. The ``__init__`` method will be called with 2 extra parameters which are the Maze object and the grid coordinates. Remember, the first parameter is always the object which is being created. Since this is the first version of the program, we're going to simplify things a little and just draw a filled circle at each Wall object. .. sourcecode:: python class Wall(Immovable): def __init__(self, maze, point): self.place = point # Store our position self.screen_point = maze.to_screen(point) self.maze = maze # Keep hold of Maze self.draw() def draw(self): (screen_x, screen_y) = self.screen_point dot_size = GRID_SIZE * 0.2 # Just draw circle Circle(self.screen_point, dot_size, color=WALL_COLOR, filled=True) Trying it out ~~~~~~~~~~~~~ And that's it. If you've typed in all of the program sections above, you should have the first working program. When you run it, you should a graphics window that looks like this: .. image:: ../illustrations/chomp_0.png :alt: Chomp illustration 0 Try it out! Show your friends! It's not very impressive yet, but it does something. When you have finished looking at the maze, close the window and the program will finish --- rather ungracefully, but never mind about that. .. describe:: Challenge 1 Design two different maze layouts and get them to load with the program. Making nicer walls ------------------ Now we've got a working program, we can begin on our improvement campaign. We've got a maze, but it doesn't look very nice. We'll start by making the maze look a bit nicer, by drawing lines between the circles. You can either leave the circles in, or take them out. Consider two ``Wall`` objects next to each other. Somehow we need to draw a line between them. The question now is: How does a Wall object know that it's next to another ``Wall`` object? If you look back at the ``make_object`` method you'll see that as soon as the ``Wall`` object has been created, it is put into the map of the ``Maze``. When a ``Wall`` object gets created we'll have it ask the ``Maze`` about the surrounding grid points and when it finds a ``Wall`` object, it will draw a line between them. We'll never draw a line twice because it's always up to the ``Wall`` object which was created later to draw the line. So far the ``Maze`` has a map which tells it what object is at each grid point. We need to know about this from the ``Wall`` class, so we need to add a method to the ``Maze`` class so we can ask it what is the object at this point? . One thing this will need to check for is the possibility of someone asking about a point which is outside our map. In this case, we'll return an ``Nothing`` object. Without further ado, here it is: .. sourcecode:: python class Maze: # This should already be in your program. # ... # Other definitions will be here. def object_at(self, point): (x, y) = point if y < 0 or y >= self.height: # If point is outside maze, return Nothing() # return nothing. if x < 0 or x >= self.width: return Nothing() return self.map[y][x] Since there are ``self.height`` rows in the maze and they are numbered from zero, there is no row with a number ``self.height`` or higher. Similarly for columns. So far, the only objects that can be at a grid point are: A ``Wall`` object or a ``Nothing`` object. When a new ``Wall`` object gets created it will want to know if the object next door is another ``Wall`` object. The simplest approach is to ask it. We'll arrange for all the objects which have ``Immovable`` as a superclass to have a method called ``is_a_wall`` which returns ``True`` if the object is a ``Wall`` object, and ``False`` otherwise. Looking ahead, we'll see that the answer for most objects will want this method to return false, but a the Wall object will want to return true. Being lazy, we only want to write a method called ``is_a_wall`` which returns ``False``, once. The way to do this is to put this method in the ``Immovable`` class, and override it in the ``Wall`` class. When we come to write the Food class we can just make it a subclass of ``Immovable`` and not bother about having to write the ``is_a_wall`` method and other methods we put in the ``Immovable`` class. .. sourcecode:: python class Immovable: # This should already be in your program. def is_a_wall(self): return False # Most objects aren't walls so say no class Wall(Immovable): # This should already be in your program. # ... # Other definitions will be here. def is_a_wall(self): return True # This object is a wall, so say yes We're now in a position for a ``Wall`` object to check for neighboring ``Wall`` objects. We'll invent the method ``check_neighbor`` that will look at one of the neighbors and draw a line between them if it's another wall. .. sourcecode:: python class Wall(Immovable): # This should already be in your program. # ... # Other definitions will be here. def draw(self): dot_size = GRID_SIZE * 0.2 # You can remove... Circle(self.screen_point, dot_size, # ...these three lines... color = WALL_COLOR, filled = 1) # ...if you like. (x, y) = self.place # Make list of our neighbors. neighbors = [(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)] # Check each neighbor in turn. for neighbor in neighbors: self.check_neighbor(neighbor) # ... # Other definitions will be here. The ``check_neighbor`` method will ask the Maze for an object and then ask that object if it's a wall or not. .. sourcecode:: python class Wall(Immovable): # This should already be in your program # ... # Other definitions will be here def check_neighbor(self, neighbor): maze = self.maze object = maze.object_at(neighbor) # Get object if object.is_a_wall(): # Is it a wall? here = self.screen_point # Draw line from here... there = maze.to_screen(neighbor) # ... to there if it is Line(here, there, color=WALL_COLOR, thickness=2) You should now be able to try the program and see the nicer maze: .. image:: ../illustrations/chomp_1.png :alt: Chomp illustration 1 Next we'll add Chomp. Introducing Chomp ----------------- Chomp is our first moving object. Lets have a look at what Chomp needs to do. We'll arrange for the Maze class to call the ``move`` method on all the Movables on a regular basis. When this gets called we need to check which keys are pressed and if a direction key is pressed, try to move in that direction. Chomp is only allowed to follow grid lines, so if the he's not on grid line which lies in that direction, we'll move him to move towards it. If Chomp is on a grid line which is in the right direction, we move him along it unless he's about to hit a wall. We'll start with the Movable class. All the initializer will do is make a note of the Maze object, set the current position to the point passed in, and remember the speed. .. sourcecode:: python class Movable: def __init__(self, maze, point, speed): self.maze = maze # For finding other objects self.place = point # Our current position self.speed = speed # Remember speed Next, we'll start the Chomp class. We'll start with the ``move`` method. This is fairly straightforward. We just check each key in turn and if it's pressed, move in that direction. We'll invent methods like ``move_left`` to make things easier. .. sourcecode:: python class Chomp(Movable): def __init__(self, maze, point): Movable.__init__(self, maze, point, # Just call the Movable initializer CHOMP_SPEED) def move(self): keys = keys_pressed() if 'left' in keys: self.move_left() # Is left arrow pressed? elif 'right' in keys: self.move_right() # Is right arrow pressed? elif 'up' in keys: self.move_up() # Is up arrow pressed? elif 'down' in keys: self.move_down() # Is down arrow pressed? The bit which says ``Movable.__init__`` is the way to get at the ``__init__`` method in the ``Movable`` class. Because we haven't yet told it which object to use, we have to pass the object in as the first parameter. Now that we've invented the 4 movement methods, we're going to have to write them. We'll invent another method called ``try_move`` which takes an amount to add to the X coordinate and an amount to add to the Y coordinate, and tries to move ``Chomp`` to the new place. To move one grid point to the right, the numbers would be (1, 0). To move one grid point down it would be (0, -1). The pair of numbers to add to the coordinates is called a **vector**. .. sourcecode:: python class Chomp(Movable): # This should already be in your program. # ... # Other definitions will be here. def move_left(self): self.try_move((-1, 0)) def move_right(self): self.try_move((1, 0)) def move_up(self): self.try_move((0, 1)) def move_down(self): self.try_move((0, -1)) Now it's down to the ``try_move`` method to figure out what needs to happen. It will first move Chomp towards the nearest grid line if he's not already on one. It will then move him along it if there isn't a wall in the way. One of the important things about movables --- like Chomp --- is that they aren't always at grid points. When asking the ``Maze`` about fixed objects like ``Food`` and ``Wall``\ s, it always needs to be given a grid point, and not some point between grid points. In other words, the coordinates given must be whole numbers. The best we can do is to find the nearest grid point to where we want. We'll pretend for the moment that we've solved this problem and have a method called ``nearest_grid_point``, which returns the nearest point to the current position of the ``Movable``, but we must remember to write it later. We'll also invent methods called ``furthest_move`` and ``move_by``. The first will take a vector to move by --- which must be no larger than 1 grid unit --- as a parameter and will return a vector which is how much we can move by without hitting a wall or going too fast. In effect, it answers the question: Is it possible to move in this direction? If so, how far? . If the movement passed as a parameter is OK, it will just return that. If we would hit a wall by going that far, it will return the movement that is possible before we hit the wall. If going that far would mean moving too fast, it will return a suitably adjusted vector. If we're right next to the wall and trying to move towards it, it will return (0, 0) to indicate that we can't move in that direction at all. The ``move_by`` method will actually move Chomp and just takes a vector as a parameter. It assumes that the vector has already been checked by ``furthest_move``. .. sourcecode:: python class Chomp(Movable): # This should already be in your program. # ... # Other definitions will be here. def try_move(self, move): (move_x, move_y) = move (current_x, current_y) = self.place (nearest_x, nearest_y) = (self.nearest_grid_point()) if self.furthest_move(move) == (0, 0): # If we can't move, do nothing. return if move_x != 0 and current_y != nearest_y: # If we're moving horizontally move_x = 0 # but aren't on grid line move_y = nearest_y - current_y # move towards grid line elif move_y != 0 and current_x != nearest_x: # If we're moving vertically move_y = 0 # but aren't on grid line move_x = nearest_x - current_x # Move towards grid line move = self.furthest_move((move_x, move_y)) # Don't go too far self.move_by(move) # Make move After we've figured out that we can move, we make sure we're on a grid line in the direction we're trying to move. If the player tries to move right and they aren't on a horizontal grid line, we'll move them up or down until they are. This is so that the player doesn't have to line Chomp up exactly with a gap in a wall in order to go through. In this case ``move_x`` will not be zero and ``current_y`` will not be ``nearest_y``, so we will set ``move_y`` to the distance to the nearest horizontal grid line. Instead of trying to move in the direction the player indicates, we move towards the nearest horizontal grid line. The same thing happens for vertical grid lines. Next we will deal with the ``furthest_move`` method. For this, we just work out where the next grid point we are going to encounter is and ask the ``Maze`` if there is a wall there. It also limits the step according to the speed of the object. As before we will use a vector to describe where we're going. We will want to use this method for Ghosts as well, so we'll put it in the ``Movable`` class so that it can be shared. .. sourcecode:: python class Movable: # This should already be in your program. # ... # Other definitions will be here. def furthest_move(self, movement): (move_x, move_y) = movement # How far to move (current_x, current_y) = self.place # Where are we now? nearest = self.nearest_grid_point() # Where's nearest grid point? (nearest_x, nearest_y) = nearest maze = self.maze if move_x > 0: # Are we moving towards a wall on right? next_point = (nearest_x+1, nearest_y) if maze.object_at(next_point).is_a_wall(): if current_x+move_x > nearest_x: # Are we close enough? move_x = nearest_x - current_x # Stop just before it elif move_x < 0: # Are we moving towards a wall on left? next_point = (nearest_x-1, nearest_y) if maze.object_at(next_point).is_a_wall(): if current_x+move_x < nearest_x: # Are we close enough? move_x = nearest_x - current_x # Stop just before it if move_y > 0: # Are we moving towards a wall above us? next_point = (nearest_x, nearest_y+1) if maze.object_at(next_point).is_a_wall(): if current_y+move_y > nearest_y: # Are we close enough? move_y = nearest_y - current_y # Stop just before it elif move_y < 0: # Are we moving towards a wall below us? next_point = (nearest_x, nearest_y-1) if maze.object_at(next_point).is_a_wall(): if current_y+move_y < nearest_y: # Are we close enough? move_y = nearest_y - current_y # Stop just before it if move_x > self.speed: # Don't move further than our speed allows move_x = self.speed elif move_x < -self.speed: move_x = -self.speed if move_y > self.speed: move_y = self.speed elif move_y < -self.speed: move_y = -self.speed return (move_x, move_y) Next comes the ``nearest_grid_point`` method we promised. It takes the current coordinates and returns the nearest grid point. That is, it returns the nearest point which has whole numbers (or integers) as its coordinates. The way to find the nearest whole number to a number ``x`` is to say ``int(x+0.5)``. Armed with this knowledge our job is quite easy. .. sourcecode:: python class Movable: # This should already be in your program. # ... # Other definitions will be here. def nearest_grid_point(self): (current_x, current_y) = self.place grid_x = int(current_x + 0.5) # Find nearest vertical grid line grid_y = int(current_y + 0.5) # Find nearest horizontal grid line return (grid_x, grid_y) # Return where they cross Now we need to tell the computer how to draw Chomp on the screen. So far the only method involved is the ``move_by`` method. This will move Chomp on the screen, but so far there isn't anything to put Chomp on the screen in the first place. We'll write a method called ``draw`` to do this. .. sourcecode:: python CHOMP_COLOR = color.YELLOW # Put these at top of your program CHOMP_SIZE = GRID_SIZE * 0.8 # How big to make Chomp CHOMP_SPEED = 0.25 # How fast Chomp moves # ... # Other classes will be here class Chomp(Movable): # This should already be in your program def __init__(self, maze, point): self.direction = 0 # Start off facing right Movable.__init__(self, maze, point, # Call Movable initializer CHOMP_SPEED) # ... # Other definitions will be here def draw(self): maze = self.maze screen_point = maze.to_screen(self.place) angle = self.get_angle() # Work out half of mouth angle endpoints = (self.direction + angle, # Rotate according to direction self.direction + 360 - angle) self.body = Arc(screen_point, CHOMP_SIZE, # Draw sector endpoints[0], endpoints[1], filled=True, color=CHOMP_COLOR) def get_angle(self): (x, y) = self.place # Work out distance (nearest_x, nearest_y) = (self.nearest_grid_point()) # to nearest grid point distance = (abs(x-nearest_x) + abs(y-nearest_y)) # Between -1/2 and 1/2 return 1 + 90*distance # Between 1 and 46 The ``abs`` function takes a number and returns its *absolute* size. If the number is positive it just returns the number. If a number ``n`` is negative, ``abs(n)`` is the same as ``-n``. The ``move_by`` method is a little tricky. It works out where Chomp is moving to, draws a new body on the screen and then removes the old body. Doing things this way means that when we create the new body, we can make it a different shape from the old one --- the mouth is wider, for example. It also means that there is always a body on the screen so you don't catch a glimpse of the background when the old body is removed. .. sourcecode:: python class Chomp(Movable): # This should already be in your program # ... # Other definitions will be here def move_by(self, move): self.update_position(move) old_body = self.body # Get old body for removal self.draw() # Make new body remove_from_screen(old_body) # Remove old body def update_position(self, move): (old_x, old_y) = self.place # Get old coordinates (move_x, move_y) = move # Unpack vector (new_x, new_y) = (old_x+move_x, old_y+move_y) # Get new coordinates self.place = (new_x, new_y) # Update coordinates if move_x > 0: # If we're moving right ... self.direction = 0 # ... turn to face right. elif move_y > 0: # If we're moving up ... self.direction = 90 # ... turn to face up. elif move_x < 0: # If we're moving left ... self.direction = 180 # ... turn to face left. elif move_y < 0: # If we're moving down ... self.direction = 270 # ... turn to face down. As you can see we've also defined a method called ``update_position``, which updates the coordinates and points ``Chomp`` in the direction in which he's moving. Plumbing it in -------------- Next we need to arrange for ``Chomp``'s ``move`` method to be called on a regular basis. In fact all the Ghosts will want this as well, so there are lots of objects which want their ``move`` methods called. We'll keep these objects in a list called ``movables`` in the ``Maze`` object, so that the ``Maze`` can call them in its ``play`` method. First we'll change the ``make_object`` method in the ``Maze`` class so that when it finds a letter P in the layout, it creates a Chomp object. .. sourcecode:: python class Maze: # This should already be in your program # ... # Other definitions will be here def make_object(self, point, character): (x, y) = point if character == '%': # Is it a wall? self.map[y][x] = Wall(self, point) elif character == 'P': # Is it Chomp? chomp = Chomp(self, point) self.movables.append(chomp) # ... # Other definitions will be here We also need to change the ``set_layout`` method in the ``Maze`` class to create a list of movables, so that ``make_object`` can put ``Movable`` objects in it. This goes in ``set_layout`` because whenever we want to change the layout, we want to start off with a clean sheet and not have Ghosts hanging about from the previous one. When all of the objects have been created it also goes through the list and asks each one to draw itself on the screen. This happens after the stationary objects have been drawn, so that the Ghosts appear on top of all the ``Food``, not on top of the ``Food`` which was created before them and below the ``Food`` which was created afterwards. .. sourcecode:: python class Maze: # This should already be in your program # ... # Other definitions will be here def set_layout(self, layout): height = len(layout) # This is as before width = len(layout[0]) self.make_window(width, height) self.make_map(width, height) self.movables = [] # Start with no Movables max_y = height - 1 for x in range(width): # Make objects as before for y in range(height): char = layout[max_y - y][x] self.make_object((x, y), char) for movable in self.movables: # Draw all movables movable.draw() # ... # Other definitions will be here def done(self): end_graphics() # We've finished self.map = [] # Forget all stationary objects self.movables = [] # Forget all moving objects # ... # Other definitions will be here Now we can change the ``Maze``'s ``play`` method to call the ``move`` method on each of the movables and then wait a short period of time. .. sourcecode:: python class Maze: # This should already be in your program # ... # Other definitions will be here def play(self): for movable in self.movables: # Move each object movable.move() update_when('next_tick') # Pause before next loop You can try your program again and you will see something like this. .. image:: ../illustrations/chomp_2.png :alt: Chomp illustration 2 Guide Chomp round the maze. Try moving through walls. Does it feel natural? Food, glorious food! ==================== The life-cycle of a ``Food`` object is as follows. It gets created and draws itself on the screen. The ``Maze`` puts the ``Food`` object in its map, so that when ``Chomp`` arrives at that grid point the food can be consumed. When this happens, ``Chomp`` tells the ``Food`` that it's been eaten and the ``Food`` object removes its image from the screen. It then tells the ``Maze`` to remove it from the map. When all the ``Food`` has been eaten, ``Chomp`` has completed his mission. The simplest way of finding out when this happens if for the ``Maze`` to keep a count of the number of ``Food`` objects there are. We'll start by modifying ``make_object`` to create ``Food``. It needs to keep a count of the number of ``Food`` objects, so we'll have to start with a count of zero. Again, this goes in the ``set_layout`` method because if we change layout, we will remove all the ``Food`` objects first. .. sourcecode:: python class Maze: # This should already be in your program # ... # Other definitions will be here def set_layout(self, layout): height = len(layout) # This is as before width = len(layout[0]) self.make_window(width, height) self.make_map(width, height) self.movables = [] self.food_count = 0 # Start with no Food max_y = height - 1 # Make the objects as before for x in range(width): for y in range(height): char = layout[max_y - y][x] self.make_object((x, y), char) for movable in self.movables: movable.draw() Then every time a ``Food`` object is created we need to add 1 to the count. .. sourcecode:: python class Maze: # This should already be in your program # ... # Other definitions will be here def make_object(self, point, character): (x, y) = point # As before... if character == '%': self.map[y][x] = Wall(self, point) elif character == 'P': chomp = Chomp(self, point) self.movables.append(chomp) elif character == '.': self.food_count = self.food_count + 1 # Add 1 to count self.map[y][x] = Food(self, point) # Put new object in map Now we can write the Food class. We'll start by defining what happens when it's created: It gets drawn on the screen. .. sourcecode:: python class Food(Immovable): def __init__(self, maze, point): self.place = point self.screen_point = maze.to_screen(point) self.maze = maze self.draw() We need to define the ``draw`` method to actually draw the food on the screen. .. sourcecode:: python FOOD_COLOR = color.RED # Put this at the top of your program FOOD_SIZE = GRID_SIZE * 0.15 # How big to make food # ... # Other definitions will be here class Food(Immovable): # This should already be in your program # ... # Other definitions will be here def draw(self): self.dot = Circle(self.screen_point, FOOD_SIZE, color=FOOD_COLOR, filled=True) Now, we need to arrange for the food to get eaten at the right time. The way we will do this is to make ``Chomp`` call a method called ``eat`` on the ``Immovable`` at a grid point when he gets close enough. It is then up to the object to do the right thing. We'll put a default method in the ``Immovable`` class which does nothing, so that nothing happens when ``Chomp`` tries to eat empty space. This is just telling Python that we really want nothing to happen and haven't forgotten about what should happen. We'll then override that method in the ``Food`` class. It may be useful to know who's doing the eating, so we will pass Chomp as a parameter. .. sourcecode:: python class Immovable: # This should already be in your program # ... # Other definitions will be here def eat(self, chomp): # Default eat method pass # Do nothing In the ``Food`` class we want this method to remove the ``Food`` from the screen and the ``Maze``. To tell the ``Maze`` that some food needs removing, we'll invent a method called ``remove_food``. .. sourcecode:: python class Food(Immovable): # This should already be in your program # ... # Other definitions will be here def eat(self, chomp): remove_from_screen(self.dot) # Remove dot from screen self.maze.remove_food(self.place) # Tell Maze We now need to write the ``remove_food`` method in the ``Maze`` class. When there is no food left, Chomp wins. .. sourcecode:: python class Maze: # This should already be in your program # ... # Other definitions will be here def remove_food(self, place): (x, y) = place self.map[y][x] = Nothing() # Make map entry empty self.food_count = self.food_count - 1 # There is 1 less bit of Food if self.food_count == 0: # If there is no food left... self.win() #... Chomp wins def win(self): print "You win!" self.game_over = True So far, nothing is going to call the ``eat`` method. We need ``Chomp`` to do this, when he gets close enough. We'll do this in the ``move_by`` method. For this purpose close enough means 3/4 of the distance ``Chomp`` moves each time round the main loop, on either side of the grid point. .. sourcecode:: python class Chomp(Movable): # This should already be in your program # ... # Other definitions will be here def move_by(self, move): self.update_position(move) # As before... old_body = self.body self.draw() remove_from_screen(old_body) (x, y) = self.place # Get distance to nearest_point = self.nearest_grid_point() # nearest grid point (nearest_x, nearest_y) = nearest_point distance = (abs(x-nearest_x) + # As before abs(y-nearest_y)) if distance < self.speed * 3/4: # Are we close enough to eat? object = self.maze.object_at(nearest_point) object.eat(self) # If so, eat it If you try the program now, you'll see that the game is starting to take shape. .. image:: ../illustrations/chomp_3.png :alt: Chomp illustration 3 There's no challenge to it yet. That comes next. Ghosts ------ A ``Ghost`` is another ``Movable`` object. The first thing we'll do is add it to ``Maze``'s ``make_object`` method. .. sourcecode:: python class Maze: # This should already be in your program # ... # Other definitions will be here def make_object(self, point, character): # You should also have this # ... # Checks for other characters go here as before elif character == 'G': # Is it a Ghost? ghost = Ghost(self, point) # Make new Ghost self.movables.append(ghost) # Add it to list of Movables The Ghosts need to wander through the maze at random. There are many ways to do this. The way we will do it is for each ``Ghost`` to choose a neighboring grid point and move towards it. When it reaches that point, it will make another choice. It may take several calls of the ``move`` method to reach the next grid point, so the ``Ghost`` will have to remember which point it decided to move towards. We'll call this ``next_point``. Our choice of next point will depend on the direction the ``Ghost`` was travelling, so we will need to store that as well. Now we'll start the ``Ghost`` class itself. The first thing we need is an initializer. It takes the ``Maze`` and the starting grid point as an argument, and it will need to call the initializer in the ``Movable``. We sneakily set the ``next_point`` to be the starting point, so that the ``Ghost`` will immediately choose to go somewhere else. The initializer also picks a color from the list of ghost colors and sets the color variable in the ``Ghost`` object. When the ghost is draw, it will get this color. .. sourcecode:: python GHOST_COLORS = [color.RED, # A list of all Ghost colors color.GREEN, # Put this at the top with the color.BLUE, # other constants color.PURPLE] GHOST_SPEED = 0.25 # How fast Ghosts move class Ghost(Movable): num = 0 def __init__(self, maze, start): Ghost.num += 1 # Tell Python to make Ghost.num # variable bigger by 1 self.next_point = start # Don't move anywhere to start with self.movement = (0, 0) # We were going nowhere self.color = GHOST_COLORS[Ghost.num % 4] # Pick a color from list Movable.__init__(self, maze, # Just call Movable's initializer start, GHOST_SPEED) The next thing that's going to happen is that the ``Maze`` will call the ``Ghost``'s ``draw`` method, so we need to write that. We need to tell the computer what a ghost looks like. We do this by giving a list of coordinates of points for the computer to join up. .. sourcecode:: python GHOST_SHAPE = [ # Place at top with other constants (0, -0.5), (0.25, -0.75), # Coordinates which define Ghost's shape (0.5, -0.5), # measured grid units (0.75, -0.75), (0.75, 0.5), (0.5, 0.75), (-0.5, 0.75), (-0.75, 0.5), (-0.75, -0.75), (-0.5, -0.5), (-0.25, -0.75 ) ] class Ghost(Movable): # This should already be in your program # ... # Other definitions will be here def draw(self): maze = self.maze (screen_x, screen_y) = ( maze.to_screen(self.place)) # Get our screen coordinates coords = [] # Build up a list of coordinates for (x, y) in GHOST_SHAPE: coords.append((x*GRID_SIZE + screen_x, y*GRID_SIZE + screen_y)) self.body = Polygon(coords, color=self.color, # Draw body filled=True) Making them move ---------------- Next we need to make the Ghosts move. To do this, we first try to move towards ``next_point``, and if we're getting nowhere, choose another point to move towards. .. sourcecode:: python class Ghost(Movable): # This should already be in your program # ... # Other definitions will be here def move(self): (current_x, current_y) = self.place # Get vector to next point (next_x, next_y) = self.next_point move = (next_x - current_x, next_y - current_y) move = self.furthest_move(move) # See how far we can go if move == (0, 0): # If we're getting nowhere... move = self.choose_move() #... try another direction self.move_by(move) # Make our move Now we need to decide how to choose the next grid point to move towards. If we allow the ``Ghost`` to reverse its direction too easily, it usually won't get very far. For this reason, we'll check all the directions we could move in which wouldn't cause us to switch into reverse, and then choose one at random. Only as a last resort do we consider turning round. For this we define a method called ``can_move_by`` which returns true if the given move is possible. As we saw back in :ref:`turning_the_tables`, to get a random number from Python, we need to include: .. sourcecode:: python from random import randint at the top of our program. We can then use ``randint`` for our random number. .. sourcecode:: python class Ghost(Movable): # This should already be in your program # ... # Other definitions will be here def choose_move(self): (move_x, move_y) = self.movement # Direction we were going (nearest_x, nearest_y) = (self.nearest_grid_point()) possible_moves = [] if move_x >= 0 and self.can_move_by((1, 0)): # Can we move right? possible_moves.append((1, 0)) if move_x <= 0 and self.can_move_by((-1, 0)): # Can we move left? possible_moves.append((-1, 0)) if move_y >= 0 and self.can_move_by((0, 1)): # Can we move up? possible_moves.append((0, 1)) if move_y <= 0 and self.can_move_by((0, -1)): # Can we move down? possible_moves.append((0, -1)) if len(possible_moves) != 0: # Is there anywhere to go? choice = randint(0,len(possible_moves)-1) # Pick random direction move = possible_moves[choice] (move_x, move_y) = move else: move_x = -move_x # Turn round as last resort move_y = -move_y move = (move_x, move_y) (x, y) = self.place self.next_point = (x+move_x, y+move_y) # Set next point self.movement = move # Store this move for next time return self.furthest_move(move) # Return move def can_move_by(self, move): move = self.furthest_move(move) # How far can we move in this direction? return move != (0, 0) # Can we actually go anywhere? We now need to define the ``move_by`` method which is called by the ``move`` method. This just takes a movement vector as a parameter, and moves the ``Ghost`` that far. .. sourcecode:: python class Ghost(Movable): # This should already be in your program # ... # Other definitions will be here def move_by(self, move): (old_x, old_y) = self.place # Get old coordinates (move_x, move_y) = move # Unpack vector (new_x, new_y) = (old_x+move_x, old_y+move_y) # Get new coordinates self.place = (new_x, new_y) # Update coordinates screen_move = (move_x * GRID_SIZE, move_y * GRID_SIZE) move_by(self.body, screen_move[0],screen_move[1]) # Move body on the screen You can now play the game! .. image:: ../illustrations/chomp_4.png :alt: Chomp illustration 4 OK, so the computer doesn't notice if you hit a Ghost and there are no Capsules yet, but you can still have fun trying to avoid the ghosts. Collision detection. -------------------- We now need to find out when ``Chomp`` bumps into a ``Ghost``. The way we will do this is for ``Chomp`` to tell the ``Maze`` where he is and for the ``Maze`` to pass this on to all the ``Movable``\ s. It is then up to the object to do the checking. If ``Chomp`` stands still, we still need to do the checking, so we need to do it in the ``move`` method. .. sourcecode:: python class Chomp(Movable): # This should already be in your program # ... # Other definitions will be here def move(self): keys = keys_pressed() # Check keys as before if 'left' in keys: self.move_left() elif 'right' in keys: self.move_right() elif 'up' in keys: self.move_up() elif 'down' in keys: self.move_down() self.maze.chomp_is(self, self.place) # Tell Maze where we are The ``Maze`` simply passes this on to each ``Movable``. .. sourcecode:: python class Maze: # This should already be in your program # ... # Other definitions will be here def chomp_is(self, chomp, point): for movable in self.movables: # Go through Movables movable.chomp_is(chomp, point) # Pass message on to each Because ``Chomp`` is a ``Movable``, he will also get the message. We don't want to know if ``Chomp`` collides with himself --- whatever that means --- so we just ignore that. .. sourcecode:: python class Chomp(Movable): # This should already be in your program # ... # Other definitions will be here def chomp_is(self, chomp, point): pass # Chomp knows where Chomp is We do want to know when ``Chomp`` bumps into a ``Ghost`` though. We'll write a simple but adequate detection method. We'll say that ``Chomp`` and the ``Ghost`` have collided when their centres are less than 1.6 grid units apart. Since ``Chomp`` is 0.8 grid units from his middle to his outer, and so is the ``Ghost``, if they are placed side by side their centre will be 1.6 grid units apart. ``Chomp`` can sometime overlap the ``Ghost`` a little before it triggers, but we could put that down to a lucky escape. We'll invent a method called ``bump_into`` which we'll call when the ``Ghost`` bumps into ``Chomp``. Before writing the method, we need to do a little maths. The question is: how do we know when the two centre points are 1.6 grid units apart? This was answered by a man called Pythagoras a few thousand years ago. He said that if the distance between two points horizontally is ``X``, the distance vertically is ``Y``, and the distance between the points is ``D`` then ``X*X + Y*Y = D*D`` (or words to that effect in ancient Greek). If the distance ``D`` is less than 1.6 grid units then ``D*D`` will be less than ``1.6*1.6`` grid units squared, but according to Pythagoras ``D*D`` is the same as ``X*X + Y*Y`` which we can work out. .. sourcecode:: python class Ghost(Movable): # This should already be in your program # ... # Other definitions will be here def chomp_is(self, chomp, point): (my_x, my_y) = self.place (his_x, his_y) = point X = my_x - his_x Y = my_y - his_y DxD = X*X + Y*Y limit = 1.6*1.6 if DxD < limit: self.bump_into(chomp) Now we need to decide what happens when the ``Ghost`` and ``Chomp`` collide. For the moment, we just want ``Chomp`` to lose, so we'll call the ``lose`` method on the ``Maze``. .. sourcecode:: python class Maze: # This should already be in your program # ... # Other definitions will be here def lose(self): print "You lose!" self.game_over = True # ... # Other classes will be here class Ghost(Movable): # This should already be in your program # ... # Other definitions will be here def bump_into(self, chomp): self.maze.lose() Try the new program out. Try out bumping into Ghosts, to check that the collision detection works. Capsules ======== The next thing we're going to do is add the capsules which allow Chomp to capture the ghosts. These are going to be objects which sit in the ``Maze`` until ``Chomp`` eats them, just like the ``Food``. The first thing we'll do is change the ``make_object`` method in the ``Maze`` class to recognise the letter o in the layout and create a Capsule object when it finds one. .. sourcecode:: python class Maze: # This should already be in your program # ... # Other definitions will be here def make_object(self, point, character): # You should also have this # ... # Checks for other characters go here as before elif character == 'o': # Is it a Capsule? self.map[y][x] = Capsule(self, point) # Put new Capsule in map Now we'll need to write the ``Capsule`` class. To start with this will look a bit like the ``Food`` class. First we need to tell the computer how to initialize a ``Capsule`` object. .. sourcecode:: python class Capsule(Immovable): def __init__(self, maze, point): self.place = point self.screen_point = maze.to_screen(point) self.maze = maze self.draw() Now we need to describe how to draw it on the screen. For this we'll just draw a circle on the screen. .. sourcecode:: python CAPSULE_COLOR = color.WHITE # Put these at top of your program CAPSULE_SIZE = GRID_SIZE * 0.3 # How big to make capsules class Capsule(Immovable): # This should already be in your program # ... # Other definitions will be here def draw(self): (screen_x, screen_y) = self.screen_point self.dot = Circle((screen_x, screen_y), CAPSULE_SIZE, color=CAPSULE_COLOR, filled=True) If you try the game now, you'll see the capsules on the screen, but ``Chomp`` just ignores them when he walks over them. What's happening here? ``Chomp`` is calling the ``eat`` method on the ``Capsule`` and Python is finding the ``eat`` method in the ``Immovable`` class, so nothing happens. To make ``Chomp`` eat the ``Capsule``, we need to write an ``eat`` method in the ``Capsule`` class telling the computer how ``Chomp`` should eat capsules. .. sourcecode:: python class Capsule(Immovable): # This should already be in your program # ... # Other definitions will be here def eat(self, chomp): remove_from_screen(self.dot) # Remove dot from screen self.maze.remove_capsule(self.place) # Tell Maze to scare ghosts The ``eat`` method works in a similar way to the ``eat`` method in ``Food`` class. The difference is that instead of calling the ``remove_food`` method on the ``Maze`` object, we call the ``remove_capsule`` method. This method will need to remove the ``Capsule`` from the map and tell all the ghosts to turn white. .. sourcecode:: python class Maze: # This should already be in your program # ... # Other definitions will be here def remove_capsule(self, place): (x, y) = place self.map[y][x] = Nothing() # Make map entry empty for movable in self.movables: # Tell all Movables that a capsule movable.capsule_eaten() # has been eaten We don't have a list of all the ``Ghost`` objects anywhere, but we do have a list of all the ``Movable`` objects. We simply tell all the movables that a capsule has been eaten. Only some of the movables (the ghosts) will want to know, so we'll write a default method in the ``Movable`` class to do nothing. Anything that doesn't want to know about capsules being eaten (Chomp) won't need to do anything special. .. sourcecode:: python class Movable: # This should already be in your program # ... # Other definitions will be here def capsule_eaten(self): # Called when a Capsule has been eaten pass # Normally, do nothing You may think that we could have written this in the ``Chomp`` class, and that is true. That would mean that if we invented another type of ``Movable`` object, we would have to write a ``capsule_eaten`` method for it even if it didn't want to know capsules being eaten. Now, we need to override the ``capsule_eaten`` method in the ``Ghost`` class. This is going to make the ``Ghost`` change color for a while. We can do this by changing the ``color`` variable. After a while the ghost will need to change back to its original color, so we will need to know two things. We will need to know how long we have left before the original color returns, and also what the original color was. We will store these two values in variables called ``time_left`` and ``original_color``. When the ghost isn't scared, we will store the value ``0`` in ``time_left``, so that we know. We need to set the values of both of these variables in the initializer. The ``change_color`` method will remove the old body from the screen, set the ``color`` variable and draw a new body. We'll invent a ``redraw`` method to remove the old body. .. sourcecode:: python # Put these at the top of your program SCARED_COLOR = color.WHITE # Color ghosts turn when Chomp eats a capsule SCARED_TIME = 300 # How long Ghosts stay scared class Ghost(Movable): # This should already be in your program num = 0 def __init__(self, maze, start): # As should this Ghost.num += 1 self.next_point = start self.movement = (0, 0) self.color = GHOST_COLORS[Ghost.num % 4] self.original_color = self.color # Store original color self.time_left = 0 # We're not scared yet Movable.__init__(self, maze, # As before start, ghost_speed) # ... # Other definitions will be here def capsule_eaten(self): self.change_color(SCARED_COLOR) # Change to scared color self.time_left = SCARED_TIME def change_color(self, new_color): self.color = new_color # Change color self.redraw() # Recreate the body Here, we've invented a ``change_color`` method to change the ghost's color. By setting the ``time_left`` variable, we'll remember that we're scared and that we should change the color back later. We now need to define the ``redraw`` method. .. sourcecode:: python class Ghost(Movable): # This should already be in your program # ... # Other definitions will be here def redraw(self): old_body = self.body self.draw() remove_from_screen(old_body) As we did in the ``move_by`` method in the ``Chomp`` class, we first draw a new body and then remove the old body to avoid catching a glimpse of the background. At this stage we have managed to change the colors of the Ghosts. We now need to make them change back again after a while. Just before they are about to change to their original color, they need to flicker between white and their original color. We will update the ``time_left`` variable within the ``move`` method of the ``Ghost`` class. .. sourcecode:: python WARNING_TIME = 50 # Put this at the top of your program # ... # Other classes will be here class Ghost(Movable): # This should already be in your program # ... # Other definitions will be here def move(self): (current_x, current_y) = self.place # As before (next_x, next_y) = self.next_point move = (next_x - current_x, next_y - current_y) move = self.furthest_move(move) if move == (0, 0): move = self.choose_move() self.move_by(move) if self.time_left > 0: # Are we scared? self.update_scared() # Update time and color def update_scared(self): self.time_left = self.time_left - 1 # Decrease time left time_left = self.time_left if time_left < WARNING_TIME: # Are we flashing? if time_left % 2 == 0: # Is ``time_left`` even? color = self.original_color # Return to our old color else: color = SCARED_COLOR # Go the scared color self.change_color(color) # Actually change color # ... # Other definitions will be here If ``time_left`` is zero, we do nothing special. Otherwise, every time ``move`` gets called we subtract one from ``time_left``. When ``time_left`` drops below ``warning`` time, we start flashing the ghost. When this is happening, we change the color of the ghost on every call to ``move``. When ``time_left`` is even, we return to our original color, and when it's odd we change to the scared color. A new thing here is the use of ``n % 2`` to check if a number is even. The ``%`` means the remainder when divided by . All even numbers have no remainder when divided by 2 and all odd numbers have a remainder of 1 when divided by 2. When ``time_left`` reaches zero, the ghost will return to its original color because zero is even. The last thing we need to do is to allow Chomp to capture the ghost when ``time_left`` is zero. In other words we need to change what happens when we ``bump_into`` Chomp. .. sourcecode:: python class Ghost(Movable): # This should already be in your program # ... # Other definitions will be here def bump_into(self, chomp): # This should also be in your program if self.time_left != 0: # Are we scared? self.captured(chomp) # We've been captured else: self.maze.lose() # Otherwise we lose as before We have invented a method called ``captured`` which we call when we're scared and we bump into Chomp. This should send the ``Ghost`` back to its starting position. If follows that we need to know what the starting position is. .. sourcecode:: python class Movable: # This should already be in your program def __init__(self, maze, point, speed): self.maze = maze # As before self.place = point self.speed = speed self.start = point # Our starting position # ... # Other definitions will be here We're now ready to write the ``captured`` method. .. sourcecode:: python class Ghost(Movable): # This should already be in your program # ... # Other definitions will be here def captured(self, chomp): self.place = self.start # Return to our original place... self.color = self.original_color #... and color self.time_left = 0 # We're not scared self.redraw() # Update screen The basic Chomp game is now finished! .. image:: ../illustrations/chomp_6.png :alt: Chomp illustration 6 Try it out and see if you can find any problems with it. Further improvements -------------------- We've got a working game, but there are improvements which can be made to it. * Make Chomp look nicer. * Make the Ghosts look nicer. * Keep track of the score. * Keep the time left for the level. * Allow Chomp 3 lives. * Do something more interesting when Chomp wins or loses. * Add more levels. * Make the Ghosts smarter.