import { MoveWarshipExecution } from "../src/core/execution/MoveWarshipExecution"; import { WarshipExecution } from "../src/core/execution/WarshipExecution"; import { Game, Player, PlayerInfo, PlayerType, UnitType, } from "../src/core/game/Game"; import { TileRef } from "../src/core/game/GameMap"; import { setup } from "./util/Setup"; import { executeTicks } from "./util/utils"; const coastX = 7; let game: Game; let player1: Player; let player2: Player; describe("Warship", () => { beforeEach(async () => { game = await setup( "half_land_half_ocean", { infiniteGold: true, instantBuild: true, }, [ new PlayerInfo("boat dude", PlayerType.Human, null, "player_1_id"), new PlayerInfo("boat dude", PlayerType.Human, null, "player_2_id"), ], ); while (game.inSpawnPhase()) { game.executeNextTick(); } player1 = game.player("player_1_id"); player2 = game.player("player_2_id"); }); test("Warship heals only if player has port", async () => { const maxHealth = game.config().unitInfo(UnitType.Warship).maxHealth; if (typeof maxHealth !== "number") { expect(typeof maxHealth).toBe("number"); throw new Error("unreachable"); } const port = player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {}); const warship = player1.buildUnit( UnitType.Warship, game.ref(coastX + 1, 10), { patrolTile: game.ref(coastX + 1, 10), }, ); game.addExecution(new WarshipExecution(warship)); game.executeNextTick(); expect(warship.health()).toBe(maxHealth); warship.modifyHealth(-10); expect(warship.health()).toBe(maxHealth - 10); game.executeNextTick(); expect(warship.health()).toBe(maxHealth - 9); port.delete(); game.executeNextTick(); expect(warship.health()).toBe(maxHealth - 9); }); test("Warship captures trade if player has port", async () => { const portTile = game.ref(coastX, 10); player1.buildUnit(UnitType.Port, portTile, {}); game.addExecution( new WarshipExecution( player1.buildUnit(UnitType.Warship, portTile, { patrolTile: portTile, }), ), ); const tradeShip = player2.buildUnit( UnitType.TradeShip, game.ref(coastX + 1, 7), { targetUnit: player2.buildUnit(UnitType.Port, game.ref(coastX, 10), {}), }, ); expect(tradeShip.owner().id()).toBe(player2.id()); // Let plenty of time for A* to execute for (let i = 0; i < 10; i++) { game.executeNextTick(); } expect(tradeShip.owner()).toBe(player1); }); test("Warship do not capture trade if player has no port", async () => { game.addExecution( new WarshipExecution( player1.buildUnit(UnitType.Warship, game.ref(coastX + 1, 11), { patrolTile: game.ref(coastX + 1, 11), }), ), ); const tradeShip = player2.buildUnit( UnitType.TradeShip, game.ref(coastX + 1, 11), { targetUnit: player1.buildUnit(UnitType.Port, game.ref(coastX, 11), {}), }, ); 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(); } expect(tradeShip.owner().id()).toBe(player2.id()); }); test("Warship does not target trade ships that are safe from pirates", async () => { // build port so warship can target trade ships player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {}); const warship = player1.buildUnit( UnitType.Warship, game.ref(coastX + 1, 10), { patrolTile: game.ref(coastX + 1, 10), }, ); game.addExecution(new WarshipExecution(warship)); const tradeShip = player2.buildUnit( UnitType.TradeShip, game.ref(coastX + 1, 10), { targetUnit: player2.buildUnit(UnitType.Port, game.ref(coastX, 10), {}), }, ); tradeShip.setSafeFromPirates(); executeTicks(game, 10); expect(tradeShip.owner().id()).toBe(player2.id()); }); test("Warship moves to new patrol tile", async () => { game.config().warshipTargettingRange = () => 1; const warship = player1.buildUnit( UnitType.Warship, game.ref(coastX + 1, 10), { patrolTile: game.ref(coastX + 1, 10), }, ); game.addExecution(new WarshipExecution(warship)); game.addExecution( new MoveWarshipExecution( player1, [warship.id()], game.ref(coastX + 5, 15), ), ); executeTicks(game, 10); expect(warship.patrolTile()).toBe(game.ref(coastX + 5, 15)); }); test("Warship does not not target trade ships outside of patrol range", async () => { game.config().warshipTargettingRange = () => 3; // build port so warship can target trade ships player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {}); const warship = player1.buildUnit( UnitType.Warship, game.ref(coastX + 1, 10), { patrolTile: game.ref(coastX + 1, 10), }, ); game.addExecution(new WarshipExecution(warship)); const tradeShip = player2.buildUnit( UnitType.TradeShip, game.ref(coastX + 1, 15), { targetUnit: player2.buildUnit(UnitType.Port, game.ref(coastX, 10), {}), }, ); executeTicks(game, 10); // Trade ship should not be captured expect(tradeShip.owner().id()).toBe(player2.id()); }); test("Warship prioritizes transport ships over warships", async () => { game.config().warshipShellAttackRate = () => Number.MAX_SAFE_INTEGER; const warship = player1.buildUnit( UnitType.Warship, game.ref(coastX + 1, 10), { patrolTile: game.ref(coastX + 1, 10), }, ); player2.buildUnit(UnitType.Warship, game.ref(coastX + 2, 10), { patrolTile: game.ref(coastX + 2, 10), }); player2.buildUnit(UnitType.TransportShip, game.ref(coastX + 1, 11), { targetTile: game.ref(coastX + 1, 11), }); game.addExecution(new WarshipExecution(warship)); let selectedType: UnitType | undefined = undefined; for (let i = 0; i < 5; i++) { game.executeNextTick(); selectedType = warship.targetUnit()?.type(); if (selectedType === UnitType.TransportShip) { break; } } expect(selectedType).toBe(UnitType.TransportShip); }); test("Warship does not target trade ships in different water components", async () => { // build port so warship can target trade ships player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {}); const warshipTile = game.ref(coastX + 1, 2); const tradeShipTile = game.ref(coastX + 1, 12); const warship = player1.buildUnit(UnitType.Warship, warshipTile, { patrolTile: warshipTile, }); game.addExecution(new WarshipExecution(warship)); const tradeShip = player2.buildUnit(UnitType.TradeShip, tradeShipTile, { targetUnit: player2.buildUnit(UnitType.Port, game.ref(coastX, 10), {}), }); // Mock different water components game.getWaterComponent = (tile: TileRef) => { if (tile === warshipTile) return 1; return 2; }; game.hasWaterComponent = (tile: TileRef, component: number) => { return game.getWaterComponent(tile) === component; }; executeTicks(game, 10); // Trade ship should not be captured because it's in a different component expect(tradeShip.owner().id()).toBe(player2.id()); }); test("MoveWarshipExecution fails if player is not the owner", async () => { const originalPatrolTile = game.ref(coastX + 1, 10); const warship = player1.buildUnit( UnitType.Warship, game.ref(coastX + 1, 5), { patrolTile: originalPatrolTile, }, ); new MoveWarshipExecution( player2, [warship.id()], game.ref(coastX + 5, 15), ).init(game, 0); expect(warship.patrolTile()).toBe(originalPatrolTile); }); test("MoveWarshipExecution fails if warship is not active", async () => { const originalPatrolTile = game.ref(coastX + 1, 10); const warship = player1.buildUnit( UnitType.Warship, game.ref(coastX + 1, 5), { patrolTile: originalPatrolTile, }, ); warship.delete(); new MoveWarshipExecution( player1, [warship.id()], game.ref(coastX + 5, 15), ).init(game, 0); expect(warship.patrolTile()).toBe(originalPatrolTile); }); test("MoveWarshipExecution fails gracefully if warship not found", async () => { const exec = new MoveWarshipExecution( player1, [123], game.ref(coastX + 5, 15), ); // Verify that no error is thrown. exec.init(game, 0); expect(exec.isActive()).toBe(false); }); test("Warship retreats when pre-heal health is below threshold", async () => { const maxHealth = game.config().unitInfo(UnitType.Warship).maxHealth; if (typeof maxHealth !== "number") { expect(typeof maxHealth).toBe("number"); throw new Error("unreachable"); } if (maxHealth <= 599) { expect(maxHealth).toBeGreaterThan(599); throw new Error("unreachable"); } game.config().warshipRetreatHealthThreshold = () => 600; const homePort = player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {}); const warship = player1.buildUnit( UnitType.Warship, game.ref(coastX + 1, 11), { patrolTile: game.ref(coastX + 1, 11), }, ); game.addExecution(new WarshipExecution(warship)); game.executeNextTick(); warship.modifyHealth(-(maxHealth - 599)); game.executeNextTick(); expect(warship.retreating()).toBe(true); const distanceToPort = game.euclideanDistSquared( warship.tile(), homePort.tile(), ); expect( distanceToPort <= 25 || warship.targetTile() === homePort.tile(), ).toBe(true); }); });