Files
OpenFrontIO/src/core/game/TransportShipUtils.ts
T
70745faac4 Enable strictNullChecks, eqeqeq (#436)
## Description:

Improve type safety and runtime correctness by:
1. Enabling TypeScript's
[strictNullChecks](https://www.typescriptlang.org/tsconfig/#strictNullChecks)
compiler option.
2. Replacing all loose equality operators (`==` and `!=`) with strict
equality operators (`===` and `!==`).
3. Cleaning up of type declarations, null handling logic, and equality
expressions throughout the project.

Currently, the code allows implicit assumptions that `null` and
`undefined` are interchangeable, and relies on type-coercing equality
checks that can introduce subtle bugs. These practices make it difficult
to reason about when values may be absent and hinder the effectiveness
of static analysis.

Migrating to strict null checks and enforcing strict equality
comparisons will clarify intent, reduce bugs, and make the codebase
safer and easier to maintain.

Fixes #466 

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] 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

---------

Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com>
Co-authored-by: evanpelle <openfrontio@gmail.com>
2025-05-15 16:39:40 -07:00

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.units(UnitType.TransportShip).length >= 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];
}