Mouser:
A Simple Game of Cat & Mouse

By Terry Hancock

A tutorial for new programmers with Python and PyGame. I wrote this originally for Python Magazine, and it was serialized in three parts over the Summer of 2008. This version has been edited into a single tutorial in three parts, with all of the example code included for you to examine directly.

Contents

Part 1: The Simplest Thing that Can Possibly Work
What You Need to Get Started; Your First Program; Save your work!; Smoothing the motion; Staying in bounds.
Part 2: Adding Clarity and Structure
The Refactor and Extend Cycle; Dictionaries; Turning the Mouse; Functions Keyboard Controls; Classes & Class Instances; Summary;
Part 3: A Simple Game of Cat & Mouse
Background; Timing; Adding Sound; Catching the Mouse; The Scoreboard; Some Ideas;

[TOP]

Part 1: The Simplest Thing that Can Possibly Work

When I first learned to program, it was pretty pictures and games that motivated me to press on. Bearing that in mind, this is a tutorial for the impatient (tested on preteens!). You'll get something fun working on the screen in just a few minutes. From there, you'll refine and learn some programming concepts to make it better.

The goal of this tutorial is to teach Python with visually-interesting examples. You will also get a quick start with PyGame, though this isn't really a PyGame tutorial. In this lesson, we'll be starting with a very plain "script" that opens a graphical window, draws a "sprite" image, and moves it around randomly. The point here is to "keep it simple". Later lessons will expand on that, to create a full-fledged game.


[TOP]

What You Need to Get Started

You will, of course, need Python and PyGame. These are included in all of the major GNU/Linux distributions, so you will simply need to use your package installation system to install them. Users of Windows and other operating systems can find installable binaries (and source) at http://www.python.org/download/ and http://www.pygame.org/download.shtml. PyGame is based on the SDL library which supports just about any platform you are likely to be using. The SDL library will typically be installed by the PyGame package or handled by your package manager, so you won't have to worry about it.

You will also need a text editor, a terminal emulator, and a web browser that you are comfortable with. I use Vim, but Emacs, or even Windows Notepad will do. Really nice editors will have an option to automatically colorize your Python source code to make it easier to read and find mistakes, but you can get by with plain text. Just make sure you aren't using a word processor that will stick unwanted formatting codes into your file.

We'll use the terminal emulator to run the Python interactive interpreter, which is a great way to test code as you go. You can use the web browser to keep up with the documentation for Python and PyGame in order to understand the libraries we are using.


[TOP]

Your First Program

Enough chit-chat. Let's start Python (you almost certainly do this by entering the command python in the terminal, though some systems will let you start it with a mouse click). Once the interactive interpreter is started, you will see the python prompt: >>>, where you can type in Python commands to be run immediately. First, you should check that PyGame is installed:

The import statement is used to access library modules. It is this command that allows us to access PyGame functions later on.

If no error message appears, then you're good to go (if it does, you need to go back and check your PyGame installation). You should then import random in the same way (this is one of Python's many standard libraries, so you don't need to install it—look up the random module in Python's "Library Reference" for more information on what it does).

We need two more lines to get PyGame ready to use:

This import statement is a little different. Instead of loading the module as a unit, it loads all of the contents of the module into the "namespace" of our program, which means we can call them without having to prepend "pygame." onto everything. Normally, this is undesireable as it clutters up your program, but the locals module from PyGame contains constants that are convenient to use directly (which we'll need later on). The next line calls a function init() that PyGame requires us to call before it can work.

With PyGame started, we can now call up a PyGame window from the interpreter:

The screen is now a python "object" which we'll use to refer to the PyGame window (and get some properties from it). At the same time, of course, you should have seen an empty window pop up. What we're calling here is a function set_mode in the module display which is contained in the pygame module. A function is just a reusable piece of code, in this case provided by the pygame module. See Figure 1 for more about how functions work.

Figure 1: Functions

Functions and calls (diagram)

We've called it with a single argument, (500,300), to tell it how large a window to make. This is an example of a Python "tuple", which is just a group of objects separated by commas, usually within parentheses. We'll typically use tuples of two numbers to represent coordinates.

That's why we have double parentheses: the inner set defines the tuple, the outer one is the function call. This is different from calling a function with two arguments (in which case, there'd be only one set of parentheses).

