Coding Task 2

Linked Lists and Inheritance


Overview

Coding Task 2 Overview


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

Overview


Problem Set GitHub Repository link: https://github.com/CSE-116/ProblemSet-2

  1. Clone the starter code from the repository linked above into a new IntelliJ project
  2. Make sure the src folder is marked as the source root (Right-click the src folder, choose “mark directory as” and choose sources root)

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.


Specification


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:
    • Constructor: create a constructor which takes a 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.
      • Ex: if a 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:
    • Constructor: create a constructor which takes a String two ints 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.
      • For example, if a 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.
      • The health of the passed in object is allowed to become negative. For example, if a 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 LinkedListNodes of Strings. 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 Strings. This method should return the entire linked list of strings.
    • setList: create a method named setList which takes a LinkedListNode of Strings 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.
      • If the list has an odd number of elements, it should return the middle element. For example, calling getMidpoint with the list ["cat","dice","lemon","juicebox","elephant"] would return "lemon", since it is at the middle index.
      • If the list has an even number of elements, it should return the two middle elements concatentated together, separated by a space. For example, calling getMidpoint with the list ["cat","dice","lemon","juicebox","elephant","apple"] would return "lemon juicebox", since indices 2 and 3 are equally close to the midpoint of the list.
      • If the list is empty, this method should return the empty 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.
      • If the list is empty, or does not contain the input value, this method should do nothing.
      • If the list contains multiple of the value being deleted, this method should only remove the first. For example, calling delete("dice") on the list ["dice","cat","dice"] would result in the list ["cat", "dice"].


Testing Utility


Create a class in the tests package named TestUtils. In this class, create a static method named compareListsOfStrings that takes in two LinkedListNodes of Strings 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 Strings. 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.



Testing Requirements


Write JUnit tests for the following methods from the specification in the tests.TestProblemSet2 class. These tests should be annotated with @Test.

  • StringList
    • Write tests for the getMidpoint method. Test that the proper value is returned in a variety of cases.
    • You DO NOT need to test the 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.
    • These methods call the testing utility to verify correctness. The testing utility must at least exist in order for these tests to compile, and must be correct in order for the tests to be useful.
  • Monster and Skeleton
    • You do not need to write tests for these classes or their methods. However, if you are struggling, it may be useful to write tests anyway.


Programming Requirements


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.



Autolab Feedback


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:

  1. Running your tests on a correct solution
    • Your tests will be run against a solution that is known to be correct. If your tests do not pass this correct solution, there is an error somewhere in your tests that must be fixed before you can move on with the assignment. If your tests don't get past this check, you should re-read this document and make sure you implemented your tests and code according the specification. You should also make sure that if there are multiple correct outputs to the input in your tests cases that you accept any of the outputs as correct.
  2. Checking your tests for feature coverage
    • The next phase is to check if your tests check for a variety of features defined by different inputs. You should write at least one test case for each feature to pass this phase.
    • Passing this phase does not necessarily mean that your testing is completely thorough. Satisfying Autolab is the bare minimum testing requirement. Not all possible inputs are checked and it is sometimes possible to pass this phase with weak testing. If you are struggling to earn credit for code that you believe is correct, you should write more than the required tests
  3. Running my tests on your solution
    • Once Autolab is happy with your tests, it will run my tests against your code to check it for correctness. If your testing is thorough, and your code passes your tests, then you should pass this phase. If you pass your tests, but fail one of mine, it is an indicator that you should write more tests to help expose your bug.

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.

Game Features - Learning Objective

Overview


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



