mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-20 02:40:00 +00:00
Pathfinding Refactor pt. 2 (#2866)
## Playtest https://pf-pt-2.openfront.dev/ ## Pathfinding Refactor pt. 2 <img width="1536" height="1024" alt="image" src="https://github.com/user-attachments/assets/9477958e-54b7-4c83-b317-ba789e809e9e" /> This is a follow-up to a previous PR introducing pathfinding changes. This time, it introduces a complete refactor of `pathfinding` directory and breakdown into composable pieces. ### Unified PathFinder interface `PathFinder<T>` and `SteppingPathFinder<T>` are introduced to unify **all** pathfinding across the application. First one exposes complete path, while stepping variant allows the callee to iterate over the path by calling `.next`. All pathfinders share this one common interface, which makes them easy to use in any scenario - `PathFinding.Water(game).search(from, to)`. `SteppingPathFinder<T>` extends `PathFinder<T>` with an ability to iterate over the path. It handles caching, storing current index and invalidation. This allows the units to not care about the inner workings of the pathfinder and just call `pf.next(current, target)` and receive instructions on what to do next. ### Common entry point All pathfinders are now exposed from common `PathFinding` entrypoint: - `PathFinding.Water` - `PathFinding.Rail` - `PathFinding.Stations` - `PathFinding.Rail` Additional entry point is introduced for pathfinders which need to work both in the worker, but also on the frontend, which lacks `Game` interface. Currently only `UniversalPathFinding.Parabola` is available. ### Spatial Query New module has been introduced close to `pathfinding` - `SpatialQuery`. It aims to resolve any questions game may have about finding tiles meeting criteria. Currently `SpatialQuery.closestShore(player, target)` and `SpatialQuery.closestShoreByWater(player, target)` are available - they help answering questions about naval invasion: "What is the best landing location from user's click?" and "Which our tile should be used to launch the transport ship?". Under the hood they use very similar mechanics to pathfinding, so it felt right to put them close by. ### Modular architecture Pathfinders now support transformers: `MiniMapTransformer`, `ShoreCoercingTransformer`, `ComponentCheckTransformer`, `SmoothingTransformer`. Transformers functions like a middleware in the pathfinding chain. They wrap around the pathfinder and provide additional functionality. This allows the pathfinder to focus on actually finding the path instead of doing unrelated things. Example chain for simple (A*) water pathfinding: ```ts static WaterSimple(game: Game): SteppingPathFinder<TileRef> { const miniMap = game.miniMap(); const pf = new AStarWater(miniMap); return PathFinderBuilder.create(pf) .wrap((pf) => new ShoreCoercingTransformer(pf, miniMap)) .wrap((pf) => new MiniMapTransformer(pf, game.map(), miniMap)) .buildWithStepper(tileStepperConfig(game)); } ``` The Pathfinder - here `AStarWater` - does not care about the conversion between minimap and main map tiles. It also does not care if the source or destination is a land tile. The transformers take care of that. The pathfinder gets a set of valid coordinates and produces the path - that's it. Modular approach makes working on a particular set of utilities much easier - for example map upscaling is handled consistently across all pathfinders. Additionally, the pathfinders are not tied to the particular map resolution used. Pass them a different map and they will work the same. ### Algorithms Algorithms used are neatly organized inside `src/core/pathfinding/algorithms`. They are prefixed with the algorithm name and suffixed with the use case. File without suffix exposes generic version ready to traverse any graph with adapters. Specialized versions either use an adapter or inline logic when performance is critical - using adapters leads to 20-30% performance loss. The directory includes `A*` and `BFS` but also other useful utils, such as `AbstractGraph` used to generate... an abstract graph on top of the tile map and `ConnectedComponents` helping to identify whether two tiles are connected by a path without actually computing the path. ### Playground The playground have been updated with new algorithms, including tweaked very greedy `A*`. <img width="2175" height="1424" alt="image" src="https://github.com/user-attachments/assets/1f833651-0024-4299-bf86-882f5368358c" /> ### Tests Yeah, there are some, a little too many if I say so myself. But there are no useless tests. I had to ensure refactored code works somehow reliably. This PR comes with trust me bro guarantee, but I would appreciate someone confirming **naval invasions, nukes (esp. MIRV) and warships**. ### Discord `moleole` GL & HF
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Config } from "../configuration/Config";
|
||||
import { NavMesh } from "../pathfinding/navmesh/NavMesh";
|
||||
import { AbstractGraph } from "../pathfinding/algorithms/AbstractGraph";
|
||||
import { PathFinder } from "../pathfinding/types";
|
||||
import { AllPlayersStats, ClientID } from "../Schemas";
|
||||
import { getClanTag } from "../Util";
|
||||
import { GameMap, TileRef } from "./GameMap";
|
||||
@@ -802,7 +803,10 @@ export interface Game extends GameMap {
|
||||
addUpdate(update: GameUpdate): void;
|
||||
railNetwork(): RailNetwork;
|
||||
conquerPlayer(conqueror: Player, conquered: Player): void;
|
||||
navMesh(): NavMesh | null;
|
||||
miniWaterHPA(): PathFinder<number> | null;
|
||||
miniWaterGraph(): AbstractGraph | null;
|
||||
getWaterComponent(tile: TileRef): number | null;
|
||||
hasWaterComponent(tile: TileRef, component: number): boolean;
|
||||
}
|
||||
|
||||
export interface PlayerActions {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { renderNumber } from "../../client/Utils";
|
||||
import { Config } from "../configuration/Config";
|
||||
import { NavMesh } from "../pathfinding/navmesh/NavMesh";
|
||||
import {
|
||||
AbstractGraph,
|
||||
AbstractGraphBuilder,
|
||||
} from "../pathfinding/algorithms/AbstractGraph";
|
||||
import { AStarWaterHierarchical } from "../pathfinding/algorithms/AStar.WaterHierarchical";
|
||||
import { PathFinder } from "../pathfinding/types";
|
||||
import { AllPlayersStats, ClientID, Winner } from "../Schemas";
|
||||
import { simpleHash } from "../Util";
|
||||
import { AllianceImpl } from "./AllianceImpl";
|
||||
@@ -87,7 +92,8 @@ export class GameImpl implements Game {
|
||||
private nextAllianceID: number = 0;
|
||||
|
||||
private _isPaused: boolean = false;
|
||||
private _navMesh: NavMesh | null = null;
|
||||
private _miniWaterGraph: AbstractGraph | null = null;
|
||||
private _miniWaterHPA: AStarWaterHierarchical | null = null;
|
||||
|
||||
constructor(
|
||||
private _humans: PlayerInfo[],
|
||||
@@ -108,8 +114,14 @@ export class GameImpl implements Game {
|
||||
this.addPlayers();
|
||||
|
||||
if (!_config.disableNavMesh()) {
|
||||
this._navMesh = new NavMesh(this, { cachePaths: true });
|
||||
this._navMesh.initialize();
|
||||
const graphBuilder = new AbstractGraphBuilder(this.miniGameMap);
|
||||
this._miniWaterGraph = graphBuilder.build();
|
||||
|
||||
this._miniWaterHPA = new AStarWaterHierarchical(
|
||||
this.miniGameMap,
|
||||
this._miniWaterGraph,
|
||||
{ cachePaths: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -966,8 +978,79 @@ export class GameImpl implements Game {
|
||||
railNetwork(): RailNetwork {
|
||||
return this._railNetwork;
|
||||
}
|
||||
navMesh(): NavMesh | null {
|
||||
return this._navMesh;
|
||||
miniWaterHPA(): PathFinder<number> | null {
|
||||
return this._miniWaterHPA;
|
||||
}
|
||||
miniWaterGraph(): AbstractGraph | null {
|
||||
return this._miniWaterGraph;
|
||||
}
|
||||
getWaterComponent(tile: TileRef): number | null {
|
||||
// Permissive fallback for tests with disableNavMesh
|
||||
if (!this._miniWaterGraph) return 0;
|
||||
|
||||
const miniX = Math.floor(this._map.x(tile) / 2);
|
||||
const miniY = Math.floor(this._map.y(tile) / 2);
|
||||
const miniTile = this.miniGameMap.ref(miniX, miniY);
|
||||
|
||||
if (this.miniGameMap.isWater(miniTile)) {
|
||||
return this._miniWaterGraph.getComponentId(miniTile);
|
||||
}
|
||||
|
||||
// Shore tile: find water neighbor (expand search for minimap resolution loss)
|
||||
for (const n of this.miniGameMap.neighbors(miniTile)) {
|
||||
if (this.miniGameMap.isWater(n)) {
|
||||
return this._miniWaterGraph.getComponentId(n);
|
||||
}
|
||||
}
|
||||
|
||||
// Extended search: check 2-hop neighbors for narrow straits
|
||||
for (const n of this.miniGameMap.neighbors(miniTile)) {
|
||||
for (const n2 of this.miniGameMap.neighbors(n)) {
|
||||
if (this.miniGameMap.isWater(n2)) {
|
||||
return this._miniWaterGraph.getComponentId(n2);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
hasWaterComponent(tile: TileRef, component: number): boolean {
|
||||
// Permissive fallback for tests with disableNavMesh
|
||||
if (!this._miniWaterGraph) return true;
|
||||
|
||||
const miniX = Math.floor(this._map.x(tile) / 2);
|
||||
const miniY = Math.floor(this._map.y(tile) / 2);
|
||||
const miniTile = this.miniGameMap.ref(miniX, miniY);
|
||||
|
||||
// Check miniTile itself (shore in full map may be water in minimap)
|
||||
if (
|
||||
this.miniGameMap.isWater(miniTile) &&
|
||||
this._miniWaterGraph.getComponentId(miniTile) === component
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check neighbors
|
||||
for (const n of this.miniGameMap.neighbors(miniTile)) {
|
||||
if (
|
||||
this.miniGameMap.isWater(n) &&
|
||||
this._miniWaterGraph.getComponentId(n) === component
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Extended search: check 2-hop neighbors for narrow straits
|
||||
for (const n of this.miniGameMap.neighbors(miniTile)) {
|
||||
for (const n2 of this.miniGameMap.neighbors(n)) {
|
||||
if (
|
||||
this.miniGameMap.isWater(n2) &&
|
||||
this._miniWaterGraph.getComponentId(n2) === component
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
conquerPlayer(conqueror: Player, conquered: Player) {
|
||||
if (conquered.isDisconnected() && conqueror.isOnSameTeam(conquered)) {
|
||||
|
||||
@@ -1259,6 +1259,6 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
bestTransportShipSpawn(targetTile: TileRef): TileRef | false {
|
||||
return bestShoreDeploymentSource(this.mg, this, targetTile);
|
||||
return bestShoreDeploymentSource(this.mg, this, targetTile) ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Unit } from "./Game";
|
||||
import { StationManager } from "./RailNetworkImpl";
|
||||
import { TrainStation } from "./TrainStation";
|
||||
|
||||
export interface RailNetwork {
|
||||
connectStation(station: TrainStation): void;
|
||||
removeStation(unit: Unit): void;
|
||||
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[];
|
||||
stationManager(): StationManager;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { RailroadExecution } from "../execution/RailroadExecution";
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { MiniAStar } from "../pathfinding/MiniAStar";
|
||||
import { SerialAStar } from "../pathfinding/SerialAStar";
|
||||
import { PathFinding } from "../pathfinding/PathFinder";
|
||||
import { Game, Unit, UnitType } from "./Game";
|
||||
import { TileRef } from "./GameMap";
|
||||
import { RailNetwork } from "./RailNetwork";
|
||||
import { Railroad } from "./Railroad";
|
||||
import { Cluster, TrainStation, TrainStationMapAdapter } from "./TrainStation";
|
||||
import { Cluster, TrainStation } from "./TrainStation";
|
||||
|
||||
/**
|
||||
* The Stations handle their own neighbors so the graph is naturally traversable,
|
||||
@@ -18,16 +16,23 @@ export interface StationManager {
|
||||
removeStation(station: TrainStation): void;
|
||||
findStation(unit: Unit): TrainStation | null;
|
||||
getAll(): Set<TrainStation>;
|
||||
getById(id: number): TrainStation | undefined;
|
||||
count(): number;
|
||||
}
|
||||
|
||||
export class StationManagerImpl implements StationManager {
|
||||
private stations: Set<TrainStation> = new Set();
|
||||
private stationsById: (TrainStation | undefined)[] = [];
|
||||
private nextId = 0;
|
||||
|
||||
addStation(station: TrainStation) {
|
||||
station.id = this.nextId++;
|
||||
this.stationsById[station.id] = station;
|
||||
this.stations.add(station);
|
||||
}
|
||||
|
||||
removeStation(station: TrainStation) {
|
||||
this.stationsById[station.id] = undefined;
|
||||
this.stations.delete(station);
|
||||
}
|
||||
|
||||
@@ -41,6 +46,14 @@ export class StationManagerImpl implements StationManager {
|
||||
getAll(): Set<TrainStation> {
|
||||
return this.stations;
|
||||
}
|
||||
|
||||
getById(id: number): TrainStation | undefined {
|
||||
return this.stationsById[id];
|
||||
}
|
||||
|
||||
count(): number {
|
||||
return this.nextId;
|
||||
}
|
||||
}
|
||||
|
||||
export interface RailPathFinderService {
|
||||
@@ -52,32 +65,11 @@ class RailPathFinderServiceImpl implements RailPathFinderService {
|
||||
constructor(private game: Game) {}
|
||||
|
||||
findTilePath(from: TileRef, to: TileRef): TileRef[] {
|
||||
const astar = new MiniAStar(
|
||||
this.game.map(),
|
||||
this.game.miniMap(),
|
||||
from,
|
||||
to,
|
||||
5000,
|
||||
20,
|
||||
false,
|
||||
3,
|
||||
);
|
||||
return astar.compute() === PathFindResultType.Completed
|
||||
? astar.reconstructPath()
|
||||
: [];
|
||||
return PathFinding.Rail(this.game).findPath(from, to) ?? [];
|
||||
}
|
||||
|
||||
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[] {
|
||||
const stationAStar = new SerialAStar(
|
||||
from,
|
||||
to,
|
||||
5000,
|
||||
20,
|
||||
new TrainStationMapAdapter(this.game),
|
||||
);
|
||||
return stationAStar.compute() === PathFindResultType.Completed
|
||||
? stationAStar.reconstructPath()
|
||||
: [];
|
||||
return PathFinding.Stations(this.game).findPath(from, to) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,22 +84,26 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
|
||||
constructor(
|
||||
private game: Game,
|
||||
private stationManager: StationManager,
|
||||
private _stationManager: StationManager,
|
||||
private pathService: RailPathFinderService,
|
||||
) {}
|
||||
|
||||
stationManager(): StationManager {
|
||||
return this._stationManager;
|
||||
}
|
||||
|
||||
connectStation(station: TrainStation) {
|
||||
this.stationManager.addStation(station);
|
||||
this._stationManager.addStation(station);
|
||||
this.connectToNearbyStations(station);
|
||||
}
|
||||
|
||||
removeStation(unit: Unit): void {
|
||||
const station = this.stationManager.findStation(unit);
|
||||
const station = this._stationManager.findStation(unit);
|
||||
if (!station) return;
|
||||
|
||||
const neighbors = station.neighbors();
|
||||
this.disconnectFromNetwork(station);
|
||||
this.stationManager.removeStation(station);
|
||||
this._stationManager.removeStation(station);
|
||||
|
||||
const cluster = station.getCluster();
|
||||
if (!cluster) return;
|
||||
@@ -142,7 +138,7 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
|
||||
for (const neighbor of neighbors) {
|
||||
if (neighbor.unit === station.unit) continue;
|
||||
const neighborStation = this.stationManager.findStation(neighbor.unit);
|
||||
const neighborStation = this._stationManager.findStation(neighbor.unit);
|
||||
if (!neighborStation) continue;
|
||||
|
||||
const distanceToStation = this.distanceFrom(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { TrainExecution } from "../execution/TrainExecution";
|
||||
import { GraphAdapter } from "../pathfinding/SerialAStar";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { Game, Player, Unit, UnitType } from "./Game";
|
||||
import { TileRef } from "./GameMap";
|
||||
@@ -72,6 +71,7 @@ export function createTrainStopHandlers(
|
||||
}
|
||||
|
||||
export class TrainStation {
|
||||
id: number = -1; // assigned by StationManager
|
||||
private readonly stopHandlers: Partial<Record<UnitType, TrainStopHandler>> =
|
||||
{};
|
||||
private cluster: Cluster | null;
|
||||
@@ -171,29 +171,6 @@ export class TrainStation {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the trainstation usable with A*
|
||||
*/
|
||||
export class TrainStationMapAdapter implements GraphAdapter<TrainStation> {
|
||||
constructor(private game: Game) {}
|
||||
|
||||
neighbors(node: TrainStation): TrainStation[] {
|
||||
return node.neighbors();
|
||||
}
|
||||
|
||||
cost(node: TrainStation): number {
|
||||
return 1;
|
||||
}
|
||||
|
||||
position(node: TrainStation): { x: number; y: number } {
|
||||
return { x: this.game.x(node.tile()), y: this.game.y(node.tile()) };
|
||||
}
|
||||
|
||||
isTraversable(from: TrainStation, to: TrainStation): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cluster of connected stations
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { MiniAStar } from "../pathfinding/MiniAStar";
|
||||
import { SpatialQuery } from "../pathfinding/spatial/SpatialQuery";
|
||||
import { Game, Player, UnitType } from "./Game";
|
||||
import { andFN, GameMap, manhattanDistFN, TileRef } from "./GameMap";
|
||||
import { TileRef } from "./GameMap";
|
||||
|
||||
export function canBuildTransportShip(
|
||||
game: Game,
|
||||
@@ -27,236 +26,20 @@ export function canBuildTransportShip(
|
||||
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];
|
||||
const spatial = new SpatialQuery(game);
|
||||
return spatial.closestShoreByWater(player, dst) ?? false;
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
const spatial = new SpatialQuery(gm);
|
||||
return spatial.closestShore(gm.owner(tile), tile);
|
||||
}
|
||||
|
||||
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);
|
||||
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];
|
||||
}
|
||||
|
||||
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,
|
||||
dst: TileRef,
|
||||
): 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];
|
||||
const spatial = new SpatialQuery(gm);
|
||||
return spatial.closestShoreByWater(player, dst);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user