Next, we'll load a graphic "sprite" to animate on the screen. A "sprite" is just a small picture used in games. We want the graphic to be transparent, so we also set a "colorkey", which is to say, a color that will be treated as transparent (the color is represented by a red-green-blue tuple, which in this case is just white).

Afterwards, we'll get the width and height of both the screen and the sprite we loaded. The method, get_size(), returns another tuple of two numbers which we can directly assign to two numbers (this technique, which is common in Python, is called "tuple unpacking"). A "method" is a special kind of function that is attached to an object.

Figure 2: Geometry of Sprite Positioning

Functions and calls (diagram)

We'll need these numbers to figure out the offsets from the centers to the upper left corner of each object, which is the origin of the coordinate grid.

After that, we just figure out where the upper left corner of the sprite needs to be if we want it to appear in the center of the window. See Figure 2 for an explanation of the geometry we are using here. Whenever we assign locations to place "Surfaces" (images stored in memory) in PyGame, we're always setting the upper left corner.

Next we're going to make a "list" of four "tuples", each representing a direction (up or "N", right or "E", left or "W", and down or "S"):

The directional moves may seem a little funny to you if you are used to regular graph coordinates as used in math class. That's because in computer graphics, the origin is in the upper left corner of the screen or window. Positive "x" values increase to the right, and positive "y" values increase towards the bottom of the screen. So, going up ("N"), means decreasing the value of the "y" coordinate. Each direction tuple is given a "name" via the "=" assignment operator so we can refer back to them later.

The last line above gathers these tuples up into a Python "list", which is just an ordered collection of objects and is represented by square brackets "[]".

Now we'll start defining the "game loop". First of all, we use a common Python idiom for an infinite loop, which is while True:. Normally, the while statement will loop until its expression is no longer true. In this case, we want it to loop forever (or until we explicitly break out of it), so we just set the expression to True.

Later on, we'll provide better ways to stop the program, but in the meantime, you can use "Ctrl-C" to interrupt it. Be aware that this will only work when the terminal window has focus (is selected with the mouse). If you use "Ctrl-C" on the PyGame graphical window, nothing will happen.

Inside the loop, we need to do two basic things: first we have to compute the new location for the sprite, and second we have to call PyGame functions to put the sprite in that place and update the display. For this very simple example, we're just going to use the function random.choice(), which picks a random object out of a Python list, to pick a random direction each time we pass through the loop. Then we'll update the (x,y) tuple with the new coordinates by moving one step in the selected direction.

We're going to use a very, very simple update method in this example, as well. We'll just repaint the window green, which will erase the old sprite position, no matter where it was. Then we'll place the sprite onto the screen surface in what is called a "blit" in computer graphics jargon ("blit" is an acronym derived from "BLock Image Transfer"). None of this actually draws anything on the screen, though. That happens when we finally call PyGame's update() function. The reason PyGame works this way is so that you can make a whole lot of changes without disturbing the display, and then flip the result onto the screen quickly, resulting in smoother animation.

Here's the final game loop. Note that the stuff inside the loop has to be indented to let Python know it's part of the same loop. Be careful to use consistent indentation: one of the more common errors for new Python users is to indent inconsistently, often because of mixing spaces and tabs (when in doubt, just avoid using tabs at all).

Note that you'll have to press "return" a couple of times to finish the loop and get back to the regular prompt. Once you do, though, your program will start to animate in the PyGame window. Congratulations! You now have a working PyGame program running.
[TOP]

Save your work!

