Homework 3: All-Pairs Shortest Path Pathfinding

One of the main uses of artificial intelligence in games is to perform path planning, the search for a sequence of movements through the virtual environment that gets an agent from one location to another without running into any obstacles. For now we will assume static obstacles.

In an environment with static obstacles and a path network that is not too large, it is possible to pre-compute the shortest path from any source location to any destination location. When using navigation meshes, the question becomes: how do I get from one convex hull to another convex hull? Pre-computation trades speed for space: all possible paths are known and stored. This is great for real-time games such as 1st-person shooters.

Djikstra's algorithm is a single-source shortest path algorithm, meaning it takes a single source node and finds the shortest path to all other nodes. It can be run on all nodes to find the shortest path between all pairs of path nodes, but this is inefficient. The Floyd-Warshall algorithm is an all-pairs shortest path algorithm. In this assignment, we will implement the Floyd-Warshall algorithm to pre-compute paths that an agent will then use to intelligently and efficiently move about the game environment.

In all-pairs shortest path (APSP) pathfinding, each path node knows where the agent needs to go next to get to a particular destination. For example, suppose you have four nodes, A, B, C, and D with edges A-B, B-C, and C-D. Further suppose the agent wants to get to D but is physically at A. The agent asks A where to go next to get to D and node A replies "go to B." This process repeats. When the agent gets to B, it asks B where to go next to get to D and node B replies "go to C." And so on until the agent actually arrives at node D. Thus, the data structure that is pre-computed is a lookup table of entries that answer the question: "if you want to get to X, where should I go next?". This is best expressed as a matrix in which the rows are possible destinations, the columns are possible current locations, and the values in the cells are the next node based on the combination of current position and desired destination.

As this is the first time we can implement intelligent pathfinding behavior, we will have an agent play a game in which it must collect as many of the resources in the environment is as little time as possible. We will build off your previous navigation mesh and path network generation solution from homework 2.

There are many optional parts of this assignment that can be used to optimize your agent to collect resources faster. Optimizations will include smoothing the path that the agent takes as it moves through the environment, cutting corners. Optimizations can also involve more intelligently selecting the order in which resources are gathered.


What you need to know

Please consult homework 1 and homework 2 for background on the Game Engine. In addition to the information about the game engine provided there, the following are new elements you need to know about.

APSPNavigator

APSPNavigator is defined in apspnavigator.py. It is a specialization of NavMeshNavigator. APSPNavigator maintains two 2D arrays, self.next, and self.dist. self.next is the matrix indicating the next node to travel to if the agent is at some other node and wants to get to some other destination. Thus self.next[current][destination] is the next node in the path. self.dist is a matrix that holds the distance from any given node to any given destination. Thus self.dist[current][destination] gives the distance the agent has yet to travel.

Member variables:

Member functions:

Gatherer

Gatherer is defined in core.py. Gatherer is a sub-class of Agent that can be tasked with navigating to a set of target points. Given a set of target points, it iteratively moves to each target in the order given.

Member variables:

Member functions:

NearestGatherer

NearestGatherer is defined in nearestgatherer.py. NearestGatherer is a sub-class of Gatherer that sorts the targets it is instructed to travel to according to a closest-first strategy.

Resource

Resource is defined in core.py. A Resource is just a base class for things that can be collected by Agents.

SimpleResource

SimpleResource is defined in core.py. A SimpleResource is a type of resource that dissapears when touched by an Agent. You can see how this behavior is implemented in its collision() method.


Instructions

To complete this assignment, you must implement the Floyd-Warshall algorithm to pre-compute the shortest path between all pairs of path nodes. You must additionally use the computed next matrix to generate a path through the path network whenever the agent needs to move.

Use your solution to homework 2 to generate a navigation mesh and a corresponding path network. The instructor can provide you with a default solution if necessary.

To run the project code, use runapspnavigator.py:

The following steps are required to complete the assignment.

Step 1: Copy your myCreatePathNetwork.py file from homework 2.

Step 2: Implement APSP() in apspnavigator.py. Given a pathnetwork as a list of nodes and a list of edges, create and return the 2D next matrix and the 2D dist matrix.

The APSP() function should create two 2D matrixes as dictionaries of dictionaries. The matrix next is the navigation table which indicates the next pathnode to go to if navigating from any source pathnode to any destination pathnode. Source nodes are rows and destination nodes are columns. Thus next[source][destination] will return a path node as a point. The matrix dist holds the shortest distance between two pathnodes along the path network. Thus dist[source][destination] will return a number. If a destination node is not reachable from a source node, then next[source][destination] = None and dist[source][destination] = INFINITY.

APSP() sets up dist and next variables as dictionaries-of-dictionaries. Although pseudocode descriptions of ASPS will assume next and dist are 2D arrays, a dictionary-of-dictionaries in python is equivalent to a 2D array. Because we are using dictionaries, you don't have to worry about the length of the arrays, and you can index into the dictionary-of-dictionaries with an (x, y) point instead of a single integer index. For example, you can do the following:

