Improve nations 🤖 (#3206)

## Description:

- `AiAttackBehavior`: Because bots delete stolen structures now, nations
prioritize attacking bots with structures
- `NationMIRVBehavior`: Nations no longer MIRV enemies who already got
MIRVed in the last 30 seconds. Some humans complained about getting
double-MIRVed by nations. And in games with very high starting gold, ALL
nations MIRVed the same player (stop steamroll logic).
- `NationAllianceBehavior`: Fixes a comparison logic bug (Thanks to
Deshack)
- `NationNukeBehavior.ts`: Little atom bomb perceived cost balance
change
- `MIRVExecution`: To make sure the MIRVing nations are attacking the
MIRVed nations (even if they don't share a border), the relation gets
updated in both directions now.
- `SinglePlayerModal` & `HostLobbyModal`: Update the default difficulty
to "Medium" (to synchronize the defaults with the public game default)

## 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:
FloPinguin
2026-02-16 11:13:07 -08:00
committed by GitHub
parent 4bc168dffb
commit 4e62114ea0
5 changed files with 69 additions and 11 deletions
@@ -87,7 +87,7 @@ export class NationAllianceBehavior {
}
return false;
}
// Reject if otherPlayer has allied with 50% or more of all players (Hard and Impossible only)
// Reject if otherPlayer has allied with a lot of 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;
@@ -148,7 +148,7 @@ export class NationAllianceBehavior {
.filter((p) => p.type() !== PlayerType.Bot).length;
const otherPlayerAlliances = otherPlayer.alliances().length;
if (difficulty !== Difficulty.Hard) {
if (difficulty === Difficulty.Hard) {
return otherPlayerAlliances >= totalPlayers * 0.5;
} else {
return otherPlayerAlliances >= totalPlayers * 0.25;
@@ -4,7 +4,9 @@ import {
Game,
Gold,
Player,
PlayerID,
PlayerType,
Tick,
UnitType,
} from "../../game/Game";
import { TileRef } from "../../game/GameMap";
@@ -18,7 +20,15 @@ import {
respondToMIRV,
} from "./NationEmojiBehavior";
// 30 seconds at 10 ticks/second
const MIRV_COOLDOWN_TICKS = 300;
export class NationMIRVBehavior {
// Shared across all NationMIRVBehavior instances.
// Tracks the last tick a MIRV was sent at each player, so multiple nations don't pile-on the same target.
// Especially important for games with very high starting gold settings.
private static recentMirvTargets = new Map<PlayerID, Tick>();
constructor(
private random: PseudoRandom,
private game: Game,
@@ -119,19 +129,19 @@ export class NationMIRVBehavior {
}
const inboundMIRVSender = this.selectCounterMirvTarget();
if (inboundMIRVSender) {
if (inboundMIRVSender && !this.wasRecentlyMirved(inboundMIRVSender)) {
this.maybeSendMIRV(inboundMIRVSender);
return true;
}
const victoryDenialTarget = this.selectVictoryDenialTarget();
if (victoryDenialTarget) {
if (victoryDenialTarget && !this.wasRecentlyMirved(victoryDenialTarget)) {
this.maybeSendMIRV(victoryDenialTarget);
return true;
}
const steamrollStopTarget = this.selectSteamrollStopTarget();
if (steamrollStopTarget) {
if (steamrollStopTarget && !this.wasRecentlyMirved(steamrollStopTarget)) {
this.maybeSendMIRV(steamrollStopTarget);
return true;
}
@@ -223,6 +233,17 @@ export class NationMIRVBehavior {
return null;
}
// MIRV Cooldown Methods
private wasRecentlyMirved(target: Player): boolean {
const lastTick = NationMIRVBehavior.recentMirvTargets.get(target.id());
if (lastTick === undefined) return false;
return this.game.ticks() - lastTick < MIRV_COOLDOWN_TICKS;
}
private recordMirvHit(target: Player): void {
NationMIRVBehavior.recentMirvTargets.set(target.id(), this.game.ticks());
}
// MIRV Helper Methods
private getValidMirvTargetPlayers(): Player[] {
if (this.player === null) throw new Error("not initialized");
@@ -261,6 +282,7 @@ export class NationMIRVBehavior {
const centerTile = this.calculateTerritoryCenter(enemy);
if (centerTile && this.player.canBuild(UnitType.MIRV, centerTile)) {
this.game.addExecution(new MirvExecution(this.player, centerTile));
this.recordMirvHit(enemy);
this.emojiBehavior.sendEmoji(AllPlayers, EMOJI_NUKE);
respondToMIRV(this.game, this.random, enemy);
}
@@ -659,8 +659,8 @@ export class NationNukeBehavior {
this.recentlySentNukes.push([tick, tile, nukeType]);
if (nukeType === UnitType.AtomBomb) {
this.atomBombsLaunched++;
// Increase perceived cost by 35% each time to simulate saving up for a MIRV (higher than hydro to make atom bombs less attractive for the lategame)
this.atomBombPerceivedCost = (this.atomBombPerceivedCost * 135n) / 100n;
// Increase perceived cost by 50% each time to simulate saving up for a MIRV (higher than hydro to make atom bombs less attractive for the lategame)
this.atomBombPerceivedCost = (this.atomBombPerceivedCost * 150n) / 100n;
} else if (nukeType === UnitType.HydrogenBomb) {
this.hydrogenBombsLaunched++;
// Increase perceived cost by 25% each time to simulate saving up for a MIRV