Naturally, you'll want to paste all of this code into your chosen text editor and save it. A copy of this example is included in the source code package for this tutorial as mouse0.py (you'll notice the file also has "comments" added, using the "#" sign. Those lines will be ignored by Python and are just included for your benefit).

mouse0.py

Mouse sprite jiggles randomly in PyGame window

Click on the image to see a screencap of this program in action. The heading is also a link to the complete source file so you can open it in your favorite text editor (all the listings in this tutorial will be presented in this format). Advanced text editors like emacs or vi will have special modes for viewing Python source code that will make it easier to understand by coloring the text according to its function in the program. Programmers use these modes to improve their efficiency and cut down on errors.

So far, we've just been typing into the interpreter application. But from now on, we'll make modifications and then save the program and run it from the terminal emulator. Assuming you named your file "mouse0.py" as I did, you can run the program like this:


[TOP]

Smoothing the motion

Now it's not a very smart program yet. The "mouse" probably seems to be violently and randomly shaking around on screen. That's because we're not letting it move smoothly in any direction for more than one pixel. Instead, we change direction on every loop iteration.

Our first update to the program, therefore, will be to add a counter to wait a hundred steps before updating the direction. We'll use a "conditional expression" (Python's "if-elif-else" statement) to figure out when the counter has counted high enough, and then put our random direction selection in that part of the code. That way, on most passes the (x,y) position will be updated, but the direction will stay the same. This will produce a much smoother motion.

Here's what the new loop will look like:

The variable i is the "counter". We use it to count to a hundred (once per loop iteration), and then get a new direction. We set it to go off immediately so that the loop will pick a direction at the beginning (otherwise, we'd get an error message telling us that dx and dy aren't defined!). I've added one more change as well: Python allows us to use a special "increment" operator += when we want to assign a new value to a variable by adding something to the old value. We use it here to increment i, and we also replace x = x + dx with x += dx, which means the same thing and avoids repetition.

mouse1.py

Change direction only every 100 loops ("more purposeful motion")

If you run this program (mouse1.py in the source package), you'll see a more satisfying display: the mouse now moves more purposefully across the screen, changing directions only every 100 pixels. The speed depends on your CPU and your graphics card. It may be quite slow or insanely fast, depending on your hardware.


[TOP]

Staying in bounds

If you let the example program run for just a bit, you'll see something definitely wrong: the mouse eventually drifts out of the window, never to be seen again! Fortunately, we can use another conditional statement block to fix this problem. We'll check for x getting too small (less than 0, which means off the left side of the window), or too big (closer than the width of the sprite to the right side of the window, which means subtracting the sprite width from the screen width). And of course, we'll do the same tests to y. When triggered, we'll use random.choice() again, but this time, just pick from directions leading away from the wall. To make things a little more interesting, I've also added the four diagonal directions to this version, so we now have eight total directions to choose from (see Figure 4).

Figure 4: Geometry Used in Boundary Checking

Functions and calls (diagram)

Finally, we can insert a timer call into the loop in order to reduce the speed to a reasonable rate on fast hardware. This will simply wait for five milliseconds:

This leads to our first simple PyGame program, seen in the listing mouse2.py.
LISTING 1:

mouse2.py

Mouse turns away from the edges of the window to avoid running off the screen. Also adds 5ms time delay to slow down the loop on fast hardware.


[TOP]

Part 2: Adding Clarity and Structure

Programming is more than just a straight set of instructions, but why? Now, we'll "refactor" the program to make use of programming structures which will make it easy to take the next steps in turning our script into a game.

When we left off, we had a single mouse sprite bouncing around on the screen randomly. There are several improvements that would be nice to make: for example, the mouse could face the direction it's moving in and it'd be a lot more fun to control the character from the keyboard.


[TOP]

The Refactor and Extend Cycle

