mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-10 06:34:36 +00:00
dfe33a05e9
## 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
233 lines
8.2 KiB
TypeScript
233 lines
8.2 KiB
TypeScript
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)
|
|
);
|
|
}
|
|
}
|
|
}
|