You must be authenticated to submit your files

CSE 102 – Tutorial 10 – Pac-Man

Based on an original tutorial by K. Chaudhuri

Sections tagged with a green marginal line are mandatory.

Sections tagged with an orange marginal line are either more advanced or supplementary, and are therefore considered optional. They will help you deepen your understanding of the material in the course. You may skip these sections on a first pass through the tutorial, and come back to them after finishing the mandatory exercises.

After you are finished with the tutorial, please upload your submission to Moodle by the indicated deadline.

Discussion of the tutorials with friends and classmates is allowed, with acknowledgment, but please read the Moodle page on “Collaboration, plagiarism, and AI”.

There are no automated tests for this tutorial. Simply check that your game is playable and has the expected behavior.

Updated installation instructions

To get the most out of this tutorial you will need to have the pygame library installed, as well as an IDE with support for type hints. Assuming you are using Spyder under Anaconda, the safest way of updating your environment is probably to create a new virtual environment with everything installed, by executing the following commands:

conda create -n CSE102_env
conda activate CSE102_env
conda install spyder
conda install -c conda-forge pylsp-mypy pyls-spyder
pip install pygame

If you are on a Windows machine, you may need to replace the first line by conda create -n CSE102_env python=3.11.

Alternatively, if you are using another IDE with integrated support for type hints, such as vscode or pycharm, then you only need to install the pygame library.

Pygame

To test that you’ve successfully installed pygame, run the following from the interpreter:

In [1]: import pygame

If that runs without errors, you have the library correctly installed.

You can read the pygame online documentation to find out more about the library.

Type hints

Once you’ve installed the necessary packages, then you can simply restart Spyder and it will indicate type errors in line. For example, if you copy the type-incorrect definition below into a file

def foo(oranges : int, apples : str) -> int:
    return oranges + apples

and save it, then Spyder should display something like the following warning:

If you run into trouble, please ask for help directly from a teacher/student, or post to the #techsupport channel on Slack.

Pac-Man

In this tutorial you are given an implementation of some aspects of the classic Pac-Man game. You are also given a Pac-Man entity that currently does not do anything but sit in its starting location.

Your task will be to make the Pac-Man entity react to your keyboard inputs and thereby play the game of Pac-Man.

You are given the following files.

Download all of them as a .zip file

Make a new project with these files, and then run the game.py file. You should see a Pac-Man window pop up. Hit the return key to start the game.

The template contains type annotations, a.k.a. type hints, which are intended both to help you understand the existing code and to debug the additional code that you write. Again, to get the most out of these type annotations you should use an IDE that can understand them and indicate type errors interactively.

We expect you to modify the files given above and to upload them using the form below. You can resubmit your solution as many times as you like.

Upload form is only available when connected
Upload form is only available when connected
Upload form is only available when connected
Upload form is only available when connected

Making Pac-Man move

The first task is to get the Pac-Man entity to move at all. We’re not going to worry about user input right yet. What we are going to do is to have the Pac-Man entity move around randomly.

The level is represented in the level.Level class. Each cell position pos : tuple[int,int] is a tuple of a row and a column coordinate, numbering from (0, 0) for the top left cell. The level object has the __getitem__() and __setitem__() special functions defined so that to get the contents of the cell at position pos from the level object l, you would say l[pos], and similarly for setting the contents of the cell.

The contents of each cell is a value of an enumeration type Cell, which includes the following values: Cell.EMPTY (empty cell), Cell.WALL (wall), Cell.PILL (a cell that has a pill – more on that below), Cell.POWERPILL (a cell with a powerpill – explained in the optional section below), or Cell.PIT (the pit where ghosts spawn).

The Pac-Man entity (in the class pacman.Pacman) has a pos data attribute that is its current (row, col) coordinate in the level. From any such coordinate, you can get the accessible neighbors by means of the .neighbors() method of level.Level. The current level is stored in the data attribute .level in the pacman.Pacman class.

  1. On every update triggered by the game loop—i.e., in the .update() method in pacman.Pacman—move the Pac-Man entity to a random neighbor of its current position. To find the neighbors of the entity’s current position, call .neighbors() to get the adjoining cells that the Pac-Man can move to from its current cell. Pick one such neighbor at random and then update the .pos data attribute of the entity.

  2. When the Pac-Man entity enters a cell that has a pill (i.e., the cell contains the value level.Cell.PILL), change the cell to be empty instead. Also increase the score of the Pac-Man by 1.

If you did this right, you’ll see the Pac-Man zoom around in the level at ludicrous speed, probably running into a ghost eventually.

