Fix cache pathfinding (#848)

## Description:
Add full path to caches
Fix A* to always start and end at given start and end
Remove pathfinder from tradeshipexecution

Should fix tradeship looping and getting stuck at ports
Fix #843


Dont know what to add so that description check is happy

[More lines to lines to make it happy]

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

Vivacious Box

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
This commit is contained in:
Vivacious Box
2025-05-25 12:39:55 -07:00
committed by GitHub
co-authored by evanpelle
parent 5b27bee3bd
commit 27792428a0
4 changed files with 120 additions and 41 deletions
+1 -3
View File
@@ -8,7 +8,6 @@ import {
UnitType,
} from "../game/Game";
import { TileRef } from "../game/GameMap";
import { PathFinder } from "../pathfinding/PathFinding";
import { PseudoRandom } from "../PseudoRandom";
import { TradeShipExecution } from "./TradeShipExecution";
@@ -79,9 +78,8 @@ export class PortExecution implements Execution {
}
const port = this.random.randElement(ports);
const pf = PathFinder.Mini(this.mg, 2500);
this.mg.addExecution(
new TradeShipExecution(this.player().id(), this.port, port, pf),
new TradeShipExecution(this.player().id(), this.port, port),
);
}
+66 -31
View File
@@ -10,8 +10,8 @@ import {
UnitType,
} from "../game/Game";
import { TileRef } from "../game/GameMap";
import { PathFindResultType } from "../pathfinding/AStar";
import { PathFinder } from "../pathfinding/PathFinding";
import { AStar, PathFindResultType } from "../pathfinding/AStar";
import { MiniAStar } from "../pathfinding/MiniAStar";
import { distSortUnit } from "../Util";
export class TradeShipExecution implements Execution {
@@ -19,15 +19,14 @@ export class TradeShipExecution implements Execution {
private mg: Game | null = null;
private origOwner: Player | null = null;
private tradeShip: Unit | null = null;
private index = 0;
private wasCaptured = false;
private tilesTraveled = 0;
private aStar: AStar | null = null;
constructor(
private _owner: PlayerID,
private srcPort: Unit,
private _dstPort: Unit,
private pathFinder: PathFinder,
) {}
init(mg: Game, ticks: number): void {
@@ -103,46 +102,82 @@ export class TradeShipExecution implements Execution {
const cachedNextTile = this._dstPort.cacheGet(this.tradeShip.tile());
if (cachedNextTile !== undefined) {
if (
this.mg.isWater(cachedNextTile) &&
this.mg.isShoreline(cachedNextTile)
) {
this.tradeShip.setSafeFromPirates();
}
this.tradeShip.move(cachedNextTile);
this.tilesTraveled++;
this.moveTradeShip(cachedNextTile);
return;
}
const result = this.pathFinder.nextTile(
this.tradeShip.tile(),
this._dstPort.tile(),
);
this.computeNewPath();
}
switch (result.type) {
case PathFindResultType.Completed:
this.complete();
break;
private fillCachePath(port: Unit, path: TileRef[]): void {
if (path.length < 2) {
throw new Error("path must have at least 2 points");
}
for (let i = 0; i < path.length - 1; i++) {
if (port.cacheGet(path[i]) !== undefined) {
continue;
}
const from = path[i];
const to = path[i + 1];
port.cachePut(from, to);
}
}
private moveTradeShip(nextTile?: TileRef): void {
if (nextTile === undefined) {
throw new Error("missing tile");
}
if (nextTile === this._dstPort.tile()) {
this.complete();
return;
}
// Update safeFromPirates status
if (this.mg!.isWater(nextTile) && this.mg!.isShoreline(nextTile)) {
this.tradeShip!.setSafeFromPirates();
}
this.tradeShip!.move(nextTile);
this.tilesTraveled++;
}
private computeNewPath(): void {
if (this.aStar === null) {
this.aStar = new MiniAStar(
this.mg!,
this.mg!.miniMap(),
this.tradeShip!.tile(),
this._dstPort.tile(),
2500,
20,
);
}
switch (this.aStar.compute()) {
case PathFindResultType.Pending:
// Fire unit event to rerender.
this.tradeShip.touch();
this.tradeShip!.touch();
break;
case PathFindResultType.NextTile:
this._dstPort.cachePut(this.tradeShip.tile(), result.tile);
// Update safeFromPirates status
if (this.mg.isWater(result.tile) && this.mg.isShoreline(result.tile)) {
this.tradeShip.setSafeFromPirates();
case PathFindResultType.Completed: {
const fullPath = this.aStar.reconstructPath();
if (fullPath === null || fullPath.length === 0) {
throw new Error("missing path");
}
this.tradeShip.move(result.tile);
this.tilesTraveled++;
this.fillCachePath(this._dstPort, fullPath);
if (!this.wasCaptured) {
this.fillCachePath(this.srcPort, fullPath.slice().reverse());
}
this.moveTradeShip(fullPath.shift());
break;
}
case PathFindResultType.PathNotFound:
consolex.warn("captured trade ship cannot find route");
if (this.tradeShip.isActive()) {
this.tradeShip.delete(false);
consolex.warn("trade ship cannot find route");
if (this.tradeShip!.isActive()) {
this.tradeShip!.delete(false);
}
this.active = false;
break;
default:
throw new Error("unexpected path finding compute result");
}
}
+2 -1
View File
@@ -215,7 +215,8 @@ export class TransportShipExecution implements Execution {
break;
case PathFindResultType.PathNotFound:
// TODO: add to poisoned port list
consolex.warn(`path not found tot dst`);
consolex.warn(`path not found to dst`);
this.attacker.addTroops(this.troops);
this.boat.delete(false);
this.active = false;
return;
+51 -6
View File
@@ -9,7 +9,7 @@ export class MiniAStar implements AStar {
constructor(
private gameMap: GameMap,
private miniMap: GameMap,
src: TileRef | TileRef[],
private src: TileRef | TileRef[],
private dst: TileRef,
iterations: number,
maxTries: number,
@@ -41,16 +41,52 @@ export class MiniAStar implements AStar {
}
reconstructPath(): TileRef[] {
const upscaled = upscalePath(
this.aStar
.reconstructPath()
.map((tr) => new Cell(this.miniMap.x(tr), this.miniMap.y(tr))),
let cellSrc: Cell | undefined;
if (!Array.isArray(this.src)) {
cellSrc = new Cell(this.gameMap.x(this.src), this.gameMap.y(this.src));
}
const cellDst = new Cell(
this.gameMap.x(this.dst),
this.gameMap.y(this.dst),
);
const upscaled = fixExtremes(
upscalePath(
this.aStar
.reconstructPath()
.map((tr) => new Cell(this.miniMap.x(tr), this.miniMap.y(tr))),
),
cellDst,
cellSrc,
);
upscaled.push(new Cell(this.gameMap.x(this.dst), this.gameMap.y(this.dst)));
return upscaled.map((c) => this.gameMap.ref(c.x, c.y));
}
}
function fixExtremes(upscaled: Cell[], cellDst: Cell, cellSrc?: Cell): Cell[] {
if (cellSrc !== undefined) {
const srcIndex = findCell(upscaled, cellSrc);
if (srcIndex === -1) {
// didnt find the start tile in the path
upscaled.unshift(cellSrc);
} else if (srcIndex !== 0) {
// found start tile but not at the start
// remove all tiles before the start tile
upscaled = upscaled.slice(srcIndex);
}
}
const dstIndex = findCell(upscaled, cellDst);
if (dstIndex === -1) {
// didnt find the dst tile in the path
upscaled.push(cellDst);
} else if (dstIndex !== upscaled.length - 1) {
// found dst tile but not at the end
// remove all tiles after the dst tile
upscaled = upscaled.slice(0, dstIndex + 1);
}
return upscaled;
}
function upscalePath(path: Cell[], scaleFactor: number = 2): Cell[] {
// Scale up each point
const scaledPath = path.map(
@@ -92,3 +128,12 @@ function upscalePath(path: Cell[], scaleFactor: number = 2): Cell[] {
return smoothPath;
}
function findCell(upscaled: Cell[], cellDst: Cell): number {
for (let i = 0; i < upscaled.length; i++) {
if (upscaled[i].x === cellDst.x && upscaled[i].y === cellDst.y) {
return i;
}
}
return -1;
}