From d35d0f38cbba0ee05b03cd167302e6fd6ea87d1f Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sat, 24 May 2025 13:42:56 -0700 Subject: [PATCH] refactor & update warships (#796) ## Description: 1. Refactor WarshipExecution so that it takes either attrs or a warship unit. This makes testing much simpler as the unit test can construct a warship and then pass it into a warship execution 2. Have MoveWarshipExecution set the patrol tile, not the move tile so warships stay in new location instead of moving back to original location. 3. Warships no longer target trade ships outside of its patrol range. this prevents warships from wandering 4. Refactored & simplified WarshipExecution 5. Added more tests for warships 6. Move health modification from PlayerExecution to WarshipExecution since Warships are the only unit that have health 7. Move fields from WarshipExecution to the Warship unit itself, this allows other executions & components to see more data about the warship. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: --- src/core/execution/ConstructionExecution.ts | 4 +- src/core/execution/MoveWarshipExecution.ts | 3 +- src/core/execution/PlayerExecution.ts | 9 +- src/core/execution/TradeShipExecution.ts | 2 +- src/core/execution/WarshipExecution.ts | 285 ++++++++------------ src/core/game/Game.ts | 21 +- src/core/game/GameImpl.ts | 4 +- src/core/game/UnitImpl.ts | 19 +- tests/Warship.test.ts | 190 ++++++++----- tests/util/Setup.ts | 1 - 10 files changed, 283 insertions(+), 255 deletions(-) diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts index 39deab1ef..49ed2e89f 100644 --- a/src/core/execution/ConstructionExecution.ts +++ b/src/core/execution/ConstructionExecution.ts @@ -104,7 +104,9 @@ export class ConstructionExecution implements Execution { this.mg.addExecution(new MirvExecution(player.id(), this.tile)); break; case UnitType.Warship: - this.mg.addExecution(new WarshipExecution(player.id(), this.tile)); + this.mg.addExecution( + new WarshipExecution({ owner: player, patrolTile: this.tile }), + ); break; case UnitType.Port: this.mg.addExecution(new PortExecution(player.id(), this.tile)); diff --git a/src/core/execution/MoveWarshipExecution.ts b/src/core/execution/MoveWarshipExecution.ts index 1d3290692..87036c3e8 100644 --- a/src/core/execution/MoveWarshipExecution.ts +++ b/src/core/execution/MoveWarshipExecution.ts @@ -24,7 +24,8 @@ export class MoveWarshipExecution implements Execution { console.log("MoveWarshipExecution: warship is already dead"); return; } - warship.setTargetTile(this.position); + warship.setPatrolTile(this.position); + warship.setTargetTile(undefined); this.active = false; } diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index 50854707f..4bf039072 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -46,17 +46,12 @@ export class PlayerExecution implements Execution { throw new Error("Not initialized"); } this.player.decayRelations(); - const hasPort = this.player.units(UnitType.Port).length > 0; this.player.units().forEach((u) => { - if (hasPort && u.type() === UnitType.Warship) { - u.modifyHealth(1); - } - if (this.mg === null) return; - const tileOwner = this.mg.owner(u.tile()); + const tileOwner = this.mg!.owner(u.tile()); if (u.info().territoryBound) { if (tileOwner.isPlayer()) { if (tileOwner !== this.player) { - this.mg.player(tileOwner.id()).captureUnit(u); + this.mg!.player(tileOwner.id()).captureUnit(u); } } else { u.delete(); diff --git a/src/core/execution/TradeShipExecution.ts b/src/core/execution/TradeShipExecution.ts index b31d34d3c..3272d2a2c 100644 --- a/src/core/execution/TradeShipExecution.ts +++ b/src/core/execution/TradeShipExecution.ts @@ -50,7 +50,7 @@ export class TradeShipExecution implements Execution { return; } this.tradeShip = this.origOwner.buildUnit(UnitType.TradeShip, spawn, { - dstPort: this._dstPort, + targetUnit: this._dstPort, lastSetSafeFromPirates: ticks, }); diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts index 3188d9476..f5400c74d 100644 --- a/src/core/execution/WarshipExecution.ts +++ b/src/core/execution/WarshipExecution.ts @@ -2,9 +2,10 @@ import { consolex } from "../Consolex"; import { Execution, Game, - Player, - PlayerID, + isUnit, + OwnerComp, Unit, + UnitParams, UnitType, } from "../game/Game"; import { TileRef } from "../game/GameMap"; @@ -15,178 +16,91 @@ import { ShellExecution } from "./ShellExecution"; export class WarshipExecution implements Execution { private random: PseudoRandom; - - private _owner: Player; - private active = true; - private warship: Unit | null = null; + private warship: Unit; private mg: Game; - - private target: Unit | undefined = undefined; - private pathfinder: PathFinder | null = null; - - private patrolTile: TileRef | undefined; - + private pathfinder: PathFinder; private lastShellAttack = 0; private alreadySentShell = new Set(); constructor( - private playerID: PlayerID, - private patrolCenterTile: TileRef, + private input: (UnitParams & OwnerComp) | Unit, ) {} init(mg: Game, ticks: number): void { this.mg = mg; - if (!mg.hasPlayer(this.playerID)) { - console.log(`WarshipExecution: player ${this.playerID} not found`); - this.active = false; - return; - } this.pathfinder = PathFinder.Mini(mg, 5000); - this._owner = mg.player(this.playerID); - this.patrolTile = this.patrolCenterTile; this.random = new PseudoRandom(mg.ticks()); - } - - // Only for warships with "moveTarget" set - goToMoveTarget(target: TileRef) { - if (this.warship === null || this.pathfinder === null) { - throw new Error("Warship not initialized"); - } - // Patrol unless we are hunting down a tradeship - const result = this.pathfinder.nextTile(this.warship.tile(), target); - switch (result.type) { - case PathFindResultType.Completed: - this.warship.setTargetTile(undefined); - this.warship.touch(); - return; - case PathFindResultType.NextTile: - this.warship.move(result.tile); - break; - case PathFindResultType.Pending: - this.warship.touch(); - break; - case PathFindResultType.PathNotFound: - consolex.log(`path not found to target`); - break; - } - } - - private shoot() { - if ( - this.mg === null || - this.warship === null || - this.target === undefined - ) { - throw new Error("Warship not initialized"); - } - const shellAttackRate = this.mg.config().warshipShellAttackRate(); - if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) { - this.lastShellAttack = this.mg.ticks(); - this.mg.addExecution( - new ShellExecution( - this.warship.tile(), - this.warship.owner(), - this.warship, - this.target, - ), + if (isUnit(this.input)) { + this.warship = this.input; + } else { + const spawn = this.input.owner.canBuild( + UnitType.Warship, + this.input.patrolTile, ); - if (!this.target.hasHealth()) { - // Don't send multiple shells to target that can be oneshotted - this.alreadySentShell.add(this.target); - this.target = undefined; + if (spawn === false) { + console.warn( + `Failed to spawn warship for ${this.input.owner.name()} at ${this.input.patrolTile}`, + ); return; } - } - } - - private patrol() { - if (this.warship === null || this.pathfinder === null) { - throw new Error("Warship not initialized"); - } - if (this.patrolTile === undefined) { - this.patrolTile = this.randomTile(); - if (this.patrolTile === undefined) { - return; - } - } - this.warship.setTargetUnit(this.target); - if ( - this.target === undefined || - this.target.type() !== UnitType.TradeShip - ) { - // Patrol unless we are hunting down a tradeship - const result = this.pathfinder.nextTile( - this.warship.tile(), - this.patrolTile, + this.warship = this.input.owner.buildUnit( + UnitType.Warship, + spawn, + this.input, ); - switch (result.type) { - case PathFindResultType.Completed: - this.patrolTile = undefined; - this.warship.touch(); - break; - case PathFindResultType.NextTile: - this.warship.move(result.tile); - break; - case PathFindResultType.Pending: - this.warship.touch(); - return; - case PathFindResultType.PathNotFound: - consolex.log(`path not found to patrol tile`); - this.patrolTile = undefined; - break; - } } } tick(ticks: number): void { - if (this.pathfinder === null) throw new Error("Warship not initialized"); - if (this.warship === null) { - if (this.patrolTile === undefined) { - console.log( - `WarshipExecution: no patrol tile for ${this._owner.name()}`, - ); - this.active = false; - return; - } - const spawn = this._owner.canBuild(UnitType.Warship, this.patrolTile); - if (spawn === false) { - this.active = false; - return; - } - this.warship = this._owner.buildUnit(UnitType.Warship, spawn, {}); + if (this.warship.health() <= 0) { + this.warship.delete(); return; } - if (!this.warship.isActive()) { - this.active = false; + const hasPort = this.warship.owner().units(UnitType.Port).length > 0; + if (hasPort) { + this.warship.modifyHealth(1); + } + + this.warship.setTargetUnit(this.findTargetUnit()); + if (this.warship.targetUnit()?.type() === UnitType.TradeShip) { + this.huntDownTradeShip(); return; } - if (this.target !== undefined && !this.target.isActive()) { - this.target = undefined; + + this.patrol(); + + if (this.warship.targetUnit() !== undefined) { + this.shootTarget(); + return; } - const hasPort = this._owner.units(UnitType.Port).length > 0; - const warship = this.warship; - if (warship === undefined) throw new Error("Warship not initialized"); + } + + private findTargetUnit(): Unit | undefined { + const hasPort = this.warship.owner().units(UnitType.Port).length > 0; + const patrolRangeSquared = this.mg.config().warshipPatrolRange() ** 2; + const ships = this.mg .nearbyUnits( - this.warship.tile(), + this.warship.patrolTile()!, this.mg.config().warshipTargettingRange(), [UnitType.TransportShip, UnitType.Warship, UnitType.TradeShip], ) .filter( ({ unit }) => - unit.owner() !== warship.owner() && - unit !== warship && - !unit.owner().isFriendly(warship.owner()) && + unit.owner() !== this.warship.owner() && + unit !== this.warship && + !unit.owner().isFriendly(this.warship.owner()) && !this.alreadySentShell.has(unit) && (unit.type() !== UnitType.TradeShip || (hasPort && - this.warship !== null && + this.mg.euclideanDistSquared(this.warship.tile(), unit.tile()) <= + patrolRangeSquared && unit.targetUnit()?.owner() !== this.warship.owner() && !unit.targetUnit()?.owner().isFriendly(this.warship.owner()) && unit.isSafeFromPirates() !== true)), ); - this.target = ships.sort((a, b) => { + return ships.sort((a, b) => { const { unit: unitA, distSquared: distA } = a; const { unit: unitB, distSquared: distB } = b; @@ -217,60 +131,48 @@ export class WarshipExecution implements Execution { // If both are the same type, sort by distance (lower `distSquared` means closer) return distA - distB; })[0]?.unit; + } - const moveTarget = this.warship.targetTile(); - if (moveTarget) { - this.goToMoveTarget(moveTarget); - // If we have a "move target" then we cannot target trade ships as it - // requires moving. - if (this.target && this.target.type() === UnitType.TradeShip) { - this.target = undefined; + private shootTarget() { + const shellAttackRate = this.mg.config().warshipShellAttackRate(); + if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) { + this.lastShellAttack = this.mg.ticks(); + this.mg.addExecution( + new ShellExecution( + this.warship.tile(), + this.warship.owner(), + this.warship, + this.warship.targetUnit()!, + ), + ); + if (!this.warship.targetUnit()!.hasHealth()) { + // Don't send multiple shells to target that can be oneshotted + this.alreadySentShell.add(this.warship.targetUnit()!); + this.warship.setTargetUnit(undefined); + return; } - } else if (!this.target || this.target.type() !== UnitType.TradeShip) { - this.patrol(); - } - - if ( - this.target === undefined || - !this.target.isActive() || - this.target.owner() === this._owner || - this.target.isSafeFromPirates() === true - ) { - // In case another warship captured or destroyed target, or the target escaped into safe waters - this.target = undefined; - return; - } - - this.warship.setTargetUnit(this.target); - - // If we have a move target we do not want to go after trading ships - if (!this.target) { - return; - } - - if (this.target.type() !== UnitType.TradeShip) { - this.shoot(); - return; } + } + private huntDownTradeShip() { for (let i = 0; i < 2; i++) { // target is trade ship so capture it. const result = this.pathfinder.nextTile( this.warship.tile(), - this.target.tile(), + this.warship.targetUnit()!.tile(), 5, ); switch (result.type) { case PathFindResultType.Completed: - this._owner.captureUnit(this.target); - this.target = undefined; + this.warship.owner().captureUnit(this.warship.targetUnit()!); + this.warship.setTargetUnit(undefined); this.warship.move(this.warship.tile()); return; case PathFindResultType.NextTile: this.warship.move(result.tile); break; case PathFindResultType.Pending: - this.warship.move(this.warship.tile()); + this.warship.touch(); break; case PathFindResultType.PathNotFound: consolex.log(`path not found to target`); @@ -279,8 +181,38 @@ export class WarshipExecution implements Execution { } } + private patrol() { + if (this.warship.targetTile() === undefined) { + this.warship.setTargetTile(this.randomTile()); + if (this.warship.targetTile() === undefined) { + return; + } + } + + const result = this.pathfinder.nextTile( + this.warship.tile(), + this.warship.targetTile()!, + ); + switch (result.type) { + case PathFindResultType.Completed: + this.warship.setTargetTile(undefined); + this.warship.move(result.tile); + break; + case PathFindResultType.NextTile: + this.warship.move(result.tile); + break; + case PathFindResultType.Pending: + this.warship.touch(); + return; + case PathFindResultType.PathNotFound: + consolex.warn(`path not found to target tile`); + this.warship.setTargetTile(undefined); + break; + } + } + isActive(): boolean { - return this.active; + return this.warship?.isActive(); } activeDuringSpawnPhase(): boolean { @@ -288,19 +220,16 @@ export class WarshipExecution implements Execution { } randomTile(allowShoreline: boolean = false): TileRef | undefined { - if (this.mg === null) { - throw new Error("Warship not initialized"); - } let warshipPatrolRange = this.mg.config().warshipPatrolRange(); const maxAttemptBeforeExpand: number = 500; let attempts: number = 0; let expandCount: number = 0; while (expandCount < 3) { const x = - this.mg.x(this.patrolCenterTile) + + this.mg.x(this.warship.patrolTile()!) + this.random.nextInt(-warshipPatrolRange / 2, warshipPatrolRange / 2); const y = - this.mg.y(this.patrolCenterTile) + + this.mg.y(this.warship.patrolTile()!) + this.random.nextInt(-warshipPatrolRange / 2, warshipPatrolRange / 2); if (!this.mg.isValidCoord(x, y)) { continue; @@ -322,7 +251,7 @@ export class WarshipExecution implements Execution { return tile; } console.warn( - `Failed to find random tile for warship for ${this._owner.name()}`, + `Failed to find random tile for warship for ${this.warship.owner().name()}`, ); if (!allowShoreline) { // If we failed to find a tile on the ocean, try again but allow shoreline diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index cab325ddc..d82862b21 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -152,13 +152,19 @@ export enum UnitType { Construction = "Construction", } +export interface OwnerComp { + owner: Player; +} + export interface UnitParamsMap { [UnitType.TransportShip]: { troops?: number; destination?: TileRef; }; - [UnitType.Warship]: {}; + [UnitType.Warship]: { + patrolTile: TileRef; + }; [UnitType.Shell]: {}; @@ -175,7 +181,7 @@ export interface UnitParamsMap { }; [UnitType.TradeShip]: { - dstPort: Unit; + targetUnit: Unit; lastSetSafeFromPirates?: number; }; @@ -334,8 +340,12 @@ export class PlayerInfo { } } +export function isUnit(unit: Unit | UnitParams): unit is Unit { + return "isUnit" in unit && typeof unit.isUnit === "function" && unit.isUnit(); +} + export interface Unit { - hash(): number; + isUnit(): this is Unit; // Common properties. id(): number; @@ -349,6 +359,7 @@ export interface Unit { isActive(): boolean; setOwner(owner: Player): void; touch(): void; + hash(): number; toUpdate(): UnitUpdate; // Targeting @@ -387,6 +398,10 @@ export interface Unit { constructionType(): UnitType | null; setConstructionType(type: UnitType): void; + // Warships + setPatrolTile(tile: TileRef): void; + patrolTile(): TileRef | undefined; + // Ports cachePut(from: TileRef, to: TileRef): void; cacheGet(from: TileRef): TileRef | undefined; diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 1e244ae9f..b54618bca 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -286,8 +286,8 @@ export class GameImpl implements Game { this.updates = createGameUpdatesMap(); this.execs.forEach((e) => { if ( - e.isActive() && - (!this.inSpawnPhase() || e.activeDuringSpawnPhase()) + (!this.inSpawnPhase() || e.activeDuringSpawnPhase()) && + e.isActive() ) { e.tick(this._ticks); } diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 9c9f965e6..86ec9680b 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -28,7 +28,7 @@ export class UnitImpl implements Unit { private _troops: number; private _cooldownStartTick: Tick | null = null; private _pathCache: Map = new Map(); - + private _patrolTile: TileRef | undefined; constructor( private _type: UnitType, private mg: GameImpl, @@ -46,6 +46,10 @@ export class UnitImpl implements Unit { "lastSetSafeFromPirates" in params ? (params.lastSetSafeFromPirates ?? 0) : 0; + this._patrolTile = + "patrolTile" in params ? (params.patrolTile ?? undefined) : undefined; + this._targetUnit = + "targetUnit" in params ? (params.targetUnit ?? undefined) : undefined; switch (this._type) { case UnitType.Warship: @@ -57,6 +61,19 @@ export class UnitImpl implements Unit { this.mg.stats().unitBuild(_owner, this._type); } } + + setPatrolTile(tile: TileRef): void { + this._patrolTile = tile; + } + + patrolTile(): TileRef | undefined { + return this._patrolTile; + } + + isUnit(): this is Unit { + return true; + } + touch(): void { this.mg.addUpdate(this.toUpdate()); } diff --git a/tests/Warship.test.ts b/tests/Warship.test.ts index 346e8eb58..bbd8ad48d 100644 --- a/tests/Warship.test.ts +++ b/tests/Warship.test.ts @@ -1,4 +1,5 @@ -import { SpawnExecution } from "../src/core/execution/SpawnExecution"; +import { MoveWarshipExecution } from "../src/core/execution/MoveWarshipExecution"; +import { WarshipExecution } from "../src/core/execution/WarshipExecution"; import { Game, Player, @@ -7,7 +8,7 @@ import { UnitType, } from "../src/core/game/Game"; import { setup } from "./util/Setup"; -import { constructionExecution } from "./util/utils"; +import { executeTicks } from "./util/utils"; const coastX = 7; let game: Game; @@ -16,44 +17,36 @@ let player2: Player; describe("Warship", () => { beforeEach(async () => { - game = await setup("half_land_half_ocean", { - infiniteGold: true, - instantBuild: true, - }); - const player_1_info = new PlayerInfo( - "us", - "boat dude", - PlayerType.Human, - null, - "player_1_id", - ); - game.addPlayer(player_1_info); - const player_2_info = new PlayerInfo( - "us", - "boat dude", - PlayerType.Human, - null, - "player_2_id", - ); - game.addPlayer(player_2_info); - - game.addExecution( - new SpawnExecution( - game.player(player_1_info.id).info(), - game.ref(coastX, 10), - ), - new SpawnExecution( - game.player(player_2_info.id).info(), - game.ref(coastX, 15), - ), + game = await setup( + "half_land_half_ocean", + { + infiniteGold: true, + instantBuild: true, + }, + [ + new PlayerInfo( + "us", + "boat dude", + PlayerType.Human, + null, + "player_1_id", + ), + new PlayerInfo( + "us", + "boat dude", + PlayerType.Human, + null, + "player_2_id", + ), + ], ); while (game.inSpawnPhase()) { game.executeNextTick(); } - player1 = game.player(player_1_info.id); - player2 = game.player(player_2_info.id); + player1 = game.player("player_1_id"); + player2 = game.player("player_2_id"); }); test("Warship heals only if player has port", async () => { @@ -67,8 +60,11 @@ describe("Warship", () => { const warship = player1.buildUnit( UnitType.Warship, game.ref(coastX + 1, 10), - {}, + { + patrolTile: game.ref(coastX + 1, 10), + }, ); + game.addExecution(new WarshipExecution(warship)); game.executeNextTick(); @@ -85,26 +81,21 @@ describe("Warship", () => { }); test("Warship captures trade if player has port", async () => { - constructionExecution(game, player1.id(), coastX, 10, UnitType.Port); - constructionExecution(game, player1.id(), coastX + 1, 10, UnitType.Warship); - // Warship need one more tick (for warship exec to actually build warship) - game.executeNextTick(); - expect(player1.units(UnitType.Warship)).toHaveLength(1); - expect(player1.units(UnitType.Port)).toHaveLength(1); - - const dstPort = player2.buildUnit( - UnitType.Port, - game.ref(coastX + 2, 10), - {}, + const portTile = game.ref(coastX, 10); + player1.buildUnit(UnitType.Port, portTile, {}); + game.addExecution( + new WarshipExecution( + player1.buildUnit(UnitType.Warship, portTile, { + patrolTile: portTile, + }), + ), ); - // Cannot buildExec with trade ship as it's not buildable (but - // we can obviously directly add it to the player) const tradeShip = player2.buildUnit( UnitType.TradeShip, game.ref(coastX + 1, 7), { - dstPort, + targetUnit: player2.buildUnit(UnitType.Port, game.ref(coastX, 10), {}), }, ); @@ -113,32 +104,111 @@ describe("Warship", () => { for (let i = 0; i < 10; i++) { game.executeNextTick(); } - expect(tradeShip.owner().id()).toBe(player1.id()); + expect(tradeShip.owner()).toBe(player1); }); test("Warship do not capture trade if player has no port", async () => { - constructionExecution(game, player1.id(), coastX, 10, UnitType.Port); - constructionExecution(game, player1.id(), coastX + 1, 10, UnitType.Warship); - expect(player1.units(UnitType.Warship)).toHaveLength(1); + game.addExecution( + new WarshipExecution( + player1.buildUnit(UnitType.Warship, game.ref(coastX + 1, 11), { + patrolTile: game.ref(coastX + 1, 11), + }), + ), + ); - const [dstPort] = player1.units(UnitType.Port); - - player1.units(UnitType.Port)[0].delete(); - // Cannot buildExec with trade ship as it's not buildable (but - // we can obviously directly add it to the player) const tradeShip = player2.buildUnit( UnitType.TradeShip, game.ref(coastX + 1, 11), { - dstPort, + targetUnit: player1.buildUnit(UnitType.Port, game.ref(coastX, 11), {}), }, ); expect(tradeShip.owner().id()).toBe(player2.id()); - // Let plenty of time for A* to execute + // 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(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()); }); }); diff --git a/tests/util/Setup.ts b/tests/util/Setup.ts index 4a8d0fbb3..9f657bffc 100644 --- a/tests/util/Setup.ts +++ b/tests/util/Setup.ts @@ -55,7 +55,6 @@ export async function setup( false, ); - // Create and return the game return createGame(humans, [], gameMap, miniGameMap, config); }