diff --git a/package-lock.json b/package-lock.json index fc4d5a314..eb28cb574 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "@types/jest": "^29.5.12", "@types/jquery": "^3.5.31", "@types/mocha": "^10.0.7", - "@types/node": "^22.7.5", + "@types/node": "^22.10.1", "@types/sinon": "^17.0.3", "@types/uuid": "^10.0.0", "@types/ws": "^8.5.11", @@ -77,7 +77,8 @@ "typescript": "^5.6.3", "webpack": "^5.91.0", "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.0.4" + "webpack-dev-server": "^5.0.4", + "worker-loader": "^3.0.8" } }, "node_modules/@ampproject/remapping": { @@ -4161,12 +4162,12 @@ } }, "node_modules/@types/node": { - "version": "22.8.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.1.tgz", - "integrity": "sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==", + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.8" + "undici-types": "~6.20.0" } }, "node_modules/@types/node-forge": { @@ -13429,9 +13430,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -14110,6 +14111,46 @@ "dev": true, "license": "MIT" }, + "node_modules/worker-loader": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", + "integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/worker-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/workerpool": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", diff --git a/package.json b/package.json index 016a4b8cf..6edabc285 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@types/jest": "^29.5.12", "@types/jquery": "^3.5.31", "@types/mocha": "^10.0.7", - "@types/node": "^22.7.5", + "@types/node": "^22.10.1", "@types/sinon": "^17.0.3", "@types/uuid": "^10.0.0", "@types/ws": "^8.5.11", @@ -50,7 +50,8 @@ "typescript": "^5.6.3", "webpack": "^5.91.0", "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.0.4" + "webpack-dev-server": "^5.0.4", + "worker-loader": "^3.0.8" }, "dependencies": { "@datastructures-js/priority-queue": "^6.3.1", diff --git a/src/client/ClientGame.ts b/src/client/GameRunner.ts similarity index 94% rename from src/client/ClientGame.ts rename to src/client/GameRunner.ts index 9a9e77da6..7afe86a9b 100644 --- a/src/client/ClientGame.ts +++ b/src/client/GameRunner.ts @@ -6,13 +6,14 @@ import { Config, getConfig } from "../core/configuration/Config"; import { createRenderer, GameRenderer } from "./graphics/GameRenderer"; import { InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent } from "./InputHandler" import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, ClientMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn } from "../core/Schemas"; -import { loadTerrainMap, TerrainMap } from "../core/game/TerrainMapLoader"; +import { loadTerrainMap, TerrainMapImpl } from "../core/game/TerrainMapLoader"; import { and, bfs, dist, manhattanDist } from "../core/Util"; import { WinCheckExecution } from "../core/execution/WinCheckExecution"; import { SendAttackIntentEvent, SendSpawnIntentEvent, Transport } from "./Transport"; import { createCanvas } from "./graphics/Utils"; import { DisplayMessageEvent, MessageType } from "./graphics/layers/EventsDisplay"; import { v4 as uuidv4 } from 'uuid'; +import { AsyncPathFinderCreator } from "../core/pathfinding/AsyncPathFinding"; export interface LobbyConfig { @@ -70,6 +71,11 @@ export async function createClientGame(gameConfig: GameConfig, eventBus: EventBu const terrainMap = await loadTerrainMap(gameConfig.map) let game = createGame(terrainMap, eventBus, config) + + const pathFinder = new AsyncPathFinderCreator(game, gameConfig.map) + console.log('going to init path finder') + await pathFinder.initialize() + console.log('inited path finder') const canvas = createCanvas() let gameRenderer = createRenderer(canvas, game, eventBus, gameConfig.clientID) @@ -82,7 +88,7 @@ export async function createClientGame(gameConfig: GameConfig, eventBus: EventBu game, gameRenderer, new InputHandler(canvas, eventBus), - new Executor(game, gameConfig.difficulty, gameConfig.gameID), + new Executor(game, gameConfig.difficulty, gameConfig.gameID, pathFinder), transport, ) } @@ -166,7 +172,7 @@ export class GameRunner { return } this.isProcessingTurn = true - this.gs.addExecution(...this.executor.createExecs(this.turns[this.currTurn])) + this.gs.addExecution(...this.executor.createExecs(this.turns[this.currTurn])) this.gs.executeNextTick() this.renderer.tick() this.currTurn++ diff --git a/src/client/Main.ts b/src/client/Main.ts index 37fc6b05e..29c98e3bf 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -1,4 +1,4 @@ -import { GameRunner, joinLobby } from "./ClientGame"; +import { GameRunner, joinLobby } from "./GameRunner"; import backgroundImage from '../../resources/images/TerrainMapFrontPage.png'; import favicon from '../../resources/images/Favicon.png'; diff --git a/src/core/PathFinding.ts b/src/core/PathFinding.ts deleted file mode 100644 index 758ba5e51..000000000 --- a/src/core/PathFinding.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { PriorityQueue } from "@datastructures-js/priority-queue"; -import { Tile } from "./game/Game"; -import { manhattanDist } from "./Util"; - -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 class AStar { - private fwdOpenSet: PriorityQueue<{ tile: Tile; fScore: number; }>; - private bwdOpenSet: PriorityQueue<{ tile: Tile; fScore: number; }>; - private fwdCameFrom: Map; - private bwdCameFrom: Map; - private fwdGScore: Map; - private bwdGScore: Map; - private meetingPoint: Tile | null; - public completed: boolean; - - constructor( - private src: Tile, - private dst: Tile, - private canMove: (t: Tile) => boolean, - private iterations: number, - private maxTries: number, - ) { - this.fwdOpenSet = new PriorityQueue<{ tile: Tile; fScore: number; }>( - (a, b) => a.fScore - b.fScore - ); - this.bwdOpenSet = new PriorityQueue<{ tile: Tile; 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.expandNode(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.expandNode(bwdCurrent, false); - } - - return this.completed ? PathFindResultType.Completed : PathFindResultType.PathNotFound - } - - private expandNode(current: Tile, isForward: boolean) { - for (const neighbor of current.neighborsWrapped()) { - 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)! + 1; - if (neighbor.magnitude() < 10) { - tentativeGScore += 1; - } - - 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: Tile, b: Tile): number { - // TODO use wrapped - return 1.1 * Math.abs(a.cell().x - b.cell().x) + Math.abs(a.cell().y - b.cell().y); - } - - public reconstructPath(): Tile[] { - if (!this.meetingPoint) return []; - - // Reconstruct path from start to meeting point - const fwdPath: Tile[] = [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 class PathFinder { - - private curr: Tile = null - private dst: Tile = null - private path: Tile[] - private aStar: AStar - private computeFinished = true - - constructor( - private iterations: number, - private canMove: (t: Tile) => boolean, - private maxTries: number = 20 - ) { } - - nextTile(curr: Tile, dst: Tile, dist: number = 1): TileResult { - if (curr == null) { - console.error('curr is null') - } - if (dst == null) { - console.error('dst is null') - } - - if (manhattanDist(curr.cell(), dst.cell()) < dist) { - return { type: PathFindResultType.Completed, tile: curr } - } - - if (this.computeFinished) { - if (this.shouldRecompute(curr, dst)) { - this.curr = curr - this.dst = dst - this.path = null - this.aStar = new AStar(curr, dst, this.canMove, this.iterations, this.maxTries) - this.computeFinished = false - return this.nextTile(curr, dst) - } else { - return { type: PathFindResultType.NextTile, tile: this.path.shift() } - } - } - - switch (this.aStar.compute()) { - case PathFindResultType.Completed: - this.computeFinished = true - this.path = this.aStar.reconstructPath() - // Remove the start tile - this.path.shift() - return this.nextTile(curr, dst) - case PathFindResultType.Pending: - return { type: PathFindResultType.Pending } - case PathFindResultType.PathNotFound: - return { type: PathFindResultType.PathNotFound } - } - } - - - - private shouldRecompute(curr: Tile, dst: Tile) { - if (this.path == null || this.curr == null || this.dst == null) { - return true - } - const dist = manhattanDist(curr.cell(), dst.cell()) - let tolerance = 10 - if (dist > 50) { - tolerance = 10 - } else if (dist > 25) { - tolerance = 5 - } else if (dist > 10) { - tolerance = 3 - } else { - tolerance = 0 - } - if (manhattanDist(this.dst.cell(), dst.cell()) > tolerance) { - return true - } - return false - } -} diff --git a/src/core/execution/BattleshipExecution.ts b/src/core/execution/BattleshipExecution.ts index aba4f021a..0334ae13b 100644 --- a/src/core/execution/BattleshipExecution.ts +++ b/src/core/execution/BattleshipExecution.ts @@ -1,5 +1,6 @@ import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, Tile, UnitType } from "../game/Game"; -import { AStar, PathFinder, PathFindResultType } from "../PathFinding"; +import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding"; +import { AStar } from "../pathfinding/AStar"; import { PseudoRandom } from "../PseudoRandom"; import { distSort, distSortUnit, manhattanDist } from "../Util"; import { ShellExecution } from "./ShellExecution"; diff --git a/src/core/execution/DestroyerExecution.ts b/src/core/execution/DestroyerExecution.ts index c51c062e4..7f7b45050 100644 --- a/src/core/execution/DestroyerExecution.ts +++ b/src/core/execution/DestroyerExecution.ts @@ -1,5 +1,6 @@ import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, Tile, UnitType } from "../game/Game"; -import { AStar, PathFinder, PathFindResultType } from "../PathFinding"; +import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding"; +import { AStar } from "../pathfinding/AStar"; import { PseudoRandom } from "../PseudoRandom"; import { distSort, distSortUnit, manhattanDist } from "../Util"; diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 2eecae9c7..4f9b0c315 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -20,6 +20,7 @@ import { DestroyerExecution } from "./DestroyerExecution"; import { PortExecution } from "./PortExecution"; import { MissileSiloExecution } from "./MissileSiloExecution"; import { BattleshipExecution } from "./BattleshipExecution"; +import { AsyncPathFinderCreator } from "../pathfinding/AsyncPathFinding"; @@ -30,7 +31,7 @@ export class Executor { // private random = new PseudoRandom(999) private random: PseudoRandom = null - constructor(private gs: Game, private difficulty: Difficulty, private gameID: GameID) { + constructor(private gs: Game, private difficulty: Difficulty, private gameID: GameID, private asyncPathFinder: AsyncPathFinderCreator) { // Add one to avoid id collisions with bots. this.random = new PseudoRandom(simpleHash(gameID) + 1) } @@ -92,7 +93,7 @@ export class Executor { case UnitType.Battleship: return new BattleshipExecution(intent.player, new Cell(intent.x, intent.y)) case UnitType.Port: - return new PortExecution(intent.player, new Cell(intent.x, intent.y)) + return new PortExecution(intent.player, new Cell(intent.x, intent.y), this.asyncPathFinder) case UnitType.MissileSilo: return new MissileSiloExecution(intent.player, new Cell(intent.x, intent.y)) default: @@ -111,6 +112,7 @@ export class Executor { const execs = [] for (const nation of this.gs.nations()) { execs.push(new FakeHumanExecution( + this.asyncPathFinder, new PlayerInfo( nation.name, PlayerType.FakeHuman, diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 79676563a..91bc18fac 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -5,6 +5,7 @@ import { AttackExecution } from "./AttackExecution"; import { TransportShipExecution } from "./TransportShipExecution"; import { SpawnExecution } from "./SpawnExecution"; import { PortExecution } from "./PortExecution"; +import { AsyncPathFinder, AsyncPathFinderCreator } from "../pathfinding/AsyncPathFinding"; export class FakeHumanExecution implements Execution { @@ -21,7 +22,7 @@ export class FakeHumanExecution implements Execution { private relations = new Map() - constructor(private playerInfo: PlayerInfo, private cell: Cell, private strength: number) { + constructor(private asyncPathFinder: AsyncPathFinderCreator, private playerInfo: PlayerInfo, private cell: Cell, private strength: number) { this.random = new PseudoRandom(simpleHash(playerInfo.id)) } @@ -151,7 +152,7 @@ export class FakeHumanExecution implements Execution { const oceanTiles = Array.from(this.player.borderTiles()).filter(t => t.isOceanShore()) if (oceanTiles.length > 0) { const buildTile = this.random.randElement(oceanTiles) - this.mg.addExecution(new PortExecution(this.player.id(), buildTile.cell())) + this.mg.addExecution(new PortExecution(this.player.id(), buildTile.cell(), this.asyncPathFinder)) } } } diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 4adb5adec..6ea5cc058 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -1,6 +1,6 @@ import { nextTick } from "process"; import { Cell, Execution, MutableGame, MutablePlayer, PlayerID, Tile, MutableUnit, UnitType } from "../game/Game"; -import { PathFinder, PathFindResultType } from "../PathFinding"; +import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding"; import { PseudoRandom } from "../PseudoRandom"; import { bfs, dist, distSortUnit, euclideanDist, manhattanDist } from "../Util"; diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index f3f636359..fbd276530 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -1,8 +1,10 @@ import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, MutableUnit, Player, PlayerID, Tile, Unit, UnitType } from "../game/Game"; -import { AStar, PathFinder, PathFindResultType } from "../PathFinding"; +import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding"; +import { AStar } from "../pathfinding/AStar"; import { PseudoRandom } from "../PseudoRandom"; import { bfs, dist, manhattanDist } from "../Util"; import { TradeShipExecution } from "./TradeShipExecution"; +import { AsyncPathFinder, AsyncPathFinderCreator } from "../pathfinding/AsyncPathFinding"; export class PortExecution implements Execution { @@ -11,11 +13,12 @@ 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, - private cell: Cell + private cell: Cell, + private asyncPathFinderCreator: AsyncPathFinderCreator ) { } @@ -25,6 +28,7 @@ export class PortExecution implements Execution { } tick(ticks: number): void { + if (this.port == null) { const tile = this.mg.tile(this.cell) const player = this.mg.player(this._owner) @@ -62,7 +66,7 @@ export class PortExecution implements Execution { const aStar = this.computingPaths.get(port) switch (aStar.compute()) { case PathFindResultType.Completed: - this.portPaths.set(port, aStar.reconstructPath()) + this.portPaths.set(port, aStar.reconstructPath().map(sn => sn as Tile)) this.computingPaths.delete(port) break case PathFindResultType.Pending: @@ -73,8 +77,9 @@ export class PortExecution implements Execution { } continue } + const asyncPF = this.asyncPathFinderCreator.createPathFinder(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, new AStar(this.port.tile(), port.tile(), t => t.isWater(), 4000, 100)) + this.computingPaths.set(port, asyncPF) } for (const port of this.portPaths.keys()) { diff --git a/src/core/execution/ShellExecution.ts b/src/core/execution/ShellExecution.ts index 65b7ec35c..3f9e81734 100644 --- a/src/core/execution/ShellExecution.ts +++ b/src/core/execution/ShellExecution.ts @@ -1,5 +1,5 @@ import { Execution, MutableGame, MutablePlayer, MutableUnit, Tile, Unit, UnitType } from "../game/Game"; -import { PathFinder, PathFindResultType } from "../PathFinding"; +import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding"; export class ShellExecution implements Execution { diff --git a/src/core/execution/TradeShipExecution.ts b/src/core/execution/TradeShipExecution.ts index 59b139329..b7e6433f0 100644 --- a/src/core/execution/TradeShipExecution.ts +++ b/src/core/execution/TradeShipExecution.ts @@ -1,7 +1,8 @@ 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 { AStar, PathFinder, PathFindResultType } from "../PathFinding"; +import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding"; +import { AStar } from "../pathfinding/AStar"; import { PseudoRandom } from "../PseudoRandom"; import { bfs, dist, distSortUnit, manhattanDist } from "../Util"; diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index 84ac53ad2..999ae2045 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -2,7 +2,8 @@ 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 { AStar, PathFinder, PathFindResultType } from "../PathFinding"; +import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding"; +import { AStar } from "../pathfinding/AStar"; export class TransportShipExecution implements Execution { diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 39d2e23b7..5cf32a1eb 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -131,7 +131,21 @@ export class PlayerInfo { ) { } } -export interface Tile { +export interface SearchNode { + cost(): number; + cell(): Cell +} + +export interface TerrainMap { + terrain(cell: Cell): TerrainTile + neighbors(terrainTile: TerrainTile): TerrainTile[] +} + +export interface TerrainTile extends SearchNode { + terrainType(): TerrainType +} + +export interface Tile extends SearchNode { isLand(): boolean isShore(): boolean isOceanShore(): boolean @@ -150,8 +164,6 @@ export interface Tile { neighbors(): Tile[] neighborsWrapped(): Tile[] onShore(): boolean - x(): number - y(): number } export interface Unit { @@ -279,7 +291,7 @@ export interface Game { displayMessage(message: string, type: MessageType, playerID: PlayerID | null): void units(...types: UnitType[]): Unit[] unitInfo(type: UnitType): UnitInfo - searchMap(): SharedArrayBuffer + terrainMap(): TerrainMap } export interface MutableGame extends Game { @@ -325,4 +337,5 @@ export class TargetPlayerEvent implements GameEvent { export class EmojiMessageEvent implements GameEvent { constructor(public readonly message: EmojiMessage) { } -} \ No newline at end of file +} + diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index b26d45655..4f2bec451 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -2,7 +2,7 @@ import { info } from "console"; import { Config } from "../configuration/Config"; import { EventBus } from "../EventBus"; import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerEvent, PlayerID, PlayerInfo, Player, TerraNullius, Tile, TileEvent, Unit, UnitEvent as UnitEvent, PlayerType, MutableAllianceRequest, AllianceRequestReplyEvent, AllianceRequestEvent, BrokeAllianceEvent, MutableAlliance, Alliance, AllianceExpiredEvent, Nation, UnitType, UnitInfo } from "./Game"; -import { TerrainMap } from "./TerrainMapLoader"; +import { TerrainMapImpl } from "./TerrainMapLoader"; import { PlayerImpl } from "./PlayerImpl"; import { TerraNulliusImpl } from "./TerraNulliusImpl"; import { TileImpl } from "./TileImpl"; @@ -12,7 +12,7 @@ import { ClientID } from "../Schemas"; import { DisplayMessageEvent, MessageType } from "../../client/graphics/layers/EventsDisplay"; import { UnitImpl } from "./UnitImpl"; -export function createGame(terrainMap: TerrainMap, eventBus: EventBus, config: Config): Game { +export function createGame(terrainMap: TerrainMapImpl, eventBus: EventBus, config: Config): Game { return new GameImpl(terrainMap, eventBus, config) } @@ -34,26 +34,24 @@ export class GameImpl implements MutableGame { private _height: number private _numLandTiles: number _terraNullius: TerraNulliusImpl - private _searchMap: SharedArrayBuffer allianceRequests: AllianceRequestImpl[] = [] alliances_: AllianceImpl[] = [] - constructor(terrainMap: TerrainMap, public eventBus: EventBus, private _config: Config) { + constructor(private _terrainMap: TerrainMapImpl, public eventBus: EventBus, private _config: Config) { this._terraNullius = new TerraNulliusImpl(this) - this._width = terrainMap.width(); - this._height = terrainMap.height(); - this._numLandTiles = terrainMap.numLandTiles - this._searchMap = terrainMap.searchBuffer + this._width = _terrainMap.width(); + this._height = _terrainMap.height(); + this._numLandTiles = _terrainMap.numLandTiles this.map = new Array(this._width); for (let x = 0; x < this._width; x++) { this.map[x] = new Array(this._height); for (let y = 0; y < this._height; y++) { let cell = new Cell(x, y); - this.map[x][y] = new TileImpl(this, this._terraNullius, cell, terrainMap.terrain(cell)); + this.map[x][y] = new TileImpl(this, this._terraNullius, cell, _terrainMap.terrain(cell)); } } - this.nations_ = terrainMap.nationMap.nations + this.nations_ = _terrainMap.nationMap.nations .map(n => new Nation( n.name, new Cell(n.coordinates[0], n.coordinates[1]), @@ -396,8 +394,8 @@ export class GameImpl implements MutableGame { this.eventBus.emit(new AllianceExpiredEvent(alliance.requestor(), alliance.recipient())) } - public searchMap(): SharedArrayBuffer { - return this._searchMap + public terrainMap(): TerrainMapImpl { + return this._terrainMap } displayMessage(message: string, type: MessageType, playerID: PlayerID | null): void { diff --git a/src/core/game/TerrainMapLoader.ts b/src/core/game/TerrainMapLoader.ts index 0b0fb794d..136e679af 100644 --- a/src/core/game/TerrainMapLoader.ts +++ b/src/core/game/TerrainMapLoader.ts @@ -1,4 +1,4 @@ -import { Cell, GameMap, TerrainType } from './Game'; +import { Cell, GameMap, SearchNode, TerrainMap, TerrainTile, TerrainType } from './Game'; import europeBin from "!!binary-loader!../../../resources/maps/Europe.bin"; import europeInfo from "../../../resources/maps/Europe.json" @@ -13,7 +13,7 @@ const maps = new Map() .set(GameMap.Europe, { bin: europeBin, info: europeInfo }) .set(GameMap.Mena, { bin: menaBin, info: menaInfo }); -const loadedMaps = new Map() +const loadedMaps = new Map() export interface NationMap { name: string; @@ -29,15 +29,57 @@ export interface Nation { } -export class TerrainMap { +export class TerrainTileImpl implements TerrainTile { + public shoreline: boolean = false + public magnitude: number = 0 + public ocean = false + public land = false + private _neighbors: TerrainTile[] | null = null + + constructor(public type: TerrainType, private _cell: Cell) { } + + terrainType(): TerrainType { + return this.type + } + + cost(): number { + return this.magnitude < 10 ? 2 : 1 + } + + cell(): Cell { + return this._cell + } + + initNeighbors(map: TerrainMapImpl): TerrainTile[] { + if (this._neighbors === null) { + const positions = [ + { x: this._cell.x - 1, y: this._cell.y }, // Left + { x: this._cell.x + 1, y: this._cell.y }, // Right + { x: this._cell.x, y: this._cell.y - 1 }, // Up + { x: this._cell.x, y: this._cell.y + 1 } // Down + ]; + + this._neighbors = positions + .filter(pos => pos.x >= 0 && pos.x < map.width() && + pos.y >= 0 && pos.y < map.height()) + .map(pos => map.terrain(new Cell(pos.x, pos.y))); + } + return this._neighbors; + } +} + +export class TerrainMapImpl implements TerrainMap { constructor( - public readonly tiles: Terrain[][], + public readonly tiles: TerrainTileImpl[][], public readonly numLandTiles: number, public readonly nationMap: NationMap, - public searchBuffer: SharedArrayBuffer ) { } - terrain(cell: Cell): Terrain { + neighbors(terrainTile: TerrainTile): TerrainTile[] { + return (terrainTile as TerrainTileImpl).initNeighbors(this); + } + + terrain(cell: Cell): TerrainTileImpl { return this.tiles[cell.x][cell.y] } @@ -50,15 +92,7 @@ export class TerrainMap { } } -export class Terrain { - public shoreline: boolean = false - public magnitude: number = 0 - public ocean = false - public land = false - constructor(public type: TerrainType) { } -} - -export async function loadTerrainMap(map: GameMap): Promise { +export async function loadTerrainMap(map: GameMap): Promise { if (loadedMaps.has(map)) { return loadedMaps.get(map) } @@ -83,7 +117,7 @@ export async function loadTerrainMap(map: GameMap): Promise { throw new Error(`Invalid data: buffer size ${fileData.length} incorrect for ${width}x${height} terrain plus 4 bytes for dimensions.`); } - const terrain: Terrain[][] = Array(width).fill(null).map(() => Array(height).fill(null)); + const terrain: TerrainTileImpl[][] = Array(width).fill(null).map(() => Array(height).fill(null)); let numLand = 0 // Start from the 5th byte (index 4) when processing terrain data @@ -115,19 +149,19 @@ export async function loadTerrainMap(map: GameMap): Promise { } } - terrain[x][y] = new Terrain(type); + terrain[x][y] = new TerrainTileImpl(type, new Cell(x, y)); terrain[x][y].shoreline = shoreline; terrain[x][y].magnitude = magnitude; terrain[x][y].ocean = ocean terrain[x][y].land = land } } - const encoder = new TextEncoder(); - const encoded = encoder.encode(fileData); - const buffer = new SharedArrayBuffer(encoded.length); - const view = new Uint8Array(buffer); - view.set(encoded) - const m = new TerrainMap(terrain, numLand, mapData.info, buffer); + // const encoder = new TextEncoder(); + // const encoded = encoder.encode(fileData); + // const buffer = new SharedArrayBuffer(encoded.length); + // const view = new Uint8Array(buffer); + // view.set(encoded) + const m = new TerrainMapImpl(terrain, numLand, mapData.info); loadedMaps.set(map, m) return m } diff --git a/src/core/game/TerrainSearchMap.ts b/src/core/game/TerrainSearchMap.ts index 4be01d053..dcb48c4e6 100644 --- a/src/core/game/TerrainSearchMap.ts +++ b/src/core/game/TerrainSearchMap.ts @@ -1,4 +1,4 @@ -import { TerrainMap } from "./TerrainMapLoader"; +import { TerrainMapImpl } from "./TerrainMapLoader"; export enum SearchMapTileType { Land, Shore, diff --git a/src/core/game/TileImpl.ts b/src/core/game/TileImpl.ts index e7f989f7c..8313b8f9d 100644 --- a/src/core/game/TileImpl.ts +++ b/src/core/game/TileImpl.ts @@ -1,5 +1,5 @@ -import { Tile, Cell, TerrainType, Player, TerraNullius, MutablePlayer } from "./Game"; -import { Terrain } from "./TerrainMapLoader"; +import { Tile, Cell, TerrainType, Player, TerraNullius, MutablePlayer, SearchNode, TerrainTile } from "./Game"; +import { TerrainTileImpl } from "./TerrainMapLoader"; import { GameImpl } from "./GameImpl"; import { PlayerImpl } from "./PlayerImpl"; import { TerraNulliusImpl } from "./TerraNulliusImpl"; @@ -14,7 +14,7 @@ export class TileImpl implements Tile { private readonly gs: GameImpl, public _owner: PlayerImpl | TerraNulliusImpl, private readonly _cell: Cell, - private readonly _terrain: Terrain + private readonly _terrain: TerrainTileImpl ) { } neighborsWrapped(): Tile[] { @@ -108,4 +108,8 @@ export class TileImpl implements Tile { } return this._neighbors; } + + cost(): number { + return this.magnitude() < 10 ? 2 : 1 + }; } diff --git a/src/core/pathfinding/AStar.ts b/src/core/pathfinding/AStar.ts new file mode 100644 index 000000000..1ffce78eb --- /dev/null +++ b/src/core/pathfinding/AStar.ts @@ -0,0 +1,138 @@ +import { PriorityQueue } from "@datastructures-js/priority-queue"; +import { SearchNode } from "../game/Game"; +import { PathFindResultType } from "./PathFinding"; + + +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; + } +} diff --git a/src/core/pathfinding/AsyncPathFinding.ts b/src/core/pathfinding/AsyncPathFinding.ts new file mode 100644 index 000000000..5b3981f8a --- /dev/null +++ b/src/core/pathfinding/AsyncPathFinding.ts @@ -0,0 +1,121 @@ +import { TerrainTile, Tile, Game, GameMap, Cell } from "../game/Game"; +import { PathFindResultType } from "./PathFinding"; + +export class AsyncPathFinderCreator { + private worker: Worker; + private isInitialized = false; + + constructor(private game: Game, private gameMap: GameMap) { + // Create a new worker using webpack worker-loader + // The import.meta.url ensures webpack can properly bundle the worker + this.worker = new Worker(new URL('./PathFinder.worker.ts', import.meta.url)); + } + + initialize(): Promise { + return new Promise((resolve, reject) => { + this.worker.postMessage({ + type: 'init', + gameMap: this.gameMap + }); + + const handler = (e: MessageEvent) => { + if (e.data.type === 'initialized') { + this.worker.removeEventListener('message', handler); + this.isInitialized = true; + resolve(); + } else { + this.worker.removeEventListener('message', handler); + reject('Failed to initialize pathfinder'); + } + }; + + this.worker.addEventListener('message', handler); + }); + } + + createPathFinder(src: Tile, dst: Tile, numTicks: number): AsyncPathFinder { + if (!this.isInitialized) { + throw new Error('PathFinder not initialized'); + } + return new AsyncPathFinder(this.game, this.worker, src, dst, numTicks); + } + + cleanup() { + this.worker.terminate(); + } +} + +// AsyncPathFinder.ts +export class AsyncPathFinder { + private path: Tile[] | 'NOT_FOUND' | null = null; + private promise: Promise; + + constructor( + private game: Game, + private worker: Worker, + private src: Tile, + private dst: Tile, + private numTicks: number + ) { } + + findPath(): Promise { + this.promise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject("Path timeout"); + }, 100_000); + + const handler = (e: MessageEvent) => { + 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 { + reject(e.data.reason || "Path not found"); + } + }; + + this.worker.addEventListener('message', handler); + this.worker.postMessage({ + type: 'findPath', + requestId: crypto.randomUUID(), + currentTick: this.game.ticks(), + duration: this.numTicks, + start: { x: this.src.cell().x, y: this.src.cell().y }, + end: { x: this.dst.cell().x, y: this.dst.cell().y } + }); + }); + + return this.promise; + } + + // TODO: rename to poll? + compute(): PathFindResultType { + if (this.promise == null) { + this.findPath() + } + 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; + } + } + throw new Error(`path not completed in time`) + } + return PathFindResultType.Pending; + } + + reconstructPath(): Tile[] { + if (this.path == "NOT_FOUND" || this.path == null) { + throw Error(`cannot reconstruct path: ${this.path}`) + } + return this.path as Tile[] + } + +} \ No newline at end of file diff --git a/src/core/pathfinding/PathFinder.worker.ts b/src/core/pathfinding/PathFinder.worker.ts new file mode 100644 index 000000000..f922e26f0 --- /dev/null +++ b/src/core/pathfinding/PathFinder.worker.ts @@ -0,0 +1,100 @@ +// pathfinding.ts +import { Cell, GameMap, SearchNode, TerrainMap, TerrainTile, TerrainType } from "../game/Game"; +import { PathFindResultType } from "./PathFinding"; +import { AStar } from "./AStar"; +import { loadTerrainMap } from "../game/TerrainMapLoader"; +import { PriorityQueue } from "@datastructures-js/priority-queue"; + +let terrainMapPromise: Promise; +let searches = new PriorityQueue((a: Search, b: Search) => (a.deadline - b.deadline)) +let processingInterval: number | null = null; +let isProcessingSearch = false + + +interface Search { + aStar: AStar, + deadline: number + requestId: string +} + +interface SearchRequest { + requestId: string + currentTick: number + // duration in ticks + duration: number + start: { x: number, y: number }, + end: { x: number, y: number } +} + +self.onmessage = (e) => { + switch (e.data.type) { + case 'init': + initializeMap(e.data); + break; + case 'findPath': + terrainMapPromise.then(tm => findPath(tm, e.data)) + break; + } +}; + +function initializeMap(data: { gameMap: GameMap }) { + terrainMapPromise = loadTerrainMap(data.gameMap) + self.postMessage({ type: 'initialized' }); + processingInterval = setInterval(computeSearches, .5) as unknown as number; +} + +function findPath(terrainMap: TerrainMap, req: SearchRequest) { + const aStar = new AStar( + 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 + ); + + searches.enqueue({ + aStar: aStar, + deadline: req.currentTick + req.duration, + requestId: req.requestId + }) +} + +function computeSearches() { + if (isProcessingSearch || searches.isEmpty()) { + return + } + + isProcessingSearch = true + + try { + for (let i = 0; i < 10; i++) { + if (searches.isEmpty()) { + return + } + const search = searches.dequeue() + switch (search.aStar.compute()) { + case PathFindResultType.Completed: + self.postMessage({ + type: 'pathFound', + requestId: search.requestId, + path: search.aStar.reconstructPath().map(sn => ({ x: sn.cell().x, y: sn.cell().y })) + }); + break; + + case PathFindResultType.Pending: + searches.push(search) + break + case PathFindResultType.PathNotFound: + console.warn(`worker: path not found to port`); + self.postMessage({ + type: 'error', + requestId: search.requestId, + }); + break + } + } + } finally { + isProcessingSearch = false + } +} diff --git a/src/core/pathfinding/PathFinding.ts b/src/core/pathfinding/PathFinding.ts new file mode 100644 index 000000000..4e95b1c99 --- /dev/null +++ b/src/core/pathfinding/PathFinding.ts @@ -0,0 +1,98 @@ +import { 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; +} + + +export class PathFinder { + + private curr: Tile = null + private dst: Tile = null + private path: Tile[] + private aStar: AStar + private computeFinished = true + + constructor( + private iterations: number, + private canMove: (t: Tile) => boolean, + private maxTries: number = 20 + ) { } + + nextTile(curr: Tile, dst: Tile, dist: number = 1): TileResult { + if (curr == null) { + console.error('curr is null') + } + if (dst == null) { + console.error('dst is null') + } + + if (manhattanDist(curr.cell(), dst.cell()) < dist) { + return { type: PathFindResultType.Completed, tile: curr } + } + + if (this.computeFinished) { + if (this.shouldRecompute(curr, dst)) { + 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.computeFinished = false + return this.nextTile(curr, dst) + } else { + return { type: PathFindResultType.NextTile, tile: this.path.shift() } + } + } + + switch (this.aStar.compute()) { + case PathFindResultType.Completed: + this.computeFinished = true + this.path = this.aStar.reconstructPath().map(sn => sn as Tile) + // Remove the start tile + this.path.shift() + return this.nextTile(curr, dst) + case PathFindResultType.Pending: + return { type: PathFindResultType.Pending } + case PathFindResultType.PathNotFound: + return { type: PathFindResultType.PathNotFound } + } + } + + private shouldRecompute(curr: Tile, dst: Tile) { + if (this.path == null || this.curr == null || this.dst == null) { + return true + } + const dist = manhattanDist(curr.cell(), dst.cell()) + let tolerance = 10 + if (dist > 50) { + tolerance = 10 + } else if (dist > 25) { + tolerance = 5 + } else if (dist > 10) { + tolerance = 3 + } else { + tolerance = 0 + } + if (manhattanDist(this.dst.cell(), dst.cell()) > tolerance) { + return true + } + return false + } +} diff --git a/src/scripts/TerrainMapGenerator.ts b/src/scripts/TerrainMapGenerator.ts index 0e770749b..3e0e4eddb 100644 --- a/src/scripts/TerrainMapGenerator.ts +++ b/src/scripts/TerrainMapGenerator.ts @@ -2,6 +2,7 @@ import { decodePNGFromStream } from 'pureimage'; import path from 'path'; import fs from 'fs/promises'; import { createReadStream } from 'fs'; import { fileURLToPath } from 'url'; +import { TerrainTile } from '../core/game/Game'; const __filename = fileURLToPath(import.meta.url); @@ -21,6 +22,8 @@ export class TerrainMap { return this.tiles[coord.x][coord.y] } + + width(): number { return this.tiles.length }