diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index 7bb9bcd51..605fc1143 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -1,6 +1,7 @@ import { Execution, Game, Player } from "../game/Game"; import { PseudoRandom } from "../PseudoRandom"; import { simpleHash } from "../Util"; +import { AllianceExtensionExecution } from "./alliance/AllianceExtensionExecution"; import { BotBehavior } from "./utils/BotBehavior"; export class BotExecution implements Execution { @@ -56,11 +57,29 @@ export class BotExecution implements Execution { return; } - this.behavior.handleAllianceRequests(); - this.behavior.handleAllianceExtensionRequests(); + this.acceptAllAllianceRequests(); this.maybeAttack(); } + private acceptAllAllianceRequests() { + // Accept all alliance requests + for (const req of this.bot.incomingAllianceRequests()) { + req.accept(); + } + + // Accept all alliance extension requests + for (const alliance of this.bot.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; + + const human = alliance.other(this.bot); + this.mg.addExecution( + new AllianceExtensionExecution(this.bot, human.id()), + ); + } + } + private maybeAttack() { if (this.behavior === null) { throw new Error("not initialized"); diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 88552570b..b82d2f707 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -18,7 +18,6 @@ import { TileRef, euclDistFN } from "../game/GameMap"; import { PseudoRandom } from "../PseudoRandom"; import { GameID } from "../Schemas"; import { boundingBoxTiles, calculateBoundingBox, simpleHash } from "../Util"; -import { AllianceRequestExecution } from "./alliance/AllianceRequestExecution"; import { ConstructionExecution } from "./ConstructionExecution"; import { MirvExecution } from "./MIRVExecution"; import { structureSpawnTileValue } from "./nation/structureSpawnTileValue"; @@ -26,12 +25,14 @@ import { NukeExecution } from "./NukeExecution"; import { SpawnExecution } from "./SpawnExecution"; import { TransportShipExecution } from "./TransportShipExecution"; import { calculateTerritoryCenter, closestTwoTiles } from "./Util"; +import { AllianceBehavior } from "./utils/AllianceBehavior"; import { BotBehavior } from "./utils/BotBehavior"; export class FakeHumanExecution implements Execution { private active = true; private random: PseudoRandom; private behavior: BotBehavior | null = null; // Shared behavior logic for both bots and fakehumans + private allianceBehavior: AllianceBehavior | null = null; private mg: Game; private player: Player | null = null; @@ -175,7 +176,7 @@ export class FakeHumanExecution implements Execution { return; } - if (this.behavior === null) { + if (this.behavior === null || this.allianceBehavior === null) { // Player is unavailable during init() this.behavior = new BotBehavior( this.random, @@ -185,6 +186,11 @@ export class FakeHumanExecution implements Execution { this.reserveRatio, this.expandRatio, ); + this.allianceBehavior = new AllianceBehavior( + this.random, + this.mg, + this.player, + ); // Send an attack on the first tick this.behavior.forceSendAttack(this.mg.terraNullius()); @@ -192,8 +198,8 @@ export class FakeHumanExecution implements Execution { } this.updateRelationsFromEmbargos(); - this.behavior.handleAllianceRequests(); - this.behavior.handleAllianceExtensionRequests(); + this.allianceBehavior.handleAllianceRequests(); + this.allianceBehavior.handleAllianceExtensionRequests(); this.handleUnits(); this.handleEmbargoesToHostileNations(); this.considerMIRV(); @@ -201,7 +207,11 @@ export class FakeHumanExecution implements Execution { } private maybeAttack() { - if (this.player === null || this.behavior === null) { + if ( + this.player === null || + this.behavior === null || + this.allianceBehavior === null + ) { throw new Error("not initialized"); } @@ -211,10 +221,13 @@ export class FakeHumanExecution implements Execution { (t) => this.mg.isLand(t) && this.mg.ownerID(t) !== this.player?.smallID(), ); - const borderingPlayers = border - .map((t) => this.mg.playerBySmallID(this.mg.ownerID(t))) - .filter((o) => o.isPlayer()) - .sort((a, b) => a.troops() - b.troops()); + const borderingPlayers = [ + ...new Set( + border + .map((t) => this.mg.playerBySmallID(this.mg.ownerID(t))) + .filter((o): o is Player => o.isPlayer()), + ), + ].sort((a, b) => a.troops() - b.troops()); const borderingFriends = borderingPlayers.filter( (o) => this.player?.isFriendly(o) === true, ); @@ -241,15 +254,7 @@ export class FakeHumanExecution implements Execution { return; } - // 5% chance to send a random alliance request - if (this.random.chance(20)) { - const toAlly = this.random.randElement(borderingEnemies); - if (this.player.canSendAllianceRequest(toAlly)) { - this.mg.addExecution( - new AllianceRequestExecution(this.player, toAlly.id()), - ); - } - } + this.allianceBehavior.maybeSendAllianceRequests(borderingEnemies); } this.behavior.assistAllies(); diff --git a/src/core/execution/utils/AllianceBehavior.ts b/src/core/execution/utils/AllianceBehavior.ts new file mode 100644 index 000000000..781b0e359 --- /dev/null +++ b/src/core/execution/utils/AllianceBehavior.ts @@ -0,0 +1,232 @@ +import { + Difficulty, + Game, + Player, + PlayerType, + Relation, +} from "../../game/Game"; +import { PseudoRandom } from "../../PseudoRandom"; +import { AllianceExtensionExecution } from "../alliance/AllianceExtensionExecution"; +import { AllianceRequestExecution } from "../alliance/AllianceRequestExecution"; + +export class AllianceBehavior { + constructor( + private random: PseudoRandom, + private game: Game, + private player: Player, + ) {} + + handleAllianceRequests() { + for (const req of this.player.incomingAllianceRequests()) { + if (this.getAllianceRequestDecision(req.requestor())) { + req.accept(); + } else { + req.reject(); + } + } + } + + 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 / nation already agreed to extend + if (!alliance.onlyOneAgreedToExtend()) continue; + + const human = alliance.other(this.player); + if (!this.getAllianceRequestDecision(human)) continue; + + this.game.addExecution( + new AllianceExtensionExecution(this.player, human.id()), + ); + } + } + + maybeSendAllianceRequests(borderingEnemies: Player[]) { + // Impossible / smart nations know the strategic value of alliances and thus send more requests + const { difficulty } = this.game.config().gameConfig(); + const shouldSendAllianceRequest = () => { + switch (difficulty) { + case Difficulty.Easy: + return this.random.chance(35); + case Difficulty.Medium: + return this.random.chance(30); + case Difficulty.Hard: + return this.random.chance(25); + default: + return this.random.chance(20); + } + }; + + // Only easy nations are allowed to send alliance requests to bots + const isAcceptablePlayerType = (p: Player) => + (p.type() === PlayerType.Bot && difficulty === Difficulty.Easy) || + p.type() !== PlayerType.Bot; + + for (const enemy of borderingEnemies) { + if ( + shouldSendAllianceRequest() && + isAcceptablePlayerType(enemy) && + this.player.canSendAllianceRequest(enemy) && + this.getAllianceRequestDecision(enemy) + ) { + this.game.addExecution( + new AllianceRequestExecution(this.player, enemy.id()), + ); + } + } + } + + private getAllianceRequestDecision(otherPlayer: Player): boolean { + // Easy (dumb) nations sometimes get confused and accept/reject randomly (Just like dumb humans do) + if (this.isConfused()) { + return this.random.chance(2); + } + // Nearly always reject traitors + if (otherPlayer.isTraitor() && this.random.nextInt(0, 100) >= 10) { + return false; + } + // Before caring about the relation, first check if the otherPlayer is a threat + // Easy (dumb) nations are blinded by hatred, they don't care about threats, they care about the relation + // Impossible (smart) nations on the other hand are analyzing the facts + if (this.isAlliancePartnerThreat(otherPlayer)) { + return true; + } + // Reject if relation is bad + if (this.player.relation(otherPlayer) < Relation.Neutral) { + return false; + } + // Maybe accept if relation is friendly + if (this.isAlliancePartnerFriendly(otherPlayer)) { + return true; + } + // Reject if we already have some alliances, we don't want to ally with the entire map + if (this.checkAlreadyEnoughAlliances(otherPlayer)) { + return false; + } + // Accept if we are similarly strong + return this.isAlliancePartnerSimilarlyStrong(otherPlayer); + } + + private isConfused(): boolean { + const { difficulty } = this.game.config().gameConfig(); + switch (difficulty) { + case Difficulty.Easy: + return this.random.chance(10); // 10% chance to be confused on easy + case Difficulty.Medium: + return this.random.chance(20); // 5% chance to be confused on medium + case Difficulty.Hard: + return this.random.chance(40); // 2.5% chance to be confused on hard + default: + return false; // No confusion on impossible + } + } + + private isAlliancePartnerThreat(otherPlayer: Player): boolean { + const { difficulty } = this.game.config().gameConfig(); + switch (difficulty) { + case Difficulty.Easy: + // On easy we are very dumb, we don't see anybody as a threat + return false; + case Difficulty.Medium: + // On medium we just see players with much more troops as a threat + return otherPlayer.troops() > this.player.troops() * 2.5; + case Difficulty.Hard: + // On hard we are smarter, we check for maxTroops to see the actual strength + return ( + otherPlayer.troops() > this.player.troops() && + this.game.config().maxTroops(otherPlayer) > + this.game.config().maxTroops(this.player) * 2 + ); + default: { + // On impossible we check for multiple factors and try to not mess with stronger players (we want to steamroll over weaklings) + const otherHasMoreTroops = + otherPlayer.troops() > this.player.troops() * 1.5; + const otherHasMoreMaxTroops = + otherPlayer.troops() > this.player.troops() && + this.game.config().maxTroops(otherPlayer) > + this.game.config().maxTroops(this.player) * 1.5; + const otherHasMoreTiles = + otherPlayer.troops() > this.player.troops() && + otherPlayer.numTilesOwned() > this.player.numTilesOwned() * 1.5; + return otherHasMoreTroops || otherHasMoreMaxTroops || otherHasMoreTiles; + } + } + } + + private checkAlreadyEnoughAlliances(otherPlayer: Player): boolean { + const { difficulty } = this.game.config().gameConfig(); + switch (difficulty) { + case Difficulty.Easy: + return false; // On easy we never think we have enough alliances + case Difficulty.Medium: + return this.player.alliances().length >= this.random.nextInt(5, 8); + default: { + // On hard and impossible we try to not ally with all our neighbors (If we have 3+ neighbors) + const borderingPlayers = this.player + .neighbors() + .filter( + (n): n is Player => n.isPlayer() && n.type() !== PlayerType.Bot, + ); + const borderingFriends = borderingPlayers.filter( + (o) => this.player?.isFriendly(o) === true, + ); + if ( + borderingPlayers.length >= 3 && + borderingPlayers.includes(otherPlayer) + ) { + return borderingPlayers.length <= borderingFriends.length + 1; + } + if (difficulty === Difficulty.Hard) { + return this.player.alliances().length >= this.random.nextInt(3, 6); + } + return this.player.alliances().length >= this.random.nextInt(2, 5); + } + } + } + + private isAlliancePartnerFriendly(otherPlayer: Player): boolean { + const { difficulty } = this.game.config().gameConfig(); + switch (difficulty) { + case Difficulty.Easy: + case Difficulty.Medium: + return this.player.relation(otherPlayer) === Relation.Friendly; + case Difficulty.Hard: + return ( + this.player.relation(otherPlayer) === Relation.Friendly && + this.random.nextInt(0, 100) >= 17 + ); + default: + return ( + this.player.relation(otherPlayer) === Relation.Friendly && + this.random.nextInt(0, 100) >= 33 + ); + } + } + + // It would make a lot of sense to use nextFloat here, but "there's a chance floats can cause desyncs" + private isAlliancePartnerSimilarlyStrong(otherPlayer: Player): boolean { + const { difficulty } = this.game.config().gameConfig(); + switch (difficulty) { + case Difficulty.Easy: + return ( + otherPlayer.troops() > + this.player.troops() * (this.random.nextInt(60, 70) / 100) + ); + case Difficulty.Medium: + return ( + otherPlayer.troops() > + this.player.troops() * (this.random.nextInt(70, 80) / 100) + ); + case Difficulty.Hard: + return ( + otherPlayer.troops() > + this.player.troops() * (this.random.nextInt(75, 85) / 100) + ); + default: + return ( + otherPlayer.troops() > + this.player.troops() * (this.random.nextInt(80, 90) / 100) + ); + } + } +} diff --git a/src/core/execution/utils/BotBehavior.ts b/src/core/execution/utils/BotBehavior.ts index 1f9843832..2d79275c3 100644 --- a/src/core/execution/utils/BotBehavior.ts +++ b/src/core/execution/utils/BotBehavior.ts @@ -1,5 +1,4 @@ import { - AllianceRequest, Difficulty, Game, Player, @@ -14,7 +13,6 @@ import { calculateBoundingBoxCenter, flattenedEmojiTable, } from "../../Util"; -import { AllianceExtensionExecution } from "../alliance/AllianceExtensionExecution"; import { AttackExecution } from "../AttackExecution"; import { EmojiExecution } from "../EmojiExecution"; import { TransportShipExecution } from "../TransportShipExecution"; @@ -41,38 +39,6 @@ export class BotBehavior { private expandRatio: number, ) {} - handleAllianceRequests() { - for (const req of this.player.incomingAllianceRequests()) { - if (shouldAcceptAllianceRequest(this.player, req)) { - req.accept(); - } else { - req.reject(); - } - } - } - - 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)); @@ -571,19 +537,3 @@ export class BotBehavior { ); } } - -function shouldAcceptAllianceRequest(player: Player, request: AllianceRequest) { - if (player.relation(request.requestor()) < Relation.Neutral) { - return false; // Reject if hasMalice - } - if (request.requestor().isTraitor()) { - return false; // Reject if isTraitor - } - if (request.requestor().numTilesOwned() > player.numTilesOwned() * 3) { - return true; // Accept if requestorIsMuchLarger - } - if (request.requestor().alliances().length >= 3) { - return false; // Reject if tooManyAlliances - } - return true; // Accept otherwise -} diff --git a/tests/AllianceBehaviour.test.ts b/tests/AllianceBehaviour.test.ts new file mode 100644 index 000000000..45ad6ff44 --- /dev/null +++ b/tests/AllianceBehaviour.test.ts @@ -0,0 +1,171 @@ +import { AllianceBehavior } from "../src/core/execution/utils/AllianceBehavior"; +import { + AllianceRequest, + Game, + Player, + PlayerInfo, + PlayerType, + Tick, +} from "../src/core/game/Game"; +import { PseudoRandom } from "../src/core/PseudoRandom"; +import { setup } from "./util/Setup"; + +let game: Game; +let player: Player; +let requestor: Player; +let allianceBehavior: AllianceBehavior; + +describe("AllianceBehavior.handleAllianceRequests", () => { + beforeEach(async () => { + game = await setup("big_plains", { + infiniteGold: true, + instantBuild: true, + }); + + const playerInfo = new PlayerInfo( + "player_id", + PlayerType.Bot, + null, + "player_id", + ); + const requestorInfo = new PlayerInfo( + "requestor_id", + PlayerType.Human, + null, + "requestor_id", + ); + + game.addPlayer(playerInfo); + game.addPlayer(requestorInfo); + + player = game.player("player_id"); + requestor = game.player("requestor_id"); + + // Use a fixed random seed for deterministic behavior + const random = new PseudoRandom(46); + + allianceBehavior = new AllianceBehavior(random, game, player); + }); + + function setupAllianceRequest({ + isTraitor = false, + relationDelta = 2, + numTilesPlayer = 10, + numTilesRequestor = 10, + alliancesCount = 0, + } = {}) { + if (isTraitor) requestor.markTraitor(); + + player.updateRelation(requestor, relationDelta); + requestor.updateRelation(player, relationDelta); + + game.map().forEachTile((tile) => { + if (game.map().isLand(tile)) { + if (numTilesPlayer > 0) { + player.conquer(tile); + numTilesPlayer--; + } else if (numTilesRequestor > 0) { + requestor.conquer(tile); + numTilesRequestor--; + } + } + }); + + jest.spyOn(player, "alliances").mockReturnValue(new Array(alliancesCount)); + + const mockRequest = { + requestor: () => requestor, + recipient: () => player, + createdAt: () => 0 as unknown as Tick, + accept: jest.fn(), + reject: jest.fn(), + } as unknown as AllianceRequest; + + jest + .spyOn(player, "incomingAllianceRequests") + .mockReturnValue([mockRequest]); + + return mockRequest; + } + + test("should accept alliance when all conditions are met", () => { + const request = setupAllianceRequest({}); + + allianceBehavior.handleAllianceRequests(); + + expect(request.accept).toHaveBeenCalled(); + expect(request.reject).not.toHaveBeenCalled(); + }); + + test("should reject alliance if requestor is a traitor", () => { + const request = setupAllianceRequest({ isTraitor: true }); + + allianceBehavior.handleAllianceRequests(); + + expect(request.accept).not.toHaveBeenCalled(); + expect(request.reject).toHaveBeenCalled(); + }); + + test("should reject alliance if relation is hostile", () => { + const request = setupAllianceRequest({ relationDelta: -2 }); + + allianceBehavior.handleAllianceRequests(); + + expect(request.accept).not.toHaveBeenCalled(); + expect(request.reject).toHaveBeenCalled(); + }); + + test("should accept alliance if requestor is much larger (> 3 times size of recipient)", () => { + const request = setupAllianceRequest({ + numTilesRequestor: 40, + }); + + allianceBehavior.handleAllianceRequests(); + + expect(request.accept).toHaveBeenCalled(); + expect(request.reject).not.toHaveBeenCalled(); + }); + + test("should reject alliance if player has too many alliances", () => { + const request = setupAllianceRequest({ alliancesCount: 10 }); + + allianceBehavior.handleAllianceRequests(); + + expect(request.accept).not.toHaveBeenCalled(); + expect(request.reject).toHaveBeenCalled(); + }); +}); + +describe("AllianceBehavior.handleAllianceExtensionRequests", () => { + let mockGame: any; + let mockPlayer: any; + let mockAlliance: any; + let mockHuman: any; + let mockRandom: any; + let allianceBehavior: AllianceBehavior; + + 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), + }; + + allianceBehavior = new AllianceBehavior(mockRandom, mockGame, mockPlayer); + }); + + it("should NOT request extension if onlyOneAgreedToExtend is false (no expiration yet or both already agreed)", () => { + mockAlliance.onlyOneAgreedToExtend.mockReturnValue(false); + allianceBehavior.handleAllianceExtensionRequests(); + expect(mockGame.addExecution).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/BotBehavior.test.ts b/tests/BotBehavior.test.ts index eaa7f0571..377d3192c 100644 --- a/tests/BotBehavior.test.ts +++ b/tests/BotBehavior.test.ts @@ -1,233 +1,8 @@ -import { AllianceExtensionExecution } from "../src/core/execution/alliance/AllianceExtensionExecution"; import { BotBehavior } from "../src/core/execution/utils/BotBehavior"; -import { - AllianceRequest, - Game, - Player, - PlayerInfo, - PlayerType, - Relation, - Tick, -} from "../src/core/game/Game"; +import { Game, Player, PlayerInfo, PlayerType } from "../src/core/game/Game"; import { PseudoRandom } from "../src/core/PseudoRandom"; import { setup } from "./util/Setup"; -let game: Game; -let player: Player; -let requestor: Player; -let botBehavior: BotBehavior; - -describe("BotBehavior.handleAllianceRequests", () => { - beforeEach(async () => { - game = await setup("big_plains", { - infiniteGold: true, - instantBuild: true, - }); - - const playerInfo = new PlayerInfo( - "player_id", - PlayerType.Bot, - null, - "player_id", - ); - const requestorInfo = new PlayerInfo( - "requestor_id", - PlayerType.Human, - null, - "requestor_id", - ); - - game.addPlayer(playerInfo); - game.addPlayer(requestorInfo); - - player = game.player("player_id"); - requestor = game.player("requestor_id"); - - const random = new PseudoRandom(42); - - botBehavior = new BotBehavior(random, game, player, 0.5, 0.5, 0.2); - }); - - function setupAllianceRequest({ - isTraitor = false, - relationDelta = 2, - numTilesPlayer = 10, - numTilesRequestor = 10, - alliancesCount = 0, - } = {}) { - if (isTraitor) requestor.markTraitor(); - - player.updateRelation(requestor, relationDelta); - requestor.updateRelation(player, relationDelta); - - game.map().forEachTile((tile) => { - if (game.map().isLand(tile)) { - if (numTilesPlayer > 0) { - player.conquer(tile); - numTilesPlayer--; - } else if (numTilesRequestor > 0) { - requestor.conquer(tile); - numTilesRequestor--; - } - } - }); - - jest - .spyOn(requestor, "alliances") - .mockReturnValue(new Array(alliancesCount)); - - const mockRequest = { - requestor: () => requestor, - recipient: () => player, - createdAt: () => 0 as unknown as Tick, - accept: jest.fn(), - reject: jest.fn(), - } as unknown as AllianceRequest; - - jest - .spyOn(player, "incomingAllianceRequests") - .mockReturnValue([mockRequest]); - - return mockRequest; - } - - test("should accept alliance when all conditions are met", () => { - const request = setupAllianceRequest({}); - - botBehavior.handleAllianceRequests(); - - expect(request.accept).toHaveBeenCalled(); - expect(request.reject).not.toHaveBeenCalled(); - }); - - test("should reject alliance if requestor is a traitor", () => { - const request = setupAllianceRequest({ isTraitor: true }); - - botBehavior.handleAllianceRequests(); - - expect(request.accept).not.toHaveBeenCalled(); - expect(request.reject).toHaveBeenCalled(); - }); - - test("should reject alliance if relation is malicious", () => { - const request = setupAllianceRequest({ relationDelta: -2 }); - - botBehavior.handleAllianceRequests(); - - expect(request.accept).not.toHaveBeenCalled(); - expect(request.reject).toHaveBeenCalled(); - }); - - test("should accept alliance if requestor is much larger (> 3 times size of recipient) and has too many alliances (>= 3)", () => { - const request = setupAllianceRequest({ - numTilesRequestor: 40, - alliancesCount: 4, - }); - - botBehavior.handleAllianceRequests(); - - expect(request.accept).toHaveBeenCalled(); - expect(request.reject).not.toHaveBeenCalled(); - }); - - test("should accept alliance if requestor is much larger (> 3 times size of recipient) and does not have too many alliances (< 3)", () => { - const request = setupAllianceRequest({ - numTilesRequestor: 40, - alliancesCount: 2, - }); - - botBehavior.handleAllianceRequests(); - - expect(request.accept).toHaveBeenCalled(); - expect(request.reject).not.toHaveBeenCalled(); - }); - - test("should reject alliance if requestor is acceptably small (<= 3 times size of recipient) and has too many alliances (>= 3)", () => { - const request = setupAllianceRequest({ alliancesCount: 3 }); - - botBehavior.handleAllianceRequests(); - - expect(request.accept).not.toHaveBeenCalled(); - 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(); - }); -}); - describe("BotBehavior Attack Behavior", () => { let game: Game; let bot: Player;