pac man tutorial

31
Pac Man Tutorial Release 1.0 Jan 22, 2022

Upload: others

Post on 23-Mar-2022

10 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Pac Man Tutorial

Pac Man TutorialRelease 1.0

Jan 22, 2022

Page 2: Pac Man Tutorial
Page 3: Pac Man Tutorial

Contents:

1 Installing Assets 3

2 Start the Tutorial 52.1 Part 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52.2 Part 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122.3 Part 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172.4 Part 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21

3 Get in touch 27

i

Page 4: Pac Man Tutorial

ii

Page 5: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

In this tutorial we’ll use Pygame Zero to create a Pac Man clone. As well as learning about sprites and game logicwe’ll use files to store each level, which means you’ll get to design your own levels, and you’ll learn how to read filesin Python.

If you’ve not yet set up Pygame Zero and the editor Mu, head over to the Flappy Bird Tutorial and follow the instruc-tions there: https://tinyurl.com/y37qxb5h

If you’re new to Python programming then that Flappy Bird tutorial is probably better place to start – this tutorial issignificantly harder.

Contents: 1

Page 6: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

2 Contents:

Page 7: Pac Man Tutorial

CHAPTER 1

Installing Assets

Download the tutorial assets by . You can close that page once the download finishes. Click on the Images buttonin the Mu editor. This will open the directory where Pygame Zero looks for images. Copy the images from theimages.zip file you just downloaded into this directory.

3

Page 8: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

4 Chapter 1. Installing Assets

Page 9: Pac Man Tutorial

CHAPTER 2

Start the Tutorial

Click on the Part 1 link below to get started.

2.1 Part 1

In part 1 we’re going to create a game world in a text file, read and process it in Python, then draw it on the screen. Bythe end of this first part you’ll have something that looks a lot like Pac-Mac, except that only your character can moveabout the maze.

2.1.1 Getting Started

• Press the New button in Mu to open a new file and enter the following lines:

WIDTH = 640HEIGHT = 640TITLE = 'Pac-Man'

• Press Save and save the file as pacman.py in your mu_code directory.

• press Play to see what this code does.

You should see a new, empty window appear.

2.1.2 Making a game world

We’re going to store your game world in a text file. This means you can design your own levels and also you’ll get tolearn about working with files in Python - a really useful skill.

5

Page 10: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

Creating the text file

So create a text file in your favourite editor and use the equals sign to draw some walls for your world, for example:

========== ========== =========== =

= ================ =========== =========

Did you notice we left some gaps for our characters to move from one side to the other for a quick escape?

Save this file in the same directory as where you saved pacman.py. Call the file level-1.txt

Now let’s try reading that file in your Python code. Add this empty array to contain the world:

world = []

Now add this function to your code underneath that:

def load_level(number):file = "level-%s.txt" % numberwith open(file) as f:

for line in f:row = []for block in line:

row.append(block)world.append(row)

Let’s test that this works, add the following two lines to the end of your code:

load_level(1)print(world)

If you typed the code in correctly then when you press Play you’ll see something like this in your console:

