mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-16 23:12:33 +00:00
## Description: Use an iterative approach to counting units to reduce array allocations. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [ ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors
261 lines
6.3 KiB
TypeScript
261 lines
6.3 KiB
TypeScript
import { PathFindResultType } from "../pathfinding/AStar";
|
|
import { MiniAStar } from "../pathfinding/MiniAStar";
|
|
import { Game, Player, UnitType } from "./Game";
|
|
import { andFN, GameMap, manhattanDistFN, TileRef } from "./GameMap";
|
|
|
|
export function canBuildTransportShip(
|
|
game: Game,
|
|
player: Player,
|
|
tile: TileRef,
|
|
): TileRef | false {
|
|
if (
|
|
player.unitCount(UnitType.TransportShip) >= game.config().boatMaxNumber()
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const dst = targetTransportTile(game, tile);
|
|
if (dst === null) {
|
|
return false;
|
|
}
|
|
|
|
const other = game.owner(tile);
|
|
if (other === player) {
|
|
return false;
|
|
}
|
|
if (other.isPlayer() && player.isFriendly(other)) {
|
|
return false;
|
|
}
|
|
|
|
if (game.isOceanShore(dst)) {
|
|
let myPlayerBordersOcean = false;
|
|
for (const bt of player.borderTiles()) {
|
|
if (game.isOceanShore(bt)) {
|
|
myPlayerBordersOcean = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let otherPlayerBordersOcean = false;
|
|
if (!game.hasOwner(tile)) {
|
|
otherPlayerBordersOcean = true;
|
|
} else {
|
|
for (const bt of (other as Player).borderTiles()) {
|
|
if (game.isOceanShore(bt)) {
|
|
otherPlayerBordersOcean = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (myPlayerBordersOcean && otherPlayerBordersOcean) {
|
|
return transportShipSpawn(game, player, dst);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Now we are boating in a lake, so do a bfs from target until we find
|
|
// a border tile owned by the player
|
|
|
|
const tiles = game.bfs(
|
|
dst,
|
|
andFN(
|
|
manhattanDistFN(dst, 300),
|
|
(_, t: TileRef) => game.isLake(t) || game.isShore(t),
|
|
),
|
|
);
|
|
|
|
const sorted = Array.from(tiles).sort(
|
|
(a, b) => game.manhattanDist(dst, a) - game.manhattanDist(dst, b),
|
|
);
|
|
|
|
for (const t of sorted) {
|
|
if (game.owner(t) === player) {
|
|
return transportShipSpawn(game, player, t);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function transportShipSpawn(
|
|
game: Game,
|
|
player: Player,
|
|
targetTile: TileRef,
|
|
): TileRef | false {
|
|
if (!game.isShore(targetTile)) {
|
|
return false;
|
|
}
|
|
const spawn = closestShoreFromPlayer(game, player, targetTile);
|
|
if (spawn === null) {
|
|
return false;
|
|
}
|
|
return spawn;
|
|
}
|
|
|
|
export function sourceDstOceanShore(
|
|
gm: Game,
|
|
src: Player,
|
|
tile: TileRef,
|
|
): [TileRef | null, TileRef | null] {
|
|
const dst = gm.owner(tile);
|
|
const srcTile = closestShoreFromPlayer(gm, src, tile);
|
|
let dstTile: TileRef | null = null;
|
|
if (dst.isPlayer()) {
|
|
dstTile = closestShoreFromPlayer(gm, dst as Player, tile);
|
|
} else {
|
|
dstTile = closestShoreTN(gm, tile, 50);
|
|
}
|
|
return [srcTile, dstTile];
|
|
}
|
|
|
|
export function targetTransportTile(gm: Game, tile: TileRef): TileRef | null {
|
|
const dst = gm.playerBySmallID(gm.ownerID(tile));
|
|
let dstTile: TileRef | null = null;
|
|
if (dst.isPlayer()) {
|
|
dstTile = closestShoreFromPlayer(gm, dst as Player, tile);
|
|
} else {
|
|
dstTile = closestShoreTN(gm, tile, 50);
|
|
}
|
|
return dstTile;
|
|
}
|
|
|
|
export function closestShoreFromPlayer(
|
|
gm: GameMap,
|
|
player: Player,
|
|
target: TileRef,
|
|
): TileRef | null {
|
|
const shoreTiles = Array.from(player.borderTiles()).filter((t) =>
|
|
gm.isShore(t),
|
|
);
|
|
if (shoreTiles.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return shoreTiles.reduce((closest, current) => {
|
|
const closestDistance = gm.manhattanDist(target, closest);
|
|
const currentDistance = gm.manhattanDist(target, current);
|
|
return currentDistance < closestDistance ? current : closest;
|
|
});
|
|
}
|
|
|
|
export function bestShoreDeploymentSource(
|
|
gm: Game,
|
|
player: Player,
|
|
target: TileRef,
|
|
): TileRef | false {
|
|
const t = targetTransportTile(gm, target);
|
|
if (t === null) return false;
|
|
|
|
const candidates = candidateShoreTiles(gm, player, t);
|
|
const aStar = new MiniAStar(gm, gm.miniMap(), candidates, t, 500_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];
|
|
}
|
|
|
|
export function candidateShoreTiles(
|
|
gm: Game,
|
|
player: Player,
|
|
target: TileRef,
|
|
): TileRef[] {
|
|
let closestManhattanDistance = Infinity;
|
|
let minX = Infinity,
|
|
minY = Infinity,
|
|
maxX = -Infinity,
|
|
maxY = -Infinity;
|
|
|
|
let bestByManhattan: TileRef | null = null;
|
|
const extremumTiles: Record<string, TileRef | null> = {
|
|
minX: null,
|
|
minY: null,
|
|
maxX: null,
|
|
maxY: null,
|
|
};
|
|
|
|
const borderShoreTiles = Array.from(player.borderTiles()).filter((t) =>
|
|
gm.isShore(t),
|
|
);
|
|
|
|
for (const tile of borderShoreTiles) {
|
|
const distance = gm.manhattanDist(tile, target);
|
|
const cell = gm.cell(tile);
|
|
|
|
// Manhattan-closest tile
|
|
if (distance < closestManhattanDistance) {
|
|
closestManhattanDistance = distance;
|
|
bestByManhattan = tile;
|
|
}
|
|
|
|
// Extremum tiles
|
|
if (cell.x < minX) {
|
|
minX = cell.x;
|
|
extremumTiles.minX = tile;
|
|
} else if (cell.y < minY) {
|
|
minY = cell.y;
|
|
extremumTiles.minY = tile;
|
|
} else if (cell.x > maxX) {
|
|
maxX = cell.x;
|
|
extremumTiles.maxX = tile;
|
|
} else if (cell.y > maxY) {
|
|
maxY = cell.y;
|
|
extremumTiles.maxY = tile;
|
|
}
|
|
}
|
|
|
|
// Calculate sampling interval to ensure we get at most 50 tiles
|
|
const samplingInterval = Math.max(
|
|
10,
|
|
Math.ceil(borderShoreTiles.length / 50),
|
|
);
|
|
const sampledTiles = borderShoreTiles.filter(
|
|
(_, index) => index % samplingInterval === 0,
|
|
);
|
|
|
|
const candidates = [
|
|
bestByManhattan,
|
|
extremumTiles.minX,
|
|
extremumTiles.minY,
|
|
extremumTiles.maxX,
|
|
extremumTiles.maxY,
|
|
...sampledTiles,
|
|
].filter(Boolean) as number[];
|
|
|
|
return candidates;
|
|
}
|
|
|
|
function closestShoreTN(
|
|
gm: GameMap,
|
|
tile: TileRef,
|
|
searchDist: number,
|
|
): TileRef | null {
|
|
const tn = Array.from(
|
|
gm.bfs(
|
|
tile,
|
|
andFN((_, t) => !gm.hasOwner(t), manhattanDistFN(tile, searchDist)),
|
|
),
|
|
)
|
|
.filter((t) => gm.isShore(t))
|
|
.sort((a, b) => gm.manhattanDist(tile, a) - gm.manhattanDist(tile, b));
|
|
if (tn.length === 0) {
|
|
return null;
|
|
}
|
|
return tn[0];
|
|
}
|