Coding a Python Pac-Man-style game is a great way to learn more about Python programming and Pygame Zero.

In the first part of our Pac-Man-style tutorial, we created a maze for our player to move around, and restricted movement to just the corridors. We provided some dots to eat and some ghosts to avoid. In this part we are going to give the ghosts some more brains so that they are a bit more challenging to the player. We will also add the bonus power-ups which turn the ghosts into tasty edibles, give Pac-Man some extra levels to explore and some extra lives. So far in this series we have not dealt with music, so we will have a go at putting some music and sound effects into the game.

The code is in this tutorial explains how the game works. Make sure you download the code from GitHub and use this tutorial as a code-along.

This tutorial was written by Mark Vanstone and first appeared in The MagPi 77. Join our newsletter for a free digital edition every month; or subscribe to The MagPi for 12-months in print and get a free Pi Zero W & Starter Kit and a copy delivered to your door every month.

See also:

A more advanced Pac-Man in Pygame Zero

In part one, we left our ghosts wandering around the maze randomly without much thought for what they were doing, which was a bit unfair as Pac-Man could evade them without too much trouble. In the original game, each ghost had a program that it followed to characterise its movements. We are going to add some brains to two of the ghosts. The first we will make follow Pac-Man, and the second we will get to ambush by moving ahead of Pac-Man. We will still leave in some random movement, otherwise it may get a bit too difficult.

Pac-Man Python: what you'll need

Raspbian Jessie or newer

An image manipulation program such as GIMP, or images available here

The latest version of Pygame Zero (1.2)

The pacman2 folder from GitHub

USB joystick or gamepad (optional)

Headphones or speakers

Follow the leader

First, let’s get the red ghost to follow Pac-Man. We already have a moveGhosts() function from part one and we can add a condition to see if we are dealing with the first ghost: if g == 0: followPlayer(g, dirs). This calls followPlayer() if it’s the first ghost. The followPlayer() function receives a list of directions that the ghost can move in. It then tests the x coordinate of the player against the x coordinate of the ghost and, if the direction is valid, sets the ghost direction to move toward the player. Then it does the same with the y coordinates.

Y over x

The keen-witted among you will have noticed that if x and y movements towards the player are both valid, then the y direction will always win. We could throw in another random number to choose between the two, but in testing this arrangement it doesn’t cause any significant problem with the movement. See figure1.py for the followPlayer() function. You will see there is a special condition aboveCentre() when we check the downward movement. We are checking that the ghost is not just above the centre, otherwise it will go back into its starting enclosure.

def followPlayer(g, dirs): d = ghosts[g].dir if d == 1 or d == 3: if player.x > ghosts[g].x and dirs[0] == 1: ghosts[g].dir = 0 if player.x < ghosts[g].x and dirs[2] == 1: ghosts[g].dir = 2 if d == 0 or d == 2: if player.y > ghosts[g].y and dirs[1] == 1 and not aboveCentre(ghosts[g]): ghosts[g].dir = 1 if player.y < ghosts[g].y and dirs[3] == 1: ghosts[g].dir = 3 def aboveCentre(ga): if ga.x > 220 and ga.x < 380 and ga.y > 300 and ga.y < 320: return True return False

The central problem

If we go back to the moveGhosts() function, we need another centre-related condition: if inTheCentre(ghosts[g]). This is because if we leave the ghost to randomly move around our centre enclosure, it may take a long time to get out. In part one, you may have noticed that from time to time one ghost would get stuck in the centre. What we do is, if we detect that a ghost is in the centre, we always default to direction 3, which is up. If we run the game with this condition and the followPlayer() function, we should see all the ghosts making their way straight out of the centre and then the red ghost making a bee-line towards Pac-Man.

It’s an ambush!

So, the next brain to implant is for the second ghost. We will add a function ambushPlayer() in the same way we did for the first ghost, but this time if g == 1:. The ambushPlayer() function works very much like the followPlayer() function, but this time we just check the direction that Pac-Man is currently moving and try to move in that direction. We, of course, cannot know which direction the player is going to move, and this may seem a bit of a simplistic approach to ambushing the player, but it is surprising how many times Pac‑Man ends up wedged between these two ghosts with this method.

Scores on the doors

Brain functions could be added to all the ghosts, but we are going to leave the ghost brains for now as there is plenty more to do to get our game completed. Before we go any further, we ought to get a scoring system going and reward Pac-Man for all the dots eaten. We can attach the score variable to the player actor near the top of our code with player.score = 0 and then each time a dot is eaten we add 10 to the score with player.score += 10. We can also display the score in the draw() function (probably top right is best) with screen.draw.text().

Three strikes and you’re out!

