bugfix: Nations rarely launch nukes (#1860)

## Description:

Simplify nation enemy selection to make nations more likely to launch
nukes.

Partially fixes #1855 by addressing a v24 regression in nation behavior.

## 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
This commit is contained in:
Scott Anderson
2025-08-28 02:16:22 -04:00
committed by evanpelle
parent 8308d7f1e7
commit d83a66196a
3 changed files with 73 additions and 25 deletions
+2 -2
View File
@@ -20,8 +20,8 @@ export class BotExecution implements Execution {
this.random = new PseudoRandom(simpleHash(bot.id()));
this.attackRate = this.random.nextInt(40, 80);
this.attackTick = this.random.nextInt(0, this.attackRate);
this.triggerRatio = this.random.nextInt(60, 90) / 100;
this.reserveRatio = this.random.nextInt(20, 30) / 100;
this.triggerRatio = this.random.nextInt(50, 60) / 100;
this.reserveRatio = this.random.nextInt(30, 40) / 100;
this.expandRatio = this.random.nextInt(10, 20) / 100;
}
+4 -15
View File
@@ -53,9 +53,9 @@ export class FakeHumanExecution implements Execution {
);
this.attackRate = this.random.nextInt(40, 80);
this.attackTick = this.random.nextInt(0, this.attackRate);
this.triggerRatio = this.random.nextInt(60, 90) / 100;
this.reserveRatio = this.random.nextInt(30, 60) / 100;
this.expandRatio = this.random.nextInt(15, 25) / 100;
this.triggerRatio = this.random.nextInt(50, 60) / 100;
this.reserveRatio = this.random.nextInt(30, 40) / 100;
this.expandRatio = this.random.nextInt(10, 20) / 100;
}
init(mg: Game) {
@@ -223,23 +223,12 @@ export class FakeHumanExecution implements Execution {
const toAlly = this.random.randElement(enemies);
if (this.player.canSendAllianceRequest(toAlly)) {
this.player.createAllianceRequest(toAlly);
return;
}
}
// 50-50 attack weakest player vs random player
const toAttack = this.random.chance(2)
? enemies[0]
: this.random.randElement(enemies);
if (this.shouldAttack(toAttack)) {
this.behavior.sendAttack(toAttack);
return;
}
this.behavior.forgetOldEnemies();
this.behavior.assistAllies();
const enemy = this.behavior.selectEnemy();
const enemy = this.behavior.selectEnemy(enemies);
if (!enemy) return;
this.maybeSendEmoji(enemy);
this.maybeSendNuke(enemy);
+67 -8
View File
@@ -1,5 +1,6 @@
import {
AllianceRequest,
Difficulty,
Game,
Player,
PlayerType,
@@ -71,11 +72,48 @@ export class BotBehavior {
this.game.addExecution(new EmojiExecution(this.player, player.id(), emoji));
}
private setNewEnemy(newEnemy: Player | null) {
private setNewEnemy(newEnemy: Player | null, force = false) {
if (newEnemy !== null && !force && !this.shouldAttack(newEnemy)) return;
this.enemy = newEnemy;
this.enemyUpdated = this.game.ticks();
}
private shouldAttack(other: Player): boolean {
if (this.player === null) throw new Error("not initialized");
if (this.player.isOnSameTeam(other)) {
return false;
}
if (this.player.isFriendly(other)) {
if (this.shouldDiscourageAttack(other)) {
return this.random.chance(200);
}
return this.random.chance(50);
} else {
if (this.shouldDiscourageAttack(other)) {
return this.random.chance(4);
}
return true;
}
}
private shouldDiscourageAttack(other: Player) {
if (other.isTraitor()) {
return false;
}
const { difficulty } = this.game.config().gameConfig();
if (
difficulty === Difficulty.Hard ||
difficulty === Difficulty.Impossible
) {
return false;
}
if (other.type() !== PlayerType.Human) {
return false;
}
// Only discourage attacks on Humans who are not traitors on easy or medium difficulty.
return true;
}
private clearEnemy() {
this.enemy = null;
}
@@ -87,7 +125,13 @@ export class BotBehavior {
}
}
private hasSufficientTroops(): boolean {
private hasReserveRatioTroops(): boolean {
const maxTroops = this.game.config().maxTroops(this.player);
const ratio = this.player.troops() / maxTroops;
return ratio >= this.reserveRatio;
}
private hasTriggerRatioTroops(): boolean {
const maxTroops = this.game.config().maxTroops(this.player);
const ratio = this.player.troops() / maxTroops;
return ratio >= this.triggerRatio;
@@ -104,7 +148,7 @@ export class BotBehavior {
largestAttacker = attack.attacker();
}
if (largestAttacker !== undefined) {
this.setNewEnemy(largestAttacker);
this.setNewEnemy(largestAttacker, true);
}
}
@@ -140,10 +184,13 @@ export class BotBehavior {
}
}
selectEnemy(): Player | null {
selectEnemy(enemies: Player[]): Player | null {
if (this.enemy === null) {
// Save up troops until we reach the trigger ratio
if (!this.hasSufficientTroops()) return null;
// Save up troops until we reach the reserve ratio
if (!this.hasReserveRatioTroops()) return null;
// Maybe save up troops until we reach the trigger ratio
if (!this.hasTriggerRatioTroops() && !this.random.chance(10)) return null;
// Prefer neighboring bots
const bots = this.player
@@ -171,11 +218,13 @@ export class BotBehavior {
// Retaliate against incoming attacks
if (this.enemy === null) {
// Only after clearing bots
this.checkIncomingAttacks();
}
// Select the most hated player
if (this.enemy === null) {
if (this.enemy === null && this.random.chance(2)) {
// 50% chance
const mostHated = this.player.allRelationsSorted()[0];
if (
mostHated !== undefined &&
@@ -184,6 +233,16 @@ export class BotBehavior {
this.setNewEnemy(mostHated.player);
}
}
// Select the weakest player
if (this.enemy === null && enemies.length > 0) {
this.setNewEnemy(enemies[0]);
}
// Select a random player
if (this.enemy === null && enemies.length > 0) {
this.setNewEnemy(this.random.randElement(enemies));
}
}
// Sanity check, don't attack our allies or teammates
@@ -193,7 +252,7 @@ export class BotBehavior {
selectRandomEnemy(): Player | TerraNullius | null {
if (this.enemy === null) {
// Save up troops until we reach the trigger ratio
if (!this.hasSufficientTroops()) return null;
if (!this.hasTriggerRatioTroops()) return null;
// Choose a new enemy randomly
const neighbors = this.player.neighbors();