As with the previous and future tasks, there are two components to this task.
To reiterate, 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. Each Problem Set will carry 20 LO points, while each Game Engine Task will carry 30 LO points and 50 CU points.
Note: Although the CU content is not required, some of it may benefit you in later tasks. Specifically, writing the
Agent.followPath
method will make testing your Task 4 code in-game much easier.
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 2 assesses Linked Lists and Inheritance. Both the Problem Set and Game Engine Task will focus on these topics.
Problem Set GitHub Repository link: https://github.com/CSE-116/ProblemSet-2
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.
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.
For this Problem Set, you will implement the following functionality. Note that all of the methods are non-static.
Monster
: Create an abstract class in the problem
package
named Monster
. This class should implement the following methods:
String
and an int
representing the name and maximum health of the monster. The current health of the monster should
initially be set to the maximum amount.
Monster
is created with a max health of 10, the initial health
would also be 10. The max health should never change, even if the current health does.
getName
: create a method named getName
which takes no arguments and returns a
String
. This method should return the name of the Monster
.
setName
: create a method named setName
which takes a single
String
and returns void
. This method should set the name of the
Monster
to the input value.
getHP
: create a method named getHP
which takes no arguments and returns an
int
. This method should return the current health of the Monster
.
setHP
: create a method named setHP
which takes a single int
and
returns void
. This method should set the current health of the Monster
to the
input value.
getMaxHP
: create a method named getMaxHP
which takes no arguments and returns
an int
. This method should return the maximum health of the Monster
.
attack
: create an abstract method named attack
which takes a
reference
to a Monster
object and returns void
.
Skeleton
: Create a class in the problem
package named Skeleton
. This
class should extend the Monster
class described above. This class should implement the following
methods:
String
two int
s
representing the name, maximum health, and damage of the skeleton, respectively.
getDamage
: create a method named getDamage
which takes no arguments and
returns an int
. This method should return the damage value of the Skeleton
.
setDamage
: create a method named setDamage
which takes a single
int
and returns void
. This method should set the damage value of the
Skeleton
to the input value.
reanimate
: create a method named reanimate
which takes no arguments and
returns void
. If the Skeleton
's health is less than or equal to 0, it should
set it to the
maximum health of that object. If its health is greater than 0, it should do nothing.
attack
: override the abstract method of the same name from the Monster
class.
It should take a reference to a Monster
object and return void
. This method
should decrease
the health of the Monster
parameter by the damage of that Skeleton
.
Skeleton
was created with a damage of 5, and a
Monster
with a health of 8 was passed in, its health would be decreased to 3.
Skeleton
was created with a damage of 5, and a Monster
with a health
of 1 was passed in, its health would be decreased to -4.
StringList
: Create a class in the problem
package named StringList
.
This class will contain methods for using and manipulating LinkedListNode
s of String
s.
You must use the LinkedListNode
class provided, which can be found in
problem.datastructures
. This class should implement the following methods:
getList
: create a method named getList
which takes no arguments and returns a
LinkedListNode
of String
s. This method should return the entire linked list of
strings.
setList
: create a method named setList
which takes a
LinkedListNode
of String
s as an argument and returns void
. This
method should
set the linked list of strings to the input value.
addElement
: create a method named addElement
which takes a single
String
as an argument and returns void
. This method should append the input
String
to the end of the linked list.
getMidpoint
: create a method named getMidpoint
which takes no arguments and
returns a String
. This method should return the value at the middle index of the linked
list.
String
.
deleteElement
: create a method named deleteElement
which takes a
String
as an argument and returns void
. This method should remove the first
occurrence of
the input String
from the list.
Create a class in the tests
package named TestUtils
. In this class, create a
static method named compareListsOfStrings
that takes in two
LinkedListNode
s of String
s and returns void. This method should contain JUnit asserts to
verify that the two linked lists are of the same length and contain all the same String
s. If either the
length or content of the lists are not the same, this method should fail a JUnit assert.
Note that this method is not a test, and thus should not have the @Test
annotation. Rather, this is a
static method intended to be used in your testing. You are encouraged to use this method to simplify your test
cases.
Write JUnit tests for the following methods from the specification in the tests.TestProblemSet2
class.
These tests should be annotated with @Test
.
StringList
getMidpoint
method. Test that the proper value is returned in a variety
of cases.
deleteElement
method. Tests for this method are provided for
you in the TestProblemSet2
class. They are likely commented out, and must be uncommented
for you to use them. You can uncomment multiple lines by selecting the lines and pressing ctrl+/
(command+/ on mac). You are encouraged to use these tests to help you write the methods they are
testing.
Monster
and Skeleton
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 Mario. To play Mario, during and after
completing this Learning Objective, navigate to the class app.Configuration
and change the
GAME
constant to "mario"
.
For this Learning Objective portion, you will be writing code in the following classes which already exist:
LinearGame
, located in the app.gameengine
package.
PhysicsEngineWithGravity
, located in the app.gameengine.model.physics
package.
LevelParser
, located in the app.gameengine
package.
TestUtils
, located in the app.tests
package.
You should create the TestTask2
class in your app.tests
package. We have provided some
tests for the PhysicsEngineWithGravity
and LevelParser
classes for you, which can be
accessed at this link: TestTask2.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.
One of the tests relies on testing files, which should all be placed in the directory "data/levels/testing". They are: mario1.csv, mario2.csv, and medium.csv
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.
Many of the methods in this task will make use of the LinkedListNode
class, found at
app.gameengine.model.datastructures
. You are required to use this class. It is identical to the
LinkedListNode
class used in the Problem Set.
For the Learning Objective portion of the Game Features component for this task, you will implement the following functionality:
LinearGame
class. This class represents a Game
with a linear sequence of levels that are typically only traveled through in order from start to finish. For
example, the MarioGame
class extends the LinearGame
class, and uses these methods.
getLevelList
getLevelList
which takes no parameters and returns a
LinkedListNode
of Level
s. It should return the head of the game's list
of levels.
null
.
setLevelList
setLevelList
which takes a LinkedListNode
of
Level
s as an argument and returns void
. It should replace this game's
list of
levels with the input value.
addLevel
addLevel
which takes a Level
object as an
argument and returns void
. This method should append the input Level
to the end of
this game's list of levels.
advanceLevel
advanceLevel
which takes no parameters and returns
void
. When
this method is called, it should find the current level within the list of levels, and call
this.loadLevel
on the next level in the list.
getCurrentLevel
method to access the current level.
getName
method on each level object to determine if it is the
current level (ie. if the names are equal). This method may assume that there will not be
multiple levels with the same name in the list.
loadLevel
with an argument of null
.
removeLevelByName
removeLevelByName
which takes a String
and
returns void
. It should remove the first level in the list which has the same name
as the input
String
.
Level
s in the list exist with this name, only the first should be
removed.
getName
method on each level object to determine if its name is
the same as the input String
.
reset
reset
method on it.
this.currentLevel
directly, and set it to the desired value. Note that this
instance variable is of type Level
, not LinkedListNode
.
reset
called on it.
Otherwise, the player will start at the wrong location.
PhysicsEngineWithGravity
class. Notice that this class
extends the PhysicsEngine
class, and as such has access to all of the public methods from that
class, including those that you wrote in the previous task.
double
as a parameter and represents the amount of gravity.
You should create an instance variable to store the gravity, and assign it in the constructor.
getGravity
getGravity
that takes no parameters and returns a
double
. This method should return the gravity of this physics engine. Note that
this is different from the static variable "DEFAULT_GRAVITY". You should return the instance
variable that was described above.
setGravity
setGravity
that takes a double
and returns
void
.
This method should set the gravity instance variable to the input value.
updateObject
updateObject
which takes a double
and a
DynamicGameObject
and returns void
. This method should override the
method of the
same name in the PhysicsEngine
class.
DynamicGameObject
's
y-velocity (note that change in velocity is equal to acceleration from gravity times the
change in time) (note also that the positive y direction is down).
updateObject
method and pass in the parameters. Be sure to
call this after applying gravity, to make sure that velocity and position are
appropriately adjusted.
isOnGround
method on the DynamciGameObject
from the parameter,
which returns true if the object is on the ground, and false otherwise.
isPlayer
method on the DynamicGameObject
from the parameter, which returns true if the
object is the player, and false otherwise.
PhysicsEngineWithGravity
object instead of a basic PhysicsEngine
will
now have gravity applied automatically. You can play Mario, as described above, to see the effects (as
long as you have also made the required changes to the LevelParser
).
LevelParser
class. These changes will allow the game engine
to parse levels which contain objects for Mario.
readDynamicObject
Goomba
objects. If the string being checked is exactly "Goomba", a
new Goomba
object should be returned. The Goomba
constructor takes
only two double
s, which should be the variables x
and y
which already exist in this method.
Koopa
objects. If the string being checked is exactly "Koopa", a new
Koopa
object should be returned. The Koopa
constructor takes only two
double
s, which should be the variables x
and y
which already exist in this method.
readStaticObject
readDynamicObject
, this method uses a switch-case statement.
Block
objects. If the string being checked is "Block", "Bricks", or
"Ground", a new Block
object should be returned. The Block
constructor
takes two double
s and a String
. The double
s should be the
variables x
and y
which already exist in this method. The
String
should be the same as the String
being checked. For example, if
the String
is "Bricks", "Bricks" should be passed into the constructor.
QuestionBlock
, HiddenBlock
,
PipeEnd
, PipeStem
. For each of these objects, the String
should exactly match the class name (eg. "QuestionBlock" or "PipeStem"), and the only
constructor parameters are two double
s, which should be the variables
x
and y
which already exist in this method.
Flag
objects. If the string being checked is exactly "Flag", a new
Flag
object should be returned. The Flag
constructor takes two
double
s and a Game
, which should be the variables x
,
y
, and game
which already exist in this method.
parseLevel
TopDownLevel
or a MarioLevel
depending on the content of the csv file being read.
TopDownLevel
object. If that field is "MarioLevel", it should be a
MarioLevel
object instead.
LevelParser
should still be capable of reading and parsing all previously assessed
levels. However, now it can also create levels for Mario with the appropriate objects.
In the app.tests.TestUtils
class, create a static method named
compareListsOfLevels
that takes in (references to) two LinkedListNode
s of
Level
s and returns void
. This method should contain JUnit asserts to verify that the two
linked lists
are of the same length and contain all the same levels. Two levels can be considered the same if they have the same
name. Recall that you can access the name of a Level
object by calling the getName
method
on it. If either the length or content of the lists are not the same, this method should fail a JUnit assert.
Note that this method is not a test, and thus should not have the @Test
annotation. Rather, this is a
static method intended to be used in your testing. You are encouraged to use this method to simplify your test
cases.
Write JUnit tests for the following methods from the specification in the TestTask2
class. These tests
should be annotated with @Test
:
LinearGame
addLevel
advanceLevel
getCurrentLevel
method to access the current level of the game.
getLevelList
method.
removeLevelByName
PhysicsEngineWithGravity
getGravity
,
setGravity
, and updateObject
. Tests are provided for you as described in the
Overview section above, which you may use to help you write the methods themselves.
LevelParser
readDynamicObject
,
readStaticObject
, and parseLevel
. Tests are provided for you as described in
the Overview section above, which you may use to help you write the methods themselves.
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 be implementing functionality in the
Agent
class (from the app.gameengine.model.gameobjects
package) and the
PathfindingUtils
class (that you should create in the app.gameengine.utils
package) such
that enemies have somewhat functional pathfinding. This will be most obvious in the Sample Game, which you can
access by changing the GAME
constant in app.Configuration
to
"Sample Game"
.
Each of the 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.
findPath
(30 CUs)
app.gameengine.utils
package named PathfindingUtils
. In
this class, implement the following static method:
PathfindingUtils
class, write a static method named
findPath
that takes in two Vector2D objects as parameters and returns a
LinkedListNode
of Vector2D
s.
Vector2D
object to the tile containing the second
Vector2D
object.
Vector2D
s themselves should not be modified. You should instead
create copies of them to modify. There are several ways to do this, such as: create a
new Vector2D
using the constructor, with the desired x and y components;
use the Vector2D.copy
method, and modify the returned vector; use the
static Vector2D.floor
method, which returns a new vector with
floored components.
Vector2D
object after the head of the list is one tile above,
below, to the left, or to the right of the previous one AND if each Vector2D object in the list is
aligned to a tile (i.e. no decimals). Considered the following paths attempting to travel from eg. (3.9,
4.6) to (5.2, 5.999)
followPath
(20 CUs)
Agent
class, there exists a method named followPath
that takes a
double
and returns void
. The input double represents the change in time since
the last
time this method was called. Complete this method so that the agent along its path if it has one.
path
, as well as a getter and setter for that instance variable, named getPath
and setPath
respectively. You will need to use these to complete this method.
Agent
's path instance variable is null
, their
velocity in the x and y directions should be set to 0, and nothing else should be done.
Agent
is directly on the tile
at the path's head, the Agent
's position is manually set to the location of that
tile. The head of the list is then removed from the list so the agent will travel to the next
node the next time this method is called.
this.getMovementSpeed
, or with the
movementSpeed
instance variable, both of which already exist in this class.
The change in time is the parameter dt
.
√((x1-x2)2 + (y1-y2)2)
.
You may use the Vector2D.euclideanDistance
method, which calculates this
for you.
Agent
's velocity should be set to move towards the tile at the head of the
Agent
's path.
Agent
's velocity must have a magnitude (speed) equal to its movement
speed. The movement speed can be accessed by calling this.getMovementSpeed
,
or with the movementSpeed
instance variable, both of which already exist in
this class.
Agent
's orientation should be set in the same direction as its
velocity, but always with a magnitude of 1.0.
Agent
can only move in four directions, only one component (x or
y) of a moving Agent
's velocity or orientation will be non-zero at any
point. This means there are only 4 possible values for the Agent
's
orientation, (1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), or (0.0, -1.0). Velocity is much the
same, except that the magnitude will depend on the movement speed.
Agent
is located at (2.5, 2), has a movement speed of 4,
and its path is [(3, 2), (4, 2), (4, 3)], it must first target the point (3, 2). To
reach that point, it must travel in the positive x direction, so its velocity would be
set to (4, 0), and its orientation would be set to (1, 0).
Agent
class named update
, which takes a
double
and a Level
object as parameters, and returns void. These paremeters
represent the change in time since the last update call, and the current level. This will override a
method from the parent class.
Agent.update
method, call super.update
, passing in both parameters.
Agent.update
method, check if the path instance variable is null
. If
so, call the findPath
method, where the start is the location of the Agent
,
and the end is the location of the Player
(obtained with
level.getPlayer().getLocation()
), and call setPath
with the returned path.
Agent.update
method, if the path is not null
, call the
followPath
method, passing in dt
as the change in time.
Agent.update
method in a different way.
Those changes should replace these ones.
Agent
class given is a minimal example that provides only the additional
imports/instance variables/code that you do not already have or are required to write. We recommend that
you copy and paste these sections into your existing file.
PathTile
class given is complete. You can either copy and paste it, or right click and
select "save file as", and download it to the correct location. It should be placed in the package
app.games.roguelikeobjects
.
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.