mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:40:44 +00:00
created path finding web worker
This commit is contained in:
Generated
+50
-9
@@ -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",
|
||||
|
||||
+3
-2
@@ -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",
|
||||
|
||||
@@ -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++
|
||||
+1
-1
@@ -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';
|
||||
|
||||
|
||||
@@ -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<Tile, Tile>;
|
||||
private bwdCameFrom: Map<Tile, Tile>;
|
||||
private fwdGScore: Map<Tile, number>;
|
||||
private bwdGScore: Map<Tile, number>;
|
||||
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<Tile, Tile>();
|
||||
this.bwdCameFrom = new Map<Tile, Tile>();
|
||||
this.fwdGScore = new Map<Tile, number>();
|
||||
this.bwdGScore = new Map<Tile, number>();
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Player, number>()
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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<MutableUnit, Tile[]>()
|
||||
private computingPaths = new Map<MutableUnit, AStar>()
|
||||
private computingPaths = new Map<MutableUnit, AsyncPathFinder>()
|
||||
|
||||
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()) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
+18
-5
@@ -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) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-12
@@ -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 {
|
||||
|
||||
@@ -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<GameMap, TerrainMap>()
|
||||
const loadedMaps = new Map<GameMap, TerrainMapImpl>()
|
||||
|
||||
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<TerrainMap> {
|
||||
export async function loadTerrainMap(map: GameMap): Promise<TerrainMapImpl> {
|
||||
if (loadedMaps.has(map)) {
|
||||
return loadedMaps.get(map)
|
||||
}
|
||||
@@ -83,7 +117,7 @@ export async function loadTerrainMap(map: GameMap): Promise<TerrainMap> {
|
||||
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<TerrainMap> {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TerrainMap } from "./TerrainMapLoader";
|
||||
import { TerrainMapImpl } from "./TerrainMapLoader";
|
||||
export enum SearchMapTileType {
|
||||
Land,
|
||||
Shore,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<SearchNode, SearchNode>;
|
||||
private bwdCameFrom: Map<SearchNode, SearchNode>;
|
||||
private fwdGScore: Map<SearchNode, number>;
|
||||
private bwdGScore: Map<SearchNode, number>;
|
||||
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<SearchNode, SearchNode>();
|
||||
this.bwdCameFrom = new Map<SearchNode, SearchNode>();
|
||||
this.fwdGScore = new Map<SearchNode, number>();
|
||||
this.bwdGScore = new Map<SearchNode, number>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
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<void>;
|
||||
|
||||
constructor(
|
||||
private game: Game,
|
||||
private worker: Worker,
|
||||
private src: Tile,
|
||||
private dst: Tile,
|
||||
private numTicks: number
|
||||
) { }
|
||||
|
||||
findPath(): Promise<void> {
|
||||
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[]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<TerrainMap>;
|
||||
let searches = new PriorityQueue<Search>((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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user