Files
OpenFrontIO/tests/core/pathfinding/PathFinding.Water.test.ts
Arkadiusz Sygulski 13b4142317 Pathfinding Refactor
2026-01-11 21:34:37 +01:00

278 lines
7.6 KiB
TypeScript

import { beforeAll, describe, expect, it, vi } from "vitest";
import { Game } from "../../../src/core/game/Game";
import { TileRef } from "../../../src/core/game/GameMap";
import { PathFinding } from "../../../src/core/pathfinding/PathFinder";
import {
PathStatus,
SteppingPathFinder,
} from "../../../src/core/pathfinding/types";
import { setup } from "../../util/Setup";
import { createGame, L, W } from "./_fixtures";
describe("PathFinding.Water", () => {
let game: Game;
let worldGame: Game;
function createPathFinder(g: Game = game): SteppingPathFinder<TileRef> {
return PathFinding.Water(g);
}
beforeAll(async () => {
game = await setup("ocean_and_land");
worldGame = await setup("world", { disableNavMesh: false });
});
describe("findPath", () => {
it("finds path between adjacent water tiles", () => {
const pathFinder = createPathFinder();
const map = game.map();
const from = map.ref(8, 0);
const to = map.ref(9, 0);
expect(map.isWater(from)).toBe(true);
expect(map.isWater(to)).toBe(true);
const path = pathFinder.findPath(from, to);
expect(path).not.toBeNull();
expect(path!.length).toBe(2);
expect(path![0]).toBe(from);
expect(path![1]).toBe(to);
});
it("returns null for land tiles", () => {
const pathFinder = createPathFinder();
const map = game.map();
const landTile = map.ref(0, 0);
const waterTile = map.ref(8, 0);
expect(map.isLand(landTile)).toBe(true);
expect(map.isShore(landTile)).toBe(false);
expect(map.isWater(waterTile)).toBe(true);
const path = pathFinder.findPath(landTile, waterTile);
expect(path).toBeNull();
});
it("returns single-tile path when from equals to", () => {
const pathFinder = createPathFinder();
const map = game.map();
const waterTile = map.ref(8, 0);
expect(map.isWater(waterTile)).toBe(true);
const path = pathFinder.findPath(waterTile, waterTile);
expect(path).not.toBeNull();
expect(path!.length).toBe(1);
expect(path![0]).toBe(waterTile);
});
it("supports multiple start tiles", () => {
const pathFinder = createPathFinder();
const map = game.map();
const dest = map.ref(8, 0);
const source1 = map.ref(9, 0);
const source2 = map.ref(8, 1);
expect(map.isWater(dest)).toBe(true);
expect(map.isWater(source1)).toBe(true);
expect(map.isWater(source2)).toBe(true);
const from = [source1, source2];
const path = pathFinder.findPath(from, dest);
expect(path).not.toBeNull();
expect(path!.length).toBe(2);
expect(from).toContain(path![0]);
expect(path![1]).toBe(dest);
});
});
describe("path validity", () => {
it("all consecutive tiles in path are connected", () => {
const pathFinder = createPathFinder();
const map = game.map();
// Distant water tiles: (8,0) → (15,4), distance = 11
const from = map.ref(8, 0);
const to = map.ref(15, 4);
expect(map.isWater(from)).toBe(true);
expect(map.isWater(to)).toBe(true);
expect(map.manhattanDist(from, to)).toBe(11);
const path = pathFinder.findPath(from, to);
expect(path).not.toBeNull();
for (let i = 1; i < path!.length; i++) {
const dist = map.manhattanDist(path![i - 1], path![i]);
expect(dist).toEqual(1);
}
});
});
describe("shore handling", () => {
it("path from shore to shore starts and ends on shore", () => {
const pathFinder = createPathFinder();
const map = game.map();
// Shore tiles at (7,0) and (7,6), distance = 6
// Both have water neighbors at (8,0) and (8,6)
const from = map.ref(7, 0);
const to = map.ref(7, 6);
expect(map.isShore(from)).toBe(true);
expect(map.isShore(to)).toBe(true);
expect(map.manhattanDist(from, to)).toBe(6);
const path = pathFinder.findPath(from, to);
expect(path).not.toBeNull();
expect(path![0]).toBe(from);
expect(path![path!.length - 1]).toBe(to);
});
});
describe("determinism", () => {
it("same inputs produce identical paths", () => {
const pathFinder1 = createPathFinder();
const pathFinder2 = createPathFinder();
const map = game.map();
// Distant water tiles: (8,0) → (15,4)
const from = map.ref(8, 0);
const to = map.ref(15, 4);
const path1 = pathFinder1.findPath(from, to);
const path2 = pathFinder2.findPath(from, to);
expect(path1).not.toBeNull();
expect(path2).not.toBeNull();
expect(path1).toEqual(path2);
});
});
describe("World map routes", () => {
it("Spain to France (Mediterranean)", () => {
const pathFinder = createPathFinder(worldGame);
const path = pathFinder.findPath(
worldGame.ref(926, 283),
worldGame.ref(950, 257),
);
expect(path).not.toBeNull();
});
it("Miami to Rio (Atlantic)", () => {
const pathFinder = createPathFinder(worldGame);
const path = pathFinder.findPath(
worldGame.ref(488, 355),
worldGame.ref(680, 658),
);
expect(path).not.toBeNull();
});
it("France to Poland (around Europe)", () => {
const pathFinder = createPathFinder(worldGame);
const path = pathFinder.findPath(
worldGame.ref(950, 257),
worldGame.ref(1033, 175),
);
expect(path).not.toBeNull();
});
it("Miami to Spain (transatlantic)", () => {
const pathFinder = createPathFinder(worldGame);
const path = pathFinder.findPath(
worldGame.ref(488, 355),
worldGame.ref(926, 283),
);
expect(path).not.toBeNull();
});
it("Rio to Poland (South Atlantic to Baltic)", () => {
const pathFinder = createPathFinder(worldGame);
const path = pathFinder.findPath(
worldGame.ref(680, 658),
worldGame.ref(1033, 175),
);
expect(path).not.toBeNull();
});
});
describe("Error handling", () => {
it("returns NOT_FOUND for null source", () => {
const pathFinder = createPathFinder();
const consoleSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});
const result = pathFinder.next(
null as unknown as TileRef,
game.ref(8, 0),
);
expect(result.status).toBe(PathStatus.NOT_FOUND);
consoleSpy.mockRestore();
});
it("returns NOT_FOUND for null destination", () => {
const pathFinder = createPathFinder();
const consoleSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});
const result = pathFinder.next(
game.ref(8, 0),
null as unknown as TileRef,
);
expect(result.status).toBe(PathStatus.NOT_FOUND);
consoleSpy.mockRestore();
});
});
describe("Known bugs", () => {
it("path can cross 1-tile land barrier", () => {
const syntheticGame = createGame({
width: 10,
height: 1,
grid: [W, L, L, W, L, W, W, L, L, W],
});
const pathFinder = createPathFinder(syntheticGame);
const path = pathFinder.findPath(
syntheticGame.ref(0, 0),
syntheticGame.ref(9, 0),
);
expect(path).not.toBeNull();
});
it("path can cross diagonal land barrier", () => {
const syntheticGame = createGame({
width: 2,
height: 2,
grid: [W, L, L, W],
});
const pathFinder = createPathFinder(syntheticGame);
const path = pathFinder.findPath(
syntheticGame.ref(0, 0),
syntheticGame.ref(1, 1),
);
expect(path).not.toBeNull();
});
});
});