initial commit

This commit is contained in:
icslucas
2025-10-01 18:58:16 +02:00
parent b31200a3ac
commit 42577e8963
41 changed files with 797 additions and 288 deletions
+1
View File
@@ -10,3 +10,4 @@ resources/.DS_Store
.DS_Store
.clinic/
CLAUDE.md
/wasm/pathfinding/target/
+1 -1
View File
@@ -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
+3
View File
@@ -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"],
+2 -2
View File
@@ -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);
+7 -5
View File
@@ -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:
+4 -5
View File
@@ -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) {
+9 -10
View File
@@ -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()!,
);
+2 -2
View File
@@ -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;
+4 -2
View File
@@ -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) => {
+29 -56
View File
@@ -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;
}
-24
View File
@@ -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
*/
+3 -26
View File
@@ -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(
-5
View File
@@ -24,8 +24,3 @@ export type AStarResult<NodeType> =
| {
type: PathFindResultType.PathNotFound;
};
export interface Point {
x: number;
y: number;
}
+5
View File
@@ -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,
-8
View File
@@ -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;
+116
View File
@@ -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 };
}
}
}
+17 -17
View File
@@ -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)!;
+15 -15
View File
@@ -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
View File
@@ -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);
+7 -7
View File
@@ -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);
}
+5 -5
View File
@@ -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);
+1 -1
View File
@@ -49,7 +49,7 @@ describe("DeleteUnitExecution Security Tests", () => {
);
while (game.inSpawnPhase()) {
game.executeNextTick();
await game.executeNextTick();
}
player = game.player(player1Info.id);
+1 -1
View File
@@ -38,7 +38,7 @@ describe("Disconnected", () => {
);
while (game.inSpawnPhase()) {
game.executeNextTick();
await game.executeNextTick();
}
});
+9 -9
View File
@@ -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
View File
@@ -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);
});
+1 -1
View File
@@ -25,7 +25,7 @@ describe("PlayerImpl", () => {
);
while (game.inSpawnPhase()) {
game.executeNextTick();
await game.executeNextTick();
}
player = game.player("player_id");
+7 -7
View File
@@ -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
View File
@@ -24,7 +24,7 @@ describe("Stats", () => {
]);
while (game.inSpawnPhase()) {
game.executeNextTick();
await game.executeNextTick();
}
player1 = game.player("player_1_id");
+2 -2
View File
@@ -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);
+9 -9
View File
@@ -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());
+7 -7
View File
@@ -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);
});
+16 -16
View File
@@ -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);
+1 -1
View File
@@ -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", () => {
+41
View File
@@ -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);
},
);
+6
View File
@@ -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
View File
@@ -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();
}
}
+31
View File
@@ -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);
});
});
+272
View File
@@ -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",
]
+14
View File
@@ -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
+102
View File
@@ -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
})
}