CPSC 124 Introduction to Programming Spring 2024

Project 2
Pac-Man

Muddiest Points Due: Thu 4/4 at 10pm
Muddiest Points Second Chance: Sun 4/7 at 10pm
Project Due: Mon 4/22 at noon


Introduction

The programming projects are an opportunity for you to work on a larger program, where there are more pieces to bring together. Unlike the labs, you have several weeks to complete this project — but this project is larger than the lab programs, so start early! There will not be a new lab assignment on 4/9 so there will be time during lab to work on the project and no other programming assignment that week, but there are labs overlapping the beginning and end of this project and the final project follows immediately after.

Collaboration

Labs and projects are for practice and learning. While it can be very productive to work on problems with your peers, it is also easy to underestimate how much you yourself understand and can do in such situations — so often something looks easy when someone else does it! With this in mind, you should always make the first attempt on a problem or programming task yourself using only the resources provided for the course (textbook, slides and examples posted on the schedule page, other resources linked on the course webpages). After that point, you are encouraged to come to office hours and/or go to Teaching Fellows. You may not collaboratively write solutions or code, and you may not copy solutions or code written by others, even if you contributed ideas.

You can discuss specific exercises with other students in general terms — such as how you might get started on that problem, or how or when to use various programming constructs, or strategies for debugging — but how to use a particular programming construct to solve a specific problem or debugging a particular program should only be discussed in office hours or with the Teaching Fellows.


Setup

Remember that you will need to compile and run your program(s) using jfxc and jfx, respectively, instead of javac and java.


Overview and Requirements

In this project you will be writing a somewhat simplified version of the classic arcade game Pac-Man. The object of Pac-Man is to eat all the pellets without touching a ghost; eating a power pellet gives Pac-Man the temporary ability to kill ghosts. See the Wikipedia entry on Pac-Man for more information on the game - or try playing it.

Compared to the real game, your program will have some modifications and simplifications. To see how your program should work, try out the demo: (type the following into a terminal window)

  jfx -jar /classes/cs124/demos/pacman.jar

Control Pac-Man using the arrow keys. Regular pellets are yellow; power pellets are red. Eating a power pellet makes Pac-Man "powered up" for a short period. While "powered up", touching ghosts kills the ghost instead of Pac-Man.

Press 'n' to start a new game.

Functionality

Your program should work like the demo, not like the real version of the game linked above! In particular:

Note that you do not have to duplicate the appearance of the demo exactly — as long as it is possible to tell the difference between the various game components and the specifications given above are met, you are free to choose exactly how things are displayed.

Design and Implementation

In addition to the functionality requirements given above, you must:

Programming Style

As with labs, you should follow the standard conventions for indentation, line breaks, naming, and comments. Using autoformat will take care of most of the formatting considerations, though you still need to put in blank lines yourself in order to visually group related statements together and separate statements dealing with separate tasks. Remember to include enough in your variable names to identify what they are for and to distinguish similar variables each other, and to comment your variable declarations if there is additional relevant information not apparent from the variable name.

The need for comments inside of methods/subroutines to describe or explain chunks of code is greatly reduced with an object-oriented design, but they are still appropriate for longer method bodies or when there's something particularly non-apparent about a chunk of code.

Your code should be reasonably compact and elegant, meaning that your program is not significantly longer or more complex than it needs to be. Two important components of this are using loops when loops are appropriate (achieiving repetition by copying-and-pasting code is not elegant!) and using subroutines/private helper methods to organize and to avoid repeated code.

Extra Credit

There are lots of possibilities for extra credit — start early so you have time to try some of them! Some possibilities include:

The tags "small" and "medium" are meant to give some idea of the level of effort involved. More involved options and more elegant solutions will earn more extra credit. (The chief inelegances to be concerned about are unnecessarily complex solutions, and unnecessary repeated code.) A maximum of 15 points (out of 100 for the project) of extra credit can be earned. If you plan to tackle a lot of extra credit or have an idea for something not listed, come discuss your plans with me.