[['=', '=', '=', '=', '=', '=', '=', '=', '=', ' ', ' ', '=', '=', '=', '=', '=', '=',→˓ '=', '=', '='], ['=', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.',→˓'.', '.', '.', '.', '.', '.', '='],...

That’s Python’s way of printing a list and it means that your code loaded your world from the text file. Each elementin the list is a character at a specific location in your world.

Note: If this didn’t work, and you didn’t make any typos, it could be that your code and level files are not in the rightplace. Check that they are both in your mu_code directory.

Did you notice that the world is all printed out on a single line, so that it is hard to read? We can make that list printmore clearly so that we can see every line of the world like this:

for row in world: print(row)

6 Chapter 2. Start the Tutorial

Page 11: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

How reading a file works

In our code above we use with open(file) as f: to open and begin the process of reading the contents of ourlevel file. Let’s look at what that line of code does:

• First the with statement tells Python that we are going to supply a block of code that will work on the file we’reabout to open – we mark this by block by indenting the lines that follow.

• At the end of this block Python will tidy up for us by closing the file automatically.

• open(file) opens the file for reading (rather than writing)

• as f stores a reference to the file in the variable f.

Inside the block we can then use a simple for loop to iterate over the lines in the file referenced in variable f. Andinside this loop another loop get each character from the each line of the file and stores it away for later refence.

The next step is to draw this on the screen. . .

2.1.3 Drawing the world

As the moment you just have ‘=’ characters in your world. Go back and put in some dots and stars (. and *) torepresent food and power-ups.

So now we need a way to map these characters in your text file to images in on the screen. Let’s use a dictionary to dothis. A dictionary is a map from one value to another, in our case we will map a single character to a file name of theimage to use on screen.

Add this code near the top of your game:

char_to_image = {'.': 'dot.png','=': 'wall.png','*': 'power.png',

}

Trying out dictionaries in the REPL

Let’s switch to the REPL to see how this dictionary works. First change your game mode to Python3–click the Modeicon to do this–then click the Run button and you’ll get a >>> prompt at the bottom of the screen.

Try typing the following and see if you understand what’s going on (don’t type the >>> characters) . . .

>>> char_to_image['=']'wall.png'>>> char_to_image['*']'power.png'>>> char_to_image['!']Traceback (most recent call last):File "<stdin>", line 1, in <module>KeyError: '!'

KeyError means that ‘!’ is not found in the dictionary, it is not a valid key because we’ve note added it tochar_to_image.

OK, make sense? Switch the game mode back to PygameZero, then continue. . .

2.1. Part 1 7

Page 12: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

From characters to images

Do you remember from previous tutorials that PygameZero expects us to define a draw method to draw the game onthe screen? Let’s add this method now, you can see the code below.

The code iterates through the rows in the world, then the blocks in each row and draws the right image for the characterit finds.

We use enumerate so that we get each item in the world and its index in the array, which gives us the right x and yco-ordinates for the screen position.

def draw():for y, row in enumerate(world):

for x, block in enumerate(row):image = char_to_image.get(block, None)if image:

screen.blit(char_to_image[block], (x*BLOCK_SIZE, y*BLOCK_SIZE))

Hooray! We should now have your map on the screen ready to add our Pac-Man character.

Wait! Did you get an error? Why do you think this is? Remember, look at the last line of the error message first.

Can you fix the error yourself? Try first before scrolling down.

. . .

. . .

. . .

OK, so you should have spotted that we’ve not yet defined BLOCK_SIZE. Add this to the top of your program:

BLOCK_SIZE = 32

2.1.4 What size is the world?

You’ve probably noticed that your world doesn’t perfectly fit in the game window. That’s because the WIDTH andHEIGHT you’ve set at the start of your code are unlikely to match the world size stored in your text file.

We can fix this by changing the constants at the start of your code.

Firstly decide on what size world you want to support, then add one new constant WORLD_SIZE and set WIDTH andHEIGHT to use this.

Here’s an example for a 32x32 world:

WORLD_SIZE = 20BLOCK_SIZE = 32WIDTH = WORLD_SIZE*BLOCK_SIZEHEIGHT = WORLD_SIZE*BLOCK_SIZE

Did you notice that this code only supports square worlds? Let’s go with that for now to keep things simpler.

2.1.5 Adding the Pac-Man

OK, time to add our Pac-Man sprite. Let’s start with an Actor to draw the sprite. We need this sprite to be avaiable toall of our code, so add these new lines near the top of your program, just under WIDTH and HEIGHT:

8 Chapter 2. Start the Tutorial

Page 13: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

# Our spritespacman = Actor('pacman_o.png', anchor=('left', 'top'))pacman.x = pacman.y = 1*BLOCK_SIZE

And then we want to draw our Pac-Man in the world, so add this new line (the one in yellow) to the end of your drawfunction:

def draw():for y, row in enumerate(world):

for x, block in enumerate(row):image = char_to_image.get(block, None)if image:

screen.blit(char_to_image[block], (x*BLOCK_SIZE, y*BLOCK_SIZE))pacman.draw()

This places Pac-Man at the top left of the screen.

Moving through the maze

Now let’s think about movement. We’ve seen code similar to this in previous tutorials:

def on_key_down(key):if key == keys.LEFT:

pacman.x += -BLOCK_SIZEif key == keys.RIGHT:

pacman.x += BLOCK_SIZEif key == keys.UP:

pacman.y += -BLOCK_SIZEif key == keys.DOWN:

pacman.y += BLOCK_SIZE

Try this out. You’ll see that our Pac-Man moves very jerkily across the screen, and has no regard for walls. We can dobetter than this.

If we remove BLOCK_SIZE (which is 32) and use a smaller number instead, such as 1, then our character certainlymoves slower, but you have to tap the arrow key so movement is still a problem.

We can fix this by adding another key event function: on_key_up so that we track key presses and releases. Changeyour on_key_down function and add the new function underneath:

def on_key_down(key):if key == keys.LEFT:

pacman.dx = -1if key == keys.RIGHT:

pacman.dx = 1if key == keys.UP:

pacman.dy = -1if key == keys.DOWN:

pacman.dy = 1

def on_key_up(key):if key in (keys.LEFT, keys.RIGHT):

pacman.dx = 0if key in (keys.UP, keys.DOWN):

pacman.dy = 0

2.1. Part 1 9

Page 14: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

You might be wondering what dx and dy are. These are two new variables that we’ve added to our pacman characterthat will track direction in x and y (-1 is up or left, 1 is down or right).

We need to initialise these so add these two lines near the top of your program, just under where we set pacman.xand pacman.y:

# Direction that we're going inpacman.dx, pacman.dy = 0,0

Right, now press Play to test. You’ll be a bit disappointed – our pacman no longer moves. We are tracking whichdirection the player wants to move in but we are not using this information anywhere.

It’s time to add an update function to fix this.

def update():pacman.x += pacman.dxpacman.y += pacman.dy

Yay! Now Pac-Man moves, and smoothly, and diagonally if you hold down two arrow keys!

OK, time to add some collision detection. . .

Collision detection

We need to spot when moving Pac-Man would cause a collision with a wall. This is a bit trickier than in other gamesbecause whilst the game world is a series of blocks, Pac-Man can move in pixels. This means that he could potentiallycollide with up to four blocks at any one time, and we need to check all of them.

Let’s add a new function to check what’s ahead of Pac-Man. Ahead is basically Pac-Man’s current position plus thedirection in dx,dy:

def blocks_ahead_of_pacman(dx, dy):"""Return a list of tiles at this position + (dx,dy)"""

# Here's where we want to move tox = pacman.x + dxy = pacman.y + dy

# Find integer block pos, using floor (so 4.7 becomes 4)ix,iy = int(x // BLOCK_SIZE), int(y // BLOCK_SIZE)# Remainder let's us check adjacent blocksrx, ry = x % BLOCK_SIZE, y % BLOCK_SIZE

blocks = [ world[iy][ix] ]if rx: blocks.append(world[iy][ix+1])if ry: blocks.append(world[iy+1][ix])if rx and ry: blocks.append(world[iy+1][ix+1])

return blocks

There’s a lot going on in that function! Let’s break it down:

• First we need to determine where Pac-Man wants to go, we add his direction dx,dy to his x,y position.

• Then we need to convert this destination x,y position into a block position in our world array, simply bydividing by BLOCK_SIZE.

10 Chapter 2. Start the Tutorial

Page 15: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

• However, arrays always take integer indexes (whole numbers) – we can’t look up world[1.6][1.0] as that doesn’tmake any sense to Python – so we set array indexes ix,iy to the integer part of the division and round down,so (1.6, 1.0) would become (1, 1).

• We determine any remainder so that we check adjacent blocks, in the example above, rx would be a positivenumber and ry would be zero.

• Now we can check the blocks, always the one at world[iy][ix] and then those to the right, below anddiagonally right/below depending upon the remainders.

That’s quite a complex algorithm. Let’s see if it works. Change your update function to the following:

def update():# To go in direction (dx, dy) check for no wallsif '=' not in blocks_ahead_of_pacman(pacman.dx, 0):

pacman.x += pacman.dxif '=' not in blocks_ahead_of_pacman(0, pacman.dy):

pacman.y += pacman.dy

You might be wondering why we check in two stages: x then y. This enables you to hold down two arrow keys (sayright and down) and have Pac-Man move through a gap without stopping – handy for escaping ghosts!

You can see how the single step update with this code, which I think you’ll agree is worse – do try it:

def update():if '=' not in blocks_ahead_of_pacman(pacman.dx, pacman.dy):

pacman.x += pacman.dxpacman.y += pacman.dy

2.1.6 Adding ghosts

Let’s add some ghosts to our game. Open up your level-1.txt file and put in some uppercase and lowercase Gsin your world where you want the ghosts to appear.

We now need to pick the images that we want to use for the ghosts. Edit your dictionary char_to_image to map theG characters to the images you want to use (which represent the different ghost colours). You can see all the imagesavailable by clicking the Images button on the toolbar.

Here’s an example:

char_to_image = {'.': 'dot.png','=': 'wall.png','*': 'power.png','g': 'ghost1.png','G': 'ghost2.png',

}

Look good? But the ghosts don’t move yet. . .

2.1.7 Next up. . .

In part two of this tutorial we’ll get the ghosts moving. Move on to Part 2.

2.1. Part 1 11

Page 16: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

2.2 Part 2

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

2.2.1 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 anddrawn 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 theworld 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 blockworld[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. Ifwe find a match we create an actor in the correct place, using x and y from the for loops, then finally we remove theblock 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 thatnow 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.

2.2.2 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 movethrough 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 = 2GHOST_SPEED = 1

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

12 Chapter 2. Start the Tutorial

Page 17: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

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 directiong.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 from4 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. . . Addthese lines to the end of the function:

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

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

2.2.3 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 movingeach ghost:

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

pacman.x += pacman.dxif '=' not in blocks_ahead_of_pacman(0, pacman.dy):

pacman.y += pacman.dy

for g in ghosts:g.x += g.dxg.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, thenwe 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 tocheck 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 + dxy = pacman.y + dy

2.2. Part 2 13

Page 18: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

To:

x = sprite.x + dxy = sprite.y + dy

Try running your code now. You should see an error, because we’ve changed the function but not the places where weuse 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 fewlines of your update function to these:

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

g.x += g.dxif '=' 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 wayif '=' not in blocks_ahead_of(pacman, pacman.dx, 0):

pacman.x += pacman.dxif '=' 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.dxif '=' 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

2.2.4 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 (orDRY). Much of the code in update is repeated. If we fix this first, then maybe we can fix the out of range error moreeasily.

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 wayif '=' not in blocks_ahead_of(sprite, sprite.dx, 0):

sprite.x += sprite.dxif '=' not in blocks_ahead_of(sprite, 0, sprite.dy):

sprite.y += sprite.dy

14 Chapter 2. Start the Tutorial

Page 19: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

This contains all the logic we need to move a sprite forward, using (dx,dy) and avoiding walls. Let’s refactor updateto 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 ofthe 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 maxielif val > maxi: return minielse: 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 screensprite.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 worldif ix == WORLD_SIZE-1: rx = 0if 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?

2.2.5 Keep on moving

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

2.2. Part 2 15

Page 20: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

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

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

sprite.x += sprite.dxif '=' not in blocks_ahead_of(sprite, 0, sprite.dy):

sprite.y += sprite.dy

# Keep sprite on the screensprite.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 endof the function like this. . .

Add these two lines to the start of the function:

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

And these two lines at the end of the function:

# Return whether we movedreturn 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 weshould 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.

2.2.6 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.

16 Chapter 2. Start the Tutorial

Page 21: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

2.3 Part 3

In part 3 we’re going to let Pac-Man eat the food on the screen, make him turn properly as we move him about theworld, and add collision detection for the ghosts. Finally we’ll add code to move the next level when all of the food iseaten.

2.3.1 Food for Pac-Man

Pac-Man currently ignores the food as he moves about. Let’s fix that.

There are a few steps to this:

• Count how much food there is, so that we know when Pac-Man has finished eating and we can move to the nextlevel

• Spot when Pac-Man moves over some food

• Eat it by removing it from the world and decrementing the food counter.

Later, we’ll move to the next level when all of the food is gone.

Let’s record how much food is left by adding a variable to the pacman actor object. Add this code just under whereyou create the pacman actor:

pacman.food_left = None

Now add these lines in the function load_level:

pacman.food_left = 0

And then inside the for block loop in the function load_level we need to spot food blocks like this:

if block == '.': pacman.food_left += 1

Your function should now look like this:

def load_level(number):file = "level-%s.txt" % numberpacman.food_left = 0with open(file) as f:

for line in f:row = []for block in line:

row.append(block)if block == '.': pacman.food_left += 1

world.append(row)

Now let’s add a new method to spot and eat food:

def eat_food():ix,iy = int(pacman.x / BLOCK_SIZE), int(pacman.y / BLOCK_SIZE)if world[iy][ix] == '.':

world[iy][ix] = Nonepacman.food_left -= 1print("Food left: ", pacman.food_left)

Finally, call this new method in the update function after the line move_ahead(pacman):

2.3. Part 3 17

Page 22: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

eat_food()

Now go and test and check that it works. You should see in your console (in the Mu editor at the bottom of the screen)an update of how much food is left each time you eat some.

2.3.2 Rotate Pac-Man when moving around

Pac-Man always looks to the right, even when moving down or to the left, let’s fix this using the rotation feature onactors.

But first we need to change Pac-Man’s anchor point, as if we stick with top-left when we rotate him he’ll won’t stayin place, but move into other blocks.

So near the top of your code replace these two lines:

pacman = Actor('pacman_o.png', anchor=('left', 'top'))pacman.x = pacman.y = 1*BLOCK_SIZE

with these two:

pacman = Actor('pacman_o.png')pacman.x = pacman.y = 1.5*BLOCK_SIZE

Now we’ve changed Pac-Man’s centre of placement and rotation we need to change a bit of maths to keep the collisiondetection working. In function blocks_ahead_of replace these lines:

# Here's where we want to move tox = sprite.x + dxy = sprite.y + dy

with these:

# Here's where we want to move to, bit of rounding to# ensure we get the exact pixel positionx = int(round(sprite.left)) + dxy = int(round(sprite.top)) + dy

Now we can rotate Pac-Man based on which direction he’s moving. In function move_ahead replace this line at theend of the function:

return oldx != sprite.x or oldy != sprite.y

with these lines:

moved = (oldx != sprite.x or oldy != sprite.y)

# Costume change for pacmanif moved and sprite == pacman:

a = 0if oldx < sprite.x: a = 0elif oldy > sprite.y: a = 90elif oldx > sprite.x: a = 180elif oldy < sprite.y: a = 270sprite.angle = a

return moved

18 Chapter 2. Start the Tutorial

Page 23: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

2.3.3 What happens when Pac-Man hits a ghost?

Right now nothing happens when Pac-Man hits a ghost, let’s fix that. Also, what should happen after a collision? Let’smove the ghosts back to where they started.

To record the ghosts’ start positions add these lines just under ghosts = [] near the top of your code:

# Where do the ghosts start?ghost_start_pos = []

Next in function make_ghost_actors add this just under ghosts.append(g):

ghost_start_pos.append((x,y))

Now we have a list that records the (x, y) co-ordinates of each ghost. Let’s add the collision decetion.

Add this test in the update function inside the for g in ghosts loop:

if g.colliderect(pacman):reset_sprites()

Finally add this new function:

def reset_sprites():pacman.x = pacman.y = 1.5 * BLOCK_SIZE# Move ghosts back to their start posfor g, (x, y) in zip(ghosts, ghost_start_pos):

g.x = x * BLOCK_SIZEg.y = y * BLOCK_SIZE

This function resets Pac-Man’s position to the top left corner, then resets each of the ghost positions. Do you noticesomething new in the for loop? We use a function called zip, but what does it do?

Introducing zip

Let’s have a play in the REPL to see how it works. . .

Click New to open a new script and set the Mode to Python 3, then open a REPL and enter these lines of code (don’ttype the prompt >>> and there’s no need to type in the comments that start with a # character):

# Make some lists>>> names = [ 'fred', 'bill', 'amy', 'martha' ]>>> ages = [ 25, 29, 21, 52 ]

# Display the lists>>> print(names)['fred', 'bill', 'amy', 'martha']>>> print(ages)[ 25, 29, 21, 52 ]

So far, no surprises (hopefully!). Now let’s try the zip function:

# First try of zip>>> print(zip(names, ages)<zip object at 0x10b699d88>

What’s that all about?! Well that’s an iterator, which means we need to use a for loop to use it:

2.3. Part 3 19

Page 24: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

# Try zip with a loop>>> for i in zip(names, ages): print(i)('fred', 25)('bill', 29)('amy', 21)('martha', 52)

OK! So zip has merged the two lists together and paired up the elements. We can extend this a bit further by capturingthe name and age at the same time:

>>> for name, age in zip(names, ages): print(name, "is", age, "years old")fred is 25 years oldbill is 29 years oldamy is 21 years oldmartha is 52 years old

Make sense? OK :) Don’t forget to change your game Mode back to PygameZero.

2.3.4 Next Level

Earlier we added code to track how much food was left. Let’s use this to move to the next level when all of the food isgone.

One other thing to consider: we need to test our game and it will take ages if we have to actually eat all of the foodeach time we want to get to the next level, so let’s add a test mode to the game. Add this line at the top of your code:

TEST_MODE = True

Now let’s do the work of moving to the next level. Have a think about what we need to do to acheive this. . . there areactually quite a few steps. See if you can come up with them before reading on further.

. . .

. . .

. . .

OK, here’s the list, how does it compare with yours?

1. Record the level we’re on, starting at 1

2. Create the next world text file level-2.txt

3. Check when all of the food is gone

4. Increment the level by 1

5. Load in the next world text file

6. Capture the ghost positions

7. Reset all the sprites

We can store the current level on the pacman sprite as we did for food_left. Add this line just after you’ve createdthe Pac-Man sprite:

pacman.level = 1

Now let’s put the rest of the next-level work in a new functin called next_level:

20 Chapter 2. Start the Tutorial

Page 25: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

def next_level():global world, ghosts, ghost_start_pos

world = []ghosts = []ghost_start_pos = []

pacman.level += 1load_level(pacman.level)make_ghost_actors()

reset_sprites()

Finally we just need to determine when to call this new function. There are two places. In update add these linesjust under the call to eat_food():

if pacman.food_left == 0:next_level()

And for our test mode, add these lines at the end of the function on_key_up:

if TEST_MODE:# Put special key commands hereif key == keys.N:

next_level()

Now as long as TEST_MODE is True we can press N to go to the next level.

2.3.5 Enjoy your game

Congratulations for getting this far! You’ve worked hard and we have covered a lot of new techniques, so take a bit oftime to relax and enjoy playing your game . . . which is beginning to be quite playable now.

2.3.6 Next up. . .

• Add a score

• End the game when lives run out

• Power ups and chasing ghosts

• Better animations e.g. when Pac-Man loses a life

Move on to Part 4

2.4 Part 4

Part 4 is a work in progress, so at the moment this page is a bit rough and not very well tested. Feel free to have a readthrough and see what you can discover.

2.4. Part 4 21

Page 26: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

2.4.1 Tell the user what’s happening

Let’s add a way to tell the user what’s just happened by displaying a big banner on the screen. As well as displayingnice big text we also need to consider how long we want the banner to be displayed.

If you’ve completed our other tutorials (Flappy Bird and Candy Crush) you’ll know that we can use screen.draw.text to draw text on the screen.

For example, add this to your draw function to see what happens:

screen.draw.text('Hello!', center=(WIDTH/2, HEIGHT/2), fontsize=120)

That’s nice and big isn’t it? But it never goes away, how do we fix this? Well we can add a counter and count down tozero then remove it.

So we need to add two variables: one to store the message and one to store the counter. As we’ve done before let’s putthese on the pacman sprite. Add these two lines under where you’ve created the pacman sprite:

pacman.banner = Nonepacman.banner_counter = 0

Now let’s display a big Ouch! when Pac-Man loses a life. . .

Find your update function and in the if-statement that tests for g.colliderect(pacman) add this line:

set_banner('Ouch!', 5)

We’ve not written that function yet, so this won’t work, but we write this line first to think about how we want thefunction to work. We don’t yet know what 5 means, maybe it is seconds? Maybe some fraction of seconds?

Now add the function:

def set_banner(message, count):pacman.banner = messagepacman.banner_counter = count

OK, now we’re in business. You can see that the function set_banner is really shorthand for setting those twovariables. Given that we’ll probably show a few different banners this will save a fair bit of typing.

Now we can update the draw function to remove the Hello message and use these variables:

if pacman.banner and pacman.banner_counter > 0:screen.draw.text(pacman.banner, center=(WIDTH/2, HEIGHT/2), fontsize=120)

Time to test. Do you see any bugs?

That’s right: the banner never disappears. Let’s fix that now.

So we could decrement (programmer speak for ‘reduce by one’) the counter in the draw function, but this is executedmany times per second so we’d need to use big numbers to keep the banner visible for long enough to read it. A bettersolution is to add a periodic function, this will be handy later too.

2.4.2 Periodic functions

A periodic function is called repeatedly at equal intervals. We can use it to reduce our banner counter, and any otherswe might create.

Here’s how we can use it for our banner counter. . . add this code at the end of your program:

22 Chapter 2. Start the Tutorial

Page 27: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

def periodic():if pacman.banner_counter > 0:

pacman.banner_counter -= 1

clock.schedule_interval(periodic, 0.2)

The function is what we want to do every period, and the last line tells PygameZero to call this function every 0.2seconds or 5 times a second.

Now when you run your game you should see Ouch! displayed for a second and no more.

2.4.3 Score and Lives

Let’s add a score and some lives so that there’s a consequence to Pac-Man hitting a ghost.

First question: we need to store these numbers in variables, but where?

Given that we’ll be accessing and updating them in various places we can put them on the pacman object, that’llmake our coding easier.

So add these lines just under where you set pacman.level:

pacman.score = 0pacman.lives = 3

Now we need to draw those numbes on the screen. In the real Pac-Man we would show one little Pac-Man sprite foreach life left, but for now we’re going to use text.

At the end of your draw function add these two lines:

screen.draw.text("Score: %s" % pacman.score, topleft=(8, 4), fontsize=40)screen.draw.text("Lives: %s" % pacman.lives, topright=(WIDTH-8,4), fontsize=40)

Have a play around with the position and size of those until you are happy.

OK, so now we have a score and lives but they never change! Where do you think we need to make changes to them?Have a think. . .

. . .

. . .

. . .

OK, here’s what you could try for the score: in the code eat_food function, inside the if-statement that checks for adot, increase the score by one. So this block now reads:

if world[iy][ix] == '.':world[iy][ix] = Nonepacman.food_left -= 1# Add this line...pacman.score += 1

We know where to decrement lives, we just added a banner there. Update the block inside the if-statement so that itreads:

set_banner("Ouch!", 5)pacman.lives -= 1reset_sprites()

2.4. Part 4 23

Page 28: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

2.4.4 Power-ups

Let’s make the power-ups do something interesting. We can spot them in the eat_food function. Add this code tothe function being careful to indent everything properly:

elif world[iy][ix] == '*':world[iy][ix] = Nonepacman.score += 5

OK, so now we get an extra 5 points on our score, but we also want the ghosts to run away from us. We need someway of knowing that the Pac-Man has a power-up, which should be time limited in some way – we can use countersagain for this.

Let’s start by adding another variable to the pacman sprite. Near the top of your program add this line:

pacman.powerup = 0

Now we can add this line in the eat_food function inside that if-statement you just changed:

pacman.powerup = 25

The last thing we need to do is to make the ghosts change direction. We need something like this – this won’t workyet, but you get the idea:

for g in ghosts: new_ghost_direction(g)

Now if we can get new_ghost_direction to take account of pacman.powerup we can make them follow orrun away from Pac-Man.

Hmmm. . .

2.4.5 Run ghosts, run!

(Do ghosts actually have legs, can they run? Never mind.)

We already have a function called set_random_dir which in theory works for any sprite, but we only use it forghosts. It doesn’t consider where Pac-Man is it just sets a random direction.

Let’s rename this function to make our intentions clearer, let’s call it new_ghost_direction and make it smarterso that ghosts can run away from Pac-Man if he has a power up.

Here’s the new function:

def new_ghost_direction(g):if pacman.powerup:

g.dx = math.copysign(GHOST_SPEED*1.5, g.x - pacman.x)g.dy = math.copysign(GHOST_SPEED*1.5, g.y - pacman.y)

else:g.dx = random.choice([-GHOST_SPEED, GHOST_SPEED])g.dy = random.choice([-GHOST_SPEED, GHOST_SPEED])

The last bit is the same as before, but the first bit is new. If Pac-Man has a power up we have some weird maths goingon. What does it mean? Here’s what:

• g.dx and g.dy are the ghost’s direction, as before

• math.copysign takes two numbers: some value and an expression which returns a positive or negative number.It applies the sign of that number to the value

24 Chapter 2. Start the Tutorial

Page 29: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

• In our function the sign is determined by the relative position of Pac-Man and the ghost.

• For example: if the ghost is to the right of Pac-Man the sign will be positive so the ghost will move to the right(away from Pac-Man)

• And if the ghost is to the left the sign will be negative and the ghost will move to the left (away)

• The value is the speed, which is 1.5 times the original, a bit faster than before.

Phew! That’s a lot going on in only a few lines. Now that you’ve renamed the old function, we need to find where weused it and update this code to use the new method.

Make the change in def make_ghost_actors.

Now we can use the new function for power ups. Plus we can add a banner to shout it out to the user. Update youreat_food function so that it looks like this:

def eat_food():ix,iy = int(pacman.x / BLOCK_SIZE), int(pacman.y / BLOCK_SIZE)if world[iy][ix] == '.':

world[iy][ix] = Nonepacman.food_left -= 1pacman.score += 1

elif world[iy][ix] == '*':world[iy][ix] = Nonepacman.powerup = 25set_banner("Power Up!", 5)for g in ghosts: new_ghost_direction(g)pacman.score += 5

Time for a test. . . what do you think?

2.4.6 Flashing ghosts

Notes:

• Choose a coloured ghost sprite for the fleeing ghosts – I used ghost2.png

• Make sure you don’t use that colour for regular ghosts, edit ghosts in char_to_image dictionary to swap outghosts2.png for another colour.

• This is a good time to add some more options for ghost colours, for example, we can use h and H as well as gand G to represent ghosts in your world file:

char_to_image = {'.': 'dot.png','=': 'wall.png','*': 'power.png','g': 'ghost1.png','G': 'ghost3.png','h': 'ghost4.png','H': 'ghost5.png',

}

• And we then need to update make_ghost_actors to spot these, so: if block in ['g', 'G', 'h','H']:

• Draw a white ghost, which we’ll use for the flashing state when the power up starts to run out. You can duplicatean existing ghost sprite and use this as a starting point.

2.4. Part 4 25

Page 30: Pac Man Tutorial

Pac Man Tutorial, Release 1.0

• I used GIMP to edit the sprites, which is a powerful, free graphics program. But it is very complex too! Thereare lots of other options, you might already have a paint program on your computer.

• Let’s do the flashing in our periodic function. Add this code:

if pacman.powerup > 0:pacman.powerup -= 1

if pacman.powerup > 10:# The blue version for fleeing ghostsfor g in ghosts: g.image = 'ghost2.png'

else:# Flash for the last few secondsfor g in ghosts:

g.image = alternate(g.image, 'ghost_white.png', 'ghost2.png')

if pacman.powerup == 0:for g in ghosts: g.image = g.orig_image

• There are two new things here: the alternate function and a reset back to g.orig_image.

• The alternate function returns the first value, then the second, then the first, and so on, each time it is called:

def alternate(value, option1, option2):if value == option1: return option2else: return option1

• The last thing is to set g.orig_image when we first create the ghost, this allows us to return the ghost to itsoriginal sprite when we’re done with the power up.

• In make_ghost_actors:

g = Actor(char_to_image[block], (x*BLOCK_SIZE, y*BLOCK_SIZE), anchor=('left', 'top→˓'))g.orig_image = g.image

26 Chapter 2. Start the Tutorial

Page 31: Pac Man Tutorial

CHAPTER 3

Get in touch

We would love to hear how you’ve got on with this tutorial or receive any other feedback. Get in touch with the author:Eric Clack <[email protected]>

Copyright © 2021, Eric Clack

Licensed under the Creative Commons Attribution-NonCommercial-ShareAlike licence v4.0

27