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 4 assesses Graphs. Both the Problem Set and Game Engine Task will focus on this topic.
Problem Set GitHub Repository link: https://github.com/CSE-116/ProblemSet-4
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 following 3 static methods that are all related to graphs. The signatures for these methods exist in the handout repo.
problem.ProblemSet4 class, complete the countEdges method which takes
a Graph as a parameter and returns an int. This method returns
number of edges in the graph. The graph, and all graphs for this task, is assumed to have only
bidirectional edges so the count should be the number of bidirectional edges in the graph.
problem.ProblemSet4 class, complete the isConnected method which takes
a Graph as a parameter and returns a boolean. This method returns
true if the input graph is connected, and false otherwise
problem.ProblemSet4 class, complete the detectCycle method which takes
a Graph as a parameter and returns a boolean. This method returns
true if the graph contains a cycle, and false otherwise
Write JUnit tests for all 3 methods from the specification in the tests.TestProblemSet4
class. These tests should be annotated with @Test.
countEdges
countEdges method. Test that the proper value is returned in a
variety of cases
Graph class to construct a graph. The number of times you call
addBidirectionalEdge should be the expected number of edges
Graph that takes a type
parameter. You may choose any type when you create your graphs (eg. Using whatever is simplest,
like Integer, is fine). Since store nodes as keys in a HashMap, your
Graph cannot contain duplicate nodes
isConnected
isConnected method. Test that the proper value is returned in a
variety of cases
addNode method in the Graph class is public in this version of the
code in case you want to add nodes with no edges while creating disconnected graphs
detectCycle
detectCycle method. Test that the proper value is returned in a
variety of cases
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.
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 the Roguelike. To play this game
navigate to the class app.Configuration and change the GAME constant to
"roguelike". Most of this content will also be testable within the sample game, accessible by changing
the GAME constant to "sample game".
For this Learning Objective portion, you will be writing code in the following classes which already exist:
PathfindingUtils, located in the app.gameengine.utils package.Minotaur, Demon, Archer, and Sorcerer, located in the
app.games.topdownobjects package.
Spike and Potion, located in the app.games.commonobjects package.LevelParser, located in the app.gameengine package.
You should create the TestTask4 class in your app.tests package. We have provided tests
for some of the Decision subclasses, the LevelParser updates, and a few miscellaneous
classes. They can be accessed at this link: TestTask4.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 several additional classes, which will be specified in the following instructions.
The largest part of this task deals with pathfinding with a BFS. If you previously completed the CU component of Task 2, enemies will be able to follow these paths around walls to navigate to the player. If you did not complete it, you can still earn full points, but your enemies will not actually move at all.
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.
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 the Learning Objective portion of the Game Features component for this task, you will implement the following functionality:
app.gameengine.utils.PathfindingUtils class. If it does not already exist, create this
class.
findPath: Create a static method named findPath which takes
two Vector2Ds as parameters and returns a LinkedListNode of Vector2Ds. If
you completed this method as part of the Task 2 CU, you should leave it as is. Otherwise, this method should
return null.
findPathAvoidWalls: Create a static method named
findPathAvoidWalls which takes a Level and two Vector2Ds as parameters,
and returns a LinkedListNode of Vector2Ds. The Vector2Ds are the starting
and ending points of the path, respectively, and the Level is the level being navigated.
GameObject (dynamic or static) can be solid or non-solid. This can be determined by the
GameObject.isSolid method. You can access the lists of static and dynamic objects of a
level with the getStaticObjects and getDynamicObjects methods respectively.
GameObject that is solid should be avoided. Specifically, the tile containing
that object should be avoided. So, if there is a Wall at (1.5, 2.8), the location (1, 2)
should be avoided. You can use the Math.floor or Vector2D.floor methods to
account for this.
findPath method: all locations in
the path must be aligned to a tile (i.e. whole numbers only) and all tiles must be one tile
above, below, to the left, or to the right from the tile preceding and succeeding it.
Math.floor or Vector2D.floor for finding the tile
that contains a given vector. Be sure not to modify the input vectors directly, though.
Level's boundary or is inside a solid
object, this
method should return null.
GameUtils.isInBounds method to determine if a vector is within the
bounds of a level.
Vector2Ds that are within the bounds. Levels are not
guaranteed to be surrounded by solid objects.
Decision subclasses that will make use of this pathfinding, along
with other behaviors used within the roguelike and sample games. These will all live in the
app.gameengine.model.ai.roguelike package, which you should create.
MoveTowardPlayer: Create a class named MoveTowardPlayer which extends
Decision. This class represents the action of acquiring a path to the player, and moving along that
path. Implement the following methods in this class.
Agent and a String, which are passed to the
super constructor.
decide method to always return false.
doAction method. If the Agent's path is null, use the
Pathfinding.findPath method to assign it a path. The starting location should be that
Agent's location, and the ending location should be the location of the Player
within the level. If the Agent's path is not null, call the followPath method
on it, passing in the double parameter.
MoveTowardPlayerAvoidWalls: Create a class named MoveTowardPlayerAvoidWalls which
extends Decision. This class's constructor and other methods should be the same as in
MoveTowardPlayer, except using Pathfinding.findPathAvoidWalls instead of
findPath.
ShootPlayer: Create a class named ShootPlayer which extends Decision.
This class represents an action of firing an EnemyArrowProjectile at the player. Implement the
following methods in this class.
Agent, a String, and a double.
The first two should be passed to the super constructor, and the third should be used as the cooldown
for a timer, which should be stored in an instance variable.
app.gameengine.utils.Timer class. This is a utility that allows for keeping
track of time, so that enemies can only shoot on a cooldown.
decide method to always return false.
doAction method. This method should always zero the Agent's
velocity. It should then advance the timer by the double parameter, and if the cooldown
has elapsed, it should set the orientation of the agent to face the player, and fire a projectile.
Timer.tick method advances the timer, and returns a boolean representing
whether the cooldown has elapsed.
Agent must be set in the direction of the player, with a
magnitude of 1. To get a vector pointing from the agent to the player, subtract the agent's
location from the player's location (either manually or with the Vector2D.sub
method). Then, normalize this vector, ie. make it's magnitude 1. You can use the
Vector2D.normalize method for this.
fireProjectile on the Agent. The projectile
argument should be a new EnemyArrowProjectile, the double speed should
be 10, and the Level should use the parameter.
ShootHomingProjectile: Create a class named ShootHomingProjectile which extends
Decision. This class's constructor and other methods should be the same as in
ShootPlayer, except using EnemyHomingProjectiles instead of
EnemyArrowProjectiles.
Heal: Create a class named Heal which extends Decision. This class
represents an action of remaining still to recover HP. Implement the following methods in this class.
Agent, a String, an int, and a
double. The first two should be passed to the super constructor. The third represents the
amount that this will heal the agent by each time. The double should be used as the
cooldown for a timer, which should be stored in an instance variable.
app.gameengine.utils.Timer class. This will be used to ensure that the
agent does not heal too frequently.
decide method to always return false.
doAction method. This method should always zero the Agent's
velocity. It should then advance the timer by the double parameter, and if the cooldown
has elapsed, it should heal the agent by the set amount.
Timer.tick method advances the timer, and returns a boolean representing
whether the cooldown has elapsed.
getHP and setHP methods to modify the agent's health.
LowHP: Create a class named LowHP which extends Decision. This class
represents a decision of whether the Agent's health is below a certain threshold. Implement the
following methods in this class.
Agent, a String, and an int.
The first two should be passed to the super constructor, and the third represents the health threshold,
which should be stored as an instance variable.
decide method to return whether the current health of the Agent
is less than or equal to the threshold.
doAction method to do nothing.
NearPlayer: Create a class named NearPlayer which extends Decision. This
class represents a decision of whether the Agent is closer than a certain distance to the player.
Implement the following methods in this class.
Agent, a String, and a double.
The first two should be passed to the super constructor, and the third represents the distance
threshold, which should be stored as an instance variable.
decide method to return whether the distance between the agent and the player
is less than or equal to the threshold. You can use the Vector2D.euclideanDistance method
for this check.
doAction method to do nothing.
Agents different behaviors. The
Agents you must modify are Demon, Minotaur, Archer,
Sorcerer, and Tower, all in the app.games.topdownobjects package.
Decision. You are encouraged to play
around and design different behaviors.
Demon: A single MoveTowardPlayer decision.Minotaur: A single MoveTowardPlayerAvoidWalls decision.Archer: A single ShootPlayer decision.Sorcerer: A single ShootHomingProjectile decision.Tower: A single ShootPlayer decision. Note that decisions that cause
the tower to move will do nothing. It is also recommended to override the isSolid
method to always return true, to test that solid DynamicGameObjects
are handled properly. We will not test for this, though.
Spike and Potion classes in the
app.games.commonobjects package, and to the LevelParser.
Spike: Most of this class already exists. You must override the
collideWithDynamicObject method to kill any player it contacts.
isPlayer method to determine if an object is or is not
the player.
destroy method to kill the player.
Potion: Most of this class already exists. You must override the
collideWithDynamicObject method to heal/harm a player on contact. Also create the
getHealAmount method.
isPlayer method to determine if an object is or is not
the player.
takeDamage method to harm the player, but note that this method does nothing if the
argument is negative, so it cannot be used to heal the player. You can use setHP
instead.
getHealAmount should be a getter for an int instance variable representing the
amount to heal. This instance variable should initially be set to the third constructor
parameter.
LevelParser class. These changes will allow the game engine
to parse levels which contain objects for the roguelike and sample games.
readDynamicObject
Minotaur objects. If the string being checked is exactly "Minotaur",
a new Minotaur object should be returned. The Minotaur constructor
takes two doubles, which should be the variables x and y
which already exist in this method, and two ints, which will be the fifth and sixth
values in the ArrayList for that object. These are the same parameters as the
Demon constructor, which already exists in this method.
Archer objects. If the string being checked is exactly "Archer", a
new Archer object should be returned. The Archer constructor takes the
same parameters as the Demon and Minotaur constructors.
Sorcerer objects. If the string being checked is exactly "Sorcerer",
a new Sorcerer object should be returned. The Sorcerer constructor
takes the same parameters as the Demon, Minotaur, and
Archer constructors.
readStaticObject
Spike objects. If the string being checked is exactly "Spike", a
new Spike object should be returned. The Spike constructor
takes only two doubles, which should be the variables x and
y which already exist in this method.
Potion objects. If the string being checked is exactly "Potion", a
new Potion object should be returned. The Potion constructor
takes two doubles and an int. The doubles should be the
variables x and y which already exist in this method. The
int should be the fifth value in the ArrayList for that object.
Marker objects. If the string being checked is exactly "Marker", a
new Marker object should be returned. The Marker constructor
takes two doubles and a String. The doubles should be the
variables x and y which already exist in this method. The
String should be the fifth value in the ArrayList for that object.
DirectionalWall objects. If the string being checked is exactly
"DirectionalWall", a new DirectionalWall object should be returned. The
DirectionalWall constructor takes two doubles and a
Level. The doubles should be the variables x and
y which already exist in this method, and the Level should be the
method parameter.
parseLevel
TopDownLevel, MarioLevel, or
RoguelikeLevel depending on the content of the csv file being read.
RoguelikeLevel object. This method should still work for
both TopDownLevels and MarioLevels.
In the app.tests.TestUtils class, create a static method named
validatePath that takes in a LinkedListNode of Vector2Ds and returns
void. This method should contain JUnit asserts to verify that the linked list is a valid path, and
should fail a JUnit assert if the path is invalid.
This uses the same criteria as the findPath method to determine validity of a path. This means that
every pair of vectors in the path must be horizontally or vertically adjacent (no diagonals), a distance of 1 tile
apart, and each vector must have x and y components that are whole numbers.
Consider an input path of null to be valid.
Note that this method does not assert every property necessary. Namely, it does nothing to guaranteed that the path avoids solid objects.
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 method from the specification in the TestTask4 class. These tests
should be annotated with @Test.
You may find it useful to create level csv files and read them with the LevelParser in your tests. You can create levels with the level editor, which should contain all of the objects and level types you may want to use.
findPathAvoidWalls:
TopDownLevel or RoguelikeLevel classes, and can use
Wall objects as obstacles.
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.games.roguelikeobjects.RoguelikeGame class to add procedurally generated levels to the roguelike.
If you haven't already, you should change the GAME constant in app.Configuration to
"roguelike" to be able to view and test the generation.
Since there is only one method you need to implement for this task, all 50 CU points will be earned upon passing our tests for that method.
Procedural Generation in game development refers to the creation of levels, maps, or any content
that is done through algorithms instead of manually. In this task, you will modify RoguelikeGame's
generateMap method to accomplish this using DFS.
The gif below shows an example of how it should work when it is finished.
As shown in the gif, generateMap works in such a way where it first generates the level and
then connects a door to the level that discovered it.
There are some important parameters here that vastly change how the generated map looks. These can be modified in
RoguelikeGame for testing purposes, or for fun!
this.mapSize in RoguelikeGame, a Vector2D representing the
size of the map grid.
this.maxRooms in RoguelikeGame, an int that changes the
maximum amount of rooms that can be generated.
Note: The order of level generation may seem unintuitive at first. Think about which room each other room was discovered from, and why this is the case.
Note: The existing implementation of this method should be completely replaced. However, before deleting the given code, you might want to look at it for examples on the using some of the helper methods as described below.
generateMap (50 CUs)
RoguelikeGame class, delete the given functionality of generateMap and
overwrite it with a version that procedurally generates levels using DFS.
Vector2D starting position. To create
this position, call getRandomStartingPosition, a method in RoguelikeGame.
Vector2D positions and a corresponding level
for each. These positions should be picked out from the starting position in random directions.
To get these random directions, you must call the given
Randomizer.shuffleArrayList(DIRECTIONS) method exactly as shown.
this.mapSize.
Vector2D is in bounds of another
Vector2D by calling GameUtils.isInBounds.
getNextLevelToGenerate. This will
return you a RoguelikeLevel object, which should be initialized with the new
position by calling the initialize method on the returned level and passing in
the generated position. This level also must be added to
this.levelMap, which maps levels by their name. (Which you can get
by calling the getName method on a level). You can also use the
addLevel method to achieve this.
openDoor method
on a RoguelikeLevel object that takes an adjacent RoguelikeLevel.
this.maxRooms amount of levels. Keep this
in mind
during your implementation.
this.loadLevel on
the first level you created, the starting level.
getRandomStartingPosition - only called once.
getNextLevelToGenerate
Randomizer.shuffleArrayList(DIRECTIONS) - this should always be called
after getNextLevelToGenerate.
Now, when you play the Roguelike, you should get a different map every time you play, with a guaranteed boss room
found somewhere throughout it. Testing this method largely involves playing the game and ensuring the correct
amount of rooms were generated, with a boss room somewhere in the map. Sometimes, using a set seed for your
randomization could also help with the testing process, but is optional. To do this, call
Randomizer.setSeed with any number of your choosing before doing anything that involves randomization.
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.