mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-24 10:15:03 +00:00
Refactor TransportShipExecution for improved pathfinding and routing
- Replace targetTransportTile with MultiSourceAnyTargetBFS bestTransportShipRoute for enhanced route determination - Update pathfinding logic to handle retreat paths
This commit is contained in:
@@ -10,9 +10,10 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { targetTransportTile } from "../game/TransportShipUtils";
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { PathFinder } from "../pathfinding/PathFinding";
|
||||
import {
|
||||
bestTransportShipRoute,
|
||||
boatPathFromTileToShore,
|
||||
} from "../game/TransportShipUtils";
|
||||
import { AttackExecution } from "./AttackExecution";
|
||||
|
||||
const malusForRetreat = 25;
|
||||
@@ -28,12 +29,13 @@ export class TransportShipExecution implements Execution {
|
||||
private target: Player | TerraNullius;
|
||||
|
||||
// TODO make private
|
||||
public path: TileRef[];
|
||||
public path: TileRef[] = [];
|
||||
private dst: TileRef | null;
|
||||
|
||||
private boat: Unit;
|
||||
|
||||
private pathFinder: PathFinder;
|
||||
private forwardPath: TileRef[] = [];
|
||||
private pathIndex = 0;
|
||||
private usingReverseRetreatPath = false;
|
||||
|
||||
private originalOwner: Player;
|
||||
|
||||
@@ -70,7 +72,6 @@ export class TransportShipExecution implements Execution {
|
||||
|
||||
this.lastMove = ticks;
|
||||
this.mg = mg;
|
||||
this.pathFinder = PathFinder.Mini(mg, 10_000, true, 100);
|
||||
|
||||
if (
|
||||
this.attacker.unitCount(UnitType.TransportShip) >=
|
||||
@@ -100,40 +101,33 @@ export class TransportShipExecution implements Execution {
|
||||
|
||||
this.startTroops = Math.min(this.startTroops, this.attacker.troops());
|
||||
|
||||
this.dst = targetTransportTile(this.mg, this.ref);
|
||||
if (this.dst === null) {
|
||||
const route = bestTransportShipRoute(
|
||||
this.mg,
|
||||
this.attacker,
|
||||
this.ref,
|
||||
this.src,
|
||||
);
|
||||
if (route === false) {
|
||||
console.warn(
|
||||
`${this.attacker} cannot send ship to ${this.target}, cannot find attack tile`,
|
||||
`${this.attacker} cannot send ship to ${this.target}, no route found`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const closestTileSrc = this.attacker.canBuild(
|
||||
UnitType.TransportShip,
|
||||
this.dst,
|
||||
);
|
||||
if (closestTileSrc === false) {
|
||||
console.warn(`can't build transport ship`);
|
||||
// Basic affordability/availability checks (avoid relying on transport-ship spawn heuristics).
|
||||
const boatCost = this.mg.unitInfo(UnitType.TransportShip).cost(this.mg, this.attacker);
|
||||
if (!this.attacker.isAlive() || this.attacker.gold() < boatCost) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.src === null) {
|
||||
// Only update the src if it's not already set
|
||||
// because we assume that the src is set to the best spawn tile
|
||||
this.src = closestTileSrc;
|
||||
} else {
|
||||
if (
|
||||
this.mg.owner(this.src) !== this.attacker ||
|
||||
!this.mg.isShore(this.src)
|
||||
) {
|
||||
console.warn(
|
||||
`src is not a shore tile or not owned by: ${this.attacker.name()}`,
|
||||
);
|
||||
this.src = closestTileSrc;
|
||||
}
|
||||
}
|
||||
this.src = route.src;
|
||||
this.dst = route.dst;
|
||||
this.forwardPath = route.path;
|
||||
this.path = route.path;
|
||||
this.pathIndex = 0;
|
||||
this.usingReverseRetreatPath = false;
|
||||
|
||||
this.boat = this.attacker.buildUnit(UnitType.TransportShip, this.src, {
|
||||
troops: this.startTroops,
|
||||
@@ -217,67 +211,96 @@ export class TransportShipExecution implements Execution {
|
||||
if (this.boat.targetTile() !== this.dst) {
|
||||
this.boat.setTargetTile(this.dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = this.pathFinder.nextTile(this.boat.tile(), this.dst);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
if (this.mg.owner(this.dst) === this.attacker) {
|
||||
const deaths = this.boat.troops() * (malusForRetreat / 100);
|
||||
const survivors = this.boat.troops() - deaths;
|
||||
this.attacker.addTroops(survivors);
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
|
||||
// Record stats
|
||||
this.mg
|
||||
.stats()
|
||||
.boatArriveTroops(this.attacker, this.target, survivors);
|
||||
if (deaths) {
|
||||
this.mg.displayMessage(
|
||||
`Attack cancelled, ${renderTroops(deaths)} soldiers killed during retreat.`,
|
||||
MessageType.ATTACK_CANCELLED,
|
||||
this.attacker.id(),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.attacker.conquer(this.dst);
|
||||
if (this.target.isPlayer() && this.attacker.isFriendly(this.target)) {
|
||||
this.attacker.addTroops(this.boat.troops());
|
||||
} else {
|
||||
this.mg.addExecution(
|
||||
new AttackExecution(
|
||||
this.boat.troops(),
|
||||
this.attacker,
|
||||
this.targetID,
|
||||
// Retreat is just the existing forward path in reverse (hot-path friendly).
|
||||
// Fallback to a recompute only if we can't safely reverse (e.g. path invalidated).
|
||||
if (!this.usingReverseRetreatPath) {
|
||||
const curr = this.boat.tile();
|
||||
const idx = curr === null ? -1 : this.forwardPath.indexOf(curr);
|
||||
if (idx >= 0) {
|
||||
this.path = this.forwardPath.slice(0, idx + 1).reverse();
|
||||
this.pathIndex = 0;
|
||||
this.usingReverseRetreatPath = true;
|
||||
} else {
|
||||
const retreatPath = boatPathFromTileToShore(
|
||||
this.mg,
|
||||
curr!,
|
||||
this.dst,
|
||||
false,
|
||||
),
|
||||
);
|
||||
);
|
||||
if (retreatPath !== null) {
|
||||
this.path = retreatPath;
|
||||
this.pathIndex = 0;
|
||||
this.usingReverseRetreatPath = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
|
||||
// Record stats
|
||||
this.mg
|
||||
.stats()
|
||||
.boatArriveTroops(this.attacker, this.target, this.boat.troops());
|
||||
return;
|
||||
case PathFindResultType.NextTile:
|
||||
this.boat.move(result.node);
|
||||
break;
|
||||
case PathFindResultType.Pending:
|
||||
break;
|
||||
case PathFindResultType.PathNotFound:
|
||||
// TODO: add to poisoned port list
|
||||
console.warn(`path not found to dst`);
|
||||
this.attacker.addTroops(this.boat.troops());
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.usingReverseRetreatPath = false;
|
||||
}
|
||||
|
||||
if (this.path.length === 0 || this.pathIndex >= this.path.length - 1) {
|
||||
// Treat as arrived; should be rare (e.g. src==dst edge).
|
||||
this.finish();
|
||||
return;
|
||||
}
|
||||
|
||||
const next = this.path[this.pathIndex + 1];
|
||||
if (next === undefined) {
|
||||
this.finish();
|
||||
return;
|
||||
}
|
||||
this.boat.move(next);
|
||||
this.pathIndex++;
|
||||
|
||||
if (this.dst !== null && next === this.dst) {
|
||||
this.finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private finish() {
|
||||
if (this.dst === null) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mg.owner(this.dst) === this.attacker) {
|
||||
const deaths = this.boat.troops() * (malusForRetreat / 100);
|
||||
const survivors = this.boat.troops() - deaths;
|
||||
this.attacker.addTroops(survivors);
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
|
||||
this.mg.stats().boatArriveTroops(this.attacker, this.target, survivors);
|
||||
if (deaths) {
|
||||
this.mg.displayMessage(
|
||||
`Attack cancelled, ${renderTroops(deaths)} soldiers killed during retreat.`,
|
||||
MessageType.ATTACK_CANCELLED,
|
||||
this.attacker.id(),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.attacker.conquer(this.dst);
|
||||
if (this.target.isPlayer() && this.attacker.isFriendly(this.target)) {
|
||||
this.attacker.addTroops(this.boat.troops());
|
||||
} else {
|
||||
this.mg.addExecution(
|
||||
new AttackExecution(
|
||||
this.boat.troops(),
|
||||
this.attacker,
|
||||
this.targetID,
|
||||
this.dst,
|
||||
false,
|
||||
),
|
||||
);
|
||||
}
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
|
||||
this.mg.stats().boatArriveTroops(this.attacker, this.target, this.boat.troops());
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
|
||||
@@ -1,8 +1,260 @@
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { MiniAStar } from "../pathfinding/MiniAStar";
|
||||
import { MultiSourceAnyTargetBFS } from "../pathfinding/MultiSourceAnyTargetBFS";
|
||||
import { Game, Player, UnitType } from "./Game";
|
||||
import { andFN, GameMap, manhattanDistFN, TileRef } from "./GameMap";
|
||||
|
||||
type BoatRoute = {
|
||||
src: TileRef;
|
||||
dst: TileRef;
|
||||
path: TileRef[];
|
||||
};
|
||||
|
||||
let boatBfs: MultiSourceAnyTargetBFS | null = null;
|
||||
let boatBfsNumTiles = 0;
|
||||
function getBoatBfs(gm: GameMap): MultiSourceAnyTargetBFS {
|
||||
const numTiles = gm.width() * gm.height();
|
||||
if (boatBfs === null || boatBfsNumTiles !== numTiles) {
|
||||
boatBfs = new MultiSourceAnyTargetBFS(numTiles);
|
||||
boatBfsNumTiles = numTiles;
|
||||
}
|
||||
return boatBfs;
|
||||
}
|
||||
|
||||
function insertTopK(
|
||||
items: { tile: TileRef; dist: number }[],
|
||||
tile: TileRef,
|
||||
dist: number,
|
||||
k: number,
|
||||
) {
|
||||
if (items.length === 0) {
|
||||
items.push({ tile, dist });
|
||||
return;
|
||||
}
|
||||
if (items.length === k && dist >= items[items.length - 1]!.dist) {
|
||||
return;
|
||||
}
|
||||
let i = items.length;
|
||||
items.push({ tile, dist });
|
||||
while (i > 0 && items[i - 1]!.dist > dist) {
|
||||
items[i] = items[i - 1]!;
|
||||
i--;
|
||||
}
|
||||
items[i] = { tile, dist };
|
||||
if (items.length > k) items.pop();
|
||||
}
|
||||
|
||||
function shoreTargetsNearClick(
|
||||
gm: Game,
|
||||
attacker: Player,
|
||||
click: TileRef,
|
||||
targetOwner: Player | ReturnType<Game["terraNullius"]>,
|
||||
maxTargets: number,
|
||||
scanRadiusTN: number,
|
||||
): TileRef[] {
|
||||
// Explicit target: if user clicks a shore tile, use that exact shore.
|
||||
if (gm.isShore(click) && gm.owner(click) !== attacker) {
|
||||
const owner = gm.owner(click);
|
||||
if (!owner.isPlayer() || !attacker.isFriendly(owner)) {
|
||||
return [click];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Water click: search a larger area but return only the closest shore tile.
|
||||
// This prevents "snapping" to far-away shores while still being usable on open water.
|
||||
if (gm.isWater(click)) {
|
||||
const cx = gm.x(click);
|
||||
const cy = gm.y(click);
|
||||
const r = 50;
|
||||
let best: TileRef | null = null;
|
||||
let bestDist = Infinity;
|
||||
for (let y = cy - r; y <= cy + r; y++) {
|
||||
for (let x = cx - r; x <= cx + r; x++) {
|
||||
if (!gm.isValidCoord(x, y)) continue;
|
||||
const tile = gm.ref(x, y);
|
||||
if (!gm.isShore(tile)) continue;
|
||||
const owner = gm.owner(tile);
|
||||
if (owner === attacker) continue;
|
||||
if (owner.isPlayer() && attacker.isFriendly(owner)) continue;
|
||||
const dist = Math.abs(x - cx) + Math.abs(y - cy);
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
best = tile;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best === null ? [] : [best];
|
||||
}
|
||||
|
||||
// Default behavior: scan a bounding box near the click for candidate shore tiles.
|
||||
// (Previously, player targets used all border tiles, which could pick very distant shores.)
|
||||
const top: { tile: TileRef; dist: number }[] = [];
|
||||
const cx = gm.x(click);
|
||||
const cy = gm.y(click);
|
||||
const r = scanRadiusTN;
|
||||
for (let y = cy - r; y <= cy + r; y++) {
|
||||
for (let x = cx - r; x <= cx + r; x++) {
|
||||
if (!gm.isValidCoord(x, y)) continue;
|
||||
const tile = gm.ref(x, y);
|
||||
if (!gm.isShore(tile)) continue;
|
||||
|
||||
if (targetOwner.isPlayer()) {
|
||||
if (gm.owner(tile) !== targetOwner) continue;
|
||||
} else {
|
||||
if (gm.hasOwner(tile)) continue;
|
||||
}
|
||||
|
||||
if (gm.owner(tile) === attacker) continue;
|
||||
const dist = Math.abs(x - cx) + Math.abs(y - cy);
|
||||
insertTopK(top, tile, dist, maxTargets);
|
||||
}
|
||||
}
|
||||
return top.map((x) => x.tile);
|
||||
}
|
||||
|
||||
function adjacentWaterTiles(gm: GameMap, shore: TileRef): TileRef[] {
|
||||
const out: TileRef[] = [];
|
||||
for (const n of gm.neighbors(shore)) {
|
||||
if (gm.isWater(n)) out.push(n);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function pickLandingForTargetWater(
|
||||
gm: GameMap,
|
||||
click: TileRef,
|
||||
targetWater: TileRef,
|
||||
targetShores: readonly TileRef[],
|
||||
): TileRef | null {
|
||||
// targetShores are already sorted by closeness to click; first adjacency wins.
|
||||
for (const shore of targetShores) {
|
||||
for (const n of gm.neighbors(shore)) {
|
||||
if (n === targetWater) return shore;
|
||||
}
|
||||
}
|
||||
// Fallback: should not happen if targetWater was built from these shores.
|
||||
let best: TileRef | null = null;
|
||||
let bestDist = Infinity;
|
||||
for (const shore of targetShores) {
|
||||
if (!gm.neighbors(shore).some((n) => gm.isWater(n))) continue;
|
||||
const d = gm.manhattanDist(click, shore);
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
best = shore;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
export function boatPathFromTileToShore(
|
||||
gm: GameMap,
|
||||
startTile: TileRef,
|
||||
dstShore: TileRef,
|
||||
): TileRef[] | null {
|
||||
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)) {
|
||||
seedNodes = [startTile];
|
||||
seedOrigins = [startTile];
|
||||
} else if (gm.isShore(startTile)) {
|
||||
const adj = adjacentWaterTiles(gm, startTile);
|
||||
if (adj.length === 0) return null;
|
||||
seedNodes = adj;
|
||||
seedOrigins = adj.map(() => startTile);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = bfs.findWaterPathFromSeeds(gm, seedNodes, seedOrigins, targetWater, {
|
||||
kingMoves: true,
|
||||
noCornerCutting: true,
|
||||
maxVisited: 300_000,
|
||||
});
|
||||
if (result === null) return null;
|
||||
|
||||
if (gm.isWater(startTile)) {
|
||||
return [...result.path, dstShore];
|
||||
}
|
||||
return [startTile, ...result.path, dstShore];
|
||||
}
|
||||
|
||||
export function bestTransportShipRoute(
|
||||
gm: Game,
|
||||
attacker: Player,
|
||||
clickTile: TileRef,
|
||||
preferredSrc: TileRef | null = null,
|
||||
maxTargetShores = 96,
|
||||
): BoatRoute | false {
|
||||
const other = gm.owner(clickTile);
|
||||
if (other === attacker) return false;
|
||||
if (other.isPlayer() && attacker.isFriendly(other)) return false;
|
||||
|
||||
const targetShores = shoreTargetsNearClick(
|
||||
gm,
|
||||
attacker,
|
||||
clickTile,
|
||||
other,
|
||||
maxTargetShores,
|
||||
10,
|
||||
);
|
||||
if (targetShores.length === 0) return false;
|
||||
|
||||
const targetWater: TileRef[] = [];
|
||||
for (const shore of targetShores) {
|
||||
targetWater.push(...adjacentWaterTiles(gm, shore));
|
||||
}
|
||||
if (targetWater.length === 0) return false;
|
||||
|
||||
const sourceShores: TileRef[] =
|
||||
preferredSrc !== null && gm.isValidRef(preferredSrc)
|
||||
? [preferredSrc]
|
||||
: candidateShoreTiles(gm, attacker, clickTile);
|
||||
|
||||
const seedNodeToOrigin = new Map<TileRef, TileRef>();
|
||||
for (const shore of sourceShores) {
|
||||
if (!gm.isValidRef(shore)) continue;
|
||||
if (gm.owner(shore) !== attacker) continue;
|
||||
if (!gm.isShore(shore)) continue;
|
||||
for (const w of adjacentWaterTiles(gm, shore)) {
|
||||
if (!seedNodeToOrigin.has(w)) {
|
||||
seedNodeToOrigin.set(w, shore);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (seedNodeToOrigin.size === 0) return false;
|
||||
|
||||
const seedNodes: TileRef[] = [];
|
||||
const seedOrigins: TileRef[] = [];
|
||||
for (const [node, origin] of seedNodeToOrigin.entries()) {
|
||||
seedNodes.push(node);
|
||||
seedOrigins.push(origin);
|
||||
}
|
||||
|
||||
const bfs = getBoatBfs(gm);
|
||||
const result = bfs.findWaterPathFromSeeds(gm, seedNodes, seedOrigins, targetWater, {
|
||||
kingMoves: true,
|
||||
noCornerCutting: true,
|
||||
// Hard budget to avoid pathological cases; tweak as needed.
|
||||
maxVisited: 300_000,
|
||||
});
|
||||
if (result === null) return false;
|
||||
|
||||
const dst = pickLandingForTargetWater(gm, clickTile, result.target, targetShores);
|
||||
if (dst === null) return false;
|
||||
|
||||
const src = result.source;
|
||||
// Full route includes the shore endpoints to drive unit movement.
|
||||
const path = [src, ...result.path, dst];
|
||||
return { src, dst, path };
|
||||
}
|
||||
|
||||
export function canBuildTransportShip(
|
||||
game: Game,
|
||||
player: Player,
|
||||
@@ -144,32 +396,9 @@ export function bestShoreDeploymentSource(
|
||||
player: Player,
|
||||
target: TileRef,
|
||||
): TileRef | false {
|
||||
const t = targetTransportTile(gm, target);
|
||||
if (t === null) return false;
|
||||
|
||||
const candidates = candidateShoreTiles(gm, player, t);
|
||||
if (candidates.length === 0) return false;
|
||||
|
||||
const aStar = new MiniAStar(gm, gm.miniMap(), candidates, t, 1_000_000, 1);
|
||||
const result = aStar.compute();
|
||||
if (result !== PathFindResultType.Completed) {
|
||||
console.warn(`bestShoreDeploymentSource: path not found: ${result}`);
|
||||
return false;
|
||||
}
|
||||
const path = aStar.reconstructPath();
|
||||
if (path.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const potential = path[0];
|
||||
// Since mini a* downscales the map, we need to check the neighbors
|
||||
// of the potential tile to find a valid deployment point
|
||||
const neighbors = gm
|
||||
.neighbors(potential)
|
||||
.filter((n) => gm.isShore(n) && gm.owner(n) === player);
|
||||
if (neighbors.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return neighbors[0];
|
||||
const route = bestTransportShipRoute(gm, player, target, null);
|
||||
if (route === false) return false;
|
||||
return route.src;
|
||||
}
|
||||
|
||||
export function candidateShoreTiles(
|
||||
|
||||
@@ -41,7 +41,17 @@ export class MultiSourceAnyTargetBFS {
|
||||
targets: readonly TileRef[],
|
||||
opts: MultiSourceAnyTargetBFSOptions = {},
|
||||
): MultiSourceAnyTargetBFSResult | null {
|
||||
if (sources.length === 0 || targets.length === 0) return null;
|
||||
return this.findWaterPathFromSeeds(gm, sources, sources, targets, opts);
|
||||
}
|
||||
|
||||
findWaterPathFromSeeds(
|
||||
gm: GameMap,
|
||||
seedNodes: readonly TileRef[],
|
||||
seedOrigins: readonly TileRef[],
|
||||
targets: readonly TileRef[],
|
||||
opts: MultiSourceAnyTargetBFSOptions = {},
|
||||
): MultiSourceAnyTargetBFSResult | null {
|
||||
if (seedNodes.length === 0 || targets.length === 0) return null;
|
||||
|
||||
const stamp = this.nextStamp();
|
||||
|
||||
@@ -58,14 +68,17 @@ export class MultiSourceAnyTargetBFS {
|
||||
let head = 0;
|
||||
let tail = 0;
|
||||
|
||||
for (const s of sources) {
|
||||
if (s < 0 || s >= this.visitedStamp.length) continue;
|
||||
if (!gm.isWater(s)) continue;
|
||||
if (this.visitedStamp[s] === stamp) continue;
|
||||
this.visitedStamp[s] = stamp;
|
||||
this.prev[s] = -1;
|
||||
this.startOf[s] = s;
|
||||
this.queue[tail++] = s;
|
||||
const count = Math.min(seedNodes.length, seedOrigins.length);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const node = seedNodes[i]!;
|
||||
const origin = seedOrigins[i]!;
|
||||
if (node < 0 || node >= this.visitedStamp.length) continue;
|
||||
if (!gm.isWater(node)) continue;
|
||||
if (this.visitedStamp[node] === stamp) continue;
|
||||
this.visitedStamp[node] = stamp;
|
||||
this.prev[node] = -1;
|
||||
this.startOf[node] = origin;
|
||||
this.queue[tail++] = node;
|
||||
}
|
||||
|
||||
if (tail === 0) return null;
|
||||
@@ -94,7 +107,7 @@ 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 (++visitedCount > maxVisited) return null;
|
||||
}
|
||||
}
|
||||
if (node < lastRowStart) {
|
||||
@@ -102,7 +115,7 @@ 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 (++visitedCount > maxVisited) return null;
|
||||
}
|
||||
}
|
||||
if (x !== 0) {
|
||||
@@ -110,7 +123,7 @@ 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 (++visitedCount > maxVisited) return null;
|
||||
}
|
||||
}
|
||||
if (x !== w - 1) {
|
||||
@@ -118,7 +131,7 @@ 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;
|
||||
if (++visitedCount > maxVisited) return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +147,7 @@ export class MultiSourceAnyTargetBFS {
|
||||
) {
|
||||
this.visit(nw, node, stamp);
|
||||
this.queue[tail++] = nw;
|
||||
if (++visitedCount >= maxVisited) return null;
|
||||
if (++visitedCount > maxVisited) return null;
|
||||
}
|
||||
}
|
||||
if (node >= w && x !== w - 1) {
|
||||
@@ -146,7 +159,7 @@ export class MultiSourceAnyTargetBFS {
|
||||
) {
|
||||
this.visit(ne, node, stamp);
|
||||
this.queue[tail++] = ne;
|
||||
if (++visitedCount >= maxVisited) return null;
|
||||
if (++visitedCount > maxVisited) return null;
|
||||
}
|
||||
}
|
||||
if (node < lastRowStart && x !== 0) {
|
||||
@@ -158,7 +171,7 @@ export class MultiSourceAnyTargetBFS {
|
||||
) {
|
||||
this.visit(sw, node, stamp);
|
||||
this.queue[tail++] = sw;
|
||||
if (++visitedCount >= maxVisited) return null;
|
||||
if (++visitedCount > maxVisited) return null;
|
||||
}
|
||||
}
|
||||
if (node < lastRowStart && x !== w - 1) {
|
||||
@@ -170,7 +183,7 @@ export class MultiSourceAnyTargetBFS {
|
||||
) {
|
||||
this.visit(se, node, stamp);
|
||||
this.queue[tail++] = se;
|
||||
if (++visitedCount >= maxVisited) return null;
|
||||
if (++visitedCount > maxVisited) return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,4 +214,3 @@ export class MultiSourceAnyTargetBFS {
|
||||
return this.stamp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user