Although we theoretically could make changes directly to the script we've written, the program would quickly become messy and unmanageable, because we haven't taken any real steps to organize the code. So, the first thing we'll do is to "refactor" the program: which means we'll make changes to its structure, without changing what it does. We'll use three Python features—"dictionaries", "functions", and "classes"—to achieve this.

After each refactoring, we'll extend the program's behavior, taking advantage of the design improvements. This two step approach is very useful, because the program can be tested at each stage to ensure that mistakes aren't made in the refactoring process. It's important not to try to skip steps, because if you make a mistake, it can be difficult to find it. Instead, try to test as frequently as you can, so that you will find errors as soon as you make them, while the code is still fresh in your mind.


[TOP]

Dictionaries

In the previous installment, we managed the directions by using simple symbolic names:

These are simply variables, and they all occupy the module "namespace"—which is to say, the collection of all variable names in the top-level of our program file (or "module"). Used together like this, it's pretty obvious what these variable names mean, but separately, N or E might be used to simply represent "a number" or "an endpoint" or some other mnemonic, and we'd wind up with a "name collision": a case where we accidentally assign two different meanings to the same name. This almost always creates a bug, and that's why we generally want to avoid crowded namespaces in programs.

Dictionaries provide a simple one-to-one relationship between different objects: one is the "key", and the other is the "value" (note the use of commas and colons in the dictionary literal below):

This is almost the same information as contained in the previous representation of the directions, but instead of eight new variables, we just have one dictionary called directions. The directions are now explicitly named with text "strings" (this will help when we need to extend the program to do more with the direction information).

These changes require additional ones: instead of assigning values in a list called directions, we index the dictionary called directions with the appropriate key. The result is mouse3.py. You can try running this program, and you'll see that it behaves exactly as the previous listing does: we've changed the structure of the program, but not its function.

mouse3.py

REFACTOR (use dictionaries) Use dictionaries for directions. Replaces ad hoc variables in mouse2.py.


[TOP]

Turning the Mouse

One advantage of using strings to represent the directions is that we can now use them to label image filenames containing different versions of the mouse pointing in each of the desired directions:

Now we have a new dictionary, sprites, which maps the same direction keys to image values instead of directions. We can also see basic usage rules of a dictionary:

This code retrieves images and associates them with each direction. So, for example, we load a west-facing mouse from the file resource/mouse_w_lg.bmp and associate it with the direction "w". The set_colorkey method call causes each image to also have white pixels (represented by the color tuple (255,255,255)) set as "transparent".

Now, we simply have to alter the loop to change which sprite image the "sprite" name is bound to, and that sprite image will be used to display the mouse, causing the mouse to face the direction of travel. This is the code in mouse4.py, which is much more aesthetically pleasing, as you can see by running it.

mouse4.py

EXTEND Now load images for each possible direction and change the sprite image according to the direction of motion.


[TOP]

Functions

The next refactoring is a little more significant. It's actually very odd to have so much code running directly in the module namespace. Most programs are constructed of functions or classes. Last month, we used functions from the modules we imported, but now we'll define our own.

As a first attempt, let's move the program loop code into its own function, which we'll simply call update. This code, in mouse5.py, is a partial success: it allows us to separate the concerns of managing the mouse sprite's position and appearance on screen from the mechanics of repainting the PyGame window and blitting the mouse image onto it.

mouse5.py

REFACTOR (functional) Use an 'update' function instead doing everything directly in the game loop.

