CPSC 124 | Introduction to Programming | Spring 2024 |
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.
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.
Remember that you will need to compile and run your program(s) using jfxc and jfx, respectively, instead of javac and java.
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.
Your program should work like the demo, not like the real version of the game linked above! In particular:
Pac-Man: Pac-Man moves in the same direction until a wall is hit. The arrow keys are used to change Pac-Man's direction of motion. Eating a power pellet causes Pac-Man to be "powered up" for a period of time, after which Pac-Man reverts to the normal state. Running into a ghost when not "powered up" causes Pac-Man to die. A dead Pac-Man does not move or change direction. Finally, Pac-Man's appearance should reflect the direction of travel (e.g. by the position of the "mouth") and whether or not Pac-Man is currently "powered up" or dead (e.g. by changing the color).
Walls: There must be walls surrounding the playing area, plus at least two walls inside the playing area. Neither Pac-Man nor ghosts may move through walls.
Pellets: There should be a bunch of regular pellets distributed around the playing area. (They can be distributed randomly, as in the demo, or in some pattern. Pellets should not be placed on top of walls.) There must be exactly four power pellets, and they should be widely separated. (Don't place the power pellets randomly, because then it is possible for two to end up near each other. Power pellets should also not be placed on top of walls.) Regular pellets and power pellets should look different. Both kinds of pellets disappear when eaten (and thus cannot be eaten again).
Ghosts: There should be four ghosts with different colors. When a ghost hits a wall, a new direction of movement is chosen randomly. In addition, a ghost may occasionally change direction spontaneously. Running into a "powered up" Pac-Man causes the ghost to die. A dead ghost does not move. Finally, the appearance of each ghost should indicate whether it is alive or dead (e.g. by using different colors).
If Pac-Man dies or all of the pellets have been eaten, movement (both Pac-Man and ghosts) stops and a "Game Over!" (Pac-Man dies) or "You Win!" (all pellets eaten) message should be displayed.
The player starts a new game when the 'n' key is pressed.
The program should run at a reasonably playable speed, so choose the speed of Pac-Man and the ghosts and the "powered up" duration accordingly.
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.
In addition to the functionality requirements given above, you must:
Use classes and objects, following the class design given below. If you think you need to make changes, discuss it with me first!
Use subroutines/functions/private helper methods as appropriate to organize your code and to avoid repeated and nearly-repeated code. ("nearly-repeated" = almost the same with only a few values changed)
Include complete Javadoc-style comments for all classes, methods, and subroutines/functions.
Use arrays to store the collections of walls, pellets, and ghosts. When a pellet is eaten, remove it from the array by storing null in that slot of the pellet array — don't just make it invisible by failing to paint it. (This does mean that you will need to be careful to skip over null values when drawing or doing other things with pellets.)
Use loops wherever it is appropriate to do so - if you find yourself cutting-and-pasting code, consider whether a loop could avoid that!
Use named constants where appropriate.
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.
There are lots of possibilities for extra credit — start early so you have time to try some of them! Some possibilities include:
[small] Allow Pac-Man to have multiple lives. (Display the number of lives remaining on the game board.)
[small] Add scoring: 10 points per pellet eaten, 50 points per power pellet eaten, and 200 points per ghost killed. Display the score on the game board.
[small/medium] More elaborate graphics.
[small/medium] Pac-Man and the ghosts have some things in common. Use inheritance to reflect this, and to avoid repeated code. (We'll get to inheritance in class towards the end of the project.)
[medium] Add multiple levels so the player gets a different configuration of pellets and walls after clearing all of the pellets.
[medium] Duplicate more of the original game's behavior with respect to power pellets: instead of Pac-Man changing color and becoming "powered up", the ghosts change color and move more slowly. If Pac-Man touches one of the changed ghosts, the ghost is regenerated instead of being killed. (A regenerated ghost starts back at its original position, no longer moves more slowly/has a different color, and will kill Pac-Man if touched instead of vice versa.) The effect of the power pellet should still wear off after some period of time.
[medium] Duplicate the look of the original Pac-Man in terms of the placement of maze walls and pellets. Also allow Pac-Man to go off the screen on one side and reappear on the other.
[medium] Add bonus items which appear occasionally. Eating a bonus item earns points. Bonus items may disappear after some period of time if they are not eaten.
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.
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!)
Read through the whole handout and run the demo to see how your program should work.
Send an email with one or two questions about things you find confusing or unclear, something you aren't sure how to do, what you anticipate to be the most difficult part or the part you are most apprehensive of, etc — in short, aspects of the assignment that you find to be the muddiest. Try to be as specific as possible in identifying what the tricky points are.
Read through the class design given below — compare that to the description of the program given above, and think about what has been discussed in class about designing classes. (There's nothing to hand in here, but send an email if you have any questions about how one might come up with a class design in general or how this specific design was arrived at — the idea is to use this as an example of designing classes.)
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.
Create a new text file named pseudocode.txt in your project2 directory. Put your name in the first line of the file!
Write pseudocode for the main program, that is, for the three steps outlined: initializing the animation variables, drawing the scene, and updating the animation variables. Starting with drawing and/or updating the animation variables may help you identify what animation variables are needed.
Look through the class design, and write pseudocode for any methods that warrant it. This should include anything where it seems like you might have to figure out something, such as how to randomly place pellets without getting any on top of a wall or determining if Pac-Man and the ghosts can move.
Email the pseudocode you've developed.
Write the skeletion for each class listed in the class design. That is, write everything but the method bodies themselves — the class declaration, declarations for the instance variables, headers for the constructor(s) and methods, and the Javadoc comments for the class and methds. So that things compile, also include a default return for everything that returns something e.g. return true (or false) for boolean types, 0 for integer types, null for object types.
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...
Implement Pac-Man as described in the "Overview and Requirements" section above.
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.
class | properties (instance variables) | operations (methods) |
---|---|---|
PacMan |
|
|
Ghost |
|
|
Wall |
|
|
Pellet |
|
|
Maze |
|
|
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.
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.*;
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 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.
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.
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.
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.
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.
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.
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.