mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-14 14:24:42 +00:00
## 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>
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.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];
|
|
}
|