diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 4937e7b5a..5f975031c 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -152,6 +152,8 @@ export interface Config { traitorDefenseDebuff(): number; traitorDuration(): number; nukeMagnitudes(unitType: UnitType): NukeMagnitude; + // Number of tiles destroyed to break an alliance + nukeAllianceBreakThreshold(): number; defaultNukeSpeed(): number; defaultNukeTargetableRange(): number; defaultSamRange(): number; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 972ab469c..03a34236d 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -805,6 +805,10 @@ export class DefaultConfig implements Config { throw new Error(`Unknown nuke type: ${unitType}`); } + nukeAllianceBreakThreshold(): number { + return 100; + } + defaultNukeSpeed(): number { return 6; } diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 651fbb1f3..82c32f400 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -64,7 +64,7 @@ export class NukeExecution implements Execution { return this.tilesToDestroyCache; } - private breakAlliances(toDestroy: Set) { + private maybeBreakAlliances(toDestroy: Set) { if (this.nuke === null) { throw new Error("Not initialized"); } @@ -77,8 +77,12 @@ export class NukeExecution implements Execution { } } + const threshold = this.mg.config().nukeAllianceBreakThreshold(); for (const [other, tilesDestroyed] of attacked) { - if (tilesDestroyed > 100 && this.nuke.type() !== UnitType.MIRVWarhead) { + if ( + tilesDestroyed > threshold && + this.nuke.type() !== UnitType.MIRVWarhead + ) { // Mirv warheads shouldn't break alliances const alliance = this.player.allianceWith(other); if (alliance !== null) { @@ -108,6 +112,7 @@ export class NukeExecution implements Execution { this.nuke = this.player.buildUnit(this.nukeType, spawn, { targetTile: this.dst, }); + this.maybeBreakAlliances(this.tilesToDestroy()); if (this.mg.hasOwner(this.dst)) { const target = this.mg.owner(this.dst); if (!target.isPlayer()) { @@ -120,7 +125,6 @@ export class NukeExecution implements Execution { MessageType.NUKE_INBOUND, target.id(), ); - this.breakAlliances(this.tilesToDestroy()); } else if (this.nukeType === UnitType.HydrogenBomb) { this.mg.displayIncomingUnit( this.nuke.id(), @@ -129,7 +133,6 @@ export class NukeExecution implements Execution { MessageType.HYDROGEN_BOMB_INBOUND, target.id(), ); - this.breakAlliances(this.tilesToDestroy()); } // Record stats @@ -198,7 +201,7 @@ export class NukeExecution implements Execution { const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type()); const toDestroy = this.tilesToDestroy(); - this.breakAlliances(toDestroy); + this.maybeBreakAlliances(toDestroy); for (const tile of toDestroy) { const owner = this.mg.owner(tile); diff --git a/tests/core/executions/NukeExecution.test.ts b/tests/core/executions/NukeExecution.test.ts index 5a614ebea..5ed577f44 100644 --- a/tests/core/executions/NukeExecution.test.ts +++ b/tests/core/executions/NukeExecution.test.ts @@ -12,31 +12,34 @@ import { executeTicks } from "../../util/utils"; let game: Game; let player: Player; +let otherPlayer: Player; describe("NukeExecution", () => { beforeEach(async () => { - game = await setup("big_plains", { - infiniteGold: true, - instantBuild: true, - }); + game = await setup( + "big_plains", + { + infiniteGold: true, + instantBuild: true, + }, + [ + new PlayerInfo("player", PlayerType.Human, "client_id1", "player_id"), + new PlayerInfo("other", PlayerType.Human, "client_id2", "other_id"), + ], + ); (game.config() as TestConfig).nukeMagnitudes = jest.fn(() => ({ inner: 10, outer: 10, })); - const player_info = new PlayerInfo( - "player_id", - PlayerType.Human, - null, - "player_id", - ); - game.addPlayer(player_info); + (game.config() as TestConfig).nukeAllianceBreakThreshold = jest.fn(() => 5); while (game.inSpawnPhase()) { game.executeNextTick(); } player = game.player("player_id"); + otherPlayer = game.player("other_id"); }); test("nuke should destroy buildings and redraw out of range buildings", async () => { @@ -94,4 +97,29 @@ describe("NukeExecution", () => { executeTicks(game, 35); expect(nukeExec.getNuke()!.isTargetable()).toBeTruthy(); }); + + test("nuke should break alliances on launch", async () => { + const req = player.createAllianceRequest(otherPlayer); + req!.accept(); + + player.conquer(game.ref(1, 1)); + player.buildUnit(UnitType.MissileSilo, game.ref(1, 1), {}); + + for (let x = 90; x < 99; x++) { + for (let y = 90; y < 99; y++) { + otherPlayer.conquer(game.ref(x, y)); + } + } + + // Add a nuke targeting just outside the other player's territory. + game.addExecution( + new NukeExecution(UnitType.AtomBomb, player, game.ref(85, 85), null), + ); + + game.executeNextTick(); // init + game.executeNextTick(); // exec + + expect(player.isTraitor()).toBe(true); + expect(player.isAlliedWith(otherPlayer)).toBe(false); + }); });