However, this is somewhat unsatisfying, as we have to keep two global variables: our counter i and the sprites image dictionary (the global statement tells Python to use the variables from the module's scope instead of creating new variables inside the function). We pass the current position to the function, and it returns the new position and which sprite image to display.

By making the variables x, y, sprite, and direction local and returning the new values, we raise the possibility of reusing the same update function for a second mouse character. We simply have to define two positions, (xa, ya) and (xb, yb), and two directions, dir_a and dir_b, and then we can update and blit the two mice separately, without having to duplicate the contents of the update function:

Which is a big savings over repeating the entire function body separately for each, not to mention the confusion of having to keep track of the a and b variants all the way through—increasing the chance of making a programming error. This example is demonstrated in mouse5b.py in the source package, but we won't be building on that version.

mouse5b.py

EXTEND (branch) Two mice moving randomly.


[TOP]

Keyboard Controls

Now, we can introduce keyboard control of the mouse character, allowing the user to set the mouse's direction by pressing the arrows on the numeric keypad. To do this, we define another function which will scan PyGame's "event queue" and then update the mouse's direction accordingly.

Under the hood, capturing key presses or other input from the user is pretty tricky, because it can occur at any time. When it does, the computer actually has to stop what it's doing, process the input data, and then pick up where it left off. During a single loop of our program this could happen once, a hundred times, or not at all.

Fortunately, the operating system and PyGame take care of these details for us: they simply build a list of "event" objects—one for each event that occurred since the last update. All our program has to do is to look through this event list element-by-element and check for the events we're interested in: namely, key press or "KEYDOWN" events. When we catch one, we'll further check to see if it is one of the keys we're interested in, and then we'll respond accordingly.

For the direction controls, we can conveniently store the key symbols as another dictionary: mapping key symbols to directions. We'll add separate checks for other events we want to check, such as the user pressing the "Escape" key or closing the PyGame window to end the program. The result should look like this:

The first part of the "if" block checks for events meant to close the program. If it finds one, it calls a function from the "sys" module (don't forget to import the new module) which actually shuts down the program (For now, it's probably best to just remember this idiom instead of trying to understand the details). The second part scans for KEYDOWN events which are key presses, and simply checks to see if any are in the keymap dictionary above. If so, the new direction is chosen from it.

Figure 5 shows how the keymap and directions dictionaries interact, and also shows the layout of the keyboard controls for the program.

Figure 5: Keymap and Directions Dictionaries

Functions and calls (diagram)

You may wonder where the key symbol names are coming from. This is one of the things that we got when we imported the contents of pygame.locals into our program. The PyGame documentation contains a complete list of these symbols (under "Key").

We can use this to put the mouse under keyboard control. In mouse5c.py, I've also added a check for automatic control, so we can eliminate the random direction resets that occur with the standard update function.

mouse5c.py

EXTEND (branch) One mouse controlled by player.


[TOP]

Classes & Class Instances

Although the functional approach allows us to manage more than one character sprite, we still have a fairly complex job, and it's hard to specialize the separate objects (for example, to have more than one set of sprites). With an "object-oriented" approach, we can organize things much better if we define a "class" for character sprites and define the mouse as an "instance" of that class. This requires a fairly major reorganization of the code, and it will be a little bit larger when we finish, but this will really pay off in the next extensions to the code.

First of all, we'll create a class called "Character". This is done with Python's "class" statement. The name object in parentheses after the class name is not an argument, but rather the parent or "base" class that we are sub-classing. For now, it's probably best to just accept the object base class as a necessity for modern Python code (we'll make more use of this next month). Listing mouse6.py shows this object-oriented refactoring of the code.

You'll notice that every method has a first parameter called "self". Python fills this with a reference which points to the class instance itself. So, for example, we'd be able to define a "mouse" character and update it like this:

Although we've passed no arguments to update, it will actually receive an implicit one, which is just the object mouse. This allows us to use Python's dot notation to access the attributes and other methods of the instance: thus we can call move_auto from within update, by referring to self.move_auto. Likewise, all of the persistent information about the class (such as position, what sprite is being displayed, and the current direction of travel) are accessible. Figure 6 shows these relationships between "classes", "class instances", "attributes", and "methods".

Figure 6: Classes, Class Instances, and Method calls

Classes (diagram)

This is much better than defining these at the module level, both because the information is defined close to the function that uses it, and also because there are separate values for each instance of Character, making it easy to keep the information separated, even if we have multiple characters.

