Files
Battle-for-Tismo/tests/hex_pathfinder_test.js
T

99 lines
3.3 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import { HexPathfinder } from "../shared/hex_pathfinder.js";
// A plain 4-neighbour square grid keeps the search easy to reason about.
function neighboursWithin(size) {
return (coords) => {
const result = [];
for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
const next = { x: coords.x + dx, y: coords.y + dy };
if (next.x >= 0 && next.y >= 0 && next.x < size.x && next.y < size.y) result.push(next);
}
return result;
};
}
function find(start, goal, canEnter = () => true) {
return new HexPathfinder().findPath(
start,
goal,
neighboursWithin({ x: 4, y: 4 }),
canEnter,
() => 1,
() => 0
);
}
function isConnected(path) {
for (let i = 1; i < path.length; i++) {
const step = { x: path[i].x - path[i - 1].x, y: path[i].y - path[i - 1].y };
if (Math.abs(step.x) + Math.abs(step.y) !== 1) return false;
}
return true;
}
export class HexPathfinderTest extends TestCase {
test_straight_path_reaches_goal() {
const path = find({ x: 0, y: 0 }, { x: 2, y: 0 });
this.assertSize(path, 3);
this.assertEqual(path[0], { x: 0, y: 0 });
this.assertEqual(path[2], { x: 2, y: 0 });
this.assertTrue(isConnected(path));
}
test_start_equals_goal_is_empty() {
this.assertEmpty(find({ x: 1, y: 1 }, { x: 1, y: 1 }));
}
test_blocked_goal_is_empty() {
this.assertEmpty(find({ x: 0, y: 0 }, { x: 1, y: 0 }, (c) => !(c.x === 1 && c.y === 0)));
}
test_unreachable_goal_is_empty() {
const onlyEndpoints = (c) =>
(c.x === 0 && c.y === 0) || (c.x === 3 && c.y === 3);
this.assertEmpty(find({ x: 0, y: 0 }, { x: 3, y: 3 }, onlyEndpoints));
}
test_path_detours_around_blocker() {
const openExceptStartRight = (c) => !(c.x === 1 && c.y === 0);
const path = find({ x: 0, y: 0 }, { x: 2, y: 0 }, openExceptStartRight);
this.assertSize(path, 5);
this.assertEqual(path[0], { x: 0, y: 0 });
this.assertEqual(path[path.length - 1], { x: 2, y: 0 });
this.assertTrue(isConnected(path));
}
test_a_tile_off_limits_for_transit_is_avoided() {
const canEnter = (c, isGoal) => !(c.x === 1 && c.y === 0) || isGoal;
const path = find({ x: 0, y: 0 }, { x: 2, y: 0 }, canEnter);
this.assertTrue(path.length > 3, "the path detours around the blocked tile");
for (const cell of path) {
this.assertFalse(cell.x === 1 && cell.y === 0, "never steps onto the blocked tile");
}
}
test_the_goal_may_be_off_limits_for_transit() {
const canEnter = (c, isGoal) => !(c.x === 1 && c.y === 0) || isGoal;
const path = find({ x: 0, y: 0 }, { x: 1, y: 0 }, canEnter);
this.assertSize(path, 2);
this.assertEqual(path[1], { x: 1, y: 0 });
}
test_prefers_a_cheaper_detour() {
// The straight road is expensive; a one-tile detour must be chosen.
const expensive = new Set(["1,0", "2,0"]);
const path = new HexPathfinder().findPath(
{ x: 0, y: 0 },
{ x: 3, y: 0 },
neighboursWithin({ x: 5, y: 5 }),
() => true,
(_from, to) => (expensive.has(`${to.x},${to.y}`) ? 10 : 1),
(from, to) => Math.abs(from.x - to.x) + Math.abs(from.y - to.y)
);
for (const cell of path) {
this.assertFalse(expensive.has(`${cell.x},${cell.y}`), "avoided the expensive tiles");
}
}
}