Add water component ID caching for improved pathfinding

- Introduced `getWaterComponentIds` function to cache water component IDs for game maps, enhancing pathfinding efficiency.
- Updated `boatPathFromTileToShore`, `boatPathFromTileToWater`, and `bestTransportShipRoute` functions to utilize water component IDs for more accurate routing.
- Removed unused `maxVisited` parameter from `MultiSourceAnyTargetBFS` options to streamline pathfinding logic.
This commit is contained in:
scamiv
2025-12-27 03:56:14 +01:00
parent 9769cf2550
commit 722c023535
4 changed files with 162 additions and 26 deletions
+5
View File
@@ -42,6 +42,7 @@ import { StatsImpl } from "./StatsImpl";
import { assignTeams } from "./TeamAssignment";
import { TerraNulliusImpl } from "./TerraNulliusImpl";
import { UnitGrid, UnitPredicate } from "./UnitGrid";
import { getWaterComponentIds } from "../pathfinding/WaterComponents";
export function createGame(
humans: PlayerInfo[],
@@ -50,6 +51,10 @@ export function createGame(
miniGameMap: GameMap,
config: Config,
): Game {
// Precompute and cache water-component IDs once per map instance.
getWaterComponentIds(gameMap);
getWaterComponentIds(miniGameMap);
const stats = new StatsImpl();
return new GameImpl(humans, nations, gameMap, miniGameMap, config, stats);
}
+83 -15
View File
@@ -1,4 +1,5 @@
import { MultiSourceAnyTargetBFS } from "../pathfinding/MultiSourceAnyTargetBFS";
import { getWaterComponentIds } from "../pathfinding/WaterComponents";
import { Game, Player, UnitType } from "./Game";
import { andFN, GameMap, manhattanDistFN, TileRef } from "./GameMap";
@@ -153,11 +154,6 @@ export function boatPathFromTileToShore(
if (!gm.isValidRef(startTile) || !gm.isValidRef(dstShore)) return null;
if (!gm.isShore(dstShore)) return null;
const targetWater = adjacentWaterTiles(gm, dstShore);
if (targetWater.length === 0) return null;
const bfs = getBoatBfs(gm);
let seedNodes: TileRef[] = [];
let seedOrigins: TileRef[] = [];
if (gm.isWater(startTile)) {
@@ -172,11 +168,46 @@ export function boatPathFromTileToShore(
return null;
}
const result = bfs.findWaterPathFromSeeds(gm, seedNodes, seedOrigins, targetWater, {
const targetWaterAll = adjacentWaterTiles(gm, dstShore);
if (targetWaterAll.length === 0) return null;
// Avoid impossible searches: restrict to the same connected water component.
const ids = getWaterComponentIds(gm);
const targetComps = new Set<number>();
for (const t of targetWaterAll) {
const id = ids[t] ?? 0;
if (id !== 0) targetComps.add(id);
}
if (targetComps.size === 0) return null;
const seedNodesFiltered: TileRef[] = [];
const seedOriginsFiltered: TileRef[] = [];
const seedComps = new Set<number>();
for (let i = 0; i < seedNodes.length; i++) {
const n = seedNodes[i]!;
const id = ids[n] ?? 0;
if (id === 0) continue;
if (!targetComps.has(id)) continue;
seedNodesFiltered.push(n);
seedOriginsFiltered.push(seedOrigins[i]!);
seedComps.add(id);
}
if (seedNodesFiltered.length === 0) return null;
const targetWater = targetWaterAll.filter((t) => seedComps.has(ids[t] ?? 0));
if (targetWater.length === 0) return null;
const bfs = getBoatBfs(gm);
const result = bfs.findWaterPathFromSeeds(
gm,
seedNodesFiltered,
seedOriginsFiltered,
targetWater,
{
kingMoves: true,
noCornerCutting: true,
maxVisited: 300_000, //todo: replace with a proper limit based on the map size
});
},
);
if (result === null) return null;
if (gm.isWater(startTile)) {
@@ -193,8 +224,6 @@ export function boatPathFromTileToWater(
if (!gm.isValidRef(startTile) || !gm.isValidRef(dstWater)) return null;
if (!gm.isWater(dstWater)) return null;
const bfs = getBoatBfs(gm);
let seedNodes: TileRef[] = [];
let seedOrigins: TileRef[] = [];
if (gm.isWater(startTile)) {
@@ -209,10 +238,25 @@ export function boatPathFromTileToWater(
return null;
}
const result = bfs.findWaterPathFromSeeds(gm, seedNodes, seedOrigins, [dstWater], {
// Avoid impossible searches: restrict seeds to the same connected water component.
const ids = getWaterComponentIds(gm);
const dstComp = ids[dstWater] ?? 0;
if (dstComp === 0) return null;
const seedNodesFiltered: TileRef[] = [];
const seedOriginsFiltered: TileRef[] = [];
for (let i = 0; i < seedNodes.length; i++) {
const n = seedNodes[i]!;
if ((ids[n] ?? 0) !== dstComp) continue;
seedNodesFiltered.push(n);
seedOriginsFiltered.push(seedOrigins[i]!);
}
if (seedNodesFiltered.length === 0) return null;
const bfs = getBoatBfs(gm);
const result = bfs.findWaterPathFromSeeds(gm, seedNodesFiltered, seedOriginsFiltered, [dstWater], {
kingMoves: true,
noCornerCutting: true,
maxVisited: 300_000,
});
if (result === null) return null;
@@ -272,12 +316,36 @@ export function bestTransportShipRoute(
seedOrigins.push(origin);
}
// Avoid impossible searches: restrict seeds/targets to overlapping water components.
const ids = getWaterComponentIds(gm);
const targetComps = new Set<number>();
for (const tw of targetWater) {
const id = ids[tw] ?? 0;
if (id !== 0) targetComps.add(id);
}
if (targetComps.size === 0) return false;
const seedNodesFiltered: TileRef[] = [];
const seedOriginsFiltered: TileRef[] = [];
const seedComps = new Set<number>();
for (let i = 0; i < seedNodes.length; i++) {
const n = seedNodes[i]!;
const id = ids[n] ?? 0;
if (id === 0) continue;
if (!targetComps.has(id)) continue;
seedNodesFiltered.push(n);
seedOriginsFiltered.push(seedOrigins[i]!);
seedComps.add(id);
}
if (seedNodesFiltered.length === 0) return false;
const targetWaterFiltered = targetWater.filter((t) => seedComps.has(ids[t] ?? 0));
if (targetWaterFiltered.length === 0) return false;
const bfs = getBoatBfs(gm);
const result = bfs.findWaterPathFromSeeds(gm, seedNodes, seedOrigins, targetWater, {
const result = bfs.findWaterPathFromSeeds(gm, seedNodesFiltered, seedOriginsFiltered, targetWaterFiltered, {
kingMoves: true,
noCornerCutting: true,
// Hard budget to avoid pathological cases; tweak as needed.
maxVisited: 300_000,
});
if (result === null) return false;
@@ -9,7 +9,6 @@ export type MultiSourceAnyTargetBFSResult = {
export type MultiSourceAnyTargetBFSOptions = {
kingMoves?: boolean;
noCornerCutting?: boolean;
maxVisited?: number;
};
/**
@@ -85,8 +84,6 @@ export class MultiSourceAnyTargetBFS {
const kingMoves = opts.kingMoves ?? true;
const noCornerCutting = opts.noCornerCutting ?? true;
const maxVisited = opts.maxVisited ?? this.visitedStamp.length;
let visitedCount = tail;
while (head < tail) {
const node = this.queue[head++] as TileRef;
@@ -107,7 +104,6 @@ export class MultiSourceAnyTargetBFS {
if (gm.isWater(n) && this.visitedStamp[n] !== stamp) {
this.visit(n, node, stamp);
this.queue[tail++] = n;
if (++visitedCount > maxVisited) return null;
}
}
if (node < lastRowStart) {
@@ -115,7 +111,6 @@ export class MultiSourceAnyTargetBFS {
if (gm.isWater(s) && this.visitedStamp[s] !== stamp) {
this.visit(s, node, stamp);
this.queue[tail++] = s;
if (++visitedCount > maxVisited) return null;
}
}
if (x !== 0) {
@@ -123,7 +118,6 @@ export class MultiSourceAnyTargetBFS {
if (gm.isWater(wv) && this.visitedStamp[wv] !== stamp) {
this.visit(wv, node, stamp);
this.queue[tail++] = wv;
if (++visitedCount > maxVisited) return null;
}
}
if (x !== w - 1) {
@@ -131,7 +125,6 @@ export class MultiSourceAnyTargetBFS {
if (gm.isWater(ev) && this.visitedStamp[ev] !== stamp) {
this.visit(ev, node, stamp);
this.queue[tail++] = ev;
if (++visitedCount > maxVisited) return null;
}
}
@@ -147,7 +140,6 @@ export class MultiSourceAnyTargetBFS {
) {
this.visit(nw, node, stamp);
this.queue[tail++] = nw;
if (++visitedCount > maxVisited) return null;
}
}
if (node >= w && x !== w - 1) {
@@ -159,7 +151,6 @@ export class MultiSourceAnyTargetBFS {
) {
this.visit(ne, node, stamp);
this.queue[tail++] = ne;
if (++visitedCount > maxVisited) return null;
}
}
if (node < lastRowStart && x !== 0) {
@@ -171,7 +162,6 @@ export class MultiSourceAnyTargetBFS {
) {
this.visit(sw, node, stamp);
this.queue[tail++] = sw;
if (++visitedCount > maxVisited) return null;
}
}
if (node < lastRowStart && x !== w - 1) {
@@ -183,7 +173,6 @@ export class MultiSourceAnyTargetBFS {
) {
this.visit(se, node, stamp);
this.queue[tail++] = se;
if (++visitedCount > maxVisited) return null;
}
}
}
+74
View File
@@ -0,0 +1,74 @@
import { GameMap, TileRef } from "../game/GameMap";
// Terrain (water/land) is immutable for a game map, so this can be cached forever per instance.
const cache = new WeakMap<GameMap, Uint32Array>();
export function getWaterComponentIds(gm: GameMap): Uint32Array {
const cached = cache.get(gm);
if (cached) return cached;
const w = gm.width();
const h = gm.height();
const numTiles = w * h;
const ids = new Uint32Array(numTiles); // 0 = not-water/unassigned, 1..N = component id
let nextId = 0;
const queue = new Int32Array(numTiles);
const lastRowStart = (h - 1) * w;
for (let start = 0; start < numTiles; start++) {
if (ids[start] !== 0) continue;
if (!gm.isWater(start)) continue;
nextId++;
ids[start] = nextId;
let head = 0;
let tail = 0;
queue[tail++] = start;
while (head < tail) {
const node = queue[head++]!;
const x = node % w;
if (node >= w) {
const n = node - w;
if (ids[n] === 0 && gm.isWater(n)) {
ids[n] = nextId;
queue[tail++] = n;
}
}
if (node < lastRowStart) {
const s = node + w;
if (ids[s] === 0 && gm.isWater(s)) {
ids[s] = nextId;
queue[tail++] = s;
}
}
if (x !== 0) {
const wv = node - 1;
if (ids[wv] === 0 && gm.isWater(wv)) {
ids[wv] = nextId;
queue[tail++] = wv;
}
}
if (x !== w - 1) {
const ev = node + 1;
if (ids[ev] === 0 && gm.isWater(ev)) {
ids[ev] = nextId;
queue[tail++] = ev;
}
}
}
}
cache.set(gm, ids);
return ids;
}
export function getWaterComponentId(gm: GameMap, tile: TileRef): number {
if (!gm.isWater(tile)) return 0;
const ids = getWaterComponentIds(gm);
return ids[tile] ?? 0;
}