// Unit tests for AStar algorithm
class Pathfinder extends AStar {
constructor(map = [], size = 0) {
super(getManhattanDistance);
this.domain = new Pathfinder.Domain(map, size);
}
static Node = class extends AStar.Node {
constructor(key = [], g = 0, h = 0, info = '', parentKey = []) {
super(key, g, h, info, parentKey);
}
// Updates the g, h and f costs and parent key of this node, then recomputes
// the info that shows direction (N/S/E/W) from the new parent to this node.
update(parentKey, g, h) {
super.update(parentKey, g, h);
// Determine the direction from the parent to this node.
if (parentKey.length > 1) {
let dy = this.key[0] - parentKey[0];
let dx = this.key[1] - parentKey[1];
if (dx == 1) this.info = 'E';
else if (dx == -1) this.info = 'W';
else if (dy == 1) this.info = 'S';
else if (dy == -1) this.info = 'N';
}
}
}
static Domain = class extends AStar.Domain {
constructor(map, size) {
super();
this.map = map;
this.size = size;
}
newNode(key) {
return new Pathfinder.Node(key);
}
getAdjacents(key) {
let j = key[0];
let i = key[1];
let array = [];
if (j >= 0 && j < this.size && i >= 0 && i < this.size) {
if (i > 0 && this.map[j * this.size + i - 1] == 0)
array.push(new Pathfinder.Node([j, i - 1], 10));
if (i < this.size - 1 && this.map[j * this.size + i + 1] == 0)
array.push(new Pathfinder.Node([j, i + 1], 10));
if (j > 0 && this.map[(j - 1) * this.size + i] == 0)
array.push(new Pathfinder.Node([j - 1, i], 10));
if (j < this.size - 1 && this.map[(j + 1) * this.size + i] == 0)
array.push(new Pathfinder.Node([j + 1, i], 10));
}
return array;
}
}
}
// The following code contains unit test function that test the functionality
// of the classes and functions for A* search.
// Non evaluated selftest. It tests the basic functionality of the Node,
// HashTable, and PriorityQueue
function test0() {
{
let closedlist = new HashTable();
let actual = closedlist.find([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
let expected = null;
compareAndReport('Test 0_1', actual, expected, true);
}
{
let closedlist = new HashTable();
closedlist.add(new Pathfinder.Node([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10, 0, 'N'));
let result1 = closedlist.find([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
let result2 = closedlist.find([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); // Repeat
let actual = result1.toString() + '|' + result2.toString();
let expected = '0,1,2,3,4,5,6,7,8,9 10 0 10 N|0,1,2,3,4,5,6,7,8,9 10 0 10 N';
compareAndReport('Test 0_2', actual, expected, true);
}
{
let closedlist = new HashTable();
closedlist.add(new Pathfinder.Node([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10, 0, 'N'));
closedlist.add(new Pathfinder.Node([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10, 0, 'S')); // Reset
let result = closedlist.find([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
let actual = result.toString();
let expected = '0,1,2,3,4,5,6,7,8,9 10 0 10 S';
compareAndReport('Test 0_3', actual, expected, true);
}
{
let closedlist = new HashTable();
closedlist.add(new Pathfinder.Node([1, 1], 10, 0, 'S', [0, 1]));
closedlist.add(new Pathfinder.Node([0, 1], 10, 0, 'E', [0, 0]));
closedlist.add(new Pathfinder.Node([0, 0], 10, 0, '', []));
let actual = closedlist.getPath([1, 1]).join(',');
let expected = 'E,S';
compareAndReport('Test 0_4', actual, expected, true);
}
{
let q = new PriorityQueue();
q.push(new Pathfinder.Node([0, 0], 3, 0, 'S'));
q.push(new Pathfinder.Node([1, 1], 2, 0, 'N'));
q.push(new Pathfinder.Node([2, 2], 5, 0, 'E'));
let actual = q.toString();
let expected = '|1,1 2 0 2 N||0,0 3 0 3 S||2,2 5 0 5 E|';
compareAndReport('Test 0_5', actual, expected, true);
}
{
let q = new PriorityQueue();
q.push(new Pathfinder.Node([0, 0], 3, 0, 'S'));
q.push(new Pathfinder.Node([1, 1], 2, 0, 'N'));
q.push(new Pathfinder.Node([2, 2], 1, 0, 'E'));
let actual = q.toString();
let expected = '|2,2 1 0 1 E||1,1 2 0 2 N||0,0 3 0 3 S|';
compareAndReport('Test 0_6', actual, expected, true);
}
{
let q = new PriorityQueue();
q.push(new Pathfinder.Node([0, 0], 3, 0, 'S'));
q.push(new Pathfinder.Node([1, 1], 2, 0, 'N'));
q.push(new Pathfinder.Node([2, 2], 1, 0, 'E'));
let found = q.pop([1, 1]);
let actual = found.toString() + q.toString();
let expected = '1,1 2 0 2 N|2,2 1 0 1 E||0,0 3 0 3 S|';
compareAndReport('Test 0_7', actual, expected, true);
}
{
let q = new PriorityQueue();
q.push(new Pathfinder.Node([0, 0], 30, 0, 'S'));
q.push(new Pathfinder.Node([1, 1], 20, 0, 'N'));
q.push(new Pathfinder.Node([2, 2], 10, 0, 'E'));
q.update([1, 1], [1, 0], 1, 2);
let actual = q.toString();
let expected = '|1,1 1 2 3 E 1,0||2,2 10 0 10 E||0,0 30 0 30 S|';
compareAndReport('Test 0_8', actual, expected, true);
}
}
// Test 1 tests the GetMapAdjacents class. It checks if the run() method returns
// all four adjacent nodes correctly.
function test1() {
let map = [
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 1, 0, 0,
0, 1, 0, 0, 0,
0, 0, 0, 0, 0
];
let pathfinder = new Pathfinder(map, 5);
let adjacents = pathfinder.domain.getAdjacents([1, 1]);
let actual = adjacents.map(node => node.toString()).join('|');
let expected = '1,0 10 0 10|1,2 10 0 10|0,1 10 0 10|2,1 10 0 10';
compareAndReport('Test 1', actual, expected);
}
// Test 2 tests the GetMapAdjacents class. It checks if the run() method returns
// adjacent nodes correctly excluding non-existing nodes.
function test2() {
let map = [
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 1, 0, 0,
0, 1, 0, 0, 0,
0, 0, 0, 0, 0
];
let pathfinder = new Pathfinder(map, 5);
let adjacents = pathfinder.domain.getAdjacents([4, 4]);
let actual = adjacents.map(node => node.toString()).join('|');
let expected = '4,3 10 0 10|3,4 10 0 10';
compareAndReport('Test 2', actual, expected);
}
// Test 3 tests the GetMapAdjacents class with a map that has obstacles. It
// checks if the adjacent nodes returned for specific coordinates correctly
// exclude the obstacles.
function test3() {
let map = [
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 1, 0, 0,
0, 1, 0, 0, 0,
0, 0, 0, 0, 0
];
let pathfinder = new Pathfinder(map, 5);
let adjacents = pathfinder.domain.getAdjacents([3, 2]);
let actual = adjacents.map(node => node.toString()).join('|');
let expected = '3,3 10 0 10|4,2 10 0 10';
compareAndReport('Test 3', actual, expected);
}
// Test 4 tests the AStar class with an empty map. It checks if the
// pathfinding algorithm correctly handles the case where there are no valid
// paths.
async function test4() {
let pathfinder = new Pathfinder();
let path = await pathfinder.run([1, 1], [1, 1]);
let actual = path.join(',');
let expected = '';
compareAndReport('Test 4', actual, expected);
}
// Test 5 tests the AStar class with a map and obstacles. It checks if
// the pathfinding algorithm correctly identifies the start point as the same
// as the end point and returns an empty path, indicating that no movement is
// needed to reach the destination.
async function test5() {
let map = [
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 1, 0, 0,
0, 1, 0, 0, 0,
0, 0, 0, 0, 0
];
let pathfinder = new Pathfinder(map, 5);
let path = await pathfinder.run([1, 1], [1, 1]);
let actual = path.join(',');
let expected = '';
compareAndReport('Test 5', actual, expected);
}
// Test 6 tests the AStar class with a map that has no obstacles blocking
// the path from the start point to the end point. It checks if the pathfinding
// algorithm can find the direct path from the left side to the
// right side of the map without any detours.
async function test6() {
let map = [
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 1, 0, 0,
0, 1, 0, 0, 0,
0, 0, 0, 0, 0
];
let pathfinder = new Pathfinder(map, 5);
let path = await pathfinder.run([1, 1], [1, 3]);
let actual = path.join(',');
let expected = 'E,E';
compareAndReport('Test 6', actual, expected);
}
// Test 7 tests the AStar class with a map that has no obstacles blocking
// the path from the start point to the end point. It checks if the pathfinding
// algorithm can find the direct path from the top-right corner to the
// bottom-right corner without any detours.
async function test7() {
let map = [
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 1, 0, 0,
0, 1, 0, 0, 0,
0, 0, 0, 0, 0
];
let pathfinder = new Pathfinder(map, 5);
let path = await pathfinder.run([0, 4], [4, 4]);
let actual = path.join(',');
let expected = 'S,S,S,S';
compareAndReport('Test 7', actual, expected);
}
// Test 8 tests the AStar class with a map that has an obstacle blocking the
// direct path from the start point to the end point. It checks if the
// pathfinding algorithm can find an alternative path around the obstacle to
// reach the destination.
async function test8() {
let map = [
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 1, 0, 0,
0, 1, 0, 0, 0,
0, 0, 0, 0, 0
];
let pathfinder = new Pathfinder(map, 5);
let path = await pathfinder.run([1, 1], [3, 3]);
let actual = path.join(',');
let expected = 'E,E,S,S';
compareAndReport('Test 8', actual, expected);
}
// Test 9 tests the AStar class with a map. End point is outside the map
// boundaries. It checks if the pathfinding algorithm correctly identifies
// that there is no valid path and returns an empty path.
async function test9() {
let map = [
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 1, 0, 0,
0, 1, 0, 0, 0,
0, 0, 0, 0, 0
];
let pathfinder = new Pathfinder(map, 5);
let path = await pathfinder.run([1, 1], [30, 30]);
let actual = path.join(',');
let expected = [].join(',');
compareAndReport('Test 9', actual, expected);
}
// Test 10 tests the AStar class with a more complex map that has multiple
// obstacles. It checks if the pathfinding algorithm can find the correct path
// from the top-left corner to the bottom-right corner while navigating around
// the obstacles.
async function test10() {
let map = [
0, 1, 0, 0, 0,
0, 1, 0, 1, 0,
0, 1, 0, 1, 0,
0, 1, 0, 1, 0,
0, 0, 0, 1, 0
];
let pathfinder = new Pathfinder(map, 5);
let path = await pathfinder.run([0, 0], [4, 4]);
let actual = path.join(',');
let expected = 'S,S,S,S,E,E,N,N,N,N,E,E,S,S,S,S';
compareAndReport('Test 10', actual, expected);
}