As is the tradition in arcade games, you get three lives before it’s game over. If you followed our previous tutorial for Space Invaders, you will know how we do this. We just add a lives variable to the player actor and then each time Pac-Man is caught by a ghost, we take a life off, set player.status = 1, and print a message to say press ENTER. When pressed, we set player.status = 0 and send Pac-Man back to the starting place. Then we continue. Have a look at figure2.py to see the code we add to reset Pac-Man to the start.

# This code goes in the update() function if player.status == 1: i = gameinput.checkInput(player) if i == 1: player.status = 0 player.x = 290 player.y = 570 # This code goes in the gameinput module # in the checkInput() function if joystick_count > 0: jb = joyin.get_button(1) else: jb = 0 if p.status == 1: if key.get_pressed()[K_RETURN] or jb: return 1

Printing lives

We have the system for keeping track of the player.lives variable, but we also need to show the player how many lives they have left. We can do this with a simple loop like we used in the previous Space Invaders tutorial. We can have a drawLives() function which we call from our draw() function. In that function, we go round a loop for the number of lives we have by saying for l in range(player.lives): and then we can use the same image that we use for the player and say screen.blit("pacman_o", (10+(l*32),40)).

Which button to press

You may notice in figure2.py that in our gameinput module we are checking a joystick button as well as the ENTER key. You may want to do a few tests with the gamepads or joysticks that you’re using, as the buttons may have different numbers. You can also prompt the player to press (in this case) the A button to continue. If you were designing a game that relied on several buttons being used, you might want to set up a way of mapping the buttons to values depending on what type of gamepad or joystick is being used.

from pygame import joystick, key from pygame.locals import * joystick.init() joystick_count = joystick.get_count() if(joystick_count > 0): joyin = joystick.Joystick(0) joyin.init() def checkInput(p): global joyin, joystick_count xaxis = yaxis = 0 if p.status == 0: if joystick_count > 0: xaxis = joyin.get_axis(0) yaxis = joyin.get_axis(1) if key.get_pressed()[K_LEFT] or xaxis < -0.8: p.angle = 180 p.movex = -20 if key.get_pressed()[K_RIGHT] or xaxis > 0.8: p.angle = 0 p.movex = 20 if key.get_pressed()[K_UP] or yaxis < -0.8: p.angle = 90 p.movey = -20 if key.get_pressed()[K_DOWN] or yaxis > 0.8: p.angle = 270 p.movey = 20 if joystick_count > 0: jb = joyin.get_button(1) else: jb = 0 if p.status == 1: if key.get_pressed()[K_RETURN] or jb: return 1 if p.status == 2: if key.get_pressed()[K_RETURN] or jb: return 1

I have the power!

# This code is in our main code file (pacman2.py) def initDots(): global pacDots pacDots = [] a = x = 0 while x < 30: y = 0 while y < 29: d = gamemaps.checkDotPoint(10+x*20, 10+y*20) if d == 1: pacDots.append(Actor("dot",(10+x*20, 90+y*20))) pacDots[a].status = 0 pacDots[a].type = 1 a += 1 if d == 2: pacDots.append(Actor("power",(10+x*20, 90+y*20))) pacDots[a].status = 0 pacDots[a].type = 2 a += 1 y += 1 x += 1 # This code is in the gamemaps module def checkDotPoint(x,y): global dotimage if dotimage.get_at((int(x), int(y))) == Color('black'): return 1 if dotimage.get_at((int(x), int(y))) == Color('red'): return 2 return False

from pygame import image, surface, Color moveimage = image.load('images/pacmanmovemap.png') dotimage = image.load('images/pacmandotmap.png') def checkMovePoint(p): global moveimage if p.x+p.movex < 0: p.x = p.x+600 if p.x+p.movex > 600: p.x = p.x-600 if moveimage.get_at((int(p.x+p.movex), int(p.y+ p.movey-80))) != Color('black'): p.movex = p.movey = 0 def checkDotPoint(x,y): global dotimage if dotimage.get_at((int(x), int(y))) == Color('black'): return 1 if dotimage.get_at((int(x), int(y))) == Color('red'): return 2 return False def getPossibleDirection(g): global moveimage if g.x-20 < 0: g.x = g.x+600 if g.x+20 > 600: g.x = g.x-600 directions = [0,0,0,0] if g.x+20 < 600: if moveimage.get_at((int(g.x+20), int(g.y-80))) == Color('black'): directions[0] = 1 if g.x < 600 and g.x >= 0: if moveimage.get_at((int(g.x), int(g.y-60))) == Color('black'): directions[1] = 1 if g.x-20 >= 0: if moveimage.get_at((int(g.x-20), int(g.y-80))) == Color('black'): directions[2] = 1 if g.x < 600 and g.x >= 0: if moveimage.get_at((int(g.x), int(g.y-100))) == Color('black'): directions[3] = 1 return directions

Not all dots are the same

High status ghosts

Why so blue?

The tables have turned