Exercises

Work through these in order. Note that there is not a specific deadline for the pseudocode, but you should take advantage of the opportunity for feedback to make sure you are on the right track. (And allow 24 hours for said feedback — don't wait until the last minute!)

Muddiest Points

Class Design

Stepwise Refinement and Pseudocode

Because Pac-Man and the ghosts move on their own, this is an animation. That means that the program's job is to produce a series of frames, and that the "main program" boils down to writing code for two tasks: drawing the scene based on the current values of the animation variables, and updating the animation variables for the next frame. All of the program's functionality other than initialization and drawing the scene will be in the "updating animation variables" step.

As usual, start with a few broad steps that cover all of the tasks (just not in great detail). This might just be:

  // initialize animation variables
  // draw scene
  // update animation variables

As you proceed with stepwise refinement, keep in mind the class design — and, in particular, what operations each class is responsible for. You do not need to refine your main program pseudocode to include details that will be the responsibility of another class. For example, Maze's constructor is responsible for setting up the walls and placing the pellets, so a step of create maze is already simple enough — it can be accomplished with a constructor call. (In fact, the refinement of the initialization and drawing steps may end up being just an identification of what needs to be initialized and drawn, without any more details of how to initialize or draw.)

Of course, you will eventually need to implement the Maze constructor and other methods whose jobs aren't just one or two lines of code. Also utilize the process of stepwise refinement of pseudocode for those methods.

Finally, Pac-Man

Practice incremental development. With classes, this means implementing classes or pieces of classes as you need them. Also, remember the idea of starting simple and building up from there. For example, you might start with getting a basic maze drawn on the screen — implement the constructor and draw for Maze, then add code to PacMain to create and initialize a Maze instance variable and draw that maze in each frame. Start with just the four walls around the edges of the maze area. Then you could move on to Pac-Man, implementing things related to drawing and moving in PacMan but skipping dealing with walls and pellets, and adding code to PacMain to get Pac-Man on the screen and moving. (You'll also need a few other things, such as createPacMan in Maze.) Next you could add the ghosts, or deal with Pac-Man and walls, or add pellets. And then...


Class Design

Your code should be organized as described below.

Choose appropriate types. double rather than int is recommended for positions and sizes, as JavaFX supports doubles and that allows finer-grained control over the speed of moving elements.

A bounding box is the smallest axis-aligned rectangle which encloses the shape — all parts of the shape just fit within the bounds of the box. Axis-aligned means that the sides of the rectangle are parallel to the x- and y-axes — don't rotate the rectangle to try to get a tighter fit to the shape.

Note the createPacMan and createGhost methods in Maze. Pac-Man and the ghosts will be animation variables (hint, hint) so they will be created and initialized in PacMain — but where they are placed in the maze to start depends on the configuration of the maze. createPacMan and createGhost allow Maze, which is responsible for the configuration of the maze, to choose those positions.

classproperties (instance variables)operations (methods)
PacMan
  • position (x,y)
  • size
  • current direction of travel
  • power remaining (number of steps)
  • alive?
  • a constructor which takes the position, size, and current direction of travel as parameters and initializes those instance variables accordingly; in addition, Pac-Man should be alive and the power remaining should be 0
  • getters getLeft, getRight, getTop, getBottom for Pac-Man's bounding box
  • getter isPowered which returns whether or not there is power remaining
  • getter isAlive
  • setters setDirection, setPower
  • decreasePower, which decreases the power remaining by one
  • kill, which makes Pac-Man no longer alive
  • move, which moves Pac-Man one step in the current direction of travel
  • draw, which takes a GraphicsContext as a parameter and draws Pac-Man
  • isBlockedBy, which takes a wall as a parameter and returns true if Pac-Man is blocked from moving forward by the wall and false otherwse
  • canEat, which takes a pellet as a parameter and returns true if Pac-Man is in the right position to eat the pellet and false otherwise
Ghost
  • position (x,y)
  • size
  • color (type Color)
  • current direction of travel
  • alive?
  • a constructor which takes the position, size, color and current direction of travel as parameters and initializes those instance variables accordingly; in addition, the ghost should be alive
  • getters getLeft, getRight, getTop, getBottom for the ghost's bounding box
  • getter isAlive
  • setter setDirection
  • kill, which makes the ghost no longer alive
  • move, which moves the ghost one step in the current direction of travel
  • draw, which takes a GraphicsContext as a parameter and draws the ghost
  • isBlockedBy, which takes a wall as a parameter and returns true if the ghost is blocked from moving forward by the wall and false otherwse
  • hasCaught, which takes Pac-Man as a parameter and returns true if the ghost is touching Pac-Man and false otherwise
Wall
  • position (x,y)
  • dimensions (width,height)
  • a constructor which takes the position and dimensions as parameters and initializes the instance variables accordingly
  • getters getLeft, getRight, getTop, getBottom for the wall's bounding box
  • draw, which takes a GraphicsContext as a parameter and draws the wall
  • contains, which takes a position (x,y) as parameters and returns true if (x,y) is inside the wall and false otherwise
Pellet
  • position (x,y)
  • power pellet?
  • a constructor which takes the position and and whether or not the pellet is a power pellet as parameters and initializes the instance variables accordingly
  • getters getX, getY
  • getter isPowered
  • draw, which takes a GraphicsContext as a parameter and draws the pellet
Maze
  • dimensions (width,height)
  • walls
  • pellets
  • a constructor which takes the dimensions as parameters and initializes those instance variable accordingly; the walls and pellets should be initialized as desired
  • getters getWidth, getHeight
  • reset, which creates and distributes a new set of pellets
  • createPacMan, which takes Pac-Man's size and direction of travel as parameters and returns a new PacMan object placed appropriately within the maze
  • createGhost, which takes the ghost's size, color, and direction of travel as parameters and return a new Ghost object placed appropriately within the maze
  • pelletsLeft, which returns whether or not there are any unconsumed pellets remaining
  • canMove, which takes Pac-Man as a parameter and returns true if Pac-Man is able to move forward (i.e. no walls in the way) and false otherwise (Pac-Man is blocked by a wall)
  • canMove, which takes a ghost as a parameter and returns true if the ghost is able to move forward (i.e. no walls in the way) and false otherwise (the ghost is blocked by a wall)
  • consumePellets, which takes Pac-Man as a parameter and consumes any pellets that Pac-Man is able to eat
  • draw, which takes a GraphicsContext as a parameter and draws the maze

Technical Details

Provided Code

Two classes have been provided.

Direction is an enum providing definitions for the four directions left, right, up, down. Direction is a type like any other, so you can (for example) declare variables and parameters of type Direction. Click on the link for the API — the four things listed under "Enum Constant Summary" show the values you can use (write as Direction.DOWN, etc). Of interest in the Method Summary section is getRandomDir() — this returns a random direction (useful for ghosts!). Note that it is static, which means that you write Direction.getRandomDir() to call it instead of needing to create an instance of Direction.

PacMain contains the main program. It's where the main program functionality goes; see the technical details about animation below for more information.

Drawing

You will need to include the following at the very beginning (before the class declaration) in any class that does drawing:

import javafx.scene.paint.*;
import javafx.scene.canvas.*;

In addition to the GraphicsContext operations introduced in section 6.2.4 in the text, you may be interested in strokeArc and fillArc. (Click on the links for the API documentation.) The final parameter, the closure type, can have the following values: ArcType.CHORD, ArcType.OPEN, ArcType.ROUND. (Find out what they mean in the "Enum Constant Summary" section in the API for ArcType.) One final note — if you use ArcType, you'll also need

import javafx.scene.shape.*;

Animation

Since Pac-Man and the ghosts move on their own, this program involves animation. Recall the structure of an animation from lab 6 — the program's job is to produce a series of frames which are displayed in quick succession. Each frame involves two elements: drawing the scene based on the current values of the animation values, and updating the animation variables to new values for the next frame.

The provided PacMain is based on the AnimationStarter template for lab 6 and sets up the necessary framework for a JavaFX program. Refer back to lab 6 for more about AnimationStarter, but the main thing is to (only) add/modify code in the places marked with // *** comments.

The Passage of Time

The passage of time comes up in handling Pac-Man's powered-up state — the effect of eating a power pellet only lasts for a certain amount of time. This time can be measured in terms of a number of frames rather than seconds (or some other unit of actual time). To handle the limited-time powered-up state, Pac-Man has an instance variable for the power remaining. Set it to a non-zero value when Pac-Man eats a power pellet, and decrease the value by 1 as part of the "update animation variables" step in drawFrame.

User Input

GUI programs are typically event-driven. This is beyond the scope of this course, but what it means for user input is that instead of the program explicitly calling for input when it wants to handle it, a method is defined to handle user input and the system calls that method when the user presses a key. In PacMain, that method is handleKey — look for the *** handle key press here! comment. The key parameter is the key that was pressed, and you can use it to determine which key was pressed in order to do the appropriate action. This comparison is the same as in Minesweeper e.g.

  if ( key == KeyCode.LEFT ) {     // left arrow pressed
    ...
  }

KeyCode.RIGHT, KeyCode.UP, KeyCode.DOWN, and KeyCode.N are the other values you'll need.

Filling in handleKey is all you need to do for user input — you don't need anything where you are updating animation variables in drawFrame.


Handling Geometry

Some math is needed to determine when Pac-Man or a ghost hits a wall, when Pac-Man eats a pellet, and when Pac-Man touches a ghost. The fact that the animation only happens in discrete chunks complicates the matter somewhat since it is unlikely that things will be caught at the exact moment of contact. This section outlines the math that is needed; skim the ideas the first time you read through the ehandout, then read more thoroughly when you actually need to write the code.

Hitting Walls

We'll consider Pac-Man to be about to hit a wall if any part of Pac-Man's bounding box would pass the leading edge of the wall during Pac-Man's next move. For example, imagine that Pac-Man is moving to the right. The figure below shows several possible situations. In the three cases on the left, Pac-Man will hit the wall. In the three cases on the right, Pac-Man won't hit the wall. (At least not in this step.) What condition distinguishes the "hit" cases from the "not hit" cases? Hint: part of the condition involves comparing Pac-Man's right side (bold line) to the wall's left side (bold line).

The other three directions of movement (left, up, down) are similar, and ghosts hitting walls are handled in exactly the same way as Pac-Man hitting walls.

Eating Pellets

Pac-Man eats a pellet if the center of the pellet falls within the hatched regions shown below. (The region depends on Pac-Man's direction of movement.)

A point is inside a rectangular region if it is simultaneously on the proper side of each of the four edges of the region i.e. the point is left of the right side, right of the left side, below the top, and above the bottom.

Touching Ghosts

Pac-Man touches a ghost (which either kills the ghost or Pac-Man, depending on whether Pac-Man is "powered up" or not) if the rectangles surrounding Pac-Man and surrounding the ghost overlap. The example shows one case of Pac-Man touching a ghost and three where Pac-Man doesn't touch the ghost. Pac-Man touches a ghost if (simultaneously) Pac-Man's left side is left of the ghost's right side, Pac-Man's right side is right of the ghost's left side, Pac-Man's top side is above the ghost's bottom side, and Pac-Man's bottom side is below the ghost's top side.


Handin and Grading

Don't forget to hand in your work when you are done!

As with labs, grading will be based on both functionality and style. The expectations for both are given in the "Overview and Specifications" section above.