Slowing the Pac-Man down

The ghosts currently move at the rate of 5 cells per second, which is set in the class ghost.Ghost. The Pac-Man should also move at this very same speed.

  1. Modify your Pac-Man class so that it only moves from one cell to the next once enough time has elapsed. This can be done using the millis argument to .update(), which contains the number of milliseconds that has elapsed since the previous call to .update(). Change the class so that the Pac-Man decides where to go and then gradually goes there at its correct speed.

    For now just make the Pac-Man move at the same speed as the Ghosts. Don’t worry about making the Pac-Man’s movements appear smooth – i.e., it’s OK if it jumps from one cell to the next about 5 times every second.

  1. Update the .render() method of pacman.Pacman so that it draws the Pac-Man smoothly between two cells while it is in the process of moving between those cells. You may find it useful to see how the ghost.Ghost class is implemented.

Responding to user-input

Now for the main challenge in this tutorial: getting the Pac-Man to move in response to user inputs.

  1. The .process_event() method in pacman.Pacman currently does nothing. You will now make it detect and respond to keyboard events, specifically the keypress event (event.type == pg.KEYDOWN) or a key release event (event.type == pg.KEYUP). In either case, the key that was pressed will be in the event.key attribute. You need to watch for the keys pg.K_LEFT, pg.K_RIGHT, pg.K_UP, and pg.K_DOWN. Alternatively, if you like to play using WASD, you may respond to the keys pg.K_w, pg.K_a, pg.K_s, and pg.K_d. Based on which event you received, set a .direction attribute for the Pac-Man entity. If the user presses any other key, just ignore it.

  2. Modify the .update() method so that the Pac-Man moves in the direction indicated by its current .direction attribute. Make sure that the entity does not enter forbidden areas such as the walls or the pit. You can check if a given cell is enterable using the .can_enter() method of level.Level. If moving the Python along its user-indicated direction would cause it to enter a forbidden cell, you should prevent the Pac-Man from moving.

  3. Figure out what should happen if a user presses multiple keys at once, such as the left and right keys at the same time.

If you have gotten this far, you will have a playable version of Pac-Man.

Powerpills

The Pac-Man can turn the table on its ghost pursuers whenever it swallows a powerpill. The level by default doesn’t have any powerpills.

  1. Modify the level.Level class to take a list of powerpill locations in its initializer. Change the level.level_1 variable to create some initial powerpills, say one near each of the four corners of the map.

  2. Whenever the Pac-Man swallows a powerpill, the following happens:

  • It gets a flat 10 points added to its score
  • Its speed doubles
  • Whenever it runs into a ghost, the ghost gets reset (.reset()), which sends it back to the pit.
  • Each ghost the Pac-Man resets in this way adds 50 to the score
  • The powerpill effect lasts for a total of 15 seconds.
  1. When the powerpill effect is active, you may want to modify the ghost’s behavior to make it run away from the Pac-Man. Modify its behavior on forks by having it select the direction that will take it farthest away from the Pac-Man entity.

Polishing the look

While the game so far is playable, it is not very fun to look at. Let’s try to fix some aspects of the presentation.

  1. Give the Pac-Man some eyes, like the ghosts.

  2. Add a death animation for the Pac-Man. You may find that you need to add a new game state that is like Game.GAME_OVER but is used just to play the death animation

  3. Change the rendering of ghosts while the Pac-Man is powerpilled. This can be achieved by calling the .blanch() method of the Ghost class.

  4. Make the Pac-Man’s mouth point in the direction that it is moving.

  5. Have the Pac-Man’s mouth open and close in a loop. Both this and the previous task will require you to figure out how the Pac-Man is being drawn in its .render() method, and then to modify it.

Differentiating the ghosts

Currently all the ghosts do the same random thing. In the original Pac-Man game, the ghosts had subtly different behaviors.

  • Blinky, the red ghost, always chases the Pac-Man. In other words, whenever it comes to a fork, it takes the fork that gets it closest to the current location of the player.

  • Pinky, the pink ghost, tries to get in front of the Pac-Man. At all choice points, it extrapolates the current direction of the Pac-Man and tries to get to the fork that is directly ahead of the Pac-Man.

  • Clyde, the orange ghost, will alternately chase the Pac-Man (like Blinky) and try to go back to the entrance of the pit.

  • Inky, the blue ghost, randomly picks a behavior of the other three ghosts and executes it for a random amount of time before switching to a different one of the behaviors.

  1. Create classes that inherit from ghost.Ghost and override the .update() method to implement the behaviors above.