mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:00:43 +00:00
Improved the nation alliance request logic 🤝 Massive upgrade to singleplayer fun (#2606)
## Response to alliance requests Previously the way nations responded to alliance requests was quite simple / boring / exploitable. Basically you couldn't ally them if you had a bad relation with them, or if you had too many alliances. Otherwise they would just take it. Now there is a **complete decision tree which is based on the difficulty**. The nations should also feel more human now. For example, just like humans, nations will now consider to take an alliance even if you have a bad relation with them (If you are a threat). Also, nations no longer check if YOU have too many alliances. Now they do what humans do: Check if THEY have too many alliances (they want to be able to attack somebody). Another big change is the default case: Previously it was just `return true`. Now it's `return isAlliancePartnerSimilarlyStrong`. So they do what humans do: Take a quick look at their troop count before allying them. ## Sending alliance requests Previously alliance requests were sent randomly. Quite boring. Now we use the same decision tree as for responding. ## Alliance extension requests They also use the same decision tree. ## Tests Tested it a lot in singleplayer. I have planned to add unit tests for all the nation/bot stuff in the upcoming cleanup phase. ## 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: FloPinguin
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
+1
-226
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user