Nations now gang up on players and prioritize traitors/AFK players 🤖 (+ other improvements) (#2730)

## Description:

- **Nations now specifically target AFK players, traitors, and players
who are already being attacked (they gang up on them).** Depends on the
difficulty.
- Added ally assistance directly to the attack order (instead of calling
it separately beforehand).
- Added ally assistance to `findBestNukeTarget`.
- Removed some checks from `findBestNukeTarget` (not necessary; better
checks will follow in a dedicated nuking PR).
- Relation updates on attack now depend on difficulty (makes Easy &
Medium nations a bit easier).
- On betrayal, every nation in the game previously received a –40
relation penalty toward the betrayer. That was too extreme, so now this
only applies only to neighbors.
- Nations send fewer alliance requests now; it felt like too many
before.
- In team games, nations may now reject alliance requests more often
(depending on difficulty).
- To ensure there are enough non-friendly players to stop the crown with
nukes, nations may now reject alliance requests if the other player has
too many alliances (on Hard and Impossible difficulty).
- Rebalanced nation emoji usage a bit.
- Nations may now send an emoji when they get MIRVed.

## 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

---------

Co-authored-by: iamlewis <lewismmmm@gmail.com>
This commit is contained in:
FloPinguin
2025-12-29 10:16:26 -08:00
committed by GitHub
co-authored by iamlewis
parent f1561df470
commit d321c08d92
7 changed files with 248 additions and 63 deletions
@@ -1,6 +1,7 @@
import {
Difficulty,
Game,
GameMode,
Player,
PlayerType,
Relation,
@@ -59,7 +60,7 @@ export class NationAllianceBehavior {
for (const enemy of borderingEnemies) {
if (
this.random.chance(20) &&
this.random.chance(30) &&
isAcceptablePlayerType(enemy) &&
this.player.canSendAllianceRequest(enemy) &&
this.getAllianceDecision(enemy, false)
@@ -86,18 +87,27 @@ export class NationAllianceBehavior {
}
return false;
}
// Reject if otherPlayer has allied with 50% or more of all players (Hard and Impossible only)
// To make sure there are enough non-friendly players in the game to stop the crown with nukes
if (this.hasTooManyAlliances(otherPlayer)) {
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)) {
if (!isResponse && this.random.chance(3)) {
if (!isResponse && this.random.chance(6)) {
this.emojiBehavior.sendEmoji(otherPlayer, EMOJI_SCARED_OF_THREAT);
}
if (isResponse && this.random.chance(3)) {
if (isResponse && this.random.chance(6)) {
this.emojiBehavior.sendEmoji(otherPlayer, EMOJI_LOVE);
}
return true;
}
// Maybe reject if we are in a team game (allying makes less sense there)
if (this.shouldRejectInTeamGame()) {
return false;
}
// Reject if relation is bad
if (this.player.relation(otherPlayer) < Relation.Neutral) {
if (isResponse && this.random.chance(3)) {
@@ -124,6 +134,21 @@ export class NationAllianceBehavior {
return this.isAlliancePartnerSimilarlyStrong(otherPlayer);
}
private hasTooManyAlliances(otherPlayer: Player): boolean {
const { difficulty } = this.game.config().gameConfig();
if (
difficulty !== Difficulty.Hard &&
difficulty !== Difficulty.Impossible
) {
return false;
}
const totalPlayers = this.game.players().length;
const otherPlayerAlliances = otherPlayer.alliances().length;
return otherPlayerAlliances >= totalPlayers * 0.5;
}
private isConfused(): boolean {
const { difficulty } = this.game.config().gameConfig();
switch (difficulty) {
@@ -207,6 +232,26 @@ export class NationAllianceBehavior {
}
}
private shouldRejectInTeamGame(): boolean {
if (this.game.config().gameConfig().gameMode !== GameMode.Team) {
return false;
}
const { difficulty } = this.game.config().gameConfig();
switch (difficulty) {
case Difficulty.Easy:
return false; // 0% chance to reject on easy
case Difficulty.Medium:
return this.random.nextInt(0, 100) < 20; // 20% chance to reject on medium
case Difficulty.Hard:
return this.random.nextInt(0, 100) < 40; // 40% chance to reject on hard
case Difficulty.Impossible:
return this.random.nextInt(0, 100) < 60; // 60% chance to reject on impossible
default:
assertNever(difficulty);
}
}
private checkAlreadyEnoughAlliances(otherPlayer: Player): boolean {
const { difficulty } = this.game.config().gameConfig();
switch (difficulty) {
@@ -15,9 +15,7 @@ import { EmojiExecution } from "../EmojiExecution";
const emojiId = (e: (typeof flattenedEmojiTable)[number]) =>
flattenedEmojiTable.indexOf(e);
export const EMOJI_ASSIST_ACCEPT = (["👍", "⛵", "🤝", "🎯"] as const).map(
emojiId,
);
export const EMOJI_ASSIST_ACCEPT = (["👍", "🤝", "🎯"] as const).map(emojiId);
export const EMOJI_ASSIST_RELATION_TOO_LOW = (["🥱", "🤦‍♂️"] as const).map(
emojiId,
);
@@ -34,7 +32,7 @@ export const EMOJI_LOVE = (["❤️", "😊", "🥰"] as const).map(emojiId);
export const EMOJI_CONFUSED = (["❓", "🤡"] as const).map(emojiId);
export const EMOJI_BRAG = (["👑", "🥇", "💪"] as const).map(emojiId);
export const EMOJI_CHARM_ALLIES = (["🤝", "😇", "💪"] as const).map(emojiId);
export const EMOJI_CLOWN = (["🤡"] as const).map(emojiId);
export const EMOJI_CLOWN = (["🤡", "🤦‍♂️"] as const).map(emojiId);
export const EMOJI_RAT = (["🐀"] as const).map(emojiId);
export const EMOJI_OVERWHELMED = (
["💀", "🆘", "😱", "🥺", "😭", "😞", "🫡", "👋"] as const
@@ -67,7 +65,7 @@ export class NationEmojiBehavior {
}
private checkOverwhelmedByAttacks(): void {
if (!this.random.chance(4)) return;
if (!this.random.chance(16)) return;
const incomingAttacks = this.player.incomingAttacks();
if (incomingAttacks.length === 0) return;
@@ -85,7 +83,7 @@ export class NationEmojiBehavior {
}
private checkVerySmallAttack(): void {
if (!this.random.chance(4)) return;
if (!this.random.chance(8)) return;
const incomingAttacks = this.player.incomingAttacks();
if (incomingAttacks.length === 0) return;
@@ -175,7 +173,7 @@ export class NationEmojiBehavior {
// Brag with our crown
private brag(): void {
if (!this.random.chance(100)) return;
if (!this.random.chance(300)) return;
const sorted = this.game
.players()
@@ -218,6 +216,7 @@ export class NationEmojiBehavior {
}
private findRat(): void {
if (this.game.ticks() < 6000) return; // Ignore first 10 minutes (everybody is small in the early game)
if (!this.random.chance(10000)) return;
const totalLand = this.game.numLandTiles();
@@ -340,3 +339,19 @@ export function respondToEmoji(
);
}
}
export function respondToMIRV(
game: Game,
random: PseudoRandom,
mirvTarget: Player,
) {
if (!random.chance(8)) return;
game.addExecution(
new EmojiExecution(
mirvTarget,
AllPlayers,
random.randElement(EMOJI_OVERWHELMED),
),
);
}
@@ -12,7 +12,11 @@ import { PseudoRandom } from "../../PseudoRandom";
import { assertNever } from "../../Util";
import { MirvExecution } from "../MIRVExecution";
import { calculateTerritoryCenter } from "../Util";
import { EMOJI_NUKE, NationEmojiBehavior } from "./NationEmojiBehavior";
import {
EMOJI_NUKE,
NationEmojiBehavior,
respondToMIRV,
} from "./NationEmojiBehavior";
export class NationMIRVBehavior {
constructor(
@@ -258,6 +262,7 @@ export class NationMIRVBehavior {
if (centerTile && this.player.canBuild(UnitType.MIRV, centerTile)) {
this.game.addExecution(new MirvExecution(this.player, centerTile));
this.emojiBehavior.sendEmoji(AllPlayers, EMOJI_NUKE);
respondToMIRV(this.game, this.random, enemy);
}
}