diff --git a/TODO.txt b/TODO.txt index 574d8f706..f86820b8f 100644 --- a/TODO.txt +++ b/TODO.txt @@ -185,7 +185,8 @@ * add missile silo DONE 11/15/2024 * nuke spawns from missile silo DONE 11/16/2024 * REFACTOR: move canbuild to player DONE 11/17/2024 -* BUG: can build destroyer on land +* BUG: can build destroyer on land DONE 11/17/2024 +* fix pathfinding bug * make two nukes: atom & hydrogen * destroyer can capture trade ships * add battleship diff --git a/src/core/PathFinding.ts b/src/core/PathFinding.ts index 6549c0a7e..b7b540e93 100644 --- a/src/core/PathFinding.ts +++ b/src/core/PathFinding.ts @@ -1,7 +1,26 @@ import { PriorityQueue } from "@datastructures-js/priority-queue"; import { Tile } from "./game/Game"; import { manhattanDist } from "./Util"; -import { colord } from "colord"; + +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; }>; @@ -15,7 +34,9 @@ export class AStar { constructor( private src: Tile, private dst: Tile, - private canMove: (t: Tile) => boolean + 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 @@ -39,12 +60,20 @@ export class AStar { this.bwdOpenSet.enqueue({ tile: dst, fScore: this.heuristic(dst, src) }); } - compute(iterations: number): boolean { - if (this.completed) return true; + 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) return false; + if (iterations <= 0) { + if (this.maxTries <= 0) { + return PathFindResultType.PathNotFound + } + return PathFindResultType.Pending; + } // Process forward search const fwdCurrent = this.fwdOpenSet.dequeue()!.tile; @@ -52,7 +81,7 @@ export class AStar { // We found a meeting point! this.meetingPoint = fwdCurrent; this.completed = true; - return true; + return PathFindResultType.Completed; } this.expandNode(fwdCurrent, true); @@ -63,13 +92,13 @@ export class AStar { // We found a meeting point! this.meetingPoint = bwdCurrent; this.completed = true; - return true; + return PathFindResultType.Completed } this.expandNode(bwdCurrent, false); } - return this.completed; + return this.completed ? PathFindResultType.Completed : PathFindResultType.PathNotFound } private expandNode(current: Tile, isForward: boolean) { @@ -131,42 +160,48 @@ export class PathFinder { private dst: Tile = null private path: Tile[] private aStar: AStar - private inProgress = false + private computeFinished = true - constructor(private iterations: number, private canMove: (t: Tile) => boolean) { + constructor( + private iterations: number, + private canMove: (t: Tile) => boolean, + private maxTries: number = 20 + ) { } - } + nextTile(curr: Tile, dst: Tile, dist: number = 1): TileResult { + if (manhattanDist(curr.cell(), dst.cell()) < dist) { + return { type: PathFindResultType.Completed, tile: curr } + } - nextTile(curr: Tile, dst: Tile): Tile { - if (this.shouldRecompute(curr, dst)) { - if (this.inProgress) { - if (this.aStar.compute(this.iterations)) { - this.path = this.aStar.reconstructPath() - this.inProgress = false - } else { - return null - } - } else { + 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) - if (this.aStar.compute(this.iterations)) { - this.inProgress = false - this.path = this.aStar.reconstructPath() - } else { - this.inProgress = true - return null - } - if (this.path.length > 0) { - this.path.shift() - } + 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() } } - } else { - return 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 diff --git a/src/core/execution/DestroyerExecution.ts b/src/core/execution/DestroyerExecution.ts index 4114e56c3..20be95601 100644 --- a/src/core/execution/DestroyerExecution.ts +++ b/src/core/execution/DestroyerExecution.ts @@ -1,5 +1,5 @@ import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, Tile, UnitType } from "../game/Game"; -import { AStar, PathFinder } from "../PathFinding"; +import { AStar, PathFinder, PathFindResultType } from "../PathFinding"; import { PseudoRandom } from "../PseudoRandom"; import { distSort, distSortUnit, manhattanDist } from "../Util"; @@ -54,39 +54,47 @@ export class DestroyerExecution implements Execution { if (this.target == null) { const ships = this.mg.units(UnitType.TransportShip) .filter(u => manhattanDist(u.tile().cell(), this.destroyer.tile().cell()) < 100) - .filter(u => u.owner() != this.destroyer.owner()) + // .filter(u => u.owner() != this.destroyer.owner()) .filter(u => u != this.destroyer) .filter(u => !u.owner().isAlliedWith(this.destroyer.owner())) if (ships.length == 0) { - if (manhattanDist(this.destroyer.tile().cell(), this.patrolTile.cell()) > 5) { - const next = this.pathfinder.nextTile(this.destroyer.tile(), this.patrolTile) - if (next == null) { - this.target = null + const result = this.pathfinder.nextTile(this.destroyer.tile(), this.patrolTile) + switch (result.type) { + case PathFindResultType.Completed: + this.patrolTile = this.randomTile() + break + case PathFindResultType.NextTile: + this.destroyer.move(result.tile) + break + case PathFindResultType.Pending: return - } - this.destroyer.move(next) - } else { - this.patrolTile = this.randomTile() + case PathFindResultType.PathNotFound: + console.log(`path not found to patrol tile`) + this.patrolTile = this.randomTile() + break } return } this.target = ships.sort(distSortUnit(this.destroyer))[0] } - if (manhattanDist(this.destroyer.tile().cell(), this.target.tile().cell()) < 5) { - this.target.delete() - this.target = null - return - } - for (let i = 0; i < 1 + this.mg.ticks() % 2; i++) { - const next = this.pathfinder.nextTile(this.destroyer.tile(), this.target.tile()) - if (next == null) { - this.target = null - console.warn(`target not found`) - return - } - this.destroyer.move(next) - } + for (let i = 0; i < 1 + this.mg.ticks() % 2; i++) { + const result = this.pathfinder.nextTile(this.destroyer.tile(), this.target.tile(), 5) + switch (result.type) { + case PathFindResultType.Completed: + this.target.delete() + this.target = null + return + case PathFindResultType.NextTile: + this.destroyer.move(result.tile) + break + case PathFindResultType.Pending: + break + case PathFindResultType.PathNotFound: + console.log(`path not found to target`) + break + } + } } owner(): MutablePlayer { diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index c44a55239..c10e8ea6c 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -1,5 +1,6 @@ +import { nextTick } from "process"; import { Cell, Execution, MutableGame, MutablePlayer, PlayerID, Tile, MutableUnit, UnitType } from "../game/Game"; -import { PathFinder } from "../PathFinding"; +import { PathFinder, PathFindResultType } from "../PathFinding"; import { PseudoRandom } from "../PseudoRandom"; import { bfs, dist, distSortUnit, euclideanDist, manhattanDist } from "../Util"; @@ -41,16 +42,24 @@ export class NukeExecution implements Execution { } this.nuke = this.player.buildUnit(UnitType.Nuke, 0, spawn) } - if (this.nuke.tile() == this.dst) { - this.detonate() - return - } + for (let i = 0; i < 4; i++) { - const nextTile = this.pathFinder.nextTile(this.nuke.tile(), this.dst) - if (nextTile == null) { - return + const result = this.pathFinder.nextTile(this.nuke.tile(), this.dst) + switch (result.type) { + case PathFindResultType.Completed: + this.nuke.move(result.tile) + this.detonate() + return + case PathFindResultType.NextTile: + this.nuke.move(result.tile) + break + case PathFindResultType.Pending: + break + case PathFindResultType.PathNotFound: + console.warn(`nuke cannot find path from ${this.nuke.tile()} to ${this.dst}`) + this.active = false + return } - this.nuke.move(nextTile) } } diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index 24ca01d3e..a4ea5a57c 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -1,5 +1,5 @@ import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, MutableUnit, Player, PlayerID, Tile, Unit, UnitType } from "../game/Game"; -import { AStar, PathFinder } from "../PathFinding"; +import { AStar, PathFinder, PathFindResultType } from "../PathFinding"; import { PseudoRandom } from "../PseudoRandom"; import { bfs, dist, manhattanDist } from "../Util"; import { TradeShipExecution } from "./TradeShipExecution"; @@ -54,14 +54,19 @@ export class PortExecution implements Execution { for (const port of allPorts) { if (this.computingPaths.has(port)) { const aStar = this.computingPaths.get(port) - if (aStar.compute(10_000)) { - this.portPaths.set(port, aStar.reconstructPath()) - this.computingPaths.delete(port) + switch(aStar.compute()) { + case PathFindResultType.Completed: + this.portPaths.set(port, aStar.reconstructPath()) + this.computingPaths.delete(port) + case PathFindResultType.Pending: + break + case PathFindResultType.PathNotFound: + console.warn(`path not found to port`) } continue } if (!this.portPaths.has(port)) { - this.computingPaths.set(port, new AStar(this.port.tile(), port.tile(), t => t.isWater())) + this.computingPaths.set(port, new AStar(this.port.tile(), port.tile(), t => t.isWater(), 10_000, 20)) continue } } diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index ab8ed174d..a7e23ade3 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -2,7 +2,7 @@ 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 } from "../PathFinding"; +import { AStar, PathFinder, PathFindResultType } from "../PathFinding"; export class TransportShipExecution implements Execution { @@ -25,7 +25,7 @@ export class TransportShipExecution implements Execution { private boat: MutableUnit - private pathFinder: PathFinder = new PathFinder(10_000, t => t.isWater()) + private pathFinder: PathFinder = new PathFinder(10_000, t => t.isWater(), 2) constructor( private attackerID: PlayerID, @@ -111,12 +111,21 @@ export class TransportShipExecution implements Execution { return } - const nextTile = this.pathFinder.nextTile(this.boat.tile(), this.dst) - if (nextTile == null) { - console.warn('boat computing') - return + const result = this.pathFinder.nextTile(this.boat.tile(), this.dst) + switch (result.type) { + case PathFindResultType.Completed: + case PathFindResultType.NextTile: + this.boat.move(result.tile) + break + case PathFindResultType.Pending: + console.warn('boat computing') + break + case PathFindResultType.PathNotFound: + // TODO: add to poisoned port list + console.warn(`path not found tot dst`) + this.dst = null + break } - this.boat.move(nextTile) } owner(): MutablePlayer {