Classes can define a "magic" method named __init__, which will be called when a new instance of the class is defined. We'll use that function to define the specifics of each character: what its name is, what sprite image is used to represent it, whether it's a player or automatic character, and so on. We can save the information for the life of the Character instance by assigning values to "instance attributes", which we access through the self object, as in these examples from the listing:

Now, instead of defining things like the sprite images at the module level, we'll define them within the class instance, so that there is a separate collection of sprite images for each character. We can do similar things with the current position and anything else we need for the character to "remember".

mouse6.py

REFACTOR (object-oriented) 'Character' class with 'update' and 'move_auto' methods.

Finally, we'll create a move_player method (adapted from the move_player function we defined previously) to provide for a keyboard-controlled character. Then we'll need to add some symbols representing possible behaviors (currently just AUTO and PLAYER, but we might want to add more later). This results in the code in mouse7.py, which gets us back to controlling the mouse within the PyGame window.

mouse7.py

EXTEND Add 'move_player' and 'behavior' switch.


[TOP]

Summary (of Part 2)

We've moved from simple scripting to object-oriented design, and we've been able to extend a simple "screensaver" program to something more like a game, with a degree of player control. Next month, we'll add backgrounds, another character, sound, an objective, and scoring, so as to make this into a real game.
[TOP]

Part 3: A Simple Game of Cat & Mouse

In the first two parts, we developed some basic programming skills, and the basics of interacting with PyGame. Now it's time to turn the program into a real game using PyGame's sprite library.

We're ready now to extend our program with another character: the cat. This is easy to do now that we have a general purpose class for sprite characters. Then we'll add a more interesting background image; improved speed control; music and sound effects. Finally, we'll see how to make the cat "catch" the mouse, and keep track of the results on an on-screen scoreboard.

As a first step, we'll use the behavior switch from the last iteration of the program to put the cat under keyboard control (behavior=PLAYER), leaving the mouse on the random walk rule (behavior=AUTO). The complete result is shown in cat_mouse1.py.

cat_mouse1.py

Added the cat character to the O-O design. Now have one character controlled by player and the other is on auto.

In order to tell when the cat has caught the mouse, though, we're going to need to be able to tell when the two sprites overlap. This is called "collision detection" and is a fundamental feature of any sprite-based game. We could theoretically implement this in the game code, using simple geometry comparisons, but we don't have to: PyGame provides this support, in addition to other sprite manipulation features, in its "sprite" module.

So, our first step will be to refactor our code to use the pygame.sprite module. You should look up "sprite" in the PyGame documentation to follow along with these steps and get additional background information.

Up until now, we've been using a complete screen fill to clear the screen between updates. This is a simple, but wasteful way to clear the screen, and it doesn't give us much flexibility. PyGame's sprite library uses a smarter solution, which only repaints the parts of the screen that have been overwritten by sprites. In order to use this approach, though, we need to make a "background" surface which the program will use to fill in the space under each sprite. We'll start with a plain green surface, as before:

