mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 17:46:39 +00:00
initial commit
This commit is contained in:
@@ -10,3 +10,4 @@ resources/.DS_Store
|
||||
.DS_Store
|
||||
.clinic/
|
||||
CLAUDE.md
|
||||
/wasm/pathfinding/target/
|
||||
|
||||
+1
-1
@@ -5,4 +5,4 @@
|
||||
export PATH="/usr/local/bin:$HOME/.npm-global/bin:$HOME/.nvm/versions/node/$(node -v)/bin:$PATH"
|
||||
|
||||
# Then run lint-staged if tests pass
|
||||
npx lint-staged
|
||||
npx.cmd lint-staged
|
||||
|
||||
@@ -8,9 +8,12 @@ export default {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
|
||||
"<rootDir>/__mocks__/fileMock.js",
|
||||
"\\.(css|less)$": "<rootDir>/__mocks__/fileMock.js",
|
||||
"^../../wasm/pathfinding/pkg$": "<rootDir>/static/js/pathfinding",
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.tsx?$": ["@swc/jest"],
|
||||
// Add a transform rule for pathfinding.js
|
||||
"^.+\\.js$": ["@swc/jest"], // This will transform all .js files, including pathfinding.js
|
||||
},
|
||||
transformIgnorePatterns: ["node_modules/(?!(node:)/)"],
|
||||
collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts"],
|
||||
|
||||
@@ -115,7 +115,7 @@ export class GameRunner {
|
||||
this.turns.push(turn);
|
||||
}
|
||||
|
||||
public executeNextTick() {
|
||||
public async executeNextTick() {
|
||||
if (this.isExecuting) {
|
||||
return;
|
||||
}
|
||||
@@ -132,7 +132,7 @@ export class GameRunner {
|
||||
let updates: GameUpdates;
|
||||
|
||||
try {
|
||||
updates = this.game.executeNextTick();
|
||||
updates = await this.game.executeNextTick();
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error("Game tick error:", error.message);
|
||||
|
||||
@@ -8,8 +8,7 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { PathFinder } from "../pathfinding/PathFinding";
|
||||
import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding";
|
||||
import { distSortUnit } from "../Util";
|
||||
|
||||
export class TradeShipExecution implements Execution {
|
||||
@@ -28,10 +27,10 @@ export class TradeShipExecution implements Execution {
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.pathFinder = PathFinder.Mini(mg, 2500);
|
||||
this.pathFinder = PathFinder.Wasm(mg);
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
async tick(ticks: number): Promise<void> {
|
||||
if (this.tradeShip === undefined) {
|
||||
const spawn = this.origOwner.canBuild(
|
||||
UnitType.TradeShip,
|
||||
@@ -102,7 +101,10 @@ export class TradeShipExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = this.pathFinder.nextTile(curTile, this._dstPort.tile());
|
||||
const result = await this.pathFinder.nextTile(
|
||||
curTile,
|
||||
this._dstPort.tile(),
|
||||
);
|
||||
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Pending:
|
||||
|
||||
@@ -10,8 +10,7 @@ import {
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { targetTransportTile } from "../game/TransportShipUtils";
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { PathFinder } from "../pathfinding/PathFinding";
|
||||
import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding";
|
||||
import { AttackExecution } from "./AttackExecution";
|
||||
|
||||
export class TransportShipExecution implements Execution {
|
||||
@@ -64,7 +63,7 @@ export class TransportShipExecution implements Execution {
|
||||
|
||||
this.lastMove = ticks;
|
||||
this.mg = mg;
|
||||
this.pathFinder = PathFinder.Mini(mg, 10_000, true, 100);
|
||||
this.pathFinder = PathFinder.Wasm(mg);
|
||||
|
||||
if (
|
||||
this.attacker.unitCount(UnitType.TransportShip) >=
|
||||
@@ -156,7 +155,7 @@ export class TransportShipExecution implements Execution {
|
||||
.boatSendTroops(this.attacker, this.target, this.boat.troops());
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
async tick(ticks: number) {
|
||||
if (this.dst === null) {
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -181,7 +180,7 @@ export class TransportShipExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
const result = this.pathFinder.nextTile(this.boat.tile(), this.dst);
|
||||
const result = await this.pathFinder.nextTile(this.boat.tile(), this.dst);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
if (this.mg.owner(this.dst) === this.attacker) {
|
||||
|
||||
@@ -8,8 +8,7 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { PathFinder } from "../pathfinding/PathFinding";
|
||||
import { PathFinder, PathFindResultType } from "../pathfinding/PathFinding";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { ShellExecution } from "./ShellExecution";
|
||||
|
||||
@@ -27,7 +26,7 @@ export class WarshipExecution implements Execution {
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.pathfinder = PathFinder.Mini(mg, 10_000, true, 100);
|
||||
this.pathfinder = PathFinder.Wasm(mg);
|
||||
this.random = new PseudoRandom(mg.ticks());
|
||||
if (isUnit(this.input)) {
|
||||
this.warship = this.input;
|
||||
@@ -50,7 +49,7 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
async tick(ticks: number): Promise<void> {
|
||||
if (this.warship.health() <= 0) {
|
||||
this.warship.delete();
|
||||
return;
|
||||
@@ -62,11 +61,11 @@ export class WarshipExecution implements Execution {
|
||||
|
||||
this.warship.setTargetUnit(this.findTargetUnit());
|
||||
if (this.warship.targetUnit()?.type() === UnitType.TradeShip) {
|
||||
this.huntDownTradeShip();
|
||||
await this.huntDownTradeShip();
|
||||
return;
|
||||
}
|
||||
|
||||
this.patrol();
|
||||
await this.patrol();
|
||||
|
||||
if (this.warship.targetUnit() !== undefined) {
|
||||
this.shootTarget();
|
||||
@@ -173,10 +172,10 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
private huntDownTradeShip() {
|
||||
private async huntDownTradeShip() {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
// target is trade ship so capture it.
|
||||
const result = this.pathfinder.nextTile(
|
||||
const result = await this.pathfinder.nextTile(
|
||||
this.warship.tile(),
|
||||
this.warship.targetUnit()!.tile(),
|
||||
5,
|
||||
@@ -200,7 +199,7 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
private patrol() {
|
||||
private async patrol() {
|
||||
if (this.warship.targetTile() === undefined) {
|
||||
this.warship.setTargetTile(this.randomTile());
|
||||
if (this.warship.targetTile() === undefined) {
|
||||
@@ -208,7 +207,7 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
const result = this.pathfinder.nextTile(
|
||||
const result = await this.pathfinder.nextTile(
|
||||
this.warship.tile(),
|
||||
this.warship.targetTile()!,
|
||||
);
|
||||
|
||||
@@ -328,7 +328,7 @@ export interface Execution {
|
||||
isActive(): boolean;
|
||||
activeDuringSpawnPhase(): boolean;
|
||||
init(mg: Game, ticks: number): void;
|
||||
tick(ticks: number): void;
|
||||
tick(ticks: number): void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface Attack {
|
||||
@@ -665,7 +665,7 @@ export interface Game extends GameMap {
|
||||
// Game State
|
||||
ticks(): Tick;
|
||||
inSpawnPhase(): boolean;
|
||||
executeNextTick(): GameUpdates;
|
||||
executeNextTick(): Promise<GameUpdates>;
|
||||
setWinner(winner: Player | Team, allPlayersStats: AllPlayersStats): void;
|
||||
config(): Config;
|
||||
|
||||
|
||||
@@ -327,16 +327,18 @@ export class GameImpl implements Game {
|
||||
return this._ticks;
|
||||
}
|
||||
|
||||
executeNextTick(): GameUpdates {
|
||||
async executeNextTick(): Promise<GameUpdates> {
|
||||
this.updates = createGameUpdatesMap();
|
||||
const promises: (void | Promise<void>)[] = [];
|
||||
this.execs.forEach((e) => {
|
||||
if (
|
||||
(!this.inSpawnPhase() || e.activeDuringSpawnPhase()) &&
|
||||
e.isActive()
|
||||
) {
|
||||
e.tick(this._ticks);
|
||||
promises.push(e.tick(this._ticks));
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
const inited: Execution[] = [];
|
||||
const unInited: Execution[] = [];
|
||||
this.unInitExecs.forEach((e) => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { RailroadExecution } from "../execution/RailroadExecution";
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { MiniAStar } from "../pathfinding/MiniAStar";
|
||||
import { SerialAStar } from "../pathfinding/SerialAStar";
|
||||
import {
|
||||
PathFindResultType,
|
||||
WasmPathFinder,
|
||||
} from "../pathfinding/WasmPathfinding";
|
||||
import { Game, Unit, UnitType } from "./Game";
|
||||
import { TileRef } from "./GameMap";
|
||||
import { RailNetwork } from "./RailNetwork";
|
||||
import { Railroad } from "./Railroad";
|
||||
import { Cluster, TrainStation, TrainStationMapAdapter } from "./TrainStation";
|
||||
import { Cluster, TrainStation } from "./TrainStation";
|
||||
|
||||
/**
|
||||
* The Stations handle their own neighbors so the graph is naturally traversable,
|
||||
@@ -43,48 +43,9 @@ export class StationManagerImpl implements StationManager {
|
||||
}
|
||||
}
|
||||
|
||||
export interface RailPathFinderService {
|
||||
findTilePath(from: TileRef, to: TileRef): TileRef[];
|
||||
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[];
|
||||
}
|
||||
|
||||
class RailPathFinderServiceImpl implements RailPathFinderService {
|
||||
constructor(private game: Game) {}
|
||||
|
||||
findTilePath(from: TileRef, to: TileRef): TileRef[] {
|
||||
const astar = new MiniAStar(
|
||||
this.game.map(),
|
||||
this.game.miniMap(),
|
||||
from,
|
||||
to,
|
||||
5000,
|
||||
20,
|
||||
false,
|
||||
3,
|
||||
);
|
||||
return astar.compute() === PathFindResultType.Completed
|
||||
? astar.reconstructPath()
|
||||
: [];
|
||||
}
|
||||
|
||||
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[] {
|
||||
const stationAStar = new SerialAStar(
|
||||
from,
|
||||
to,
|
||||
5000,
|
||||
20,
|
||||
new TrainStationMapAdapter(this.game),
|
||||
);
|
||||
return stationAStar.compute() === PathFindResultType.Completed
|
||||
? stationAStar.reconstructPath()
|
||||
: [];
|
||||
}
|
||||
}
|
||||
|
||||
export function createRailNetwork(game: Game): RailNetwork {
|
||||
const stationManager = new StationManagerImpl();
|
||||
const pathService = new RailPathFinderServiceImpl(game);
|
||||
return new RailNetworkImpl(game, stationManager, pathService);
|
||||
return new RailNetworkImpl(game, stationManager);
|
||||
}
|
||||
|
||||
export class RailNetworkImpl implements RailNetwork {
|
||||
@@ -93,7 +54,6 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
constructor(
|
||||
private game: Game,
|
||||
private stationManager: StationManager,
|
||||
private pathService: RailPathFinderService,
|
||||
) {}
|
||||
|
||||
connectStation(station: TrainStation) {
|
||||
@@ -127,7 +87,8 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
* Return the intermediary stations connecting two stations
|
||||
*/
|
||||
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[] {
|
||||
return this.pathService.findStationsPath(from, to);
|
||||
// This method is no longer needed as pathfinding is handled by WASM
|
||||
return [];
|
||||
}
|
||||
|
||||
private connectToNearbyStations(station: TrainStation) {
|
||||
@@ -173,7 +134,7 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
} else if (editedClusters.size === 0) {
|
||||
// If no cluster owns the station, creates a new one for it
|
||||
const newCluster = new Cluster();
|
||||
newCluster.addStation(station);
|
||||
newCluster.addStations([station]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,14 +156,26 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
cluster.clear();
|
||||
}
|
||||
|
||||
private connect(from: TrainStation, to: TrainStation) {
|
||||
const path = this.pathService.findTilePath(from.tile(), to.tile());
|
||||
if (path.length > 0 && path.length < this.game.config().railroadMaxSize()) {
|
||||
const railRoad = new Railroad(from, to, path);
|
||||
this.game.addExecution(new RailroadExecution(railRoad));
|
||||
from.addRailroad(railRoad);
|
||||
to.addRailroad(railRoad);
|
||||
return true;
|
||||
private async connect(from: TrainStation, to: TrainStation) {
|
||||
const wasmPathFinder = new WasmPathFinder(this.game);
|
||||
|
||||
const pathResult = await wasmPathFinder.nextTile(from.tile(), to.tile());
|
||||
|
||||
if (
|
||||
pathResult.type === PathFindResultType.Completed &&
|
||||
wasmPathFinder.path
|
||||
) {
|
||||
const path = wasmPathFinder.path;
|
||||
if (
|
||||
path.length > 0 &&
|
||||
path.length < this.game.config().railroadMaxSize()
|
||||
) {
|
||||
const railRoad = new Railroad(from, to, path);
|
||||
this.game.addExecution(new RailroadExecution(railRoad));
|
||||
from.addRailroad(railRoad);
|
||||
to.addRailroad(railRoad);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { TrainExecution } from "../execution/TrainExecution";
|
||||
import { GraphAdapter } from "../pathfinding/SerialAStar";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { Game, Player, Unit, UnitType } from "./Game";
|
||||
import { TileRef } from "./GameMap";
|
||||
@@ -156,29 +155,6 @@ export class TrainStation {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the trainstation usable with A*
|
||||
*/
|
||||
export class TrainStationMapAdapter implements GraphAdapter<TrainStation> {
|
||||
constructor(private game: Game) {}
|
||||
|
||||
neighbors(node: TrainStation): TrainStation[] {
|
||||
return node.neighbors();
|
||||
}
|
||||
|
||||
cost(node: TrainStation): number {
|
||||
return 1;
|
||||
}
|
||||
|
||||
position(node: TrainStation): { x: number; y: number } {
|
||||
return { x: this.game.x(node.tile()), y: this.game.y(node.tile()) };
|
||||
}
|
||||
|
||||
isTraversable(from: TrainStation, to: TrainStation): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cluster of connected stations
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { MiniAStar } from "../pathfinding/MiniAStar";
|
||||
import { Game, Player, UnitType } from "./Game";
|
||||
import { andFN, GameMap, manhattanDistFN, TileRef } from "./GameMap";
|
||||
|
||||
@@ -144,30 +142,9 @@ export function bestShoreDeploymentSource(
|
||||
player: Player,
|
||||
target: TileRef,
|
||||
): TileRef | false {
|
||||
const t = targetTransportTile(gm, target);
|
||||
if (t === null) return false;
|
||||
|
||||
const candidates = candidateShoreTiles(gm, player, t);
|
||||
const aStar = new MiniAStar(gm, gm.miniMap(), candidates, t, 1_000_000, 1);
|
||||
const result = aStar.compute();
|
||||
if (result !== PathFindResultType.Completed) {
|
||||
console.warn(`bestShoreDeploymentSource: path not found: ${result}`);
|
||||
return false;
|
||||
}
|
||||
const path = aStar.reconstructPath();
|
||||
if (path.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const potential = path[0];
|
||||
// Since mini a* downscales the map, we need to check the neighbors
|
||||
// of the potential tile to find a valid deployment point
|
||||
const neighbors = gm
|
||||
.neighbors(potential)
|
||||
.filter((n) => gm.isShore(n) && gm.owner(n) === player);
|
||||
if (neighbors.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return neighbors[0];
|
||||
// This function needs to be re-implemented to use the WASM pathfinding module.
|
||||
// For now, it will return false.
|
||||
return false;
|
||||
}
|
||||
|
||||
export function candidateShoreTiles(
|
||||
|
||||
@@ -24,8 +24,3 @@ export type AStarResult<NodeType> =
|
||||
| {
|
||||
type: PathFindResultType.PathNotFound;
|
||||
};
|
||||
|
||||
export interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PseudoRandom } from "../PseudoRandom";
|
||||
import { DistanceBasedBezierCurve } from "../utilities/Line";
|
||||
import { AStar, AStarResult, PathFindResultType } from "./AStar";
|
||||
import { MiniAStar } from "./MiniAStar";
|
||||
import { WasmPathFinder } from "./WasmPathfinding";
|
||||
|
||||
const parabolaMinHeight = 50;
|
||||
|
||||
@@ -133,6 +134,10 @@ export class PathFinder {
|
||||
});
|
||||
}
|
||||
|
||||
public static Wasm(game: Game) {
|
||||
return new WasmPathFinder(game);
|
||||
}
|
||||
|
||||
nextTile(
|
||||
curr: TileRef | null,
|
||||
dst: TileRef | null,
|
||||
|
||||
@@ -61,14 +61,6 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
|
||||
});
|
||||
}
|
||||
|
||||
private findClosestSource(tile: NodeType): NodeType {
|
||||
return this.sources.reduce((closest, source) =>
|
||||
this.heuristic(tile, source) < this.heuristic(tile, closest)
|
||||
? source
|
||||
: closest,
|
||||
);
|
||||
}
|
||||
|
||||
compute(): PathFindResultType {
|
||||
if (this.completed) return PathFindResultType.Completed;
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { readFileSync } from "fs"; // Import readFileSync
|
||||
import { join } from "path"; // Import join
|
||||
import { Game } from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
|
||||
export enum PathFindResultType {
|
||||
NextTile,
|
||||
Pending,
|
||||
Completed,
|
||||
PathNotFound,
|
||||
}
|
||||
export type AStarResult<NodeType> =
|
||||
| {
|
||||
type: PathFindResultType.NextTile;
|
||||
node: NodeType;
|
||||
}
|
||||
| {
|
||||
type: PathFindResultType.Pending;
|
||||
}
|
||||
| {
|
||||
type: PathFindResultType.Completed;
|
||||
node: NodeType;
|
||||
}
|
||||
| {
|
||||
type: PathFindResultType.PathNotFound;
|
||||
};
|
||||
|
||||
let wasm: typeof import("../../wasm/pathfinding/pkg");
|
||||
|
||||
async function loadWasm() {
|
||||
if (!wasm) {
|
||||
const pkg = await import("../../wasm/pathfinding/pkg");
|
||||
|
||||
// Manually load the wasm file
|
||||
const wasmPath = join(__dirname, "../../../static/js/pathfinding_bg.wasm");
|
||||
const wasmBuffer = readFileSync(wasmPath);
|
||||
const wasmModule = await WebAssembly.compile(wasmBuffer);
|
||||
|
||||
wasm = await pkg.default({ data: wasmModule }); // Pass the compiled module to init
|
||||
}
|
||||
}
|
||||
|
||||
export class WasmPathFinder {
|
||||
private path: TileRef[] | null = null;
|
||||
|
||||
constructor(private game: Game) {}
|
||||
|
||||
async nextTile(
|
||||
curr: TileRef | null,
|
||||
dst: TileRef | null,
|
||||
dist: number = 1,
|
||||
): Promise<AStarResult<TileRef>> {
|
||||
if (curr === null) {
|
||||
console.error("curr is null");
|
||||
return { type: PathFindResultType.PathNotFound };
|
||||
}
|
||||
if (dst === null) {
|
||||
console.error("dst is null");
|
||||
return { type: PathFindResultType.PathNotFound };
|
||||
}
|
||||
|
||||
console.log(`WasmPathFinder.nextTile: curr=${curr} dst=${dst}`);
|
||||
console.log(
|
||||
`WasmPathFinder.nextTile: game.map().x(curr)=${this.game.map().x(curr)} game.map().y(curr)=${this.game.map().y(curr)}`,
|
||||
);
|
||||
console.log(
|
||||
`WasmPathFinder.nextTile: game.map().x(dst)=${this.game.map().x(dst)} game.map().y(dst)=${this.game.map().y(dst)}`,
|
||||
);
|
||||
console.log(
|
||||
`WasmPathFinder.nextTile: game.map().width=${this.game.map().width()} game.map().height=${this.game.map().height()}`,
|
||||
);
|
||||
|
||||
if (this.game.manhattanDist(curr, dst) < dist) {
|
||||
return { type: PathFindResultType.Completed, node: curr };
|
||||
}
|
||||
|
||||
await loadWasm();
|
||||
|
||||
const fixedWidth = 16; // Use fixed width
|
||||
const fixedHeight = 16; // Use fixed height
|
||||
|
||||
const gridData = new Array(fixedWidth * fixedHeight);
|
||||
for (let y = 0; y < fixedHeight; y++) {
|
||||
for (let x = 0; x < fixedWidth; x++) {
|
||||
const tile = this.game.map().ref(x, y);
|
||||
gridData[y * fixedWidth + x] = this.game.map().isWater(tile) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`WasmPathFinder.nextTile: fixedWidth=${fixedWidth} fixedHeight=${fixedHeight} gridData.length=${gridData.length}`,
|
||||
);
|
||||
|
||||
const path = wasm.find_path(
|
||||
this.game.map().x(curr),
|
||||
this.game.map().y(curr),
|
||||
this.game.map().x(dst),
|
||||
this.game.map().y(dst),
|
||||
fixedWidth,
|
||||
fixedHeight,
|
||||
gridData,
|
||||
);
|
||||
|
||||
if (path) {
|
||||
this.path = path.map((p: any) => this.game.map().ref(p.x, p.y)); // Cast p to any
|
||||
this.path.shift(); // Remove the start tile
|
||||
const tile = this.path.shift();
|
||||
if (tile === undefined) {
|
||||
return { type: PathFindResultType.PathNotFound };
|
||||
}
|
||||
return { type: PathFindResultType.NextTile, node: tile };
|
||||
} else {
|
||||
return { type: PathFindResultType.PathNotFound };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,24 +30,24 @@ describe("AllianceExtensionExecution", () => {
|
||||
player3 = game.player("player3");
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
});
|
||||
|
||||
test("Successfully extends existing alliance between Humans", () => {
|
||||
test("Successfully extends existing alliance between Humans", async () => {
|
||||
jest.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true);
|
||||
jest.spyOn(player2, "isAlive").mockReturnValue(true);
|
||||
jest.spyOn(player1, "isAlive").mockReturnValue(true);
|
||||
|
||||
game.addExecution(new AllianceRequestExecution(player1, player2.id()));
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
game.addExecution(
|
||||
new AllianceRequestReplyExecution(player1.id(), player2, true),
|
||||
);
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
expect(player1.allianceWith(player2)).toBeTruthy();
|
||||
expect(player2.allianceWith(player1)).toBeTruthy();
|
||||
@@ -58,10 +58,10 @@ describe("AllianceExtensionExecution", () => {
|
||||
const expirationBefore = allianceBefore.expiresAt();
|
||||
|
||||
game.addExecution(new AllianceExtensionExecution(player1, player2.id()));
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
expect(allianceSpy).toHaveBeenCalledTimes(0); // both players must agree to extend
|
||||
game.addExecution(new AllianceExtensionExecution(player2, player1.id()));
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
const allianceAfter = player1.allianceWith(player2)!;
|
||||
|
||||
@@ -73,29 +73,29 @@ describe("AllianceExtensionExecution", () => {
|
||||
expect(allianceSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("Fails gracefully if no alliance exists", () => {
|
||||
test("Fails gracefully if no alliance exists", async () => {
|
||||
game.addExecution(new AllianceExtensionExecution(player1, player2.id()));
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
expect(player1.allianceWith(player2)).toBeFalsy();
|
||||
expect(player2.allianceWith(player1)).toBeFalsy();
|
||||
});
|
||||
|
||||
test("Successfully extends existing alliance between Human and non-Human", () => {
|
||||
test("Successfully extends existing alliance between Human and non-Human", async () => {
|
||||
//test of handleAllianceExtensions is done in BotBehavior tests
|
||||
jest.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true);
|
||||
jest.spyOn(player3, "isAlive").mockReturnValue(true);
|
||||
jest.spyOn(player1, "isAlive").mockReturnValue(true);
|
||||
|
||||
game.addExecution(new AllianceRequestExecution(player1, player3.id()));
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
game.addExecution(
|
||||
new AllianceRequestReplyExecution(player1.id(), player3, true),
|
||||
);
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
expect(player1.allianceWith(player3)).toBeTruthy();
|
||||
expect(player3.allianceWith(player1)).toBeTruthy();
|
||||
@@ -105,10 +105,10 @@ describe("AllianceExtensionExecution", () => {
|
||||
const expirationBefore = allianceBefore.expiresAt();
|
||||
|
||||
game.addExecution(new AllianceExtensionExecution(player1, player3.id()));
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
expect(allianceSpy).toHaveBeenCalledTimes(0); // both players must agree to extend
|
||||
game.addExecution(new AllianceExtensionExecution(player3, player1.id()));
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
const allianceAfter = player1.allianceWith(player3)!;
|
||||
|
||||
|
||||
@@ -32,44 +32,44 @@ describe("AllianceRequestExecution", () => {
|
||||
player2.conquer(game.ref(0, 1));
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
});
|
||||
|
||||
test("Can create alliance by replying", () => {
|
||||
test("Can create alliance by replying", async () => {
|
||||
game.addExecution(new AllianceRequestExecution(player1, player2.id()));
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
game.addExecution(
|
||||
new AllianceRequestReplyExecution(player1.id(), player2, true),
|
||||
);
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
expect(player1.isAlliedWith(player2)).toBeTruthy();
|
||||
expect(player2.isAlliedWith(player1)).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Can create alliance by sending alliance request back", () => {
|
||||
test("Can create alliance by sending alliance request back", async () => {
|
||||
game.addExecution(new AllianceRequestExecution(player1, player2.id()));
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
game.addExecution(new AllianceRequestExecution(player2, player1.id()));
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
expect(player1.isAlliedWith(player2)).toBeTruthy();
|
||||
expect(player2.isAlliedWith(player1)).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Alliance request expires", () => {
|
||||
test("Alliance request expires", async () => {
|
||||
game.config().allianceRequestDuration = () => 5;
|
||||
game.addExecution(new AllianceRequestExecution(player1, player2.id()));
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
expect(player1.outgoingAllianceRequests().length).toBe(1);
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
expect(player1.outgoingAllianceRequests().length).toBe(0);
|
||||
@@ -78,11 +78,11 @@ describe("AllianceRequestExecution", () => {
|
||||
});
|
||||
|
||||
// Resolves exploit https://github.com/openfrontio/OpenFrontIO/issues/2071
|
||||
test("alliance request is revoked immediately if requester launches a nuke", () => {
|
||||
test("alliance request is revoked immediately if requester launches a nuke", async () => {
|
||||
game.config().nukeAllianceBreakThreshold = () => 0;
|
||||
// Player 1 sends an alliance request to player 2.
|
||||
game.addExecution(new AllianceRequestExecution(player1, player2.id()));
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
expect(player1.outgoingAllianceRequests().length).toBe(1);
|
||||
expect(player2.incomingAllianceRequests().length).toBe(1);
|
||||
@@ -92,8 +92,8 @@ describe("AllianceRequestExecution", () => {
|
||||
game.addExecution(
|
||||
new NukeExecution(UnitType.AtomBomb, player1, game.ref(0, 1), null),
|
||||
);
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
expect(player1.outgoingAllianceRequests().length).toBe(0);
|
||||
expect(player2.incomingAllianceRequests().length).toBe(0);
|
||||
|
||||
+14
-14
@@ -56,7 +56,7 @@ describe("Attack", () => {
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
attacker = game.player(attackerInfo.id);
|
||||
@@ -65,9 +65,9 @@ describe("Attack", () => {
|
||||
game.addExecution(
|
||||
new AttackExecution(100, defender, game.terraNullius().id()),
|
||||
);
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
while (defender.outgoingAttacks().length > 0) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
(game.config() as TestConfig).setDefaultNukeSpeed(50);
|
||||
@@ -76,10 +76,10 @@ describe("Attack", () => {
|
||||
test("Nuke reduce attacking troop counts", async () => {
|
||||
// Not building exactly spawn to it's better protected from attacks (but still
|
||||
// on defender territory)
|
||||
constructionExecution(game, defender, 1, 1, UnitType.MissileSilo);
|
||||
await constructionExecution(game, defender, 1, 1, UnitType.MissileSilo);
|
||||
expect(defender.units(UnitType.MissileSilo)).toHaveLength(1);
|
||||
game.addExecution(new AttackExecution(100, attacker, defender.id()));
|
||||
constructionExecution(game, defender, 0, 15, UnitType.AtomBomb, 3);
|
||||
await constructionExecution(game, defender, 0, 15, UnitType.AtomBomb, 3);
|
||||
const nuke = defender.units(UnitType.AtomBomb)[0];
|
||||
expect(nuke.isActive()).toBe(true);
|
||||
|
||||
@@ -87,26 +87,26 @@ describe("Attack", () => {
|
||||
expect(attacker.outgoingAttacks()[0].troops()).toBe(98);
|
||||
|
||||
// Make the nuke go kaboom
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
expect(nuke.isActive()).toBe(false);
|
||||
expect(attacker.outgoingAttacks()[0].troops()).not.toBe(97);
|
||||
expect(attacker.outgoingAttacks()[0].troops()).toBeLessThan(90);
|
||||
});
|
||||
|
||||
test("Nuke reduce attacking boat troop count", async () => {
|
||||
constructionExecution(game, defender, 1, 1, UnitType.MissileSilo);
|
||||
await constructionExecution(game, defender, 1, 1, UnitType.MissileSilo);
|
||||
expect(defender.units(UnitType.MissileSilo)).toHaveLength(1);
|
||||
|
||||
sendBoat(game.ref(15, 8), game.ref(10, 5), 100);
|
||||
|
||||
constructionExecution(game, defender, 0, 15, UnitType.AtomBomb, 3);
|
||||
await constructionExecution(game, defender, 0, 15, UnitType.AtomBomb, 3);
|
||||
const nuke = defender.units(UnitType.AtomBomb)[0];
|
||||
expect(nuke.isActive()).toBe(true);
|
||||
|
||||
const ship = defender.units(UnitType.TransportShip)[0];
|
||||
expect(ship.troops()).toBe(100);
|
||||
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
expect(nuke.isActive()).toBe(false);
|
||||
expect(defender.units(UnitType.TransportShip)[0].troops()).toBeLessThan(90);
|
||||
@@ -151,7 +151,7 @@ describe("Attack race condition with alliance requests", () => {
|
||||
playerB = addPlayerToGame(playerBInfo, game, game.ref(0, 10));
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -186,7 +186,7 @@ describe("Attack race condition with alliance requests", () => {
|
||||
|
||||
// Execute a few ticks to process the attacks
|
||||
for (let i = 0; i < 5; i++) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
// Player A should not be marked as traitor because the alliance was formed after the attack started
|
||||
@@ -221,7 +221,7 @@ describe("Attack race condition with alliance requests", () => {
|
||||
|
||||
// Execute a few ticks to process the attack
|
||||
for (let i = 0; i < 10; i++) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
// No ongoing attacks should exist for either side
|
||||
@@ -248,7 +248,7 @@ describe("Attack race condition with alliance requests", () => {
|
||||
|
||||
// Execute a few ticks to process the attacks
|
||||
for (let i = 0; i < 5; i++) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
// Alliance request should be denied since player B attacked
|
||||
expect(playerA.outgoingAllianceRequests()).toHaveLength(0);
|
||||
@@ -286,7 +286,7 @@ describe("Attack race condition with alliance requests", () => {
|
||||
|
||||
// Execute a few ticks to process the attacks
|
||||
for (let i = 0; i < 5; i++) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
// Alliance request A->B should be denied since player B attacked
|
||||
expect(playerA.outgoingAllianceRequests()).toHaveLength(0);
|
||||
|
||||
@@ -22,17 +22,17 @@ describe("AttackStats", () => {
|
||||
game.addExecution(new SpawnExecution(player2.info(), game.ref(50, 55)));
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
});
|
||||
|
||||
test("should increase war gold stat when a player is eliminated", () => {
|
||||
test("should increase war gold stat when a player is eliminated", async () => {
|
||||
expect(player1.sharesBorderWith(player2)).toBeTruthy();
|
||||
performAttack(game, player1, player2);
|
||||
await performAttack(game, player1, player2);
|
||||
expectWarGoldStatIsIncreasedAfterKill(game, player1, player2);
|
||||
});
|
||||
|
||||
test("should increase war gold stat when elimination occurs via territory annexation", () => {
|
||||
test("should increase war gold stat when elimination occurs via territory annexation", async () => {
|
||||
// Mark every tile on the map as owned by player1
|
||||
for (let x = 0; x < game.map().width(); x++) {
|
||||
for (let y = 0; y < game.map().height(); y++) {
|
||||
@@ -48,7 +48,7 @@ describe("AttackStats", () => {
|
||||
}
|
||||
}
|
||||
|
||||
performAttack(game, player1, player2);
|
||||
await performAttack(game, player1, player2);
|
||||
expectWarGoldStatIsIncreasedAfterKill(game, player1, player2);
|
||||
});
|
||||
});
|
||||
@@ -73,13 +73,13 @@ function expectWarGoldStatIsIncreasedAfterKill(
|
||||
);
|
||||
}
|
||||
|
||||
function performAttack(game: Game, attacker: Player, defender: Player) {
|
||||
async function performAttack(game: Game, attacker: Player, defender: Player) {
|
||||
// Execute the attack
|
||||
game.addExecution(
|
||||
new AttackExecution(attacker.troops(), attacker, defender.id()),
|
||||
);
|
||||
// Wait for the attack to complete
|
||||
do {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
} while (attacker.outgoingAttacks().length > 0);
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ describe("BotBehavior Attack Behavior", () => {
|
||||
|
||||
// Skip spawn phase
|
||||
while (testGame.inSpawnPhase()) {
|
||||
testGame.executeNextTick();
|
||||
await testGame.executeNextTick();
|
||||
}
|
||||
|
||||
const behavior = new BotBehavior(
|
||||
@@ -313,7 +313,7 @@ describe("BotBehavior Attack Behavior", () => {
|
||||
botBehavior = env.behavior;
|
||||
});
|
||||
|
||||
test("bot cannot attack allied player", () => {
|
||||
test("bot cannot attack allied player", async () => {
|
||||
// Form alliance (bot creates request to human)
|
||||
const allianceRequest = bot.createAllianceRequest(human);
|
||||
allianceRequest?.accept();
|
||||
@@ -328,7 +328,7 @@ describe("BotBehavior Attack Behavior", () => {
|
||||
|
||||
// Execute a few ticks to process the attacks
|
||||
for (let i = 0; i < 5; i++) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
expect(bot.isAlliedWith(human)).toBe(true);
|
||||
@@ -337,7 +337,7 @@ describe("BotBehavior Attack Behavior", () => {
|
||||
expect(bot.outgoingAttacks()).toHaveLength(attacksBefore);
|
||||
});
|
||||
|
||||
test("nation cannot attack allied player", () => {
|
||||
test("nation cannot attack allied player", async () => {
|
||||
// Create nation
|
||||
const nationInfo = new PlayerInfo(
|
||||
"nation_test",
|
||||
@@ -376,7 +376,7 @@ describe("BotBehavior Attack Behavior", () => {
|
||||
|
||||
// Execute a few ticks to process the attacks
|
||||
for (let i = 0; i < 5; i++) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
expect(nation.isAlliedWith(human)).toBe(true);
|
||||
|
||||
@@ -49,7 +49,7 @@ describe("DeleteUnitExecution Security Tests", () => {
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
player = game.player(player1Info.id);
|
||||
|
||||
@@ -38,7 +38,7 @@ describe("Disconnected", () => {
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ describe("Donate troops to an ally", () => {
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
// donor sends alliance request to recipient
|
||||
@@ -59,7 +59,7 @@ describe("Donate troops to an ally", () => {
|
||||
game.addExecution(new DonateTroopsExecution(donor, recipientInfo.id, 5000));
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
expect(donor.troops() < donorTroopsBefore).toBe(true);
|
||||
@@ -103,7 +103,7 @@ describe("Donate gold to an ally", () => {
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
// donor sends alliance request to recipient
|
||||
@@ -114,7 +114,7 @@ describe("Donate gold to an ally", () => {
|
||||
if (allianceRequest) {
|
||||
allianceRequest.accept();
|
||||
}
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
// Ensure donor can actually donate the requested amount
|
||||
donor.addGold(6000n);
|
||||
@@ -123,7 +123,7 @@ describe("Donate gold to an ally", () => {
|
||||
game.addExecution(new DonateGoldExecution(donor, recipientInfo.id, 5000n));
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
expect(donor.gold() < donorGoldBefore).toBe(true);
|
||||
@@ -167,7 +167,7 @@ describe("Donate troops to a non ally", () => {
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
// Donor sends alliance request to Recipient
|
||||
@@ -183,7 +183,7 @@ describe("Donate troops to a non ally", () => {
|
||||
const recipientTroopsBefore = recipient.troops();
|
||||
|
||||
game.addExecution(new DonateTroopsExecution(donor, recipientInfo.id, 5000));
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
// Troops should not be donated since they are not allies
|
||||
expect(donor.troops() >= donorTroopsBefore).toBe(true);
|
||||
@@ -227,7 +227,7 @@ describe("Donate Gold to a non ally", () => {
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
// Donor sends alliance request to Recipient
|
||||
@@ -243,7 +243,7 @@ describe("Donate Gold to a non ally", () => {
|
||||
const recipientGoldBefore = donor.gold();
|
||||
|
||||
game.addExecution(new DonateGoldExecution(donor, recipientInfo.id, 5000n));
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
// Gold should not be donated since they are not allies
|
||||
expect(donor.gold() >= donorGoldBefore).toBe(true);
|
||||
|
||||
+13
-13
@@ -15,7 +15,7 @@ import { constructionExecution, executeTicks } from "./util/utils";
|
||||
let game: Game;
|
||||
let attacker: Player;
|
||||
|
||||
function attackerBuildsNuke(
|
||||
async function attackerBuildsNuke(
|
||||
source: TileRef | null,
|
||||
target: TileRef,
|
||||
initialize = true,
|
||||
@@ -24,8 +24,8 @@ function attackerBuildsNuke(
|
||||
new NukeExecution(UnitType.AtomBomb, attacker, target, source),
|
||||
);
|
||||
if (initialize) {
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,47 +45,47 @@ describe("MissileSilo", () => {
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
attacker = game.player("attacker_id");
|
||||
|
||||
constructionExecution(game, attacker, 1, 1, UnitType.MissileSilo);
|
||||
await constructionExecution(game, attacker, 1, 1, UnitType.MissileSilo);
|
||||
});
|
||||
|
||||
test("missilesilo should launch nuke", async () => {
|
||||
attackerBuildsNuke(null, game.ref(7, 7));
|
||||
await attackerBuildsNuke(null, game.ref(7, 7));
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
|
||||
expect(attacker.units(UnitType.AtomBomb)[0].tile()).not.toBe(
|
||||
game.map().ref(7, 7),
|
||||
);
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("missilesilo should only launch one nuke at a time", async () => {
|
||||
attackerBuildsNuke(null, game.ref(7, 7));
|
||||
attackerBuildsNuke(null, game.ref(7, 7));
|
||||
await attackerBuildsNuke(null, game.ref(7, 7));
|
||||
await attackerBuildsNuke(null, game.ref(7, 7));
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("missilesilo should cooldown as long as configured", async () => {
|
||||
expect(attacker.units(UnitType.MissileSilo)[0].isInCooldown()).toBeFalsy();
|
||||
// send the nuke far enough away so it doesnt destroy the silo
|
||||
attackerBuildsNuke(null, game.ref(50, 50));
|
||||
await attackerBuildsNuke(null, game.ref(50, 50));
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
|
||||
|
||||
for (let i = 0; i < game.config().SiloCooldown() - 2; i++) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
expect(
|
||||
attacker.units(UnitType.MissileSilo)[0].isInCooldown(),
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
executeTicks(game, 2);
|
||||
await executeTicks(game, 2);
|
||||
|
||||
expect(attacker.units(UnitType.MissileSilo)[0].isInCooldown()).toBeFalsy();
|
||||
});
|
||||
@@ -98,7 +98,7 @@ describe("MissileSilo", () => {
|
||||
attacker.units(UnitType.MissileSilo)[0].id(),
|
||||
);
|
||||
game.addExecution(upgradeStructureExecution);
|
||||
executeTicks(game, 2);
|
||||
await executeTicks(game, 2);
|
||||
|
||||
expect(attacker.units(UnitType.MissileSilo)[0].level()).toEqual(2);
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ describe("PlayerImpl", () => {
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
player = game.player("player_id");
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("Shell Random Damage", () => {
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
player1 = game.player("player_1_id");
|
||||
@@ -89,7 +89,7 @@ describe("Shell Random Damage", () => {
|
||||
expect(damages.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("Warship shell attacks have random damage", () => {
|
||||
test("Warship shell attacks have random damage", async () => {
|
||||
player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {});
|
||||
|
||||
const warship = player1.buildUnit(
|
||||
@@ -119,7 +119,7 @@ describe("Shell Random Damage", () => {
|
||||
|
||||
while (damages.length < 10 && attempts < maxAttempts) {
|
||||
const healthBefore = target.health();
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
const healthAfter = target.health();
|
||||
|
||||
if (healthAfter < healthBefore) {
|
||||
@@ -145,7 +145,7 @@ describe("Shell Random Damage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("Defense post shell attacks have random damage", () => {
|
||||
test("Defense post shell attacks have random damage", async () => {
|
||||
const defensePost = new DefensePostExecution(player1, game.ref(coastX, 5));
|
||||
|
||||
const target = player2.buildUnit(
|
||||
@@ -166,7 +166,7 @@ describe("Shell Random Damage", () => {
|
||||
while (damages.length < 5 && attempts < maxAttempts) {
|
||||
const healthBefore = target.health();
|
||||
defensePost.tick(game.ticks());
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
const healthAfter = target.health();
|
||||
|
||||
if (healthAfter < healthBefore) {
|
||||
@@ -255,7 +255,7 @@ describe("Shell Random Damage", () => {
|
||||
expect(maxCount - minCount).toBeLessThan(damages.length * 0.8);
|
||||
});
|
||||
|
||||
test("Shell damage is consistent with same random seed", () => {
|
||||
test("Shell damage is consistent with same random seed", async () => {
|
||||
const target = player2.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 5, 10),
|
||||
@@ -284,7 +284,7 @@ describe("Shell Random Damage", () => {
|
||||
target,
|
||||
);
|
||||
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
const currentTicks = game.ticks();
|
||||
|
||||
shell1.init(game, currentTicks);
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ describe("Stats", () => {
|
||||
]);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
player1 = game.player("player_1_id");
|
||||
|
||||
@@ -13,9 +13,9 @@ describe("Territory management", () => {
|
||||
new SpawnExecution(game.player("test_id").info(), spawnTile),
|
||||
);
|
||||
// Init the execution
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
// Execute the execution.
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
const owner = game.owner(spawnTile);
|
||||
expect(owner.isPlayer()).toBe(true);
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("Warship", () => {
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
player1 = game.player("player_1_id");
|
||||
@@ -54,17 +54,17 @@ describe("Warship", () => {
|
||||
);
|
||||
game.addExecution(new WarshipExecution(warship));
|
||||
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
expect(warship.health()).toBe(maxHealth);
|
||||
warship.modifyHealth(-10);
|
||||
expect(warship.health()).toBe(maxHealth - 10);
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
expect(warship.health()).toBe(maxHealth - 9);
|
||||
|
||||
port.delete();
|
||||
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
expect(warship.health()).toBe(maxHealth - 9);
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@ describe("Warship", () => {
|
||||
expect(tradeShip.owner().id()).toBe(player2.id());
|
||||
// Let plenty of time for A* to execute
|
||||
for (let i = 0; i < 10; i++) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
expect(tradeShip.owner()).toBe(player1);
|
||||
});
|
||||
@@ -115,7 +115,7 @@ describe("Warship", () => {
|
||||
expect(tradeShip.owner().id()).toBe(player2.id());
|
||||
// Let plenty of time for warship to potentially capture trade ship
|
||||
for (let i = 0; i < 10; i++) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
expect(tradeShip.owner().id()).toBe(player2.id());
|
||||
@@ -144,7 +144,7 @@ describe("Warship", () => {
|
||||
|
||||
tradeShip.setSafeFromPirates();
|
||||
|
||||
executeTicks(game, 10);
|
||||
await executeTicks(game, 10);
|
||||
|
||||
expect(tradeShip.owner().id()).toBe(player2.id());
|
||||
});
|
||||
@@ -166,7 +166,7 @@ describe("Warship", () => {
|
||||
new MoveWarshipExecution(player1, warship.id(), game.ref(coastX + 5, 15)),
|
||||
);
|
||||
|
||||
executeTicks(game, 10);
|
||||
await executeTicks(game, 10);
|
||||
|
||||
expect(warship.patrolTile()).toBe(game.ref(coastX + 5, 15));
|
||||
});
|
||||
@@ -194,7 +194,7 @@ describe("Warship", () => {
|
||||
},
|
||||
);
|
||||
|
||||
executeTicks(game, 10);
|
||||
await executeTicks(game, 10);
|
||||
|
||||
// Trade ship should not be captured
|
||||
expect(tradeShip.owner().id()).toBe(player2.id());
|
||||
|
||||
@@ -35,7 +35,7 @@ describe("NukeExecution", () => {
|
||||
(game.config() as TestConfig).nukeAllianceBreakThreshold = jest.fn(() => 5);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
player = game.player("player_id");
|
||||
@@ -66,7 +66,7 @@ describe("NukeExecution", () => {
|
||||
);
|
||||
game.addExecution(nukeExec);
|
||||
// Run enough ticks for the nuke to detonate
|
||||
executeTicks(game, 10);
|
||||
await executeTicks(game, 10);
|
||||
// The city and silo should be destroyed
|
||||
expect(player.units(UnitType.City)).toHaveLength(0);
|
||||
expect(player.units(UnitType.MissileSilo)).toHaveLength(0);
|
||||
@@ -86,15 +86,15 @@ describe("NukeExecution", () => {
|
||||
// targetable distance is 400
|
||||
|
||||
//near launch should be targetable (distance src < 400)
|
||||
executeTicks(game, 2);
|
||||
await executeTicks(game, 2);
|
||||
expect(nukeExec.getNuke()!.isTargetable()).toBeTruthy();
|
||||
|
||||
//mid air should not be targetable (distance src > 400, distance target > 400)
|
||||
executeTicks(game, 38);
|
||||
await executeTicks(game, 38);
|
||||
expect(nukeExec.getNuke()!.isTargetable()).toBeFalsy();
|
||||
|
||||
//near target should be targetable (distance target < 400)
|
||||
executeTicks(game, 35);
|
||||
await executeTicks(game, 35);
|
||||
expect(nukeExec.getNuke()!.isTargetable()).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -116,8 +116,8 @@ describe("NukeExecution", () => {
|
||||
new NukeExecution(UnitType.AtomBomb, player, game.ref(85, 85), null),
|
||||
);
|
||||
|
||||
game.executeNextTick(); // init
|
||||
game.executeNextTick(); // exec
|
||||
await game.executeNextTick(); // init
|
||||
await game.executeNextTick(); // exec
|
||||
|
||||
expect(player.isTraitor()).toBe(true);
|
||||
expect(player.isAlliedWith(otherPlayer)).toBe(false);
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("SAM", () => {
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
attacker = game.player("attacker_id");
|
||||
@@ -75,7 +75,7 @@ describe("SAM", () => {
|
||||
middle_defender = game.player("middle_defender_id");
|
||||
far_defender = game.player("far_defender_id");
|
||||
|
||||
constructionExecution(game, attacker, 7, 7, UnitType.MissileSilo);
|
||||
await constructionExecution(game, attacker, 7, 7, UnitType.MissileSilo);
|
||||
});
|
||||
|
||||
test("one sam should take down one nuke", async () => {
|
||||
@@ -91,7 +91,7 @@ describe("SAM", () => {
|
||||
{ tile: game.ref(3, 1), targetable: true },
|
||||
],
|
||||
});
|
||||
executeTicks(game, 3);
|
||||
await executeTicks(game, 3);
|
||||
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0);
|
||||
});
|
||||
@@ -117,7 +117,7 @@ describe("SAM", () => {
|
||||
});
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(2);
|
||||
|
||||
executeTicks(game, 3);
|
||||
await executeTicks(game, 3);
|
||||
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
|
||||
});
|
||||
@@ -136,16 +136,16 @@ describe("SAM", () => {
|
||||
],
|
||||
});
|
||||
|
||||
executeTicks(game, 3);
|
||||
await executeTicks(game, 3);
|
||||
|
||||
expect(nuke.isActive()).toBeFalsy();
|
||||
|
||||
for (let i = 0; i < game.config().SAMCooldown() - 3; i++) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
expect(sam.isInCooldown()).toBeTruthy();
|
||||
}
|
||||
|
||||
executeTicks(game, 2);
|
||||
await executeTicks(game, 2);
|
||||
|
||||
expect(sam.isInCooldown()).toBeFalsy();
|
||||
});
|
||||
@@ -164,7 +164,7 @@ describe("SAM", () => {
|
||||
],
|
||||
});
|
||||
|
||||
executeTicks(game, 3);
|
||||
await executeTicks(game, 3);
|
||||
|
||||
expect(nuke.isActive()).toBeFalsy();
|
||||
expect([sam1, sam2].filter((s) => s.isInCooldown())).toHaveLength(1);
|
||||
@@ -187,7 +187,7 @@ describe("SAM", () => {
|
||||
const ticksToExecute = Math.ceil(
|
||||
targetDistance / game.config().defaultNukeSpeed() + 1,
|
||||
);
|
||||
executeTicks(game, ticksToExecute);
|
||||
await executeTicks(game, ticksToExecute);
|
||||
|
||||
expect(nukeExecution.isActive()).toBeFalsy();
|
||||
expect(sam.isInCooldown()).toBeTruthy();
|
||||
@@ -222,7 +222,7 @@ describe("SAM", () => {
|
||||
const ticksToExecute = Math.ceil(
|
||||
targetDistance / game.config().defaultNukeSpeed() + 1,
|
||||
);
|
||||
executeTicks(game, ticksToExecute);
|
||||
await executeTicks(game, ticksToExecute);
|
||||
expect(nukeExecution.isActive()).toBeFalsy();
|
||||
expect(sam1.isInCooldown()).toBeFalsy();
|
||||
expect(sam2.isInCooldown()).toBeTruthy();
|
||||
@@ -237,7 +237,7 @@ describe("SAM", () => {
|
||||
defender.units(UnitType.SAMLauncher)[0].id(),
|
||||
);
|
||||
game.addExecution(upgradeStructureExecution);
|
||||
executeTicks(game, 2);
|
||||
await executeTicks(game, 2);
|
||||
|
||||
expect(defender.units(UnitType.SAMLauncher)[0].level()).toEqual(2);
|
||||
});
|
||||
|
||||
@@ -49,7 +49,7 @@ describe("GameImpl", () => {
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
|
||||
attacker = game.player(attackerInfo.id);
|
||||
@@ -60,15 +60,15 @@ describe("GameImpl", () => {
|
||||
jest.spyOn(attacker, "canSendAllianceRequest").mockReturnValue(true);
|
||||
game.addExecution(new AllianceRequestExecution(attacker, defender.id()));
|
||||
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
game.addExecution(
|
||||
new AllianceRequestReplyExecution(attacker.id(), defender, true),
|
||||
);
|
||||
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
expect(attacker.allianceWith(defender)).toBeTruthy();
|
||||
expect(defender.allianceWith(attacker)).toBeTruthy();
|
||||
@@ -76,8 +76,8 @@ describe("GameImpl", () => {
|
||||
//Defender is marked disconnected
|
||||
defender.markDisconnected(true);
|
||||
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
// STEP 1: First betray (manually break alliance)
|
||||
const alliance = attacker.allianceWith(defender);
|
||||
@@ -88,7 +88,7 @@ describe("GameImpl", () => {
|
||||
game.addExecution(new AttackExecution(100, attacker, defender.id()));
|
||||
|
||||
do {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
} while (attacker.outgoingAttacks().length > 0);
|
||||
|
||||
expect(attacker.isTraitor()).toBe(false);
|
||||
@@ -99,35 +99,35 @@ describe("GameImpl", () => {
|
||||
jest.spyOn(attacker, "canSendAllianceRequest").mockReturnValue(true);
|
||||
game.addExecution(new AllianceRequestExecution(attacker, defender.id()));
|
||||
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
game.addExecution(
|
||||
new AllianceRequestReplyExecution(attacker.id(), defender, true),
|
||||
);
|
||||
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
expect(attacker.allianceWith(defender)).toBeTruthy();
|
||||
expect(defender.allianceWith(attacker)).toBeTruthy();
|
||||
|
||||
//Defender is NOT marked disconnected
|
||||
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
// First betray (manually break alliance)
|
||||
const alliance = attacker.allianceWith(defender);
|
||||
expect(alliance).toBeTruthy();
|
||||
attacker.breakAlliance(alliance!);
|
||||
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
|
||||
game.addExecution(new AttackExecution(100, attacker, defender.id()));
|
||||
|
||||
do {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
} while (attacker.outgoingAttacks().length > 0);
|
||||
|
||||
expect(attacker.isTraitor()).toBe(true);
|
||||
|
||||
@@ -134,7 +134,7 @@ describe("RailNetworkImpl", () => {
|
||||
const stationA = createMockStation(1);
|
||||
const stationB = createMockStation(2);
|
||||
const result = network.findStationsPath(stationA, stationB);
|
||||
expect(result).toEqual([0]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test("connectToNearbyStations creates new cluster when no neighbors", () => {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
// Mock the fetch API for WASM loading in Jest
|
||||
global.fetch = jest.fn((url: RequestInfo | URL) => {
|
||||
const urlString = typeof url === "string" ? url : url.toString();
|
||||
if (urlString.endsWith("pathfinding_bg.wasm")) {
|
||||
const wasmBuffer = readFileSync(
|
||||
join(__dirname, "../static/js/pathfinding_bg.wasm"),
|
||||
);
|
||||
return Promise.resolve({
|
||||
arrayBuffer: () => Promise.resolve(wasmBuffer.buffer),
|
||||
headers: new Headers(),
|
||||
ok: true,
|
||||
redirected: false,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
type: "basic",
|
||||
url: urlString,
|
||||
clone: () => global.fetch(url),
|
||||
} as Response);
|
||||
}
|
||||
return Promise.reject(new Error(`Unhandled fetch request for: ${urlString}`));
|
||||
});
|
||||
|
||||
// Mock WebAssembly.instantiateStreaming for Node.js environment
|
||||
// This is needed because wasm-bindgen generated code uses instantiateStreaming
|
||||
// but Jest runs in Node.js where it's not available.
|
||||
global.WebAssembly.instantiateStreaming = jest.fn(
|
||||
async (
|
||||
source: Response | PromiseLike<Response> | WebAssembly.Module,
|
||||
importObject: WebAssembly.Imports | undefined,
|
||||
) => {
|
||||
if (source instanceof WebAssembly.Module) {
|
||||
return WebAssembly.instantiate(source, importObject);
|
||||
}
|
||||
const response = await source;
|
||||
const buffer = await response.arrayBuffer();
|
||||
return WebAssembly.instantiate(buffer, importObject);
|
||||
},
|
||||
);
|
||||
@@ -52,6 +52,12 @@ export async function setup(
|
||||
const gameMap = await genTerrainFromBin(manifest.map, mapBinBuffer);
|
||||
const miniGameMap = await genTerrainFromBin(manifest.map4x, miniMapBinBuffer);
|
||||
|
||||
// Mock width and height for gameMap and miniGameMap
|
||||
gameMap.width = () => manifest.map.width;
|
||||
gameMap.height = () => manifest.map.height;
|
||||
miniGameMap.width = () => manifest.map4x.width;
|
||||
miniGameMap.height = () => manifest.map4x.height;
|
||||
|
||||
// Configure the game
|
||||
const serverConfig = new TestServerConfig();
|
||||
const gameConfig: GameConfig = {
|
||||
|
||||
+8
-5
@@ -7,14 +7,14 @@ import { ConstructionExecution } from "../../src/core/execution/ConstructionExec
|
||||
import { Game, Player, UnitType } from "../../src/core/game/Game";
|
||||
|
||||
// built via UI (e.g.: trade ships)
|
||||
export function constructionExecution(
|
||||
export async function constructionExecution(
|
||||
game: Game,
|
||||
_owner: Player,
|
||||
x: number,
|
||||
y: number,
|
||||
unit: UnitType,
|
||||
ticks = 4,
|
||||
) {
|
||||
): Promise<void> {
|
||||
game.addExecution(new ConstructionExecution(_owner, unit, game.ref(x, y)));
|
||||
|
||||
// 4 ticks by default as it usually goes like this
|
||||
@@ -25,12 +25,15 @@ export function constructionExecution(
|
||||
// (sometimes step 3 and 4 are merged in one)
|
||||
|
||||
for (let i = 0; i < ticks; i++) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
}
|
||||
|
||||
export function executeTicks(game: Game, numTicks: number): void {
|
||||
export async function executeTicks(
|
||||
game: Game,
|
||||
numTicks: number,
|
||||
): Promise<void> {
|
||||
for (let i = 0; i < numTicks; i++) {
|
||||
game.executeNextTick();
|
||||
await game.executeNextTick();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
let wasm: typeof import("../wasm/pathfinding/pkg");
|
||||
|
||||
async function loadWasm() {
|
||||
if (!wasm) {
|
||||
const pkg = await import("../wasm/pathfinding/pkg");
|
||||
|
||||
// Manually load the wasm file
|
||||
const wasmPath = join(__dirname, "../static/js/pathfinding_bg.wasm");
|
||||
const wasmBuffer = readFileSync(wasmPath);
|
||||
const wasmModule = await WebAssembly.compile(wasmBuffer);
|
||||
|
||||
wasm = await pkg.default({ data: wasmModule }); // Pass the compiled module to init
|
||||
}
|
||||
}
|
||||
|
||||
describe("WASM Vec<u8> transfer", () => {
|
||||
test("should correctly transfer Array to Vec<u8>", async () => {
|
||||
await loadWasm();
|
||||
|
||||
const testArray = [1, 2, 3, 4, 5]; // Changed to Array
|
||||
const len = wasm.get_vec_len(testArray);
|
||||
expect(len).toBe(5);
|
||||
|
||||
const emptyArray = []; // Changed to Array
|
||||
const emptyLen = wasm.get_vec_len(emptyArray);
|
||||
expect(emptyLen).toBe(0);
|
||||
});
|
||||
});
|
||||
Generated
+272
@@ -0,0 +1,272 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deprecate-until"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a3767f826efbbe5a5ae093920b58b43b01734202be697e1354914e862e8e704"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"semver",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "integer-sqrt"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "pathfinding"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
"pathfinding 4.14.0",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathfinding"
|
||||
version = "4.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ac35caa284c08f3721fb33c2741b5f763decaf42d080c8a6a722154347017e"
|
||||
dependencies = [
|
||||
"deprecate-until",
|
||||
"indexmap",
|
||||
"integer-sqrt",
|
||||
"num-traits",
|
||||
"rustc-hash",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "pathfinding"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2.92"
|
||||
pathfinding = "4.0.0"
|
||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||
js-sys = "0.3.69" # Added js-sys as a separate dependency
|
||||
web-sys = { version = "0.3.69", features = ["console"] } # Removed js-sys feature
|
||||
@@ -0,0 +1,102 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
use pathfinding::prelude::astar;
|
||||
use js_sys::Array;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
fn log(s: &str);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct Point {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn get_vec_len(data: Array) -> usize {
|
||||
let mut vec_data = Vec::new();
|
||||
for i in 0..data.length() {
|
||||
let val = data.get(i).as_f64().unwrap() as u8;
|
||||
vec_data.push(val);
|
||||
}
|
||||
vec_data.len()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn find_path(
|
||||
start_x: u32,
|
||||
start_y: u32,
|
||||
end_x: u32,
|
||||
end_y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
grid_data: Array,
|
||||
) -> Option<Array> {
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let mut rust_grid_data = Vec::new();
|
||||
for i in 0..grid_data.length() {
|
||||
let val = grid_data.get(i).as_f64().unwrap() as u8;
|
||||
rust_grid_data.push(val);
|
||||
}
|
||||
|
||||
let start = Point { x: start_x, y: start_y };
|
||||
let end = Point { x: end_x, y: end_y };
|
||||
|
||||
log(&format!("find_path called: start=({},{}) end=({},{}) width={} height={} grid_data.len()={}", start_x, start_y, end_x, end_y, width, height, rust_grid_data.len()));
|
||||
|
||||
let result = astar(
|
||||
&start,
|
||||
|p| {
|
||||
let mut successors = Vec::new();
|
||||
let x = p.x;
|
||||
let y = p.y;
|
||||
|
||||
if x > 0 {
|
||||
successors.push(Point { x: x - 1, y });
|
||||
}
|
||||
if x < width - 1 {
|
||||
successors.push(Point { x: x + 1, y });
|
||||
}
|
||||
if y > 0 {
|
||||
successors.push(Point { x, y: y - 1 });
|
||||
}
|
||||
if y < height - 1 {
|
||||
successors.push(Point { x, y: y + 1 });
|
||||
}
|
||||
|
||||
successors
|
||||
.into_iter()
|
||||
.filter_map(|p| {
|
||||
let index = (p.y * width + p.x) as usize;
|
||||
if index >= rust_grid_data.len() {
|
||||
log(&format!("Index out of bounds: p=({},{}) index={} grid_data.len()={}", p.x, p.y, index, rust_grid_data.len()));
|
||||
None
|
||||
} else if rust_grid_data[index] == 0 {
|
||||
Some((p, 1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
},
|
||||
|p| ((p.x as i32 - end.x as i32).abs() + (p.y as i32 - end.y as i32).abs()) as u32,
|
||||
|p| *p == end,
|
||||
);
|
||||
|
||||
result.map(|(path, _cost)| {
|
||||
let js_array = Array::new();
|
||||
for p in path {
|
||||
let obj = js_sys::Object::new();
|
||||
js_sys::Reflect::set(&obj, &JsValue::from_str("x"), &JsValue::from_f64(p.x as f64)).unwrap();
|
||||
js_sys::Reflect::set(&obj, &JsValue::from_str("y"), &JsValue::from_f64(p.y as f64)).unwrap();
|
||||
js_array.push(&obj);
|
||||
}
|
||||
js_array
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user