Here is how computePath() works in APSPNavigator. It first checks if there is a clear shot from the Agent's current position to the given destination. If so the agent is instructed to move directly to the destination. Otherwise, the agent looks for the closest path nodes to itself and the destination and uses these path nodes to index into the next matrix. It uses the next matrix to compute the path as a set of path nodes. Once it has the path, it tries to optimize it. After all of that, if the path is not empty, the first node in the path is popped and the Agent is instructed to start moving to this node.

Step 3: Modify findPath() in apspnavigator.py. This function should return a path, as a list of points of the form (x, y).

findPath() takes in three parameters. Start is the nearest path node to the original location of the agnet. End is the nearest path node to the desired destination point. Next is the navigation table.

findPath() returns a list of path nodes (points). If there is no path, return the empty list. Use shallow copies of path nodes so that the path nodes in the path correspond back to path nodes in self.pathnodes.

Step 4: Modify clearShot() in apspnavigator.py

This function should return true if the agent can move from point p1 to point p2 without running into any obstacles. APSPNavigator.computePath() uses this to avoid having to generate a path when two points are within line of sight of each other and the agent could walk straight from p1 to p2 without colliding with anything. clearShot() takes in two points, a list of lines from obstacles (not including world borders), a list of points from obstacles, and the agent.

Step 5 (optional): Optimize the path that the Agent takes when traversing the path.

Modify shortcutPath() in mynavigatorhelpers.py. This function takes the path generated by APSP and attempts to shorten it by removing nodes that can be skipped by the agent. The function returns a path, which may or may not be the same as the path passed in. If there is a shortcut, meaning the agent does not need to visit all path nodes in the path, then the returned path should be shorter than the given path. shortcutPath() takes the folowing input parameters:

  1. source: the original location of the agent (not necessarily a path node point)
  2. destination: the desired point the agent wants to get to (not necessarily a path node point)
  3. path: the path computed from findPath() that may not be optimal.
  4. world: reference to the world object.
  5. agent: reference to the agent object.

Modify mySmooth() in mynavigatorhelpers.py. This function attempts to determine if shortcuts can be taken while the Agent is in motion. The Navigator object is passed in directly and it should modify the path by side-effect if necessary. The function returns true if the path has been dynamically modified, and false otherwise. mySmooth() works a little bit differently than other functions you've seen. The navigator object is passed in and you have access to all of its members. mySmooth may need to make an explicit call to Navigator.agent.moveToTarget() and may also need to directly modify Navigator.path. For example, if the agent can traverse directly to the destination despite having more nodes in the path, it may want to set the path to the empty list and instruct the agent to traverse directly to the destination point. As another example, if the agent can traverse directly to a path node that is not immediately next in the path, then it may choose to pop one or more nodes from the path.

The key difference between shortcutPath() and mySmooth() is that shortcutPath() happens at the time the path is created (before the agent has started moving) and thus probably only considers whether path nodes can be skipped by looking at each path node individually. mySmooth() is called every tick and can thus account for new information about whether the destination or future path nodes are reachable while the agent is part-way between path nodes.


Grading

Your solution should be able to find the shortest path between any two points.

The optional portion of the assignment will be evaluated as follows. The agent must collect all resources on the screen. If this is accomplished, the total distance traveled by the agent will be compared to the total distance necessary if no smoothing occurs (i.e., the distance traveled if agent always goes to the nearest path node to the starting point and the nearest path node to the destination point). The maximum extra credit score is 3 points. The number of points you will receive will be 3 * (Δ your agent's distance traveled) / (Δ best agent's distance traveled). The best agent is the one with the greatest delta between smoothing and non-smoothing distances traveled, selected among student solutions, TA solutions, and instructor solutions.


Hints

Make sure your dist[][] contains INFINITY values if two nodes are unreachable from each other.

Path networks are bidirectional graphs. Make sure next[a][b] == next[b][a] and dist[a][b] == dist[b][a].

Press the 'd' button to see how far the player-controlled agent has traveled.

Read how computePath() works in APSPNavigator. If findPath() is not being called, it is likely there is something wrong with your APSP() implementation such as a None value in next where there should be a valid point, or an INFINITY value in dist where there should be a non-infinite value.

Be sure to test your code with runromania.py, which uses a larger network but doesn't istantiate the graphical engine. The map is the Romania example from Rusell and Norvig's Artificial Intelligence: A Modern Approach, Third Edition (pp. 68)


Submission

To submit your solution, upload your modified apspnavigator.py and mycreatepathnetwork.py.

To be considered for extra credit, upload mynavigatorhelpers.py. If you do not upload mynavigatorhelpers.py, you will still receive full credit for the non-optional parts of the assignment.

You should not modify any other files in the game engine.

DO NOT upload the entire game engine.