mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-06 15:26:46 +00:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user