diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index 5915b6223..ddd635cc8 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -57,6 +57,7 @@ export class BotExecution implements Execution { } this.behavior.handleAllianceRequests(); + this.behavior.handleAllianceExtensionRequests(); this.maybeAttack(); } diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 828631757..0f41f7e86 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -155,6 +155,7 @@ export class FakeHumanExecution implements Execution { this.updateRelationsFromEmbargos(); this.behavior.handleAllianceRequests(); + this.behavior.handleAllianceExtensionRequests(); this.handleUnits(); this.handleEmbargoesToHostileNations(); this.maybeAttack(); diff --git a/src/core/execution/alliance/AllianceExtensionExecution.ts b/src/core/execution/alliance/AllianceExtensionExecution.ts index 2469d9038..df24a9fa8 100644 --- a/src/core/execution/alliance/AllianceExtensionExecution.ts +++ b/src/core/execution/alliance/AllianceExtensionExecution.ts @@ -39,7 +39,7 @@ export class AllianceExtensionExecution implements Execution { // Mark this player's intent to extend alliance.addExtensionRequest(this.from); - if (alliance.canExtend()) { + if (alliance.bothAgreedToExtend()) { alliance.extend(); mg.displayMessage( diff --git a/src/core/execution/utils/BotBehavior.ts b/src/core/execution/utils/BotBehavior.ts index 4525c4ad9..3cf85c249 100644 --- a/src/core/execution/utils/BotBehavior.ts +++ b/src/core/execution/utils/BotBehavior.ts @@ -9,6 +9,7 @@ import { } from "../../game/Game"; import { PseudoRandom } from "../../PseudoRandom"; import { flattenedEmojiTable } from "../../Util"; +import { AllianceExtensionExecution } from "../alliance/AllianceExtensionExecution"; import { AttackExecution } from "../AttackExecution"; import { EmojiExecution } from "../EmojiExecution"; @@ -37,6 +38,28 @@ export class BotBehavior { } } + handleAllianceExtensionRequests() { + for (const alliance of this.player.alliances()) { + // Alliance expiration tracked by Events Panel, only human ally can click Request to Renew + // Skip if no expiration yet/ ally didn't request extension yet/ bot already agreed to extend + if (!alliance.onlyOneAgreedToExtend()) continue; + + // Nation is either Friendly or Neutral as an ally. Bot has no attitude + // If Friendly or Bot, always agree to extend. If Neutral, have random chance decide + const human = alliance.other(this.player); + if ( + this.player.type() === PlayerType.FakeHuman && + this.player.relation(human) === Relation.Neutral + ) { + if (!this.random.chance(1.5)) continue; + } + + this.game.addExecution( + new AllianceExtensionExecution(this.player, human.id()), + ); + } + } + private emoji(player: Player, emoji: number) { if (player.type() !== PlayerType.Human) return; this.game.addExecution(new EmojiExecution(this.player, player.id(), emoji)); diff --git a/src/core/game/AllianceImpl.ts b/src/core/game/AllianceImpl.ts index 6d2782595..fa74ca766 100644 --- a/src/core/game/AllianceImpl.ts +++ b/src/core/game/AllianceImpl.ts @@ -47,12 +47,21 @@ export class AllianceImpl implements MutableAlliance { } } - canExtend(): boolean { + bothAgreedToExtend(): boolean { return ( this.extensionRequestedRequestor_ && this.extensionRequestedRecipient_ ); } + onlyOneAgreedToExtend(): boolean { + // Requestor / Recipient of the original alliance request, not of the extension request + // False if: no expiration or neither requested extension yet (both false), or both agreed to extend (both true) + // True if: one requested extension, other didn't yet or actively ignored (one true, one false) + return ( + this.extensionRequestedRequestor_ !== this.extensionRequestedRecipient_ + ); + } + public id(): number { return this.id_; } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 6272d4622..5e89f06bd 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -362,10 +362,11 @@ export interface Alliance { export interface MutableAlliance extends Alliance { expire(): void; other(player: Player): Player; - canExtend(): boolean; + bothAgreedToExtend(): boolean; addExtensionRequest(player: Player): void; id(): number; extend(): void; + onlyOneAgreedToExtend(): boolean; } export class PlayerInfo { diff --git a/tests/AllianceExtensionExecution.test.ts b/tests/AllianceExtensionExecution.test.ts index 8e4cbcebb..6ae6340ed 100644 --- a/tests/AllianceExtensionExecution.test.ts +++ b/tests/AllianceExtensionExecution.test.ts @@ -7,6 +7,7 @@ import { playerInfo, setup } from "./util/Setup"; let game: Game; let player1: Player; let player2: Player; +let player3: Player; describe("AllianceExtensionExecution", () => { beforeEach(async () => { @@ -20,18 +21,20 @@ describe("AllianceExtensionExecution", () => { [ playerInfo("player1", PlayerType.Human), playerInfo("player2", PlayerType.Human), + playerInfo("player3", PlayerType.FakeHuman), ], ); player1 = game.player("player1"); player2 = game.player("player2"); + player3 = game.player("player3"); while (game.inSpawnPhase()) { game.executeNextTick(); } }); - test("Successfully extends existing alliance", () => { + test("Successfully extends existing alliance between Humans", () => { jest.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true); jest.spyOn(player2, "isAlive").mockReturnValue(true); jest.spyOn(player1, "isAlive").mockReturnValue(true); @@ -51,8 +54,8 @@ describe("AllianceExtensionExecution", () => { const allianceBefore = player1.allianceWith(player2)!; const allianceSpy = jest.spyOn(allianceBefore, "extend"); - const expirationBefore = - allianceBefore.createdAt() + game.config().allianceDuration(); + + const expirationBefore = allianceBefore.expiresAt(); game.addExecution(new AllianceExtensionExecution(player1, player2.id())); game.executeNextTick(); @@ -64,10 +67,9 @@ describe("AllianceExtensionExecution", () => { expect(allianceAfter.id()).toBe(allianceBefore.id()); - const expirationAfter = - allianceAfter.createdAt() + game.config().allianceDuration(); + const expirationAfter = allianceAfter.expiresAt(); - expect(expirationAfter).toBeGreaterThanOrEqual(expirationBefore); + expect(expirationAfter).toBeGreaterThan(expirationBefore); expect(allianceSpy).toHaveBeenCalledTimes(1); }); @@ -78,4 +80,43 @@ describe("AllianceExtensionExecution", () => { expect(player1.allianceWith(player2)).toBeFalsy(); expect(player2.allianceWith(player1)).toBeFalsy(); }); + + test("Successfully extends existing alliance between Human and non-Human", () => { + //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(); + + game.addExecution( + new AllianceRequestReplyExecution(player1.id(), player3, true), + ); + game.executeNextTick(); + game.executeNextTick(); + + expect(player1.allianceWith(player3)).toBeTruthy(); + expect(player3.allianceWith(player1)).toBeTruthy(); + + const allianceBefore = player1.allianceWith(player3)!; + const allianceSpy = jest.spyOn(allianceBefore, "extend"); + const expirationBefore = allianceBefore.expiresAt(); + + game.addExecution(new AllianceExtensionExecution(player1, player3.id())); + game.executeNextTick(); + expect(allianceSpy).toHaveBeenCalledTimes(0); // both players must agree to extend + game.addExecution(new AllianceExtensionExecution(player3, player1.id())); + game.executeNextTick(); + + const allianceAfter = player1.allianceWith(player3)!; + + expect(allianceAfter.id()).toBe(allianceBefore.id()); + + const expirationAfter = allianceAfter.expiresAt(); + + expect(expirationAfter).toBeGreaterThan(expirationBefore); + expect(allianceSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/tests/BotBehavior.test.ts b/tests/BotBehavior.test.ts index 51084b509..71b14ac0b 100644 --- a/tests/BotBehavior.test.ts +++ b/tests/BotBehavior.test.ts @@ -1,3 +1,4 @@ +import { AllianceExtensionExecution } from "../src/core/execution/alliance/AllianceExtensionExecution"; import { BotBehavior } from "../src/core/execution/utils/BotBehavior"; import { AllianceRequest, @@ -5,6 +6,7 @@ import { Player, PlayerInfo, PlayerType, + Relation, Tick, } from "../src/core/game/Game"; import { PseudoRandom } from "../src/core/PseudoRandom"; @@ -149,3 +151,79 @@ describe("BotBehavior.handleAllianceRequests", () => { expect(request.reject).toHaveBeenCalled(); }); }); + +describe("BotBehavior.handleAllianceExtensionRequests", () => { + let mockGame: any; + let mockPlayer: any; + let mockAlliance: any; + let mockHuman: any; + let mockRandom: any; + let botBehavior: BotBehavior; + + beforeEach(() => { + mockGame = { addExecution: jest.fn() }; + mockHuman = { id: jest.fn(() => "human_id") }; + mockAlliance = { + onlyOneAgreedToExtend: jest.fn(() => true), + other: jest.fn(() => mockHuman), + }; + mockRandom = { chance: jest.fn() }; + + mockPlayer = { + alliances: jest.fn(() => [mockAlliance]), + relation: jest.fn(), + id: jest.fn(() => "bot_id"), + type: jest.fn(() => PlayerType.FakeHuman), + }; + + botBehavior = new BotBehavior( + mockRandom, + mockGame, + mockPlayer, + 0.5, + 0.5, + 0.2, + ); + }); + + it("should NOT request extension if onlyOneAgreedToExtend is false (no expiration yet or both already agreed)", () => { + mockAlliance.onlyOneAgreedToExtend.mockReturnValue(false); + botBehavior.handleAllianceExtensionRequests(); + expect(mockGame.addExecution).not.toHaveBeenCalled(); + }); + + it("should always extend if type Bot", () => { + mockPlayer.type.mockReturnValue(PlayerType.Bot); + botBehavior.handleAllianceExtensionRequests(); + expect(mockGame.addExecution).toHaveBeenCalledTimes(1); + expect(mockGame.addExecution.mock.calls[0][0]).toBeInstanceOf( + AllianceExtensionExecution, + ); + }); + + it("should always extend if Nation and relation is Friendly", () => { + mockPlayer.relation.mockReturnValue(Relation.Friendly); + botBehavior.handleAllianceExtensionRequests(); + expect(mockGame.addExecution).toHaveBeenCalledTimes(1); + expect(mockGame.addExecution.mock.calls[0][0]).toBeInstanceOf( + AllianceExtensionExecution, + ); + }); + + it("should extend if Nation, relation is Neutral and random chance is true", () => { + mockPlayer.relation.mockReturnValue(Relation.Neutral); + mockRandom.chance.mockReturnValue(true); + botBehavior.handleAllianceExtensionRequests(); + expect(mockGame.addExecution).toHaveBeenCalledTimes(1); + expect(mockGame.addExecution.mock.calls[0][0]).toBeInstanceOf( + AllianceExtensionExecution, + ); + }); + + it("should NOT extend if Nation, relation is Neutral and random chance is false", () => { + mockPlayer.relation.mockReturnValue(Relation.Neutral); + mockRandom.chance.mockReturnValue(false); + botBehavior.handleAllianceExtensionRequests(); + expect(mockGame.addExecution).not.toHaveBeenCalled(); + }); +});