99 lines
3.3 KiB
JavaScript
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");
|
|
}
|
|
}
|
|
}
|