diff --git a/TODO.txt b/TODO.txt index 8f0921ab7..3f41a2be7 100644 --- a/TODO.txt +++ b/TODO.txt @@ -198,12 +198,16 @@ * make ports cost more for more ports DONE 11/25/2024 * add battleship DONE 11/26/2024 * use drawRect() instead of putImageData DONE 11/26/2024 -* run a* in a web worker +* run a* in a web worker DONE 11/29/2024 +* create async pathfinder DONE 11/29/2024 +* captured tradeships use async pathfinder DONE 11/29/2024 +* run name calculation in web worker +* BUG: tradeships wrong color * have NPCs build destroyers and battleships * spread out calculate clusters * add radiation from nuke -* NPC has relations * add defense post +* NPC has relations * only show units you can build in the build menu * REFACTOR: make TransportShip follow build unit flow * use twitter emojis diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index e25e10d40..f55d6a291 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -25,6 +25,9 @@ export const devConfig = new class extends DefaultConfig { turnIntervalMs(): number { return 100 } + boatMaxDistance(): number { + return 5000 + } // numBots(): number { // return 0 diff --git a/src/core/execution/BattleshipExecution.ts b/src/core/execution/BattleshipExecution.ts index 0334ae13b..7699d94b8 100644 --- a/src/core/execution/BattleshipExecution.ts +++ b/src/core/execution/BattleshipExecution.ts @@ -1,6 +1,7 @@ import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, Tile, UnitType } from "../game/Game"; -import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding"; -import { AStar } from "../pathfinding/AStar"; +import { PathFinder } from "../pathfinding/PathFinding"; +import { PathFindResultType } from "../pathfinding/AStar"; +import { SerialAStar } from "../pathfinding/SerialAStar"; import { PseudoRandom } from "../PseudoRandom"; import { distSort, distSortUnit, manhattanDist } from "../Util"; import { ShellExecution } from "./ShellExecution"; @@ -13,7 +14,7 @@ export class BattleshipExecution implements Execution { private battleship: MutableUnit = null private mg: MutableGame = null - private pathfinder = new PathFinder(5000, t => t.isWater()) + private pathfinder = PathFinder.Serial(5000, t => t.isWater()) private patrolTile: Tile; private patrolCenterTile: Tile diff --git a/src/core/execution/DestroyerExecution.ts b/src/core/execution/DestroyerExecution.ts index 7f7b45050..373189bfe 100644 --- a/src/core/execution/DestroyerExecution.ts +++ b/src/core/execution/DestroyerExecution.ts @@ -1,6 +1,7 @@ import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, Tile, UnitType } from "../game/Game"; -import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding"; -import { AStar } from "../pathfinding/AStar"; +import { PathFinder } from "../pathfinding/PathFinding"; +import { PathFindResultType } from "../pathfinding/AStar"; +import { SerialAStar } from "../pathfinding/SerialAStar"; import { PseudoRandom } from "../PseudoRandom"; import { distSort, distSortUnit, manhattanDist } from "../Util"; @@ -13,7 +14,7 @@ export class DestroyerExecution implements Execution { private mg: MutableGame = null private target: MutableUnit = null - private pathfinder = new PathFinder(5000, t => t.isWater()) + private pathfinder = PathFinder.Serial(5000, t => t.isWater()) private patrolTile: Tile; private patrolCenterTile: Tile diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 4f9b0c315..24c71c286 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -21,6 +21,7 @@ import { PortExecution } from "./PortExecution"; import { MissileSiloExecution } from "./MissileSiloExecution"; import { BattleshipExecution } from "./BattleshipExecution"; import { AsyncPathFinderCreator } from "../pathfinding/AsyncPathFinding"; +import { PathFinder } from "../pathfinding/PathFinding"; diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 91bc18fac..c0448e94f 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -5,7 +5,8 @@ import { AttackExecution } from "./AttackExecution"; import { TransportShipExecution } from "./TransportShipExecution"; import { SpawnExecution } from "./SpawnExecution"; import { PortExecution } from "./PortExecution"; -import { AsyncPathFinder, AsyncPathFinderCreator } from "../pathfinding/AsyncPathFinding"; +import { ParallelAStar, AsyncPathFinderCreator } from "../pathfinding/AsyncPathFinding"; +import { PathFinder } from "../pathfinding/PathFinding"; export class FakeHumanExecution implements Execution { diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 6ea5cc058..df4570c78 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -1,6 +1,7 @@ import { nextTick } from "process"; import { Cell, Execution, MutableGame, MutablePlayer, PlayerID, Tile, MutableUnit, UnitType } from "../game/Game"; -import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding"; +import { PathFinder } from "../pathfinding/PathFinding"; +import { PathFindResultType } from "../pathfinding/AStar"; import { PseudoRandom } from "../PseudoRandom"; import { bfs, dist, distSortUnit, euclideanDist, manhattanDist } from "../Util"; @@ -15,7 +16,7 @@ export class NukeExecution implements Execution { private nuke: MutableUnit private dst: Tile - private pathFinder: PathFinder = new PathFinder(10_000, () => true) + private pathFinder: PathFinder = PathFinder.Serial(10_000, () => true) constructor( private type: UnitType.AtomBomb | UnitType.HydrogenBomb, private senderID: PlayerID, diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index fbd276530..627d31860 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -1,10 +1,11 @@ import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, MutableUnit, Player, PlayerID, Tile, Unit, UnitType } from "../game/Game"; -import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding"; -import { AStar } from "../pathfinding/AStar"; +import { PathFinder } from "../pathfinding/PathFinding"; +import { PathFindResultType } from "../pathfinding/AStar"; +import { SerialAStar } from "../pathfinding/SerialAStar"; import { PseudoRandom } from "../PseudoRandom"; import { bfs, dist, manhattanDist } from "../Util"; import { TradeShipExecution } from "./TradeShipExecution"; -import { AsyncPathFinder, AsyncPathFinderCreator } from "../pathfinding/AsyncPathFinding"; +import { ParallelAStar, AsyncPathFinderCreator } from "../pathfinding/AsyncPathFinding"; export class PortExecution implements Execution { @@ -13,7 +14,7 @@ export class PortExecution implements Execution { private port: MutableUnit private random: PseudoRandom private portPaths = new Map() - private computingPaths = new Map() + private computingPaths = new Map() constructor( private _owner: PlayerID, @@ -77,7 +78,7 @@ export class PortExecution implements Execution { } continue } - const asyncPF = this.asyncPathFinderCreator.createPathFinder(this.port.tile(), port.tile(), 100) + const asyncPF = this.asyncPathFinderCreator.createParallelAStar(this.port.tile(), port.tile(), 100) // console.log(`adding new port path from ${this.player().name()}:${this.port.tile().cell()} to ${port.owner().name()}:${port.tile().cell()}`) this.computingPaths.set(port, asyncPF) } @@ -95,7 +96,8 @@ export class PortExecution implements Execution { const port = this.random.randElement(portConnections) const path = this.portPaths.get(port) if (path != null) { - this.mg.addExecution(new TradeShipExecution(this.player().id(), this.port, port, path)) + const pf = PathFinder.Parallel(this.asyncPathFinderCreator, 30) + this.mg.addExecution(new TradeShipExecution(this.player().id(), this.port, port, pf, path)) } } } diff --git a/src/core/execution/ShellExecution.ts b/src/core/execution/ShellExecution.ts index 3f9e81734..7d92e5169 100644 --- a/src/core/execution/ShellExecution.ts +++ b/src/core/execution/ShellExecution.ts @@ -1,10 +1,11 @@ import { Execution, MutableGame, MutablePlayer, MutableUnit, Tile, Unit, UnitType } from "../game/Game"; -import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding"; +import { PathFinder } from "../pathfinding/PathFinding"; +import { PathFindResultType } from "../pathfinding/AStar"; export class ShellExecution implements Execution { private active = true - private pathFinder = new PathFinder(2000, () => true, 10) + private pathFinder = PathFinder.Serial(2000, () => true, 10) private shell: MutableUnit constructor(private spawn: Tile, private _owner: MutablePlayer, private target: MutableUnit) { diff --git a/src/core/execution/TradeShipExecution.ts b/src/core/execution/TradeShipExecution.ts index b7e6433f0..7af54be8b 100644 --- a/src/core/execution/TradeShipExecution.ts +++ b/src/core/execution/TradeShipExecution.ts @@ -1,10 +1,12 @@ import { MessageType } from "../../client/graphics/layers/EventsDisplay"; import { renderNumber } from "../../client/graphics/Utils"; import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, MutableUnit, Player, PlayerID, Tile, Unit, UnitType } from "../game/Game"; -import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding"; -import { AStar } from "../pathfinding/AStar"; +import { PathFinder } from "../pathfinding/PathFinding"; +import { PathFindResultType } from "../pathfinding/AStar"; +import { SerialAStar } from "../pathfinding/SerialAStar"; import { PseudoRandom } from "../PseudoRandom"; import { bfs, dist, distSortUnit, manhattanDist } from "../Util"; +import { AsyncPathFinderCreator } from "../pathfinding/AsyncPathFinding"; export class TradeShipExecution implements Execution { @@ -13,13 +15,13 @@ export class TradeShipExecution implements Execution { private origOwner: MutablePlayer private tradeShip: MutableUnit private index = 0 - private pathFinder: PathFinder = new PathFinder(5_000, t => t.isOcean(), 10) private wasCaptured = false constructor( private _owner: PlayerID, private srcPort: MutableUnit, private dstPort: MutableUnit, + private pathFinder: PathFinder, // don't modify private path: Tile[] ) { } diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index 999ae2045..3dbdaa8da 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -2,8 +2,9 @@ import { Unit, Cell, Execution, MutableUnit, MutableGame, MutablePlayer, Player, import { and, bfs, manhattanDistWrapped, sourceDstOceanShore, targetTransportTile } from "../Util"; import { AttackExecution } from "./AttackExecution"; import { DisplayMessageEvent, MessageType } from "../../client/graphics/layers/EventsDisplay"; -import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding"; -import { AStar } from "../pathfinding/AStar"; +import { PathFinder } from "../pathfinding/PathFinding"; +import { PathFindResultType } from "../pathfinding/AStar"; +import { SerialAStar } from "../pathfinding/SerialAStar"; export class TransportShipExecution implements Execution { @@ -26,7 +27,7 @@ export class TransportShipExecution implements Execution { private boat: MutableUnit - private pathFinder: PathFinder = new PathFinder(10_000, t => t.isWater(), 2) + private pathFinder: PathFinder = PathFinder.Serial(10_000, t => t.isWater(), 2) constructor( private attackerID: PlayerID, @@ -123,7 +124,6 @@ export class TransportShipExecution implements Execution { this.boat.move(result.tile) break case PathFindResultType.Pending: - console.warn('boat computing') break case PathFindResultType.PathNotFound: // TODO: add to poisoned port list diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 5cf32a1eb..683236759 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -2,6 +2,7 @@ import { Config } from "../configuration/Config" import { GameEvent } from "../EventBus" import { ClientID, GameID } from "../Schemas" import { MessageType } from "../../client/graphics/layers/EventsDisplay" +import { SearchNode } from "../pathfinding/AStar" export type PlayerID = string export type Tick = number @@ -131,11 +132,6 @@ export class PlayerInfo { ) { } } -export interface SearchNode { - cost(): number; - cell(): Cell -} - export interface TerrainMap { terrain(cell: Cell): TerrainTile neighbors(terrainTile: TerrainTile): TerrainTile[] diff --git a/src/core/game/TerrainMapLoader.ts b/src/core/game/TerrainMapLoader.ts index 136e679af..54fd1c00f 100644 --- a/src/core/game/TerrainMapLoader.ts +++ b/src/core/game/TerrainMapLoader.ts @@ -1,4 +1,5 @@ -import { Cell, GameMap, SearchNode, TerrainMap, TerrainTile, TerrainType } from './Game'; +import { Cell, GameMap, TerrainMap, TerrainTile, TerrainType } from './Game'; +import { SearchNode } from "../pathfinding/AStar"; import europeBin from "!!binary-loader!../../../resources/maps/Europe.bin"; import europeInfo from "../../../resources/maps/Europe.json" diff --git a/src/core/game/TileImpl.ts b/src/core/game/TileImpl.ts index 8313b8f9d..d55818423 100644 --- a/src/core/game/TileImpl.ts +++ b/src/core/game/TileImpl.ts @@ -1,4 +1,5 @@ -import { Tile, Cell, TerrainType, Player, TerraNullius, MutablePlayer, SearchNode, TerrainTile } from "./Game"; +import { Tile, Cell, TerrainType, Player, TerraNullius, MutablePlayer, TerrainTile } from "./Game"; +import { SearchNode } from "../pathfinding/AStar"; import { TerrainTileImpl } from "./TerrainMapLoader"; import { GameImpl } from "./GameImpl"; import { PlayerImpl } from "./PlayerImpl"; diff --git a/src/core/pathfinding/AStar.ts b/src/core/pathfinding/AStar.ts index 1ffce78eb..5056af3c1 100644 --- a/src/core/pathfinding/AStar.ts +++ b/src/core/pathfinding/AStar.ts @@ -1,138 +1,29 @@ -import { PriorityQueue } from "@datastructures-js/priority-queue"; -import { SearchNode } from "../game/Game"; -import { PathFindResultType } from "./PathFinding"; +import { Cell, Tile } from "../game/Game"; - -export class AStar { - private fwdOpenSet: PriorityQueue<{ tile: SearchNode; fScore: number; }>; - private bwdOpenSet: PriorityQueue<{ tile: SearchNode; fScore: number; }>; - private fwdCameFrom: Map; - private bwdCameFrom: Map; - private fwdGScore: Map; - private bwdGScore: Map; - private meetingPoint: SearchNode | null; - public completed: boolean; - - constructor( - private src: SearchNode, - private dst: SearchNode, - private canMove: (t: SearchNode) => boolean, - private neighbors: (sn: SearchNode) => SearchNode[], - private iterations: number, - private maxTries: number - ) { - this.fwdOpenSet = new PriorityQueue<{ tile: SearchNode; fScore: number; }>( - (a, b) => a.fScore - b.fScore - ); - this.bwdOpenSet = new PriorityQueue<{ tile: SearchNode; fScore: number; }>( - (a, b) => a.fScore - b.fScore - ); - this.fwdCameFrom = new Map(); - this.bwdCameFrom = new Map(); - this.fwdGScore = new Map(); - this.bwdGScore = new Map(); - this.meetingPoint = null; - this.completed = false; - - // Initialize forward search - this.fwdGScore.set(src, 0); - this.fwdOpenSet.enqueue({ tile: src, fScore: this.heuristic(src, dst) }); - - // Initialize backward search - this.bwdGScore.set(dst, 0); - this.bwdOpenSet.enqueue({ tile: dst, fScore: this.heuristic(dst, src) }); - } - - compute(): PathFindResultType { - if (this.completed) return PathFindResultType.Completed; - - this.maxTries -= 1; - let iterations = this.iterations; - - while (!this.fwdOpenSet.isEmpty() && !this.bwdOpenSet.isEmpty()) { - iterations--; - if (iterations <= 0) { - if (this.maxTries <= 0) { - return PathFindResultType.PathNotFound; - } - return PathFindResultType.Pending; - } - - // Process forward search - const fwdCurrent = this.fwdOpenSet.dequeue()!.tile; - if (this.bwdGScore.has(fwdCurrent)) { - // We found a meeting point! - this.meetingPoint = fwdCurrent; - this.completed = true; - return PathFindResultType.Completed; - } - - this.expandSearchNode(fwdCurrent, true); - - // Process backward search - const bwdCurrent = this.bwdOpenSet.dequeue()!.tile; - if (this.fwdGScore.has(bwdCurrent)) { - // We found a meeting point! - this.meetingPoint = bwdCurrent; - this.completed = true; - return PathFindResultType.Completed; - } - - this.expandSearchNode(bwdCurrent, false); - } - - return this.completed ? PathFindResultType.Completed : PathFindResultType.PathNotFound; - } - - private expandSearchNode(current: SearchNode, isForward: boolean) { - for (const neighbor of this.neighbors(current)) { - if (neighbor !== (isForward ? this.dst : this.src) && !this.canMove(neighbor)) continue; - - const gScore = isForward ? this.fwdGScore : this.bwdGScore; - const openSet = isForward ? this.fwdOpenSet : this.bwdOpenSet; - const cameFrom = isForward ? this.fwdCameFrom : this.bwdCameFrom; - - let tentativeGScore = gScore.get(current)! + neighbor.cost(); - - if (!gScore.has(neighbor) || tentativeGScore < gScore.get(neighbor)!) { - cameFrom.set(neighbor, current); - gScore.set(neighbor, tentativeGScore); - const fScore = tentativeGScore + this.heuristic( - neighbor, - isForward ? this.dst : this.src - ); - openSet.enqueue({ tile: neighbor, fScore: fScore }); - } - } - } - - private heuristic(a: SearchNode, b: SearchNode): number { - // TODO use wrapped - try { - return 1.1 * Math.abs(a.cell().x - b.cell().x) + Math.abs(a.cell().y - b.cell().y); - } catch { - console.log('uh oh') - } - } - - public reconstructPath(): SearchNode[] { - if (!this.meetingPoint) return []; - - // Reconstruct path from start to meeting point - const fwdPath: SearchNode[] = [this.meetingPoint]; - let current = this.meetingPoint; - while (this.fwdCameFrom.has(current)) { - current = this.fwdCameFrom.get(current)!; - fwdPath.unshift(current); - } - - // Reconstruct path from meeting point to goal - current = this.meetingPoint; - while (this.bwdCameFrom.has(current)) { - current = this.bwdCameFrom.get(current)!; - fwdPath.push(current); - } - - return fwdPath; - } +export interface AStar { + compute(): PathFindResultType + reconstructPath(): SearchNode[] } + +export enum PathFindResultType { + NextTile, + Pending, + Completed, + PathNotFound +} export type TileResult = { + type: PathFindResultType.NextTile; + tile: Tile; +} | { + type: PathFindResultType.Pending; +} | { + type: PathFindResultType.Completed; + tile: Tile; +} | { + type: PathFindResultType.PathNotFound; +}; + +export interface SearchNode { + cost(): number + cell(): Cell +} + diff --git a/src/core/pathfinding/AsyncPathFinding.ts b/src/core/pathfinding/AsyncPathFinding.ts index 5b3981f8a..2e6771e9a 100644 --- a/src/core/pathfinding/AsyncPathFinding.ts +++ b/src/core/pathfinding/AsyncPathFinding.ts @@ -1,5 +1,5 @@ import { TerrainTile, Tile, Game, GameMap, Cell } from "../game/Game"; -import { PathFindResultType } from "./PathFinding"; +import { AStar, PathFindResultType } from "./AStar"; export class AsyncPathFinderCreator { private worker: Worker; @@ -33,11 +33,11 @@ export class AsyncPathFinderCreator { }); } - createPathFinder(src: Tile, dst: Tile, numTicks: number): AsyncPathFinder { + createParallelAStar(src: Tile, dst: Tile, numTicks: number): ParallelAStar { if (!this.isInitialized) { throw new Error('PathFinder not initialized'); } - return new AsyncPathFinder(this.game, this.worker, src, dst, numTicks); + return new ParallelAStar(this.game, this.worker, src, dst, numTicks); } cleanup() { @@ -45,8 +45,7 @@ export class AsyncPathFinderCreator { } } -// AsyncPathFinder.ts -export class AsyncPathFinder { +export class ParallelAStar implements AStar { private path: Tile[] | 'NOT_FOUND' | null = null; private promise: Promise; @@ -59,18 +58,24 @@ export class AsyncPathFinder { ) { } findPath(): Promise { + const requestId = crypto.randomUUID() this.promise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject("Path timeout"); }, 100_000); const handler = (e: MessageEvent) => { + if (e.data.requestId != requestId) { + return + } clearTimeout(timeout); this.worker.removeEventListener('message', handler); if (e.data.type === 'pathFound') { this.path = e.data.path.map(pos => this.game.tile(new Cell(pos.x, pos.y))); resolve(); + } else if (e.data.type === 'pathNotFound') { + this.path = 'NOT_FOUND' } else { reject(e.data.reason || "Path not found"); } @@ -79,7 +84,7 @@ export class AsyncPathFinder { this.worker.addEventListener('message', handler); this.worker.postMessage({ type: 'findPath', - requestId: crypto.randomUUID(), + requestId: requestId, currentTick: this.game.ticks(), duration: this.numTicks, start: { x: this.src.cell().x, y: this.src.cell().y }, @@ -97,14 +102,11 @@ export class AsyncPathFinder { } this.numTicks--; if (this.numTicks <= 0) { - for (let i = 0; i < 1_000_000; i++) { - if (this.path == 'NOT_FOUND') { - return PathFindResultType.PathNotFound - } - if (this.path != null) { - console.log('in Asyncclient: found a path!!') - return PathFindResultType.Completed; - } + if (this.path == 'NOT_FOUND') { + return PathFindResultType.PathNotFound + } + if (this.path != null) { + return PathFindResultType.Completed; } throw new Error(`path not completed in time`) } diff --git a/src/core/pathfinding/PathFinder.worker.ts b/src/core/pathfinding/PathFinder.worker.ts index f922e26f0..cc5e911c4 100644 --- a/src/core/pathfinding/PathFinder.worker.ts +++ b/src/core/pathfinding/PathFinder.worker.ts @@ -1,7 +1,8 @@ // pathfinding.ts -import { Cell, GameMap, SearchNode, TerrainMap, TerrainTile, TerrainType } from "../game/Game"; -import { PathFindResultType } from "./PathFinding"; -import { AStar } from "./AStar"; +import { Cell, GameMap, TerrainMap, TerrainTile, TerrainType } from "../game/Game"; +import { SearchNode } from "./AStar"; +import { PathFindResultType } from "./AStar"; +import { SerialAStar } from "./SerialAStar"; import { loadTerrainMap } from "../game/TerrainMapLoader"; import { PriorityQueue } from "@datastructures-js/priority-queue"; @@ -12,7 +13,7 @@ let isProcessingSearch = false interface Search { - aStar: AStar, + aStar: SerialAStar, deadline: number requestId: string } @@ -40,17 +41,17 @@ self.onmessage = (e) => { function initializeMap(data: { gameMap: GameMap }) { terrainMapPromise = loadTerrainMap(data.gameMap) self.postMessage({ type: 'initialized' }); - processingInterval = setInterval(computeSearches, .5) as unknown as number; + processingInterval = setInterval(computeSearches, .1) as unknown as number; } function findPath(terrainMap: TerrainMap, req: SearchRequest) { - const aStar = new AStar( + const aStar = new SerialAStar( terrainMap.terrain(new Cell(req.start.x, req.start.y)), terrainMap.terrain(new Cell(req.end.x, req.end.y)), (sn: SearchNode) => (sn as TerrainTile).terrainType() == TerrainType.Ocean, (sn: SearchNode): SearchNode[] => terrainMap.neighbors((sn as TerrainTile)), - 100_000, - 1000 + 10_000, + req.duration, ); searches.enqueue({ @@ -88,7 +89,7 @@ function computeSearches() { case PathFindResultType.PathNotFound: console.warn(`worker: path not found to port`); self.postMessage({ - type: 'error', + type: 'pathNotFound', requestId: search.requestId, }); break diff --git a/src/core/pathfinding/PathFinding.ts b/src/core/pathfinding/PathFinding.ts index 4e95b1c99..9405d953c 100644 --- a/src/core/pathfinding/PathFinding.ts +++ b/src/core/pathfinding/PathFinding.ts @@ -1,26 +1,8 @@ -import { Tile } from "../game/Game"; +import { Game, Tile } from "../game/Game"; import { manhattanDist } from "../Util"; -import { AStar } from "./AStar"; - -export enum PathFindResultType { - NextTile, - Pending, - Completed, - PathNotFound -} - -export type TileResult = { - type: PathFindResultType.NextTile; - tile: Tile -} | { - type: PathFindResultType.Pending; -} | { - type: PathFindResultType.Completed; - tile: Tile -} | { - type: PathFindResultType.PathNotFound; -} - +import { AStar, PathFindResultType, TileResult } from "./AStar"; +import { AsyncPathFinderCreator, ParallelAStar } from "./AsyncPathFinding"; +import { SerialAStar } from "./SerialAStar"; export class PathFinder { @@ -30,12 +12,31 @@ export class PathFinder { private aStar: AStar private computeFinished = true - constructor( - private iterations: number, - private canMove: (t: Tile) => boolean, - private maxTries: number = 20 + private constructor( + private newAStar: (curr: Tile, dst: Tile) => AStar ) { } + public static Serial(iterations: number, canMove: (t: Tile) => boolean, maxTries: number = 20): PathFinder { + return new PathFinder( + (curr: Tile, dst: Tile) => { + return new SerialAStar( + curr, + dst, + canMove, + sn => ((sn as Tile).neighbors()), iterations, maxTries + ) + } + ) + } + + public static Parallel(creator: AsyncPathFinderCreator, numTicks: number): PathFinder { + return new PathFinder( + (curr: Tile, dst: Tile) => { + return creator.createParallelAStar(curr, dst, numTicks) + } + ) + } + nextTile(curr: Tile, dst: Tile, dist: number = 1): TileResult { if (curr == null) { console.error('curr is null') @@ -53,7 +54,7 @@ export class PathFinder { this.curr = curr this.dst = dst this.path = null - this.aStar = new AStar(curr, dst, this.canMove, sn => ((sn as Tile).neighbors()), this.iterations, this.maxTries) + this.aStar = this.newAStar(curr, dst) this.computeFinished = false return this.nextTile(curr, dst) } else { diff --git a/src/core/pathfinding/SerialAStar.ts b/src/core/pathfinding/SerialAStar.ts new file mode 100644 index 000000000..e2b1b58a6 --- /dev/null +++ b/src/core/pathfinding/SerialAStar.ts @@ -0,0 +1,138 @@ +import { PriorityQueue } from "@datastructures-js/priority-queue"; +import { AStar, SearchNode } from "./AStar"; +import { PathFindResultType } from "./AStar"; + + +export class SerialAStar implements AStar{ + private fwdOpenSet: PriorityQueue<{ tile: SearchNode; fScore: number; }>; + private bwdOpenSet: PriorityQueue<{ tile: SearchNode; fScore: number; }>; + private fwdCameFrom: Map; + private bwdCameFrom: Map; + private fwdGScore: Map; + private bwdGScore: Map; + private meetingPoint: SearchNode | null; + public completed: boolean; + + constructor( + private src: SearchNode, + private dst: SearchNode, + private canMove: (t: SearchNode) => boolean, + private neighbors: (sn: SearchNode) => SearchNode[], + private iterations: number, + private maxTries: number + ) { + this.fwdOpenSet = new PriorityQueue<{ tile: SearchNode; fScore: number; }>( + (a, b) => a.fScore - b.fScore + ); + this.bwdOpenSet = new PriorityQueue<{ tile: SearchNode; fScore: number; }>( + (a, b) => a.fScore - b.fScore + ); + this.fwdCameFrom = new Map(); + this.bwdCameFrom = new Map(); + this.fwdGScore = new Map(); + this.bwdGScore = new Map(); + this.meetingPoint = null; + this.completed = false; + + // Initialize forward search + this.fwdGScore.set(src, 0); + this.fwdOpenSet.enqueue({ tile: src, fScore: this.heuristic(src, dst) }); + + // Initialize backward search + this.bwdGScore.set(dst, 0); + this.bwdOpenSet.enqueue({ tile: dst, fScore: this.heuristic(dst, src) }); + } + + compute(): PathFindResultType { + if (this.completed) return PathFindResultType.Completed; + + this.maxTries -= 1; + let iterations = this.iterations; + + while (!this.fwdOpenSet.isEmpty() && !this.bwdOpenSet.isEmpty()) { + iterations--; + if (iterations <= 0) { + if (this.maxTries <= 0) { + return PathFindResultType.PathNotFound; + } + return PathFindResultType.Pending; + } + + // Process forward search + const fwdCurrent = this.fwdOpenSet.dequeue()!.tile; + if (this.bwdGScore.has(fwdCurrent)) { + // We found a meeting point! + this.meetingPoint = fwdCurrent; + this.completed = true; + return PathFindResultType.Completed; + } + + this.expandSearchNode(fwdCurrent, true); + + // Process backward search + const bwdCurrent = this.bwdOpenSet.dequeue()!.tile; + if (this.fwdGScore.has(bwdCurrent)) { + // We found a meeting point! + this.meetingPoint = bwdCurrent; + this.completed = true; + return PathFindResultType.Completed; + } + + this.expandSearchNode(bwdCurrent, false); + } + + return this.completed ? PathFindResultType.Completed : PathFindResultType.PathNotFound; + } + + private expandSearchNode(current: SearchNode, isForward: boolean) { + for (const neighbor of this.neighbors(current)) { + if (neighbor !== (isForward ? this.dst : this.src) && !this.canMove(neighbor)) continue; + + const gScore = isForward ? this.fwdGScore : this.bwdGScore; + const openSet = isForward ? this.fwdOpenSet : this.bwdOpenSet; + const cameFrom = isForward ? this.fwdCameFrom : this.bwdCameFrom; + + let tentativeGScore = gScore.get(current)! + neighbor.cost(); + + if (!gScore.has(neighbor) || tentativeGScore < gScore.get(neighbor)!) { + cameFrom.set(neighbor, current); + gScore.set(neighbor, tentativeGScore); + const fScore = tentativeGScore + this.heuristic( + neighbor, + isForward ? this.dst : this.src + ); + openSet.enqueue({ tile: neighbor, fScore: fScore }); + } + } + } + + private heuristic(a: SearchNode, b: SearchNode): number { + // TODO use wrapped + try { + return 1.1 * Math.abs(a.cell().x - b.cell().x) + Math.abs(a.cell().y - b.cell().y); + } catch { + console.log('uh oh') + } + } + + public reconstructPath(): SearchNode[] { + if (!this.meetingPoint) return []; + + // Reconstruct path from start to meeting point + const fwdPath: SearchNode[] = [this.meetingPoint]; + let current = this.meetingPoint; + while (this.fwdCameFrom.has(current)) { + current = this.fwdCameFrom.get(current)!; + fwdPath.unshift(current); + } + + // Reconstruct path from meeting point to goal + current = this.meetingPoint; + while (this.bwdCameFrom.has(current)) { + current = this.bwdCameFrom.get(current)!; + fwdPath.push(current); + } + + return fwdPath; + } +}