| CPSC 124 | Introduction to Programming | Fall 2006 |
The programming projects are an opportunity for you to work on a larger program, where you create more of it on your own from scratch. To begin, copy the files from /classes/f06/cs124/labs/pa3 to your home directory. This directory should contain three files: SnakePit.java, Snake.java, and GameBoard.java. You will complete Snake.java and GameBoard.java (which contain some skeleton code). SnakePit.java contains my solution to programming assignment 2. You should not have to modify this file. Feel free to swap in your copy of SnakePit.java. The directory also contains a directory called working/ which has class files for running a working copy of SnakePit. This will allow you to see how the program should work.
For an overview of the game and a description of the structure of the program see the writeup for programming assignment 2. Below are some details that are specific to this assignment.
The GameBoard class extends JPanel and overrides the paintComponent() method to draw the board onto the panel. Around the outside of the board, the paintComponent() method should draw a yellow border, which will represent the walls of the game (there should some space between the walls and the edges of the panel). The yellow border should be 10 pixels thick. Within the yellow border is the actual playing field. It is split into 50x50 block squares where each block contains 10x10 pixels. The GameBoard class should store the color of each block in a 2-dimensional array grid[][]. This array is set in two places: initBoard() when the game is started and setColor() (discussed below). There are three possible colors for any particular block: black (unoccupied), blue, (occupied by the snake) or red (occupied by an apple). The Snake class (discussed below) handles coloring the Snake cells blue. GameBoard class colors all the other blocks. It uses the snake object (passed in via initBoard()) to determine, which blocks belong to the snake. All other blocks it colors black (if empty) or red (if contains an apple). GameBoard uses another 2-dimensional array, apples[][], to keep track of which blocks in the playing field hold apples. This array is a boolean array with an entry per block (the dimensions and sizes are the same as Grid[][]). If an entry is true, then that corresponding block holds an apple, otherwise, it does not hold an apple. This array should be randomly set using the number of apples inputted to initBoard(). At the start of the game, the apples[][] array is used to set the color in Grid[][] for the blocks that contain apples. As the game is running, this array is used to determine when a snake eats an apple (i.e., its head moves to a block containing an apple). The GameBoard class also keeps track of how many apples have been eaten. Each time an apple is eaten, a counter (called applesEaten) is incremented. This is used to calculate the score at the end of the game (score = 10*applesEaten). In addition, the GameBoard class listens for both button clicks and key pushes (i.e., implements ActionListener, KeyListener). You will need to implement the actionPerformed() and keyReleased() methods to handle these two types of events. The methods that you will have to modify or implement are discussed below.
initBoard() initializes the blocks that are not occupied by the snake to black and randomly sets some of the blocks to red, to indicate positions occupied by an apple. initBoard() does this by setting the entries in Grid[][] (which has an entry per block in the playing field) to the appropriate color. The snake object, which is an input to initBoard(), is used to avoid coloring a block that is occupied by the snake either black or red. The number of apples is also an input to initBoard(). initBoard() can assume that this is a reasonable value: greater than 0 and not larger than the number of blocks on the board. initBoard() randomly selects blocks on the board to contain apples based on this input. For each block that will contain an apple, initBoard() sets the corresponding entry in apples[][] to true. All other entries are set to false.
setColor() allows the color of a block to be set by other classes. In particular, the snake class should use setColor() to the set the blocks occupied by a snake cell to blue.
paintComponent() does the actual painting of the board (note: all other methods just manipulate grid[][]). paintComponent() should call the paintComponent() in the parent class (JPanel class), before painting. It should first paint everything black, then paint the walls yellow, and finally use grid[][] to paint each block the appropriate color. paintComponent() must also check if the gameEnded flag has been set to true (done when the SnakePit class calls gameOver()), and if so, to paint the final text on the screen. If the game is over, it should print three lines: (i) "Game Over", (ii), either "You win!" or "You lose", and (iii) "Final Score: 40" ("40" varies per game based on how many apples were eaten - the number should be 10 * number of apples eaten).
isAppleAtPosition() takes as input a row and column, and should return a boolean indicating whether that position contains an apple. It should use the array apples[][] to determine if an apple exists at a particular block.
eatApple() takes as input a row and a column. It should only be called if there is an apple at the specified position. If there is not an apple at the inputted position (row, column) it should throw an exception. Otherwise, it should increment its count of the number of apples eaten, it should decrement its count of the number of apples remaining, and it should set the corresponding entry in apples[][] to false.
applesLeft() returns a boolean indicating whether their are apples remaining (used by the SnakePit class to determine when the game is over).
actionPerformed() is called whenever the start button is clicked. This should simply call beginGame() (a static method) in the SnakePit class.
keyReleased() is called whenever a key is released. You should check which key was released. If the key was an arrow, then your method should call changeDirection() (a static method) in the SnakePit class. Note that changeDirection() requires an argument that represents the direction that the snake should be moved (LEFT, RIGHT, UP, or DOWN). You should see chapter 6 for details on how to implement this method. Note: you should leave keyPressed() and keyTyped() blank You will only need keyReleased() to determine when an arrow key was hit.
The Snake class models the snake. Its main purpose is to keep track of the positions on the board of the snake, and to move the snake, when either move() or moveCurrentDirection() are called. The Snake class uses an array called snakeCells to keep track of each position of the snake cell. This array is an array of objects of type SnakeCell. The SnakeCell class is provided in the Snake.java file. It is a simple class with only a row (int) and column (int). So each entry of the snakeCells array is an object with two states of information: (i) the row on the board of that cell, and (ii) the column on the board of that cell. The head of the snake is always at entry 0 in the array (I still use a variable to mark the index of the head - but it's always 0). The tail of the snake can vary. The variable snakeTailIdx marks the index of the tail of the snake. At the beginning of the game, the tail should be 9, since the snake starts out with length 10. But each time the snake eats an apple, it should grow by 2. In that case the tail index should increase by 2 (discussed more below). When the snake is moved, the head should move to the new position, and each subsequent cell of the snake should follow the preceeding cell. For example, if the head is at row=1, cell=2 and moves left to row=1, cell=1, then the second cell of the snake should change to row=1, cell=2. The third cell of the snake should move to the position previously occupied by the second cell, and so on. The Snake class is also responsible for updating the colors on the board when it moves. It should use the setColor() method in the GameBoard class (the GameBoard object is passed in via the constructor) to update the colors on the board. Notice that it only has to color the new position of the head to blue (since that block is now occupied by the snake), and the old position of the tail to black (since that block is now empty). The methods that need to be modified or implemented are discussed below.
The constructor should initialize the snakeCells array, head and tail indices, and should color the blocks on the game board to blue that are occupied by the snake (using setColor() in the GameBoard class). The snakeCells array can grow, so you cannot give it length 10. However, it will never grow more than the number of total blocks, so you use this value to conservatively size the array. The tail index will indicate how much of the array is actually being used. For each entry of the array, you will need to create a new SnakeCell object and set the row and column appropriately. You can position the snake somewhere in the middle of the game board.
getHeadRow() should return the row of the head of the snake. This is stored within the first entry in the snakeCells array (snakeCells[0]).
getHeadCol() should return the column of the head of the snake. This is stored within the first entry in the snakeCells array (snakeCells[0]).
isMe() takes a row (int) and column (int) as input. It should return a boolean indicating whether the snake occupies this position (this method will be needed within the GameBoard class). This method should inspect each entry of snakeCells looking for entries that have the same row and column.
move() takes a row (int) and column (int) as input. It should move the snake head to this new position, and move all other snake cells the cell of the preceeding snake cell. If the boolean dead is true, then move() should immediately return since the snake is dead. move() must also make sure that the new position is a legal one. The snake is only allowed to move one block in either the left, right, up or down directions. If it is an illegal position to move to, then an exception should be thrown. move() should also check if the position is outside of the playing field (i.e., a wall). If so, it should set the dead boolean to true, and return without moving. Otherwise, move() should set the variable prevDirection (an int), which is used by moveCurrentDirection(), and then move the snake. The head of the snake should move to the new position, and each subsequent cell should move to the position of the previous cell (i.e., the second cell of the snake should move to the previous position of the first cell of the snake, and so on). move() must also color the board using setColor() in the GameBoard() class. It should color the new head position to blue, and the old tail position to black. Finally, it should check if an apple was eaten and if so to increase the tail index by 2. There are some variations on growing the snake. You can grow it immediately, or you can grow it one cell per call to move (which makes it look more like it's growing).
moveCurrentDirection() used prevDirection(), which is set in move(), to move in the same direction. moveCurrentDirection() should compute the new row and column of the head, and then use these to call move().
Use my .class files to run the program and see how it's supposed to work. To run the program, cd into the directory working/ and then use "java SnakePit". Your program should eventually work like mine.
You should write this program incrementally. Start out by writing just the GameBoard.java file. You can copy the Snake class files (Snake.class, Snake$1.class, and Snake$SnakeCell.class) from the working/ directory to your current directory. This will give you a working Snake class with which you can test your GameBoard class. After you finish writing the GameBoard class, you can then start working on the Snake class.
Remember the collaboration policy for individual programming assignments: This assignment is to be completed by you. You are not allowed to talk to other students about any aspect of this assignment, including general plans or specific debugging problems. You may ask a tutor for debugging help only and must indicate in writing who helped you and explain how they helped. You may talk to me as much as you want about any aspect of the assignment - general strategy, programming details, debugging help, etc.
Your programs should be heavily commented. At the start of the two programs should be a multiline comment describing the program and indicating the author. At the start of all methods should be a description of the method does, what it's inputs are (i.e., parameters), and what it returns. You should also comment all complicated statements within the methods. You should also properly indent all of your code and use indicative variable names.
In addition, you should write up how your program works in a file called SnakePit.txt. This should be at least a half of page describing in detail how your program works. It should also describe how any help you received from tutors, teaching assistants, or the instructor.
Verify that your pa3 folder contains all the files you modified, then copy your entire folder pa3 to the handin directory ~mcorliss/handin/124/username (where username is replaced with your username).