Coding Task 6 - Graphs


Time Estimate: 7 hours

Jump to current week
Requirements

Project Structure


You will continue to add functionality to your existing project from the previous task. There is no new repository to clone.



Overview Video


Video link: https://youtu.be/2Kd3yTYmTrc

Last one!

As before, watching this video is optional, and all of the information you need for this task is contained within this page. However, you may find it helpful for getting a better understanding of the task, and what is expected of you


Specification


In this task, you will implement the following specs. Upon completion, you will have implemented a new, smarter enemy type that can chase the Player while avoiding obstacles.

Before you can begin this, however, you will need to modify the Vector2D class in the app.gameengine.model.physics package to work well with HashMaps, and by extension Graphs. In order to achieve this, you must override the equals and hashCode methods that the Vector2D class inherits from the Object class. You do not need to, and are not expected to, fully understand these methods, however if you wish to understand them an instructor or TA would be happy to explain them to you.

  • Implement a method named equals, which takes one parameter of type Object and returns a boolean. It should appear as follows:
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (obj == null || this.getClass() != obj.getClass()) {
            return false;
        }
        Vector2D other = (Vector2D) obj;
        return Math.abs(this.x - other.x) < 1e-9 && Math.abs(this.y - other.y) < 1e-9;
    }

  • Implement a method named hashCode which takes no parameters and returns an int. You will also need to import java.util.Objects. The method should appear as follows:
    @Override
    public int hashCode() {
        return Objects.hash(Math.round(x / 1e-9), Math.round(y / 1e-9));
    }

In the app.gameengine.model.ai package, access the Pathfinding class and implement the following method:

  • Implement a static method named findPathAvoidWalls that takes in a Level, a Vector2D representing starting location, and a Vector2D representing ending location that returns a LinkedListNode of Vector2D.
    • This method should return a shortest valid path between the tile containing the starting location and the tile containing the ending location such that no point in the returned path shares a location with any StaticGameObject contained in the Level.
    • This uses the same criteria for a valid path as the previous Pathfinding 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.
      • Recall that this still applies to the starting and ending nodes in the path. The first and last nodes should be the tiles containing the starting and ending location rather than the starting and ending locations themselves.
      • As with Task 2, you should use the Math.floor method for finding the tile that contains a given vector.
    • If the start/end location is either not within the Level's boundary or is inside a StaticGameObject, this method should return null.
    • If there does not exist a valid path between the start and end locations, this method should return null.
    • Note that as you've seen with the dynamic objects in the previous task, you can also access a Level's static objects in a similar way with the provided getter.

From here, you will now implement a new Decision and Enemy type that utilizes this pathfinding.

  • In the app.gameengine.model.ai package, create a class named MoveTowardsPlayerAvoidWalls that extends Decision.
    • Write a constructor that takes in a String that simply calls the super constructor with the provided String.
    • Override the doAction method. This method should generate a path for or manipulate the DynamicGameObject from the parameter.
      • This method should follow the exact same behavior as the doAction method from the MoveTowardsPlayer class, except if it generates a path it should use the output of findPathAvoidWalls instead of findPath.
  • In the app.games.topdownobjects package, create a class named SmartEnemy that extends Enemy.
    • Write a constructor that takes in a Vector2D representing location. This constructor should:
      • Call the super constructor with the provided location from the parameter and a strength of 1.
      • Set the decision tree to a new DecisionTree that contains only a MoveTowardsPlayerAvoidWalls decision with no child nodes.
      • Optionally sets the spriteSheetFilename to "Characters/Monsters/Orcs/ArcherGoblin.png" and the defaultSpriteLocation to column 0, row 0.

You may now want to add a new SmartEnemy to some topdown level to view the new behavior you've written.



Testing Utilities


There is no testing utility for this task. However, you may find your previously written testing utility validatePath helpful. You can access this testing utility by creating a new instance of a TestTask2 object.



Testing Requirements


In the app.tests package, create a class called TestTask6 and write tests for the findPathAvoidWalls method.

  • Your tests should verify that the path returned by calling this method is valid and of the shortest possible length that avoids StaticGameObjects.
  • You will need to create Levels with obstacles for your tests (note that the base Level class is abstract).
  • Since there may be multiple shortest paths that avoid StaticGameObjects, you should not test for a specific path to be returned in such cases.


Programming Requirements


Implement the methods and classes from the specification. You may find it helpful to write tests for findPathAvoidWalls before beginning your implementation.



Autolab Feedback


The feedback in Autolab will be given in 3 phases. If you don't complete a phase, then feedback for the following phase(s) will not be provided.

  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 complete all 3 phases, you will have completed this Task and Autolab will confirm this with a score of 1.0 for complete.