Game Features 3 assesses Polymorphism, Trees, and Testing. You will continue to build functionality for the Game Engine. This will be done on top of the code written for the previous tasks; you should not reclone or remove said code for this task or any future Game Features tasks.
This document is split into three sections for convenience. The first two of these sections will have a graded testing component. It is highly suggested you complete this testing component first to guide your implementation. You should not expect much, if any partial credit for buggy implementations. The third section does not explicitly require testing. You are nonetheless expected to thoroughly test it.
You'll need to create a class named TestTask3 in your tests package for this assignment. We have
provided some tests for the various Decision and Collectible subclasses, as well as the
LevelParser updates. They can be accessed at this link: TestTask3.java. You can open this link in a new tab to copy
and paste the content into your project, or right click and select "save link as", then download the file to the
correct location within the project.
In this section, you'll be testing and implementing an inventory system and collectible items via polymorphism. You will be given a specification for this inventory system that precisely defines the behavior of it, which should guide your development of tests.
For this portion, you will be writing code in the following classes which already exist:
Collectible, located in the app.gameengine.model.gameobjects package.MagicPickup, AxePickup, and PotionPickup, located in the
app.games.commonobjects package.
Player, located in the app.gameengine.model.gameobjects package.LevelParser, located in the app.gameengine package.You will also be writing code in the TestTask3 class as described in the "Game Features 3 Overview"
Read through this entire specification before you begin your implementation. You should set up
the Collectible class first by stubbing out (i.e. creating methods that
don't yet function) the specified methods in the Player class (some of them do not exist yet), write tests
for said methods in the Player class, then finally implement them.
Alongside your unit tests, you can test the functionality of your implementation in the sample game.
Collectible: Navigate to the app.gameengine.model.gameobjects.Collectible class, which
is an abstract class representing an object that can be picked up and used by the player. Currently this class
contains very little, but you will add extra functionality by adding methods and extending this class.
getItemID (note the capital "ID"), which is a getter for the itemID
instance variable. It should take no parameters and return a String.
use, which takes a
Level as a parameter and returns void. This represents what occurs when the
Collectible is used in game by the player.
collideWithDynamicObject method, which takes one
DynamicGameObject parameter and returns void. If the dynamic object is the
player, add this object to the player's inventory, and destroy this object.
GameObject is a Player
object by calling the isPlayer method on it.
addInventoryItem
method. This method likely does not exist yet. Read ahead to see the specification for that
method.
Player class instead of the
DynamicGameObject class, you cannot call it on the parameter. Instead, call
getPlayer on the game that you stored as an instance variable.
destroy method on it. This will remove
it from the level, so that it is not rendered on the screen anymore and will not continue
colliding with the player.
AxePickup: navigate to the app.games.commonobjects.AxePickup class, which is a pickup
that should allow the player to throw axe projectiles at enemies. Notice that this class extends
Collectible, and that the name passed to the super constructor is "Axe". Add the following
functionality.
Timer. Use the app.gameengine.utils.Timer
class, not one of the built-in Java timers. In the constructor, assign this instance variable to a new
Timer with a cooldown of 0.25.
Timer class allows objects to limit how frequently they can use used. In this
case, you will use the timer to limit how frequently axes can be thrown. You should look through
the methods in this class, but do not need to fully understand them.
Timer instance variable called getTimer, which takes
no parameters and returns a Timer.
use method from the Collectible class. If the
cooldown period on the timer is up, this method should make the player fire an axe projectile.
Timer.check method on your
instance variable. You should read the Javadoc comment to understand how to use this method.
Note that you do not care how many times the timer has overflown, just that it has.
Level parameter and call getPlayer to
get the Player object. On this Player, call
fireProjectile, which takes three arguments. The first argument is the projectile
to fire, which should be a new PlayerAxeProjectile object. The location of this
projectile does not matter, as it will be set within the fireProjectile method. The
second argument is the speed the projectile will be fired at, which should be set to exactly
5. The third argument should be the Level parameter.
update method, which has parameters of type double and
Level, representing the time since the last update and the current level, respectively, and
returns void.
Timer.advance on the Timer instance variable, passing in the
double parameter dt. This will ensure that the cooldown actually
decreases and the pickup can be used.
MagicPickup: Navigate to the app.games.commonobjects.MagicPickup class, which is a
pickup that should allow the player to fire magic projectiles at enemies. Notice that this class extends
Collectible, and that the name passed to the super constructor is "Magic". You should create the
same instance variables and methods as in the AxePickup class. The only difference should be that
in the use method, when calling fireProjectile, you should pass in a
PlayerMagicProjectile object, and a speed of 10.
PotionPickup: Navigate to the app.games.commonobjects.PotionPickup class, which is a
pickup that should allow the player to regain some health when used. Notice that this class extends
Collectible, and that the name passed to the super constructor is "Health Potion". Add the
following functionality.
int, and in the constructor, assign this instance
variable to the constructor parameter "heal". This is the amount that the potion will heal the player
by.
getHealAmount, which takes no
parameters and returns an int.
use method from the Collectible class. Increase the
health of the player by the heal instance variable, and remove the potion from the player's inventory.
getPlayer method on the Level
parameter.
getHP and
setHP methods respectively.
removeActiveItem method. This method likely does not exist yet. Read ahead to see
the specification for that method.
Now you will add methods for using and manipulating the player's inventory. In the
app.gameengine.model.gameobjects package, make the following additions to the
Player class to implement a functioning inventory system. Some of these methods will already exist, and
you should just modify them. Others you will have to create.
Note that the specification will not include specific implementation details for the following methods. For instance, it does not state an underlying data structure to use. You'll need to determine these details on your own. Any solution that satisfies the required functionality of the inventory is acceptable.
addInventoryItem: Create a method named addInventoryItem that takes in a
Collectible and returns void. This method should add the object from the parameter to
the Player's inventory.
removeActiveItem: Add a method named removeActiveItem which takes no parameters and
returns void. This method should remove whatever the currently active item is from the
Player's inventory.
getActiveItem: Add a method named getActiveItem that takes no parameters and returns a
Collectible. This method should return whichever Collectible in the inventory is
active.
getActiveItemID: Add a method called getActiveItemID (note the capital "ID") that
takes in no parameters and returns a String.
Collectible from the inventory, by calling
the Collectible.getItemID method on it.
String "No item equipped".
cycleInventory: Add a method called cycleInventory that takes no parameters and
returns void. This method will cycle through the inventory, changing the item currently marked as
active.
clearInventory: Add a method called clearInventory that takes no parameters and
returns void. This method will reset the inventory to its initial state, which should be empty.
update: The update method already exists in this class. Add code which iterates over every item in
the inventory and calls the update method on it, passing in both parameters.
You also need to implement the ability to use objects, which will occur in the app.gameengine.Level
class.
actionButtonPressed: In the Level class, implement the
actionButtonPressed method. The signature for this method already exists, you should simply add
functionality to it.
getActiveItem method on the level's player to access the
currently equipped Collectible. If there is an item equipped (ie. it is not null), this
method should call the use method on that item, passing in this as the
argument.
Player for a level by calling the getPlayer method.
Level.actionButtonPressed method, allowing you to use the active item to fire projectiles or use
potions.
The final features you will add are modifications to the LevelParser class so that the kinds of
collectibles can be parsed. The sample game already contains an AxePickup and a
MagicPickup in a couple of the levels. If you want to use the PotionPickup in game, you
can add it to one of the levels. An example of this would be adding this line to one of the level csv files:
"StaticGameObject,PotionPickup,13,13,50".
You can also create levels with the level editor which contain these objects.
readStaticObject: Modify this method so that it can recognize and return the following objects.
AxePickup objects. If the string being checked is "AxePickup", a new
AxePickup object should be returned. The AxePickup constructor
takes two doubles and a Game. The doubles should be the variables
x and y, and the Game should be the parameter game,
all of which already exist in this method.
MagicPickup objects. If the string being checked is "MagicPickup", a new
MagicPickup object should be returned. The MagicPickup constructor
takes two doubles and a Game. The doubles should be the variables
x and y, and the Game should be the parameter game,
all of which already exist in this method.
PotionPickup objects. If the string being checked is "PotionPickup", a new
PotionPickup object should be returned. The PotionPickup constructor
takes two doubles, an int, and a Game. The doubles
should be the variables x and y, and the Game should be the
parameter game, all of which already exist in this method. The int should be
the amount to heal, which is the fifth value in the line. Refer to the example above, where the amount
to heal would be 50.
As previously stated, you are given some tests for the Collectible
subclasses and LevelParser updates. You are NOT given tests for the
Player methods from the specification. For this portion of the task,
you will be writing tests for these methods.
Autolab will run your code against "incorrect solutions" to determine if you wrote tests for a given feature. If your tests can catch all the bugs the "incorrect solutions" contain for this feature, you'll earn points for successfully testing this feature. In total there will be 20 points available for testing. Since these tests are not being ran on your own solution, you can (and are encouraged to!) submit to Autolab before you complete your implementation to check your tests and earn points for feature coverage.
Note that to earn any points for testing, all of your tests must work on a correct solution. That is, your tests must be testing the behavior correctly and follow the specification. Additionally, while you can make whatever helper methods you want in your code to aid your implementation, the tests can only contain methods that were given to you in the handout code or methods that were defined in a handout and completed as part of a task. If you include something you shouldn't, you won't be able to earn any points for testing as the grader won't be able to run the tests.
With that in mind, your objective is to write JUnit tests for the following methods from
the specification. These tests should go in the TestTask3 class and should be annotated
with @Test.
Note that these are NOT static methods. The class name is included for identification.
Player.addInventoryItem and Player.cycleInventory
cycleInventory will go through every
added Collectible and warp around to the beginning.
Player.removeActiveItem
removeActiveItem
properly removes the active item from the inventory and sets the new
active item.
Implement the methods from the specification. You should complete the Testing Requirements before or during your implementation to aid you.
In total, your implementation will be worth 20 points. You may need to have several components of the specification fully functional before you earn any points for a given feature.
Autolab feedback will be given in several phases. These phases dictate the order you are expected to complete this assignment. Although you can still earn points from completing these phases out of order, you are nonetheless expected to have the previous phases completed before trying to fix a later one.
The content of this portion is primarily used in the Game Engine by Pacman. To play Pacman, during and
after completing this portion, navigate to the class app.Configuration and change the
GAME constant to "pacman". As the size of the Pacman level is quite large, you should also
set the value of ZOOM to 2.0 or 1.0 (it can technically be non-whole, but not
without serious graphical bugs).
It may look like there are a lot of classes to create and a lot of code to write for this task, but don't let this intimiate you. While that is true, most of the individual pieces are not overly complex, and have a lot of similarity between them. The amount of unique code you must write, and problems you must solve, is roughly in line with earlier tasks.
For this portion, you will be writing code in the following classes which already exist:
Agent, located in the app.gameengine.model.gameobjects package.Ghost, located in the app.games.pacman package.
Your tests for this portion should also go in TestTask3. As stated before, you
are given some tests for the Decision subclasses. These tests are not overly thorough, but should validate some of the basic behavior of the features that they test,
and let you know if you are on the right track.
You will also have to create many additional classes, which will be specified in the following instructions.
As with the previous part, you should read through all the following sections before you begin to implement the methods from the specification. Note that you can receive feedback for your tests before your implementation is complete, however, you must at least create every class/method from this specification to receive feedback. You can "stub out" these methods by having them always return a fixed value, but they must exist so the grading code, and your tests, can compile and run.
Some of the methods in this task will make use of the BinaryTreeNode class, found in the
app.gameengine.model.datastructures package. You are required to use this class. It is identical to the
BinaryTreeNode class used in the Problem Set. Note that this is not a BST.
For this part, you will implement the following functionality:
Create the package app.gameengine.model.ai. Remember that a package is just a folder containing
Java files. This package will contain a number of files that allow the ghosts in Pacman to have dynamic behavior
depending on the state of the game at any given time.
You will be using a decision tree to determine behavior, which will be represented as a binary tree of
Decision objects. These decisions will determine either which action to take, depending on the game
state, or perform an action, like movement.
Decision: In the app.gameengine.model.ai package, create an
abstract class named Decision. This represents a node in the decision
tree that will either make a decision or take an action.
Agent and a String as parameters, and
store them as instance variables. These represent the Agent whose behavior is being is
being controlled by this Decision, and the name of this node.
getName and setName, which are a getter and setter for
the String name from the constructor. The purpose of the name is primarly for testing, and
for differentiating nodes of the same type within a single tree.
getAgent which is a getter for the Agent from the
constructor. You do not need to create a setter for this instance variable.
decide which returns a boolean
and takes in a double and a Level, representing the time since the last call
and the current level, respectively.
doAction which returns
void and takes in a double and a Level, representing the time
since the last call and the current level, respectively.
Agent to achieve the desired behavior.
This method will only be called if it is determined that this is the action that should be
taken, according to the rest of the decision tree.
DecisionTree: In the app.gameengine.model.ai package, create a class named
DecisionTree. This class will possess a binary tree of Decision objects, to determine
what behavior an Agent should take, and accordingly take that action.
BinaryTreeNode of Decisions, and store
it as an instance variable.
getTree and setTree, which are a getter and setter for
the BinaryTreeNode from the constructor.
traverse that returns a Decision and takes a
BinaryTreeNode of Decisions, a double, and a Level
as parameters. The double represents the amount of time passed since the last update, and
the Level is the current level.
decide on
the Decision stored in each tree node. A value of true indicates that
you should "travel" to the right child node, and a value of false means travel to
the left child node.
Decision value at that node should be returned. Keep in mind that the node in the
parameter itself might be a leaf node.
decide, you should pass in the double and
Level from the parameters as arguments.
decide method returns true, you cannot traverse to the right, and
should return null. This is useful for handling poor tree structures, or having a state that
performs no action.
traverse method that returns void and takes a
double and a Level, which represent the amount of time passed since the last
update and the current level, respectively.
traverse method, passing the root of the tree (the
instance variable) as the first argument, and the parameters as the remaining arguments. Then
call doAction on the returned Decision, again using the parameters of
this method as the arguments.
Decision is null, this method will do nothing.
Now you will create subclasses of the Decision class which implement behavior for the ghosts in
Pacman. You should look at the app.gameengine.utils.PacmanUtils class, as you will need to use
these methods within your Decision subclasses. You do not need to understand all of this code, and
the specific methods to use will be mentioned each time.
Though you do not need to understand the implementation of these methods, you should understand what they do. They each include a Javadoc comment that explains the purpose of the method, the parameters, and the return value. In IntelliJ, you can hover over the name of a method to read the Javadocs.
All of these classes will exist in the app.gameengine.model.ai.pacman package, which you should
create.
Remember that the Decision class has two main functionalities, the decide and
doAction methods. For some subclasses, only one of these will be implemented, and such classes will
only make sense in certain parts of the tree. For example, a subclass which stubs out the decide
method and actually implements the doAction method would only make sense as a leaf node.
Other decisions and actions are so closely related that it makes sense to include them within the same class. These classes could be placed anywhere within the tree, and in fact there will likely be duplicates, where one is used as the decision, and the other is used as the action.
IsActive: In the app.gameengine.model.ai.pacman package, create a class named
IsActive which extends Decision. This class will represent a decision on whether a
ghost is in an active state, which is either chasing the player or retreating to one corner of the level
(scattering).
Ghost and a String, which are passed to
the super constructor as the Agent and name. This is possible due to polymorphism, since
Ghost is a subclass of Agent. You should also store the ghost as an instance
variable.
decide method, which should return true if the Ghost
from the constructor is in a chasing or scattering state, and false otherwise.
getState method on a Ghost to access its state. This
method returns a String representing which of several states the ghost is in.
"Chase" or "Scatter", then the ghost is
considered to be in an active state, and this method should return true.
doAction method to do nothing.
Idle: In the app.gameengine.model.ai.pacman package, create a class named
Idle which extends Decision. This class will represent an action of bouncing up and
down while the ghost is at its home location in the center of the level.
Ghost and a String, which are passed to
the super constructor as the Agent and name. You should also store the ghost as an instance
variable.
decide method to always return false.
doAction method to do the following: If the magnitude of the ghost's velocity
is greater than 0, this method should do nothing. Otherwise, you should negate both the x and y
components of the ghost's
orientation. Afterwards, you should call the followPath method on the Ghost
object, passing in the double from the parameter.
magnitude method within the Vector2D
class.
setOrientation method. Although
there exists a negate method in the Vector2D class, which negates both
the x and y components of a Vector2D, the setOrientation method is
needed to update the sprite of the ghost.
followPath method is the same as the one from Game Features 2, but is
overridden by the Ghost class. So, even if you did not complete
that method, this will still function as intended.
Chase: In the app.gameengine.model.ai.pacman package, create a class named
Chase which extends Decision. This class will represent both a decision of whether a
ghost is actively chasing the player, as well as the action of chasing.
Ghost, a PacmanGame, and a
String. You should pass the Ghost and String to the super
constructor, and store the Ghost and PacmanGame as instance variables.
Vector2D instance variable to store the last whole-numbered
location of the ghost. The initial value of this vector should be null. This will be used by several of
the utilities provided, namely to ensure that the ghost does turn 180 degrees, and only changes
directions when fully aligned to a tile.
decide method to return whether the ghost is in the chasing state.
getState method on a Ghost to access its state. This
method returns a String representing which of several states the ghost is in.
"Chase", then the ghost is chasing the player, and this
method should return true.
doAction method. This method should use the methods in the
PacmanUtils class to do the following sequence of actions. Only take these actions if the
PacmanUtils.canAct method returns true, passing in the Ghost and
Vector2D instance variables and the double parameter.
PacmanUtils.getChaseTarget
method. Note that you must pass in a PacmanLevel, so you cannot use the
Level parameter. Instead you should call getCurrentLevel on the
PacmanGame instance variable.
PacmanUtils.getValidDirs method, again with the game's current level and the ghost.
PacmanUtils.getBestDirection
method, passing in the valid directions, the location of the ghost, and the target
Vector2D.
Vector2D returned by getBestDirection may be null. If so, skip
this step. If it is not null, set the orientation of the ghost to the x and y components of that
best direction. Again, you should use the setOrientation method to do so.
Vector2D instance variable equal to the rounded value of the ghost's
location. You may call Math.round on the individual components, or use the
Vector2D.round method.
followPath method on the ghost object, passing in the double
parameter. You should do this even if the canAct method returns false.
Scatter: In the app.gameengine.model.ai.pacman package, create a class named
Scatter which extends Decision. This class will represent the action of scattering,
which is when the ghosts retreat to the corners of the level to give the player some breathing room.
Ghost, a PacmanGame, and a
String. You should pass the Ghost and String to the super
constructor, and store the Ghost and PacmanGame as instance variables.
Vector2D instance variable to store the last whole-numbered
location of the ghost. The initial value of this vector should be null. This will be used by several of
the utilities provided, namely to ensure that the ghost does turn 180 degrees, and only changes
directions when fully aligned to a tile.
decide method to always return false.
doAction method. The behavior of this method should be identical to that of
the Chase class, except using the PacmanUtils.getScatterTarget method instead
of the PacmanUtils.getChaseTarget method.
Dead: In the app.gameengine.model.ai.pacman package, create a class named
Dead which extends Decision. This class will represent the decision of whether a ghost
has been eaten and should return to the spawn area, as well as the action of returning home.
Ghost, a PacmanGame, and a
String. You should pass the Ghost and String to the super
constructor, and store the Ghost and PacmanGame as instance variables.
Vector2D instance variable to store the last whole-numbered
location of the ghost. The initial value of this vector should be null. This will be used by several of
the utilities provided, namely to ensure that the ghost does turn 180 degrees, and only changes
directions when fully aligned to a tile.
decide method to return whether the ghost is in the dead state.
getState method on a Ghost to access its state. This
method returns a String representing which of several states the ghost is in.
"Dead", then the ghost is dead, and this method should
return true.
doAction method. The behavior of this method should be identical to that of
the Chase class, except using the PacmanUtils.getHomeTarget method instead
of the PacmanUtils.getChaseTarget method. Note that this method only takes one argument,
and does not need the ghost itself to be passed in.
Flee: In the app.gameengine.model.ai.pacman package, create a class named
Flee which extends Decision. This class will represent the decision of whether a ghost
is fleeing from the player, which happens when a power pellet has been eaten, as well as the action of running
from the player, which involves moving in a random direction at each intersection.
Ghost, a PacmanGame, and a
String. You should pass the Ghost and String to the super
constructor, and store the Ghost and PacmanGame as instance variables.
Vector2D instance variable to store the last whole-numbered
location of the ghost. The initial value of this vector should be null. This will be used by several of
the utilities provided, namely to ensure that the ghost does turn 180 degrees, and only changes
directions when fully aligned to a tile.
decide method to return whether the ghost is in the frightened state.
getState method on a Ghost to access its state. This
method returns a String representing which of several states the ghost is in.
"Frightened", then the ghost is fleeing, and this method
should return true.
doAction method. The behavior of this method is similar to but not exactly
the same as that of the Chase class. Take the following actions if the
PacmanUtils.canAct method returns true, passing in the Ghost and
Vector2D instance variables and the double parameter.
PacmanUtils.getValidDirs method, again with the game's current level and the ghost.
PacmanUtils.getRandomDirection
method, passing in the valid directions.
Vector2D returned by getRandomDirection may be null. If so, skip
this step. If it is not null, set the orientation of the ghost to the x and y components of that
random direction. Again, you should use the setOrientation method to do so.
Vector2D instance variable equal to the rounded value of the ghost's
location. You may call Math.round on the individual components, or use the
Vector2D.round method.
followPath method on the ghost object, passing in the double
parameter. You should do this even if the canAct method returns false.
Agent: Navigate the to app.gameengine.model.gameobjects.Agent class. Each
Agent will possess a DecisionTree to determine and follow their behavior.
DecisionTree within this class. It will initially be
null.
DecisionTree, named getDecisionTree and
setDecisionTree respectively.
update method.
DecisionTree
instance variable is not null. If it isn't null, call traverse on it. You should call the version of the method which does not take in a
BinaryTreeNode. If the DecisionTree is null, you do not need to do
anything.
Ghost: Navigate to the app.games.pacman.Ghost class. You will modify the constructor
to give ghosts a decision tree matching classic pacman behavior. You can add this anywhere in the constructor.
Ghost extends Enemy, which extends Agent, so
Ghost has access to all of the methods in the Agent class.
IsActive node.
Chase node, acting as a decision;
if the ghost is in the chase state (to the right), there is another Chase node,
this time acting as an action; if the ghost is not in the chase state (to the left), there is a
Scatter node.
Dead node, acting as a
decision; if the ghost is dead, there is another Dead node, this time acting as an
action; if the ghost is not dead, there is a Flee node, acting as a decision; from
here, if the ghost is fleeing, there is another Flee node, this time acting as an
action; if the ghost is not fleeing, there is an Idle node.
DecisionTree structure:
You should now be able to play Pac-Man and see the ghosts react dynamically to your actions. Every so often, on a timer, all of the ghosts will retreat (scatter) to the corners. If you eat a power pellet (the big ones), they will turn blue and start moving randomly. If you eat them they will return home, respawn, and begin chasing you again.
You may notice ghosts occasionally getting stuck on level geometry when moving around corners. That is an unfortunate amount of jankiness within the game engine at the moment, and does not signify any issues with your implementation.
In the tests package, create a class called TestDecision, which extends the
Decision class. This class will be used to help you test by simplifying the behavior of the
decide and doAction methods. Specifically, we want the behavior of these methods to not
depend on the actual state of the game, so that you can test the DecisionTree methods without setting
complex properties of the game, player, and ghosts.
The constructor should take an Agent, a String, and a boolean. The first two
parameters can be passed to the super constuctor, and the last should be stored as an instance variable. This
boolean will be the value returned by decide, so it will decide which direction the traversal
progresses.
You should create another boolean instance variable representing whether the TestDecision
has been used. It should initially be false.
Create a method called isUsed, which takes no parameters and returns a boolean. It should return the
instance variable described above.
Override the decide method to return the instance variable for the traversal direction.
Override the doAction method to set the instance variable for whether it has been used to
true.
If your testing utility is correct, you will earn 5 points.
The testing requirement is done in a similar matter to the "Inventory" part of the assignment. It will be worth 15 points, with points earned for each feature successfully tested.
Write JUnit tests for the following methods from the specification in the TestTask3 class. These tests
should be annotated with @Test.
DecisionTree:
Decision objects to populate the
DecisionTree. You are highly encouraged to use your TestDecision class.
traverse methods in this class.
BinaryTreeNode returns the proper
Decision, or null, with a variety of trees.
doAction on the correct Decision node, and
only on that node. Using your TestDecision class will greatly simplify
this check.
Implement the methods from the specification. You may wish to complete the Testing Requirements before you begin implementation, and it's suggested that you run these tests as you implement the methods.
Your implementation of this specification is worth 20 points.
Autolab feedback will be given in several phases. These phases dictate the order you are expected to complete this assignment. Although you can still earn points from completing these phases out of order, you are nonetheless expected to have the previous phases completed before trying to fix a later one.
For this portion of this task, you will implement functionality in the
app.gameengine.statistics.Scoreboard class, and several classes that you will create in the
app.gameengine.model.datastructures package, in order to create a functional scoreboard for several of
the games. At the moment, the only games with scoreboards are Minesweeper, Pacman, and Snake. Each of these games adds a
score to the scoreboard whenever you win a game.
There is no graded testing for this part. You are expected to write tests on your own to verify your functionality.
app.gameengine.statistics.GameState
class. It is a small, simple class, so you should look at and understand this class.
app.gameengine.model.datastructures.Comparator class. Currently, this is a
concrete class with a stubbed out method called compare. Modify this class such that it is
an interface instead of a class, and make this method abstract. Do not modify the rest of the method
signature. This is not worth any CUs on its own, but is required for the rest of the comparator classes.
Scoreboard class, which tries to
instantiate a Comparator. You can fix this by changing this to be any of the
classes which implement the Comparator below, or by removing this constructor
entirely. If you remove the constructor, you should call the other constructor from within the
Game class when creating the scoreboard instance variable. Any of these solutions
are fine, and we won't test this constructor, or the default comparator.
LevelNameComparator: In the app.gameengine.model.datastructures package,
create a class named LevelNameComparator. This should implement the
Comparator<GameStat>.
compare method to compare two GameStat objects based on
their entry names. If the first name is lexicographically less than the second, this method
should return true, and it should return false in all other cases.
String.compareTo method useful for performing this comparison.
false, since a
String cannot be lexicographically less than itself.
PlaytimeComparator: In the app.gameengine.model.datastructures package, create
a class named PlaytimeComparator. This should implement the
Comparator<GameStat>.
compare method to compare two GameStat objects based on
their playtimes. If the first playtime is less than the second, this method should return
true, and it should return false in all other cases.
false.ScoreComparator: In the app.gameengine.model.datastructures package, create a
class named ScoreComparator. This should implement the
Comparator<GameStat>.
compare method to compare two GameStat objects based on
their scores. If the first score is greater than the second, this method should return
true, and it should return false in all other cases.
false.app.gameengine.statistics.Scoreboard class. Modify the constructor which
takes two parameters so that the Comparator is stored as an instance variable.
getComparator and
setComparator respectively.
BinaryTreeNode of GameStats. This will
act as a BST to store and sort the scores in a game according to the comparator.
getScoreTree and
setScoreTree, respectively.
addScore method to add the input GameStat to the correct
location in the tree, according to the comparator.
setComparator method will only ever be called before any scores are added.
loadStats method
loadStats method to load the existing stats from previous games. This
will ensure that high scores carry carry over when the game is closed and restarted. Note that this
method requires a functioning implementation of the addScore method.
statsPath instance variable will be the location of the csv file which stores
the stats. Each csv file will have one score per line, as "entryName,playTime,score", which are a
String and two doubles, respectively. You should read each line in this file,
parse it to create a GameStat object corresponding to that line, and add it to the tree
using the addScore method.
getScoreList methods
getScoreList method to return a LinkedListNode that is
the head of a sorted list containing every element in the tree, according to the comparator.
getScoreList which takes a BinaryTreeNode of
GameStat as a parameter and returns a LinkedListNode that is the head of a
sorted list containing every element in the input tree.
If you wish to see the Scoreboard in game, you should go to the respective Game subclass for the
game you wish to enable it for (e.g. MinesweeperGame) and uncomment the line
from the constructor that instantiates the scoreboard. Now, when you complete a game of Minesweeper or Pacman,
you should see an entry added to the scoreboard, which is accessible from the pause menu, reached by pressing
the escape key. The easiest way to test this is by
playing Minesweeper, and pressing escape before selecting a difficulty. This will default to the trivial
difficulty, which only has 3 mines and should be quite easy (as long as you understand the rules of
Minesweeper).
If you wish to add a scoreboard to other games, like snake, you can do so, but this is optional. There are a few
adjustments you need to make in order for this to work. First, in the SnakeGame constructor, you
should set the comparator to the desired metric. You must then choose a place to add a score to the scoreboard.
It would likely make the most sense to add this in both the advanceLevel and
resetCurrentLevel methods, so that both winning and losing the game save the score. In either case,
you can use the getScoreboard method to get the scoreboard, and then add a new
GameStat entry. You can use the getScore and getPlaytime methods of the
Level class to create the GameState.
Since there is no testing requirement for this part, Autolab feedback will only come in one phase for correctness. For each feature you pass, you will earn some amount of points. If you pass all of the features associated with this part, you will earn 20 points.