mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-10 05:34:36 +00:00
Cleanup nations (Part 1) 🧹 (#2637)
## Description: 1. Using the wording `"Nation"`, `"FakeHuman"` and `"NPC"` at the same time is confusing. So I renamed every mention of `"FakeHuman"` and `"NPC"` in the entire project to `"Nation"`. Just like they are called ingame. 2. `BotBehavior.ts` was originally intended for sharing the logic between nations and bots. But at the moment, the logic there isn't really shared and it's basically just about attacking. So I renamed `BotBehavior.ts` to `AiAttackBehavior.ts`. I use "Ai" to indicate that this file is used by bots AND nations. 3. Moved `execuction/utils/AllianceBehavior.ts` to `execuction/nation/NationAllianceBehavior.ts` to make sure everybody understands that this file is not about alliances in general. It's just about nations and how they handle alliances. 4. Removed `difficultyModifier` from `DefaultConfig`. It's unused and I think we usually want to finetune the difficulty instead of using that method. 5. Added `assertNever` in all `switch (difficulty)` default cases. ## 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:
+7
-3
@@ -9,6 +9,7 @@ import {
|
||||
} from "../../game/Game";
|
||||
import { PseudoRandom } from "../../PseudoRandom";
|
||||
import {
|
||||
assertNever,
|
||||
boundingBoxCenter,
|
||||
calculateBoundingBoxCenter,
|
||||
flattenedEmojiTable,
|
||||
@@ -26,7 +27,7 @@ const EMOJI_TARGET_ME = (["🥺", "💀"] as const).map(emojiId);
|
||||
const EMOJI_TARGET_ALLY = (["🕊️", "👎"] as const).map(emojiId);
|
||||
const EMOJI_HECKLE = (["🤡", "😡"] as const).map(emojiId);
|
||||
|
||||
export class BotBehavior {
|
||||
export class AiAttackBehavior {
|
||||
private botAttackTroopsSent: number = 0;
|
||||
private readonly lastEmojiSent = new Map<Player, Tick>();
|
||||
|
||||
@@ -233,8 +234,11 @@ export class BotBehavior {
|
||||
case Difficulty.Hard:
|
||||
return 4;
|
||||
// On impossible difficulty, attack as much bots as possible in parallel
|
||||
default:
|
||||
case Difficulty.Impossible: {
|
||||
return 100;
|
||||
}
|
||||
default:
|
||||
assertNever(difficulty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,7 +384,7 @@ export class BotBehavior {
|
||||
if (!neighbor.isPlayer()) continue;
|
||||
if (this.player.isFriendly(neighbor)) continue;
|
||||
if (
|
||||
neighbor.type() === PlayerType.FakeHuman ||
|
||||
neighbor.type() === PlayerType.Nation ||
|
||||
neighbor.type() === PlayerType.Human
|
||||
) {
|
||||
if (this.random.chance(2) || difficulty === Difficulty.Easy) {
|
||||
@@ -1,232 +0,0 @@
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user