As usual, there are two components to this task. Both the Problem Set and Game Features - Learning Objective portions of this document MUST be completed to achieve the Learning Objective points and pass the course. The Game Features - Comprehensive Understanding is not needed for the Learning Objective requirement, but completing/not completing it will directly impact your grade through CU points. The Problem Set is worth 20 LO points, while the Game Engine Task is worth 30 LO points and 50 CU points.
You should complete the components below in the order they are listed; you will not be able to earn credit for the following component(s) until you complete the previous ones with all available points.
Coding Task 3 assesses Polymorphism and Binary Trees. Both the Problem Set and Game Engine Task will focus on these topics.
The video for this task can be found at https://www.youtube.com/watch?v=l3NGl37kjY4. This video gives an overview of the requirements for this task, namely the Game Features Learning Objective. As always, watching it is optional, but you will find it very useful.
Problem Set GitHub Repository link: https://github.com/CSE-116/ProblemSet-3
Once you have the project opened in IntelliJ, you'll see a src folder containing 2 Java packages named problem and tests.
To submit your project, run problem.Zipper, which will create a zip file containing the
problem set, and submit it to Autolab.
For this problem set, you will write the follow 3 static methods that are all related to binary trees (the Game Features will include Polymorphism). The signatures for these methods exist in the handout repo and the methods are all stubbed out.
problem.ProblemSet3 class, complete the maxBST method, which takes a
BST of Integers as a parameter and returns an int. This method
returns the maximum value of the BST according to its Comparator.
Comparator. Maximum in this context can be thought of as the
value that would come last if the values were fully sorted using this Comparator.
BST is empty (ie. the root is null) this method should return 0.
problem.ProblemSet3 class, complete the maxBinaryTree method, which
takes a BinaryTreeNode of Integers and an Integer
Comparator as parameters and returns an int. This method returns the maximum
value of the binary tree, defined by the parameter node as the root of the tree, according to the
provided comparator.
maxBST, except the tree no longer has the structure of a
BST. You should use the compartor to perform any comparisons that are needed to find the maximum
value according to that comparator.
problem.ProblemSet3 class, complete the isBST method that takes a
BinaryTreeNode of Integers and an Integer Comparator
as parameters and returns a boolean. This method returns true if the binary tree, defined
by the parameter node as the root of the tree, is a valid BST according to the provided comparator, and
false otherwise.
Write JUnit tests for the following methods from the specification in the tests.TestProblemSet3
class. These tests should be annotated with @Test.
maxBST
maxBST method. Test that the proper value is returned in a
variety of cases.
Integer Comparators to create these BSTs.
maxBinaryTree and isBST
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.
Feedback from Autolab will be given in several phases. If you don't complete a phase, then feedback for the following phase(s) will not be given. You must complete all phases to earn the required score of 20 LOs. For the problem set, the phases will be as follows:
Once you have successfully completed all three phases, you will have completed this Problem Set and Autolab will confirm this with a score of 20 LOs.
You will continue to build functionality for the Game Engine in this component of the task. 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 components.
As with the previous tasks, there are two parts to the Game Features component: the Learning Objective and Comprehensive Understanding. The Learning Objective portion must be completed with a score of 30 LOs before the Comprehensive Understanding unlocks. Both portions of the Game Features will be submitted to the same assignment on Autolab.
The content of the Learning Objective is primarily used in the Game Engine by Pacman. To play Pacman, during and
after completing this Learning Objective, navigate to the class app.Configuration and change the
GAME constant to "pacman".
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.
You will need to use various utility methods found in the app.gameengine.utils.PacmanUtils class for
this task. However, one of these methods is missing from the handout. You can find it here, and should copy that method into your own class.
For this Learning Objective 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.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 should create the TestTask3 class in your app.tests package. 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.
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.
Many of the features of this task, primarily dealing with the player's inventory and collectibles, heavily rely on each other, so one feature being implemented incorrectly may cause tests for other features to fail. Feedback in Autolab will usually note when this applies, but keep this in mind when looking over your feedback.
You should read through all the following sections before you begin to implement the methods from the specification. Note that you will not receive feedback on the correctness of these methods until you've completed the testing component of this task (see the "Autolab Feedback" section for more details). 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 the BST class
from the Problem Set.
For the Learning Objective portion of the Game Features component for this task, 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. If you did not already, download the missing method
canAct from here and add it to this class.
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 the Task 2 CU, but is
implemented differently within the Ghost class. So, even if you did not complete
that CU, 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. You
should create an additional Vector2D instance variable to store the last whole-numbered
location of the ghost, which will be used by several of the utility methods provided. The initial value
of this vector should be null.
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
Vector2D. 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. You
should create an additional Vector2D instance variable to store the last whole-numbered
location of the ghost, which will be used by several of the utility methods provided. The initial value
of this vector should be null.
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. You
should create an additional Vector2D instance variable to store the last whole-numbered
location of the ghost, which will be used by several of the utility methods provided. The initial value
of this vector should be null.
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
has 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. You
should create an additional Vector2D instance variable to store the last whole-numbered
location of the ghost, which will be used by several of the utility methods provided. The initial value
of this vector should be null.
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
Vector2D. 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 from the super class, which takes a double and a
Level as parameters and returns void.
DecisionTree instance variable is not 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.
Agent paths, you may keep that
here. You can place it before or after the new code.
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:
app.Configuration and change the GAME constant to "sample game".
Player class that are referenced here do not exist yet. You may want to
implement those methods first, or along with these classes and methods.
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.
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
MagicProjectile 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.
getHealth and
setHealth methods respectively.
removeActiveItem method. This method likely does not exist yet. Read ahead to see
the specification for that method.
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.
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.
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.
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".
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.
In the app.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 constuctor 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.
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.
Player:
addInventoryItem and cycleInventory:
cycleInventory will go through every added
Collectible and wrap around to the beginning.
removeActiveItem:
removeActiveItem properly removes the active
item from the inventory and sets the new active item.
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.
Feedback for the Learning Objective component of the Game Features will be given in several phases. If you don't complete a phase, then feedback for the following phase(s) will not be given. You must complete all the following phases to earn the required score of 30 LOs to complete this task. The phases for the Learning Objective are as follows:
Once you have successfully passed all four phases, you will have completed the Learning Objective component of the Game Features, and Autolab will confirm this with a score of 30 LOs.
For the Comprehensive Understanding 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 and Pacman. Each of these games adds a
score to the scoreboard whenever you win a game.
Each of the classes and methods you implement will have an associated CU count that you will earn upon passing our tests for that method. Partially implemented methods may result in some partial credit being earned, depending on the method.
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 (5 CUs)
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". 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 (25 CUs)
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.
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.
The Comprehensive Understanding tests will unlock once you have earned 30 LOs on the assignment. These will be run in a single phase for correctness. For each test you successfully pass, you will earn a varying amount of CUs with a total maximum score of 50 CUs.
There is no required testing component for the Comprehensive Understanding; however, you are expected to test your code nonetheless as the Autolab feedback is unlikely to be sufficient for debugging.