From 6fd1576d7b383e615ecd909ec9cf48d9b07b58c6 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Thu, 30 Oct 2025 20:15:18 -0700 Subject: [PATCH 1/3] use sigmoid function for trade ship gold to punish short trades --- src/core/configuration/DefaultConfig.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index a1fd6dcef..a503cda31 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -370,8 +370,9 @@ export class DefaultConfig implements Config { } tradeShipGold(dist: number, numPorts: number): Gold { - // Smooth anti-cheese formula: base reward scales with distance using rational function, heavily penalizing short trades while converging to original rewards at long distances - const baseGold = Math.floor(100_000 * (dist / (dist + 50)) + 100 * dist); + // Sigmoid: concave start, sharp S-curve middle, linear end - heavily punishes trades under 200 + const baseGold = + 100_000 / (1 + Math.exp(-0.03 * (dist - 200))) + 100 * dist; const numPortBonus = numPorts - 1; // Hyperbolic decay, midpoint at 5 ports, 3x bonus max. const bonus = 1 + 2 * (numPortBonus / (numPortBonus + 5)); From bf980b970d1d0bc5b1885a34414b34522830f58a Mon Sep 17 00:00:00 2001 From: evanpelle Date: Fri, 31 Oct 2025 11:28:36 -0700 Subject: [PATCH 2/3] add no warranty to the agpl license notice --- resources/lang/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index 04581e7b8..9559fd73c 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -263,7 +263,7 @@ "game_starting_modal": { "title": "Game is Starting...", "credits": "Credits", - "code_license": "Code licensed under AGPL-3.0" + "code_license": "Code licensed under AGPL-3.0 (no warranty)" }, "difficulty": { "difficulty": "Difficulty", From 896a8ebe922d695cf33ee37405a3287bdd229d4c Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Sat, 1 Nov 2025 18:48:56 +0100 Subject: [PATCH 3/3] AFK team mate: better ship handling + tests + bugfix (#2203) ## Description: Have AFK player's Warships not attack team members ships, like Transport Ships boating in. If team mate conquers the AFK player, transfer over Warships and Transport Ships to conqueror. The transfered Transport ships attack in the name of the new owner when landing, and when they are retreated they move back to a new owner shore tile if they have any. Added tests. Expectation is this PR will be merged in v26 as the real solution for the temporary workaround of deleting warships. **Currently:** - An AFK player can be attacked without troop loss by their team members. For this purpose, isFriendly now returns false if the other player isDisconnected - But that meant Warships would get False from isFriendly too, and attack the ships and boats of their team members. - [Temporary workaround was to delete warships](https://github.com/openfrontio/OpenFrontIO/commit/eea8db7a06aed50c005db35ad55ece026f7a3643) as soon as the player was deemed AFK. But this is a disadvantage to the team. For example the AFK player could have 6 warships in the waters, either defending team land or helping the team cross over to the enemy team. - Transport Ships that were on the way to attack, were still deleted after the AFK player was conquered. But this is also a disadvantage, if say a transport ship has just managed to breach through to the enemy lands despite warships all around. That could have made the win for the team. (Left to think about: do we want to transfer part of the defender troops to the isOnSameTeam attacker? Defender looses less troops in the attack from their team mate. You'd expect troops to lay down their arms mostly, if the attacker is on the same team and doesn't loose troops themselves. Those troops that they loose less than normal, are then added to the attacker once they've been deemed conqueror. The enemy team can still attack and do normal damage, and can still also be the conqueror so the team members have to be fast.) **Changes in this PR:** - GameImpl > conquerPlayer: Transfer ownership of the warships to the conquerer of their lands. If the conqueror is not a team member (other team can still attack, in their case with troop loss), they won't get the warships and the ships will be deleted like normal. If the conqueror is a team member, have them capture the warships. (Captures need to happen in conquerPlayer since this is right before the last tiles are conquered and PlayerExecution finds out the player is dead and deletes its units. Captures will be recorded in the stats just like normal. Things like this add an extra incentive to be the fastest to conquer the AFK player, next to getting their gold which is also recorded in stats. The normal Event Box messages are also displayed.) - GameImpl > conquerPlayer: Also transfer Transport Ships. As a note: the limit of 3 transport ships concurrently out on water for one player, can be exceeded in this specific case (the boatMaxNumber is only checked for canBuild via TransportShipUtils, and in the init of a TransportShipExecution, not for an existing TransportShipExecution with a changing owner). This keeps the situation even for the team in terms of ships that are already out to attack, which is fair. captureUnit/setOwner won't do the full job here though, so changes in TransportShipExecution are needed. - TransportShipExecution: Added originalOwner. So we can check within the execution itself if the Original owner disconnected, and if so if the new owner is on the same team. Only in that case change private this.attacker. This.attacker can still not be changed from the outside in this way. Find new src of new owner to retreat boat to if needed, and if new owner has no shore set it to null so the boat will be deleted upon retreat. To find new src tile of new owner, use bestTransportShipSpawn instead of canBuild because canBuild checks for max boats = 3 etcera but the boat is already on its way so those checks don't apply (we could get false back from canBuild because there's 3 ships out, while we only need to find the source tile so use bestTransportShipSpawn). - TransportShipUtils: to use bestTransportShipSpawn to find new owner source tile to retreat to, we need to make sure it can handle a new owner without shore tiles. When the new owner has no shore tiles (candidates.length === 0), return false. This way it won't go on to call MiniAStar which would have SerialAStar error on an empty this.sources array. - WarshipExecution: Changed isFriendly. This makes sure we have the wanted behavior: allied/team ships should not attack each other once one of their owners goes AFK. - AttackExecution: added one more test specifically to check if attack on AFK teammate is still witthout troop loss "Player can attack disconnected team mate without troop loss". Also a bugfix that I left in after removing a related change from this PR: Add a check for removeTroops === false in the retreat() function, so at the end of the attack we don't add troops back to owner troops if they were never removed in the init. This check in retreat() is actually a bug fix because removeTroops is in the constructor and can be set to True, but in retreat() troops would then have been given back after not being removed at init. - DefaultConfig: small addition to comment. - Disconnected.test.ts: added tests. Added useRealAttackLogic because at least "Player can attack disconnected team mate without troop loss" needs to use the real attackLogic. - TestConfig: new class useRealAttackLogic extends TestConfig class, so a test setup can use the real attackLogic from DefaultConfig instead of the mock function in TestConfig. - Setup.ts: for the test setup, add parameter to accept useRealAttackLogic extension class. Defaults to TestConfig. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: tryout33 --------- Co-authored-by: Evan --- src/core/configuration/DefaultConfig.ts | 2 +- src/core/execution/AttackExecution.ts | 6 + src/core/execution/TransportShipExecution.ts | 46 ++- src/core/execution/WarshipExecution.ts | 6 +- src/core/game/Game.ts | 2 +- src/core/game/GameImpl.ts | 14 + src/core/game/PlayerImpl.ts | 4 +- src/core/game/TransportShipUtils.ts | 2 + tests/Disconnected.test.ts | 322 ++++++++++++++++++- tests/util/Setup.ts | 3 +- tests/util/TestConfig.ts | 23 ++ 11 files changed, 414 insertions(+), 16 deletions(-) diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index a503cda31..ab0edc6a1 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -676,7 +676,7 @@ export class DefaultConfig implements Config { if (attacker.isPlayer() && defender.isPlayer()) { if (defender.isDisconnected() && attacker.isOnSameTeam(defender)) { - // No troop loss if defender is disconnected. + // No troop loss if defender is disconnected and on same team mag = 0; } if ( diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index 13099b7b6..2230483d7 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -181,6 +181,12 @@ export class AttackExecution implements Execution { this._owner.id(), ); } + if (this.removeTroops === false) { + // startTroops are always added to attack troops at init but not always removed from owner troops + // subtract startTroops from attack troops so we don't give back startTroops to owner that were never removed + this.attack.setTroops(this.attack.troops() - (this.startTroops ?? 0)); + } + const survivors = this.attack.troops() - deaths; this._owner.addTroops(survivors); this.attack.delete(); diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index 913abb817..8b5376f84 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -33,13 +33,17 @@ export class TransportShipExecution implements Execution { private pathFinder: PathFinder; + private originalOwner: Player; + constructor( private attacker: Player, private targetID: PlayerID | null, private ref: TileRef, private startTroops: number, private src: TileRef | null, - ) {} + ) { + this.originalOwner = this.attacker; + } activeDuringSpawnPhase(): boolean { return false; @@ -173,11 +177,43 @@ export class TransportShipExecution implements Execution { } this.lastMove = ticks; - if (this.boat.retreating()) { - this.dst = this.src!; // src is guaranteed to be set at this point + // Team mate can conquer disconnected player and get their ships + // captureUnit has changed the owner of the unit, now update attacker + if ( + this.originalOwner.isDisconnected() && + this.boat.owner() !== this.originalOwner && + this.boat.owner().isOnSameTeam(this.originalOwner) + ) { + this.attacker = this.boat.owner(); + this.originalOwner = this.boat.owner(); // for when this owner disconnects too + } - if (this.boat.targetTile() !== this.dst) { - this.boat.setTargetTile(this.dst); + if (this.boat.retreating()) { + // Ensure retreat source is valid for the new owner + if (this.mg.owner(this.src!) !== this.attacker) { + // Use bestTransportShipSpawn, not canBuild because of its max boats check etc + const newSrc = this.attacker.bestTransportShipSpawn(this.dst); + if (newSrc === false) { + this.src = null; + } else { + this.src = newSrc; + } + } + + if (this.src === null) { + console.warn( + `TransportShipExecution: retreating but no src found for new attacker`, + ); + this.attacker.addTroops(this.boat.troops()); + this.boat.delete(false); + this.active = false; + return; + } else { + this.dst = this.src; + + if (this.boat.targetTile() !== this.dst) { + this.boat.setTargetTile(this.dst); + } } } diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts index 67cb17aef..1ddb064cb 100644 --- a/src/core/execution/WarshipExecution.ts +++ b/src/core/execution/WarshipExecution.ts @@ -55,10 +55,6 @@ export class WarshipExecution implements Execution { this.warship.delete(); return; } - if (this.warship.owner().isDisconnected()) { - this.warship.delete(); - return; - } const hasPort = this.warship.owner().unitCount(UnitType.Port) > 0; if (hasPort) { @@ -93,7 +89,7 @@ export class WarshipExecution implements Execution { if ( unit.owner() === this.warship.owner() || unit === this.warship || - unit.owner().isFriendly(this.warship.owner()) || + unit.owner().isFriendly(this.warship.owner(), true) || this.alreadySentShell.has(unit) ) { continue; diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 01dd18c5d..65639d47b 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -584,7 +584,7 @@ export interface Player { decayRelations(): void; isOnSameTeam(other: Player): boolean; // Either allied or on same team. - isFriendly(other: Player): boolean; + isFriendly(other: Player, treatAFKFriendly?: boolean): boolean; team(): Team | null; clan(): string | null; incomingAllianceRequests(): AllianceRequest[]; diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index b4712954a..e60887e39 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -877,6 +877,20 @@ export class GameImpl implements Game { return this._railNetwork; } conquerPlayer(conqueror: Player, conquered: Player) { + if (conquered.isDisconnected() && conqueror.isOnSameTeam(conquered)) { + const ships = conquered + .units() + .filter( + (u) => + u.type() === UnitType.Warship || + u.type() === UnitType.TransportShip, + ); + + for (const ship of ships) { + conqueror.captureUnit(ship); + } + } + const gold = conquered.gold(); this.displayMessage( `Conquered ${conquered.displayName()} received ${renderNumber( diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 7d52ec28d..4c1a1c1dc 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -784,8 +784,8 @@ export class PlayerImpl implements Player { return this._team === other.team(); } - isFriendly(other: Player): boolean { - if (other.isDisconnected()) { + isFriendly(other: Player, treatAFKFriendly: boolean = false): boolean { + if (other.isDisconnected() && !treatAFKFriendly) { return false; } return this.isOnSameTeam(other) || this.isAlliedWith(other); diff --git a/src/core/game/TransportShipUtils.ts b/src/core/game/TransportShipUtils.ts index b457ad94a..a0af53526 100644 --- a/src/core/game/TransportShipUtils.ts +++ b/src/core/game/TransportShipUtils.ts @@ -148,6 +148,8 @@ export function bestShoreDeploymentSource( if (t === null) return false; const candidates = candidateShoreTiles(gm, player, t); + if (candidates.length === 0) return false; + const aStar = new MiniAStar(gm, gm.miniMap(), candidates, t, 1_000_000, 1); const result = aStar.compute(); if (result !== PathFindResultType.Completed) { diff --git a/tests/Disconnected.test.ts b/tests/Disconnected.test.ts index e03138efa..ab630eb73 100644 --- a/tests/Disconnected.test.ts +++ b/tests/Disconnected.test.ts @@ -1,12 +1,25 @@ +import { AttackExecution } from "../src/core/execution/AttackExecution"; import { MarkDisconnectedExecution } from "../src/core/execution/MarkDisconnectedExecution"; import { SpawnExecution } from "../src/core/execution/SpawnExecution"; -import { Game, Player, PlayerInfo, PlayerType } from "../src/core/game/Game"; +import { TransportShipExecution } from "../src/core/execution/TransportShipExecution"; +import { WarshipExecution } from "../src/core/execution/WarshipExecution"; +import { + Game, + GameMode, + Player, + PlayerInfo, + PlayerType, + UnitType, +} from "../src/core/game/Game"; +import { toInt } from "../src/core/Util"; import { setup } from "./util/Setup"; +import { UseRealAttackLogic } from "./util/TestConfig"; import { executeTicks } from "./util/utils"; let game: Game; let player1: Player; let player2: Player; +let enemy: Player; describe("Disconnected", () => { beforeEach(async () => { @@ -158,4 +171,311 @@ describe("Disconnected", () => { expect(player1.isDisconnected()).toBe(true); }); }); + + describe("Disconnected team member interactions", () => { + const coastX = 7; + + beforeEach(async () => { + const player1Info = new PlayerInfo( + "[CLAN]Player1", + PlayerType.Human, + null, + "player_1_id", + ); + const player2Info = new PlayerInfo( + "[CLAN]Player2", + PlayerType.Human, + null, + "player_2_id", + ); + + game = await setup( + "half_land_half_ocean", + { + infiniteGold: true, + instantBuild: true, + gameMode: GameMode.Team, + playerTeams: 2, // ignore player2 "kicked" console warn + }, + [player1Info, player2Info], + undefined, + UseRealAttackLogic, // don't use TestConfig's mock attackLogic + ); + + game.addExecution( + new SpawnExecution(player1Info, game.map().ref(coastX - 2, 1)), + new SpawnExecution(player2Info, game.map().ref(coastX - 2, 4)), + ); + + while (game.inSpawnPhase()) { + game.executeNextTick(); + } + + player1 = game.player(player1Info.id); + player2 = game.player(player2Info.id); + player2.markDisconnected(false); + + expect(player1.team()).not.toBeNull(); + expect(player2.team()).not.toBeNull(); + expect(player1.isOnSameTeam(player2)).toBe(true); + }); + + test("Team Warships should not attack disconnected team mate ships", () => { + const warship = player1.buildUnit( + UnitType.Warship, + game.map().ref(coastX + 1, 10), + { + patrolTile: game.map().ref(coastX + 1, 10), + }, + ); + game.addExecution(new WarshipExecution(warship)); + + const transportShip = player2.buildUnit( + UnitType.TransportShip, + game.map().ref(coastX + 1, 11), + { + troops: 100, + }, + ); + + player2.markDisconnected(true); + executeTicks(game, 10); + + expect(warship.targetUnit()).toBe(undefined); + expect(transportShip.isActive()).toBe(true); + expect(transportShip.owner()).toBe(player2); + }); + + test("Disconnected player Warship should not attack team members' ships", () => { + const warship = player2.buildUnit( + UnitType.Warship, + game.map().ref(coastX + 1, 5), + { + patrolTile: game.map().ref(coastX + 1, 10), + }, + ); + game.addExecution(new WarshipExecution(warship)); + + const transportShip = player1.buildUnit( + UnitType.TransportShip, + game.map().ref(coastX + 1, 6), + { + troops: 100, + }, + ); + + player2.markDisconnected(true); + executeTicks(game, 10); + + expect(warship.targetUnit()).toBe(undefined); + expect(transportShip.isActive()).toBe(true); + expect(transportShip.owner()).toBe(player1); + }); + + test("Player can attack disconnected team mate without troop loss", () => { + player2.conquer(game.map().ref(coastX - 2, 2)); + player2.conquer(game.map().ref(coastX - 2, 3)); + player2.markDisconnected(true); + + const troopsBeforeAttack = player1.troops(); + const startTroops = troopsBeforeAttack * 0.25; + + game.addExecution( + new AttackExecution(startTroops, player1, player2.id(), null), + ); + + let expectedTotalGrowth = 0n; + let afterTickZero = false; + + while (player2.isAlive()) { + if (afterTickZero) { + // No growth on tick 0, troop additions start from tick 1 + const troopIncThisTick = game.config().troopIncreaseRate(player1); + expectedTotalGrowth += toInt(troopIncThisTick); + } + + game.executeNextTick(); + afterTickZero = true; + } + + // Tick for retreat() in AttackExecution to add back startTtoops to owner troops + const troopIncThisTick1 = game.config().troopIncreaseRate(player1); + expectedTotalGrowth += toInt(troopIncThisTick1); + + game.executeNextTick(); + + const expectedFinalTroops = Number( + toInt(troopsBeforeAttack) + expectedTotalGrowth, + ); + + // Verify no troop loss + expect(player1.troops()).toBe(expectedFinalTroops); + }); + + test("Conqueror gets conquered disconnected team member's transport- and warships", () => { + const warship = player2.buildUnit( + UnitType.Warship, + game.map().ref(coastX + 1, 1), + { + patrolTile: game.map().ref(coastX + 1, 1), + }, + ); + const transportShip = player2.buildUnit( + UnitType.TransportShip, + game.map().ref(coastX + 1, 3), + { + troops: 100, + }, + ); + + player2.conquer(game.map().ref(coastX - 2, 1)); + player2.markDisconnected(true); + + game.addExecution(new AttackExecution(1000, player1, player2.id(), null)); + + executeTicks(game, 10); + + expect(player2.isAlive()).toBe(false); + expect(warship.owner()).toBe(player1); + expect(transportShip.owner()).toBe(player1); + }); + + test("Captured transport ship landing attack should be in name of new owner", () => { + player2.conquer(game.map().ref(coastX, 1)); + player2.conquer(game.map().ref(coastX - 1, 1)); + player2.conquer(game.map().ref(coastX, 2)); + + const enemyShoreTile = game.map().ref(coastX, 15); + + game.addExecution( + new TransportShipExecution( + player2, + null, + enemyShoreTile, + 100, + game.map().ref(coastX, 1), + ), + ); + + executeTicks(game, 1); + + expect(player2.isAlive()).toBe(true); + const transportShip = player2.units(UnitType.TransportShip)[0]; + expect(player2.units(UnitType.TransportShip).length).toBe(1); + + player2.markDisconnected(true); + game.addExecution(new AttackExecution(1000, player1, player2.id(), null)); + + executeTicks(game, 10); + + expect(player2.isAlive()).toBe(false); + expect(transportShip.owner()).toBe(player1); + + executeTicks(game, 30); + + // Verify ship landed and tile ownership transferred to new ship owner + expect(game.owner(enemyShoreTile)).toBe(player1); + }); + + test("Captured transport ship should retreat to owner's shore tile", () => { + player1.conquer(game.map().ref(coastX, 4)); + player2.conquer(game.map().ref(coastX, 1)); + + const enemyShoreTile = game.map().ref(coastX, 8); + + game.addExecution( + new TransportShipExecution( + player2, + null, + enemyShoreTile, + 100, + game.map().ref(coastX, 1), + ), + ); + executeTicks(game, 1); + + const transportShip = player2.units(UnitType.TransportShip)[0]; + expect(player2.units(UnitType.TransportShip).length).toBe(1); + + expect(transportShip.targetTile()).toBe(enemyShoreTile); + + player2.markDisconnected(true); + game.addExecution(new AttackExecution(1000, player1, player2.id(), null)); + executeTicks(game, 10); + + expect(player2.isAlive()).toBe(false); + expect(transportShip.owner()).toBe(player1); + + transportShip.orderBoatRetreat(); + executeTicks(game, 2); + + expect(transportShip.targetTile()).not.toBe(enemyShoreTile); + expect(game.owner(transportShip.targetTile()!)).toBe(player1); + }); + + test("Retreating transport ship is deleted if new owner has no shore tiles", () => { + player2.conquer(game.map().ref(coastX, 1)); + player2.conquer(game.map().ref(coastX - 6, 2)); + player1.conquer(game.map().ref(coastX - 6, 3)); + + const enemyShoreTile = game.map().ref(coastX, 15); + + const boatTroops = 100; + game.addExecution( + new TransportShipExecution( + player2, + null, + enemyShoreTile, + boatTroops, + game.map().ref(coastX, 1), + ), + ); + executeTicks(game, 1); + + const transportShip = player2.units(UnitType.TransportShip)[0]; + expect(player2.units(UnitType.TransportShip).length).toBe(1); + + player2.markDisconnected(true); + game.addExecution(new AttackExecution(1000, player1, player2.id(), null)); + executeTicks(game, 10); + + expect(player2.isAlive()).toBe(false); + expect(transportShip.owner()).toBe(player1); + + // Make sure player1 has no shore tiles for the ship to retreat to anymore + const enemyInfo = new PlayerInfo( + "Enemy", + PlayerType.Human, + null, + "enemy_id", + ); + enemy = game.addPlayer(enemyInfo); + + const shoreTiles = Array.from(player1.borderTiles()).filter((t) => + game.isShore(t), + ); + shoreTiles.forEach((tile) => { + enemy.conquer(tile); + }); + + expect( + Array.from(player1.borderTiles()).filter((t) => game.isShore(t)).length, + ).toBe(0); + + executeTicks(game, 1); + + const troopIncPerTick = game.config().troopIncreaseRate(player1); + const expectedTroopGrowth = toInt(troopIncPerTick * 1); + const expectedFinalTroops = Number( + toInt(player1.troops()) + expectedTroopGrowth, + ); + + transportShip.orderBoatRetreat(); + executeTicks(game, 1); + + expect(transportShip.isActive()).toBe(false); + // Also test if boat troops were returned to player1 as new ship owner + expect(player1.troops()).toBe(expectedFinalTroops + boatTroops); + }); + }); }); diff --git a/tests/util/Setup.ts b/tests/util/Setup.ts index d90b391e2..9ef2a00be 100644 --- a/tests/util/Setup.ts +++ b/tests/util/Setup.ts @@ -25,6 +25,7 @@ export async function setup( _gameConfig: Partial = {}, humans: PlayerInfo[] = [], currentDir: string = __dirname, + ConfigClass: typeof TestConfig = TestConfig, ): Promise { // Suppress console.debug for tests. console.debug = () => {}; @@ -69,7 +70,7 @@ export async function setup( instantBuild: false, ..._gameConfig, }; - const config = new TestConfig( + const config = new ConfigClass( serverConfig, gameConfig, new UserSettings(), diff --git a/tests/util/TestConfig.ts b/tests/util/TestConfig.ts index f0b10ac37..222568365 100644 --- a/tests/util/TestConfig.ts +++ b/tests/util/TestConfig.ts @@ -77,3 +77,26 @@ export class TestConfig extends DefaultConfig { return 1; } } +export class UseRealAttackLogic extends TestConfig { + // Override to use DefaultConfig's real attackLogic + attackLogic( + gm: Game, + attackTroops: number, + attacker: Player, + defender: Player | TerraNullius, + tileToConquer: TileRef, + ): { + attackerTroopLoss: number; + defenderTroopLoss: number; + tilesPerTickUsed: number; + } { + return DefaultConfig.prototype.attackLogic.call( + this, + gm, + attackTroops, + attacker, + defender, + tileToConquer, + ); + } +}