Part 2

In part 2 we’re going to get the ghosts moving, first by making Actor objects for them (sprites), then by adding code to move them intelligently (well, sort of) around the screen.

Making ghost sprites

We can see the ghosts on the screen, but they don’t move yet. That’s because they are just part of the background and drawn in one place in the draw function.

So let’s pick them out of the world and make them into actors. We can use a similar method to iterate through the world as we did in the draw function. Add this code…

ghosts = []

def make_ghost_actors():
    for y, row in enumerate(world):
        for x, block in enumerate(row):
            if block == 'g' or block == 'G':
                g = Actor(char_to_image[block], (x*BLOCK_SIZE, y*BLOCK_SIZE), anchor=('left', 'top'))
                ghosts.append(g)
                # Now we have the ghost sprite we don't need this block
                world[y][x] = None

And then right at the end of your program add a line to call this new function, right under your load_level(1) line:

make_ghost_actors()

You can see from the code above that we are looking for two letters in the world: a lower case and upper case G. If we find a match we create an actor in the correct place, using x and y from the for loops, then finally we remove the block from the world as otherwise we’d have two ghosts: one that moves and one that stays in place.

Do run your code now to check that you’ve not made any typos. If it runs without any syntax errors you’ll notice that now we have no ghosts :( Let’s fix that…

We need to add code to draw the ghost actors, we do this in the draw function. Add these lines to the end:

for g in ghosts: g.draw()

Now we have our ghosts back, but they are not moving yet.

Moving the ghosts

We can use similar logic to move our ghosts as we use to move Pac-Man, after all we don’t want ghosts to move through the walls.

First let’s add some constants to the top of our code, plus we need to use the random library:

import random

SPEED = 2
GHOST_SPEED = 1

Now when we create a ghost let’s set a random direction. Just under this line:

g = Actor(char_to_image[block], (x*BLOCK_SIZE, y*BLOCK_SIZE), anchor=('left', 'top'))

Add these lines, making sure that you match the indentation.

# Random direction
g.dx = random.choice([-GHOST_SPEED,GHOST_SPEED])
g.dy = random.choice([-GHOST_SPEED,GHOST_SPEED])

OK, so what we’ve done just there is to record an x-direction and y-direction for each ghost, picking at random from 4 combinations: (-2,-2), (-2,2), (2,-2), (2,2).

So now we need to use these to actually move each ghost. Let’s add code to the update function to do this… Add these lines to the end of the function:

for g in ghosts:
    g.x += g.dx
    g.y += g.dy

Press Play to test. Hmmm… not great, the ghosts can move through the walls. Maybe that’s what ghosts do in real life, but not in Pac-Man!

Don’t move through walls

Look at all the code in that update function, you can see we’ve moving Pac-Man differently to how we’re moving each ghost:

def update():
    # In order to go in direction dx, dy there must be no wall that way
    if '=' not in blocks_ahead_of_pacman(pacman.dx, 0):
        pacman.x += pacman.dx
    if '=' not in blocks_ahead_of_pacman(0, pacman.dy):
        pacman.y += pacman.dy

    for g in ghosts:
        g.x += g.dx
        g.y += g.dy

You can see that with Pac-Man we’re checking for walls (the = character) but not for the ghosts. Let’s fix this.

What we want is a general purpose version of blocks_ahead_of_pacman that we can use with ghosts too, then we can check for walls for any sprite.

So first up, rename the blocks_ahead_of_pacman function, add a new argument so we can pass in the sprite to check and change the two instances of pacman to sprite

Let’s go through those steps. (1) change the function from:

def blocks_ahead_of_pacman(dx, dy):

To:

def blocks_ahead_of(sprite, dx, dy):

Now (2) change these two lines:

x = pacman.x + dx
y = pacman.y + dy

To:

x = sprite.x + dx
y = sprite.y + dy

Try running your code now. You should see an error, because we’ve changed the function but not the places where we use it, which still refer to the old function.

So in the update function, change the function calls to use the new method. See if you can figure out how to do this. (You can see the complete function below if you are stuck).

OK, so we can now use this general purpose function blocks_ahead_of with ghosts too, so change the last few lines of your update function to these:

for g in ghosts:
    if '=' not in blocks_ahead_of(g, g.dx, 0):
        g.x += g.dx
    if '=' not in blocks_ahead_of(g, 0, g.dy):
        g.y += g.dy

So that the complete function looks like this:

def update():
    # In order to go in direction dx, dy there must be no wall that way
    if '=' not in blocks_ahead_of(pacman, pacman.dx, 0):
        pacman.x += pacman.dx
    if '=' not in blocks_ahead_of(pacman, 0, pacman.dy):
        pacman.y += pacman.dy

    for g in ghosts:
        if '=' not in blocks_ahead_of(g, g.dx, 0):
            g.x += g.dx
        if '=' not in blocks_ahead_of(g, 0, g.dy):
            g.y += g.dy

Now we have some good ghost movement, but if you leave it running for a bit chances are you’ll get an error like this (assuming you left gaps in your walls):

IndexError: list index out of range

Wrapping around

We get this error because a ghost has gone off the screen and its (x,y) co-ordinates are outside the range of our world. You’ll also get this error if you move Pac-Man off the screen.

There’s one other problem, not a defect as such, but a violation of a good coder principle: Don’t Repeat Yourself (or DRY). Much of the code in update is repeated. If we fix this first, then maybe we can fix the out of range error more easily.

Let’s create a new function move_ahead like so:

def move_ahead(sprite):
    # In order to go in direction dx, dy there must be no wall that way
    if '=' not in blocks_ahead_of(sprite, sprite.dx, 0):
        sprite.x += sprite.dx
    if '=' not in blocks_ahead_of(sprite, 0, sprite.dy):
        sprite.y += sprite.dy

This contains all the logic we need to move a sprite forward, using (dx,dy) and avoiding walls. Let’s refactor update to use this. Replace the function with this new, much shorter one:

def update():
    move_ahead(pacman)
    for g in ghosts:
        move_ahead(g)

Now we have less code, and also just as importantly it’s really easy to see what update is actually doing.

Let’s look at that IndexError. We can see that it’s being generated from inside the blocks_ahead_of function. We need to do two things to fix it.

  1. Wrap the sprites around, so that if they go off one side of the screen, they come back on the other side.
  2. Don’t check for blocks outside of the world.

For the wrap around we want to keep our sprite’s x and y position in between two values: 0 and the width or height of the screen. If we go outside this range we want to wrap to the other end of the range.

We can do this with a simple function:

def wrap_around(mini, val, maxi):
    if val < mini: return maxi
    elif val > maxi: return mini
    else: return val

You can test this in a Python3 script (in Mu or IDLE) to see how it works. Here’s an example:

>>> wrap_around(0, 5, 10)
5                          # No change
>>> wrap_around(0, 15, 10)
0                          # 15 is too big, so wrap to 0
>>> wrap_around(0, -1, 10)
10                         # -1 is too small, so wrap to 10

OK, let’s use this function. Add these lines to the end of move_ahead:

# Keep sprite on the screen
sprite.x = wrap_around(0, sprite.x, WIDTH-BLOCK_SIZE)
sprite.y = wrap_around(0, sprite.y, HEIGHT-BLOCK_SIZE)

Finally to stop checking blocks off the world, add these lines to blocks_ahead_of just under the definition of rx, ry =

# Keep in bounds of world
if ix == WORLD_SIZE-1: rx = 0
if iy == WORLD_SIZE-1: ry = 0

Phew! That was quite a bit of work. So how are our ghosts behaving now? Press Play to test them out.

Notice anything odd?

Have any ideas how to fix it?

Keep on moving

Yes, our ghosts eventually stop, usually in a corner. That’s because we never change their direction.

If we can tell that they’ve stopped moving we can do something about it. The function move_ahead is the place to start. Here’s the current function:

def move_ahead(sprite):
    # In order to go in direction dx, dy there must be no wall that way
    if '=' not in blocks_ahead_of(sprite, sprite.dx, 0):
        sprite.x += sprite.dx
    if '=' not in blocks_ahead_of(sprite, 0, sprite.dy):
        sprite.y += sprite.dy

    # Keep sprite on the screen
    sprite.x = wrap_around(0, sprite.x, WIDTH-BLOCK_SIZE)
    sprite.y = wrap_around(0, sprite.y, HEIGHT-BLOCK_SIZE)

How do we tell if the sprite has moved? We can record the position at the start of the funciton and compare at the end of the function like this…

Add these two lines to the start of the function:

# Record current pos so we can see if the sprite moved
oldx, oldy = sprite.x, sprite.y

And these two lines at the end of the function:

# Return whether we moved
return oldx != sprite.x or oldy != sprite.y

So now anyone that calls this function can find out, if they want, whether the sprite has moved.

OK, so back in the update function we can use this new information… Change your function to read:

def update():
    move_ahead(pacman)

    for g in ghosts:
        if not move_ahead(g):
            set_random_dir(g, GHOST_SPEED)

There’s one more new function here so that we Don’t Repeat Ourselves. Can you spot it? What do you think we should put in it? Hint: the code is already written, it’s just not in a function yet.

If you are completely stuck, have a look at the code for part 2 on GitHub.

Next up…

In the next part of this tutorial we’ll work on:

  • Pac-Man eating the food
  • Ghosts killing Pac-Man
  • Moving to the next level.

Move on to Part 3.