Specification


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:

  • Implement the following methods in the 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
      • Create a method named getLevelList which takes no parameters and returns a LinkedListNode of Levels. It should return the head of the game's list of levels.
      • If no levels have been added, this method should return null.
    • setLevelList
      • Create a method named setLevelList which takes a LinkedListNode of Levels as an argument and returns void. It should replace this game's list of levels with the input value.
    • addLevel
      • Create a method named 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
      • Create a method named 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.
      • You should use the getCurrentLevel method to access the current level.
      • You should use the 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.
      • If the game is already on the last level, this method should do nothing. You should never call loadLevel with an argument of null.
    • removeLevelByName
      • Create a method named 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.
      • If multiple Levels in the list exist with this name, only the first should be removed.
      • You should use the getName method on each level object to determine if its name is the same as the input String.
    • Optional - reset
      • This method is entirely optional. Although it improves the quality of the game somewhat, it will not be tested, and you do not need to complete it if you do not wish to. Whether you complete it or not will not affect your submission or grading, as long as your code still compiles.
      • There are several actions which can be taken within the game that reset a game to its initial state, such as pressing "F2". To fully reset the game, each level in the linked list of levels must be reset, and the current level must be set to the head of the list.
      • To reset a level, you can call the reset method on it.
      • To change the current level to the head of the list, you can access the instance variable this.currentLevel directly, and set it to the desired value. Note that this instance variable is of type Level, not LinkedListNode.
      • The head of the linked list should be the last to have reset called on it. Otherwise, the player will start at the wrong location.
  • Implement the following methods in the 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.
    • Constructor
      • You do not need to create a new constructor, but you should modify the existing constructor which takes a single 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
      • Write a method named 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
      • Write a method named setGravity that takes a double and returns void. This method should set the gravity instance variable to the input value.
    • updateObject
      • Write a method named 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.
      • This method should apply gravity to the passed in object, according to the change in time passed in. You should add the change in velocity from gravity to the 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).
      • Call the super class' 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.
      • This method should only apply gravity to objects that are in the air. This is because when an object is on the ground, applying gravity would only cause it to clip into the ground, and make movement more difficult. To determine if an object is in the air or on the ground, you can use the isOnGround method on the DynamciGameObject from the parameter, which returns true if the object is on the ground, and false otherwise.
      • This method should not apply gravity to the object if that object is the player. This is because the player's gravity is handled separately, within the controls, and we do not want to apply it twice. To determine if an object is the player, you can use the isPlayer method on the DynamicGameObject from the parameter, which returns true if the object is the player, and false otherwise.
    • With these few modifications, and the power of inheritance, you have just created gravity. Any game that uses a 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).
  • Implement the following updates to the LevelParser class. These changes will allow the game engine to parse levels which contain objects for Mario.
    • readDynamicObject
      • The existing structure of this method uses a switch-case statement. A switch-case statement is much like an if statement, where it checks if the expression after the keyword "switch" is equal to each value after "case". The first case that is equal is entered, and the code inside is performed. You may modify this method to use a series of if-else statements if you prefer, but it will likely be easier to extend the existing switch-case structure.
      • Add a case for Goomba objects. If the string being checked is exactly "Goomba", a new Goomba object should be returned. The Goomba constructor takes only two doubles, which should be the variables x and y which already exist in this method.
      • Add a case for Koopa objects. If the string being checked is exactly "Koopa", a new Koopa object should be returned. The Koopa constructor takes only two doubles, which should be the variables x and y which already exist in this method.
    • readStaticObject
      • As with readDynamicObject, this method uses a switch-case statement.
      • Add a case for Block objects. If the string being checked is "Block", "Bricks", or "Ground", a new Block object should be returned. The Block 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 same as the String being checked. For example, if the String is "Bricks", "Bricks" should be passed into the constructor.
      • Add a case each of the following objects: 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 doubles, which should be the variables x and y which already exist in this method.
      • Add a case for Flag objects. If the string being checked is exactly "Flag", a new Flag object should be returned. The Flag constructor takes two doubles and a Game, which should be the variables x, y, and game which already exist in this method.
    • parseLevel
      • Modify this method to return either a TopDownLevel or a MarioLevel depending on the content of the csv file being read.
      • The level type can be determined from the first field in the first line of the level file. If that field is exactly "TopDownLevel", then the level you create, modify, and return should be a TopDownLevel object. If that field is "MarioLevel", it should be a MarioLevel object instead.
    • Your 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.


Testing Utility


In the app.tests.TestUtils class, create a static method named compareListsOfLevels that takes in (references to) two LinkedListNodes of Levels 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.



Testing Requirements


Write JUnit tests for the following methods from the specification in the TestTask2 class. These tests should be annotated with @Test:

  • LinearGame
    • To test these methods, you will have to create a LinearGame object in order to call the methods
    • addLevel
      • Verify that adding new levels to the game adds them to the end of the list of levels.
      • You should use the testing utility to help you verify this.
    • advanceLevel
      • Verify that this method changes the current level of the game. Recall that you can use the getCurrentLevel method to access the current level of the game.
      • Verify that this method does not modify the entire list of levels, ie. the list returned by the getLevelList method.
    • removeLevelByName
      • Verify that this method removes the level with the given name from the game's list of levels.
      • Verify that this method does not remove multiple levels that share the same name.
  • PhysicsEngineWithGravity
    • You DO NOT need to test the methods within this class, including 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
    • You DO NOT need to test the methods within this class, including 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.


Programming Requirements


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.



