From 2a3293a388f6747b388d618c7b68aaf7b1bf733d Mon Sep 17 00:00:00 2001 From: evanpelle Date: Mon, 28 Jul 2025 11:29:55 -0700 Subject: [PATCH] Fix non-human player never responding to alliance renewal request --- src/core/execution/ExecutionManager.ts | 6 +- .../alliance/AllianceExtensionExecution.ts | 50 +++++++++------ tests/AllianceExtensionExecution.test.ts | 64 ++++++++++++++++--- 3 files changed, 90 insertions(+), 30 deletions(-) diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 7a7ea2b95..86c6cb97d 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -105,7 +105,11 @@ export class Executor { case "build_unit": return new ConstructionExecution(player, intent.unit, intent.tile); case "allianceExtension": { - return new AllianceExtensionExecution(player, intent.recipient); + return new AllianceExtensionExecution( + this.gameID, + player, + intent.recipient, + ); } case "upgrade_structure": diff --git a/src/core/execution/alliance/AllianceExtensionExecution.ts b/src/core/execution/alliance/AllianceExtensionExecution.ts index 2469d9038..3479d4fa4 100644 --- a/src/core/execution/alliance/AllianceExtensionExecution.ts +++ b/src/core/execution/alliance/AllianceExtensionExecution.ts @@ -4,13 +4,22 @@ import { MessageType, Player, PlayerID, + PlayerType, } from "../../game/Game"; +import { PseudoRandom } from "../../PseudoRandom"; +import { GameID } from "../../Schemas"; +import { simpleHash } from "../../Util"; export class AllianceExtensionExecution implements Execution { + private random: PseudoRandom; + constructor( + gameID: GameID, private readonly from: Player, private readonly toID: PlayerID, - ) {} + ) { + this.random = new PseudoRandom(simpleHash(toID) + simpleHash(gameID)); + } init(mg: Game, ticks: number): void { if (!mg.hasPlayer(this.toID)) { @@ -36,27 +45,26 @@ export class AllianceExtensionExecution implements Execution { return; } - // Mark this player's intent to extend - alliance.addExtensionRequest(this.from); - - if (alliance.canExtend()) { - alliance.extend(); - - mg.displayMessage( - "events_display.alliance_renewed", - MessageType.ALLIANCE_ACCEPTED, - this.from.id(), - undefined, - { name: to.displayName() }, - ); - mg.displayMessage( - "events_display.alliance_renewed", - MessageType.ALLIANCE_ACCEPTED, - this.toID, - undefined, - { name: this.from.displayName() }, - ); + if (to.type() !== PlayerType.Human) { + if (!this.random.chance(1.3)) return; + } else { + // Mark this player's intent to extend + alliance.addExtensionRequest(this.from); + if (!alliance.canExtend()) return; } + + alliance.extend(); + + mg.displayMessage( + "events_display.alliance_renewed", + MessageType.ALLIANCE_ACCEPTED, + this.from.id(), + ); + mg.displayMessage( + "events_display.alliance_renewed", + MessageType.ALLIANCE_ACCEPTED, + this.toID, + ); } tick(ticks: number): void { diff --git a/tests/AllianceExtensionExecution.test.ts b/tests/AllianceExtensionExecution.test.ts index 98bd93af1..7c4a8b277 100644 --- a/tests/AllianceExtensionExecution.test.ts +++ b/tests/AllianceExtensionExecution.test.ts @@ -7,6 +7,8 @@ import { playerInfo, setup } from "./util/Setup"; let game: Game; let player1: Player; let player2: Player; +let player3: Player; +const gameID = "1b3xq"; describe("AllianceExtensionExecution", () => { beforeEach(async () => { @@ -20,19 +22,23 @@ 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); game.addExecution(new AllianceRequestExecution(player1, player2.id())); game.executeNextTick(); game.executeNextTick(); @@ -47,27 +53,69 @@ describe("AllianceExtensionExecution", () => { expect(player2.allianceWith(player1)).toBeTruthy(); const allianceBefore = player1.allianceWith(player2)!; - const expirationBefore = - allianceBefore.createdAt() + game.config().allianceDuration(); + const allianceSpy = jest.spyOn(allianceBefore, "extend"); + const expirationBefore = allianceBefore.expiresAt(); - game.addExecution(new AllianceExtensionExecution(player1, player2.id())); + game.addExecution( + new AllianceExtensionExecution(gameID, player1, player2.id()), + ); + game.executeNextTick(); + expect(allianceSpy).toHaveBeenCalledTimes(0); // both players must agree to extend + game.addExecution( + new AllianceExtensionExecution(gameID, player2, player1.id()), + ); game.executeNextTick(); const allianceAfter = player1.allianceWith(player2)!; 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); }); test("Fails gracefully if no alliance exists", () => { - game.addExecution(new AllianceExtensionExecution(player1, player2.id())); + game.addExecution( + new AllianceExtensionExecution(gameID, player1, player2.id()), + ); game.executeNextTick(); expect(player1.allianceWith(player2)).toBeFalsy(); expect(player2.allianceWith(player1)).toBeFalsy(); }); + + test("Successfully extends existing alliance between Human and non-Human", () => { + 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 expirationBefore = allianceBefore.expiresAt(); + + const exec = new AllianceExtensionExecution(gameID, player1, player3.id()); + jest.spyOn(exec["random"], "chance").mockReturnValue(true); + game.addExecution(exec); + game.executeNextTick(); + + const allianceAfter = player1.allianceWith(player3)!; + expect(allianceAfter.id()).toBe(allianceBefore.id()); + + const expirationAfter = allianceBefore.expiresAt(); + expect(expirationAfter).toBeGreaterThan(expirationBefore); + }); });