# This code is in the update() function for g in range(len(ghosts)): if ghosts[g].status > 0: ghosts[g].status -= 1 if ghosts[g].collidepoint((player.x, player.y)): if ghosts[g].status > 0: player.score += 100 animate(ghosts[g], pos=(290, 370), duration=1/SPEED, tween='linear', on_finished=flagMoveGhosts) else: player.lives -= 1 if player.lives == 0: player.status = 3 else: player.status = 1

The next item on our list is power-ups. These are large glowing dots that, when eaten, turn all the ghosts dark blue. In their blue form they can be eaten for bonus points and they return to the centre of the maze. First, let’s devise a way to place the power-ups in the maze. We have updated the pacmandotmap.png image to include some red squares, instead of black, in the positions where we want our power-ups to be. Then, when we initialise our dots and call checkDotPoint(x,y), we look for red as well as black – figure3.py shows how we change our code to do this.We now have a system to place our power-ups in the maze. The next thing to do is to change what happens when Pac-Man eats a power-up compared to a normal dot. At the moment we just add ten points to the player’s score if a dot is eaten, so we need to add more code to handle the event of a power-up being eaten. In the draw() function, where we look to see if the player has collided with a dot using collidepoint(), we then check the status of the dot (to make sure it’s still there) and after this we can add a new condition: if pacDots[a].type == 2:.As we have determined that we are dealing with a power-up (type 2), we can add a loop that goes through the list of ghosts and changes the status of the ghost. Normally the status for a ghost is 0. What we are going to do is change the status to a fairly high number (try 1200 to start with). This will indicate that the ghosts are in their alternate state and we will use the status as a countdown. We will decrement this value each time update() is called; when it reaches 0, the ghosts will turn back to normal.To make our ghost turn blue, we are going to add some conditions to our drawGhosts() function. We want them to be blue when the status is more than 0, but just to make it interesting we will make them flash when they are about to turn back. So we can write if ghosts[g].status > 200 or (ghosts[g].status > 1 and ghosts[g].status%2 == 0): ghosts[g].image = "ghost5". What this is saying is that if the status is over 200 then make the ghost blue, but if it’s less that 200 but greater than 1 then make it blue every other frame. We then have an else condition underneath that will set the image to its normal colour.Now we have our ghosts all turning blue when a power-up is eaten, we need to change what happens when Pac-Man collides with them. Instead of taking a life from the player.lives variable, we are going to add to the player.score variable and send the ghost back to the centre. So, the first job is to add a condition in update() when we check the ghost collidepoint() with the player, which would be if ghosts[g].status > 0:. We then add 100 to the player.score and animate() the ghost back to the centre. See figure4.py for the updated code.

Back to the start

You will notice that when Pac-Man comes into contact with a dark blue ghost, we just animate the actor straight back to the centre in the same time that we normally animate a ghost from one position to the next. This is so that we don’t hold up the animation on the other ghosts waiting for the eaten one to get back to the centre. In the original game, the ghosts would turn into a pair of eyes and then make their way back to the centre along the corridors, but that would take too much extra code for this tutorial.

Time for some music

So far in this series, we have not covered adding music to games. In the documentation of Pygame Zero, music is labelled as experimental, so we will just have to try it out and see what happens. In the sample GitHub files for this tutorial, there is a directory called music and in that directory is an MP3 file that we can use as eighties arcade game background music. To start our music, all we need to do is write music.play("pm1") in our init() function to start the music/pm1.mp3 file. You may also want to set the volume with music.set_volume(0.3).

More sound effects

The MP3 file will continue playing in a loop until we stop it, so when the game is over (player.lives = 0) we can fade the music out with music.fadeout(3). At this stage we can also add some sound effects for when Pac-Man is eating dots. We have a sound in our sounds directory called pac1.mp3 which we will use for this purpose and we can add a line of code just before we animate the player: sounds.pac1.play(). This will play the sound every time Pac-Man moves. We can do the same with pac2.mp3 when a life is lost.

Level it up

The last thing we need to put into our game is to allow the player to progress to the next level when all the dots have been eaten. We could incorporate several things to make each level harder, but for the moment let’s concentrate on resetting the screen and changing the level. If we define our level variable near the top of our code as level = 0, then inside our init() function we say level += 1, then each time we call init() we will increase our level variable. This means that instead of saying that the player has won, we just prompt them to continue, and call init() to reset everything and level up.

Code Pac-Man in Python: so much to do

The Pac-Man game has many more things that can be added to it. The original had bonus fruits to collect, the ghosts would move faster as the levels continued, there were animations between some of the levels, and the power-ups would run out quicker. You could add all of these things to this game, but we will have to leave you to do that yourself. Take a look into the history of the Pac-Man game – it’s fascinating – and we will be starting a new Pygame Zero game in the next instalment of this series.