Autolab Feedback


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:

  1. Testing your testing utility method
    • Your testing utility method will be checked with a variety of test cases to ensure that it makes all the required checks. This phase will ensure that your utility method is accurate before you start using it in your tests.
  2. Running your tests on a correct solution
    • Your tests will be run against a solution that is known to be correct. If your tests do not pass this correct solution, there is an error somewhere in your tests that must be fixed before you can move on with the assignment. If your tests don't get past this check, you should re-read this document and make sure you implemented your tests and code according the specification. You should also make sure that if there are multiple correct outputs to the input in your tests cases that you accept any of the outputs as correct.
  3. Checking your tests for feature coverage
    • The next phase is to check if your tests check for a variety of features defined by different inputs. You should write at least one test case for each feature to pass this phase.
    • Passing this phase does not necessarily mean that your testing is completely thorough. Satisfying Autolab is the bare minimum testing requirement. Not all possible inputs are checked and it is sometimes possible to pass this phase with weak testing. If you are struggling to earn credit for code that you believe is correct, you should write more than the required tests.
  4. Running my tests on your solution
    • Once Autolab is happy with your tests, it will run my tests against your code to check it for correctness. If your testing is thorough, and your code passes your tests, then you should pass this phase. If you pass your tests, but fail one of mine, it is an indicator that you should write more tests to help expose your bug.

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.

Game Features - Comprehensive Understanding

Overview


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.


Programming Requirements


  • findPath (30 CUs)
    • Create a class in the app.gameengine.utils package named PathfindingUtils. In this class, implement the following static method:
    • In the PathfindingUtils class, write a static method named findPath that takes in two Vector2D objects as parameters and returns a LinkedListNode of Vector2Ds.
    • This method should return a shortest valid path that travels from the tile containing the first Vector2D object to the tile containing the second Vector2D object.
      • To compute the tile containing a vector, you can take the floor of each coordinate in the vector. Recall that locations for rectangles are the location of their upper-left corner.
      • The vector (3.9, 4.6) is contained on the tile at location (3.0, 4.0)
      • The input Vector2Ds 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.
    • A path is valid if each 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)
      • [(3.0, 4.0), (3.0, 5.0), (4.0, 5.0), (5.0, 5.0)] is valid.
      • [(3.0, 4.0), (3.0, 5.0), (5.0, 5.0)] is not valid as the third Vector2D is two tiles right of the second.
      • [(3.0, 4.0), (4.0, 5.0), (5.0, 5.0)] is not valid as the second Vector2D is diagonal to the first one.
      • [(3.9, 4.6), (3.1, 5.9), (4.6, 5.5), (5.2, 5.999)] is not valid as it is not aligned to a tile.
    • You may find it helpful to review the overview of the coordinate system in the task 1 handout.
    • Note that there are many correct solutions for this method since there are multiple valid paths that all of minimal length (unless the start and end share a coordinate). For example, traveling from (0, 0) to (1, 1) both [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)] and [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)] are correct solutions. Any valid path will be accepted by our test cases.
  • followPath (20 CUs)
    • In the 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.
    • This class already contains an instance variable representing this object's path, called 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.
    • In each update call, only one of these three things can happen. If one of these conditions is met, in this order, it should be done, and then nothing else should be done.
      1. Idle: If the 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.
      2. Advance to the next node in the path: If the 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.
        • An enemy is considered to be "directly on" a tile if its distance from the tile is less than its movement speed times the change in time since the last update. The movement speed can be accessed by calling this.getMovementSpeed, or with the movementSpeed instance variable, both of which already exist in this class. The change in time is the parameter dt.
        • Use Euclidean distance for this calculation, which is calculated as the square root of the sum of the squares of differences in each dimension. It is calculated as √((x1-x2)2 + (y1-y2)2). You may use the Vector2D.euclideanDistance method, which calculates this for you.
        • You must also set the agent's velocity in the x and y directions to 0.
        • These steps are necessary to prevent enemies from getting caught at the edges of hitboxes.
      3. Travel along the path: If neither of the 2 previous conditions apply, the Agent's velocity should be set to move towards the tile at the head of the Agent's path.
        • The 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.
        • The Agent's orientation should be set in the same direction as its velocity, but always with a magnitude of 1.0.
        • Since the 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.
        • For example, if an 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).
  • These two methods comprise the Comprehensive Understanding portion of this assignment. However, you will not yet be able to see these changes in-game. This is intentional, as the next two tasks add a modular behavior system that can make use of this pathfinding. However, if you want to see this behavior in game early, you can do the following:
    1. Write a method in the 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.
    2. In the Agent.update method, call super.update, passing in both parameters.
    3. In the 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.
    4. In the Agent.update method, if the path is not null, call the followPath method, passing in dt as the change in time.
    5. When completing Task 3, you will have to modify the Agent.update method in a different way. Those changes should replace these ones.
  • We also provide another way to visualize the paths in-game, so that you can see whether your pathfinding is working. If you download/copy these files correctly, enemies with paths will display them in the game-world. You may find it useful to do this now, even if you have not completed the CU, as it will be helpful for Task 4 as well.
    • The code can be accessed at these links: Agent.java and PathTile.java.
    • The 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.
    • The 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.

Autolab Feedback


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.