The first line creates a surface, the second paints it green (using the same method we've been using on the screen surface up until now). Finally, the third line defines the boundary marking the limits of the playing field. Later on, this will allow us to limit the field to less than the full background.

Then, we'll need to change the Character class to inherit from PyGame's "Sprite" class instead of from the built-in "object":

One more change is that we need to define a PyGame "rectangle" or "Rect" object for the attribute rect of the Character class. This is needed by the rendering and clearing routines which are optimized to only update the bounding rectangle of the sprite object, and which use the location of the bounding rectangle to position the sprite image. So we derive this from the values we've already defined:

More interestingly, we'll now collect the sprites using the "RenderUpdates" object. This is one of several "sprite group" objects provided by the pygame.sprite module. This particular one is able to draw the sprites and also "clear" them (which really means to copy part of the background over them). We'll define and name each character, and then use those names to initialize the characters sprite group. We'll also need these names later on to refer to the characters separately.

The game loop will now look a little different, too, because we're going to call methods on the sprite group, instead of manipulating our objects directly:

The first line within the loop clears the sprites from their existing positions. The second line calls "update" on the sprite group. This has an interesting effect which is that it calls the "update" method on every single sprite in the group—and, of course, we have defined an update method for our Character class, so it will get called for each character.

We call characters.draw(screen) to draw the updated sprites onto the screen. This function returns a list of "dirty rectangles", which means rectangles of the screen which have been affected by the sprites. We feed this list as an optional argument to the pygame.display.update() function, which will then be optimized to repaint only the affected areas. The time delay call is the same as before (we'll improve on this soon). The resulting refactored code is included in cat_mouse2.py in the source package.

cat_mouse2.py

Changed over to using 'pygame.sprite.Sprite' instead of 'object' as base-class of 'Character', and 'RenderUpdates' as a "sprite group" instead of a list.


[TOP]

Background

So far, we've used a very plain background, but at this point it's easy to fix that. We'll just load a background image instead of creating one. Also, we want to have some boundary regions that are off-limits on this screen, so we'll need to explicitly define the playing area boundary. This now requires only a small change at the beginning (replacing the background code from the last section):

If you run this code (cat_mouse3.py), it will work almost exactly as before, but the cat and mouse will be limited to a playing area within the game window, and the background is much more attractive.

This background adds some window-dressing (floor, walls, and mouse-holes), and also a blank space at the top that we will fill in with a scoreboard soon.

cat_mouse3.py

Replaced boring green with image background. Added the 'boundary' to contain the action to a subset of the PyGame window.


[TOP]

Timing

So far, we haven't done much about getting the timing and speeds right in this game. The speed of the cat and mouse are always the same, and both depend on the CPU speed, which is very undesirable, because CPUs run at different speeds from one computer to the next. We've added a simple timing delay of 5ms to keep the speed under control, but the actual update time is that 5ms plus however long it takes the game loop code to run, so it still varies (on most modern hardware, this won't be very noticeable, but if you have a slow video card, the animation will be slower than expected).

To solve this problem, we need to actually measure the elapsed time, and update sprite positions based on that. Instead of always moving one pixel per update, we'll specify speeds for each character in pixels per second.

This also means we have to switch to using "floating point" numbers instead of integers (that just means numbers with decimals, like "0.20287" or "-10.2") for the internal representation of each character's position. That way movements smaller than a full pixel will be accumulated on each update. Otherwise, the characters would probably "freeze" whenever the computed interval is less than 1 pixel (they'd round down to 0, and not move at all). With floating point numbers, the fractional part is kept and will increase on each update.

The actual display position has to be an integer, so we will convert whenever we update the sprite's rect attribute (which is what actually determines where it will be displayed by RenderUpdates). Here are the changes (the "..." marks some omitted unchanged code):

We can also make two other changes to make the program more interesting: first of all we no longer need to "cheat" on the directions. Up until now we've used (1,1) for "NE", but the speed will be more consistent if we adjust this to the proper coordinates for a distance of one pixel at 45 degrees, which is approximately (0.707,0.707). All of the diagonals are adjusted to this in this version of the program (see Figure 7 for the correct geometry).

Figure 7: Correct Geometry for Diagonals

Diagonals (diagram)

Finally, the cat and mouse no longer have to move at the same speed: we'll make the game a little more challenging (and probably more realistic), by making the mouse a little faster. The resulting program is cat_mouse4.py.

cat_mouse4.py

Convert to using explicit timing and pixel speeds.


[TOP]

Adding Sound

Now we'll add some sound to the game. PyGame provides two different kinds of sound support which we'll use in this example: first, you can play background "music", which means a continuous sound loop that plays until you stop it; and second, you can load sound "effects" which you play in response to game events. Music for free software games can be found from online sites, I picked a track ("Handless" by Mouche), from a music archive in France called Dogmazic. We can load and play this file in PyGame using the pygame.mixer.music module:

For a sound effect, after failing to find any suitable cat sounds online, I used Audacity to tweak a sound file from Flobopuyo until it sounded a bit like a rather scary cat snarl. These can be loaded using pygame.mixer:

These sound files are included along with the sprite images in the resources directory of the online source code package.

cat_mouse5.py

Add music and load sound effect.


[TOP]

Catching the Mouse

But as you can see by running the program (cat_mouse4.py), nothing happens when the cat catches up with the mouse! Now we need to add the collision detection. Now, we'll play the sound effect we loaded above when the mouse is captured, and also remove the mouse character from the board. Then we'll place a new mouse randomly on the playing field.

In order to set the random starting point, we'll need a set of entry points to choose from. These are chosen to correspond to the visible mouseholes in the background image:

To detect the "capture" we need to use the "collision detection" provided by the function pygame.sprite.spritecollideany(). This function takes two arguments: a sprite and a sprite group. If any sprite in the group has "collided" with the sprite (in other words, overlaps the sprite), then it will return the sprite that collided.

In our example, this is very simple: we can just check for a collision between the cat and any character. If the colliding sprite is the mouse, then the cat has "captured" the mouse, so we remove the mouse from the character sprite group, using the kill() method. We also play the sound effect we loaded before, and then we place a new mouse at a random choice from the mouseholes list. Here's the collision detection code, which appears in the main game loop:

cat_mouse6.py

Add collision detection & play sound on capture. Mouse replaced randomly after capture.


[TOP]

The Scoreboard

The final step to make this into a game is to define a scoreboard. In fact, we'll call this the Game class, containing the background description, the boundaries of the play area, the background music filename, and other aspects of the current game. Later on, we might split this into separate Game and Level classes so as to define a multi-level game, but we'll just keep it simple for now.

One of the most important things for this object to do is to paint a scoreboard across the top of the game area. We'll add a status window to encourage the player, and keep track of how many mice the cat has caught.

To write text using PyGame, we need a TrueType (or OpenType) font. I've chosen "Bitstream Vera Sans", because it's free-licensed and available from the Debian archive. PyGame needs access to the actual TrueType font file, though, so we need to put a copy in the resources directory (if you want to distribute a game, including fonts ensures availability). We load the font in the Game.__init__ method:

The number tells how many pixels high the font will be rendered. Once loaded, the font object can be used to write text onto the screen, like this:

The font.render() method returns a surface, using the specified font. The True tells the method to anti-alias the characters (this is slower, but produces smoother-looking text). The next argument is the foreground color of the text. There is an optional last argument, which would set the background color, but by omitting it, we get the text rendered onto a transparent surface allowing the background to show through. Finally, of course, the resulting surface is blitted to the screen, as with the other surfaces we've defined.

We use this technique to write both the screen messages above and to write the current score. I won't explain the rest of the game object in detail: you should be able to understand it by examining the source code and using what you've learned so far. The final game code is shown in Listing 3.3 (cat_mouse7.py). If you've gotten this far, you should be pretty proud of yourself: you've gone from just learning Python to producing a playable game. With these skills you should be able to continue on your own, or create your own game from scratch.

LISTING 3.3:

cat_mouse7.py

Add the game object & scoreboard.


[TOP]

Some Ideas

Now at this point we have a working game, although it is fairly simplistic. There's a whole lot of things we could try out to improve the game play. At this point, we are considering game design, though, and you should be entirely capable of playing with these ideas on your own.

Here are some improvements to consider:

In the source code package, the file cat_mouse8.py provides a few additional enhancements beyond this tutorial, for you to look at, and possibly get ideas from. There are also some extra sprites in the resource directory for you to use in making enhancements.

cat_mouse8.py

Extra stuff: smarter mouse, obstacles, two game levels, etc.