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:
FloPinguin
2025-12-19 01:20:23 +01:00
committed by GitHub
parent f60aef65e1
commit 4d5bb7a835
26 changed files with 290 additions and 278 deletions
+13 -13
View File
@@ -38,7 +38,7 @@ export class HostLobbyModal extends LitElement {
};
@state() private selectedMap: GameMapType = GameMapType.World;
@state() private selectedDifficulty: Difficulty = Difficulty.Medium;
@state() private disableNPCs = false;
@state() private disableNations = false;
@state() private gameMode: GameMode = GameMode.FFA;
@state() private teamCount: TeamCountConfig = 2;
@state() private bots: number = 400;
@@ -358,17 +358,17 @@ export class HostLobbyModal extends LitElement {
)
? html`
<label
for="disable-npcs"
class="option-card ${this.disableNPCs
for="disable-nations"
class="option-card ${this.disableNations
? "selected"
: ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="disable-npcs"
@change=${this.handleDisableNPCsChange}
.checked=${this.disableNPCs}
id="disable-nations"
@change=${this.handleDisableNationsChange}
.checked=${this.disableNations}
/>
<div class="option-card-title">
${translateText("host_modal.disable_nations")}
@@ -556,7 +556,7 @@ export class HostLobbyModal extends LitElement {
: translateText("host_modal.players")
}
<span style="margin: 0 8px;">•</span>
${this.disableNPCs ? 0 : this.nationCount}
${this.disableNations ? 0 : this.nationCount}
${
this.nationCount === 1
? translateText("host_modal.nation_player")
@@ -569,7 +569,7 @@ export class HostLobbyModal extends LitElement {
.clients=${this.clients}
.lobbyCreatorClientID=${this.lobbyCreatorClientID}
.teamCount=${this.teamCount}
.nationCount=${this.disableNPCs ? 0 : this.nationCount}
.nationCount=${this.disableNations ? 0 : this.nationCount}
.onKickPlayer=${(clientID: string) => this.kickPlayer(clientID)}
></lobby-team-view>
</div>
@@ -735,9 +735,9 @@ export class HostLobbyModal extends LitElement {
this.putGameConfig();
}
private async handleDisableNPCsChange(e: Event) {
this.disableNPCs = Boolean((e.target as HTMLInputElement).checked);
console.log(`updating disable npcs to ${this.disableNPCs}`);
private async handleDisableNationsChange(e: Event) {
this.disableNations = Boolean((e.target as HTMLInputElement).checked);
console.log(`updating disable nations to ${this.disableNations}`);
this.putGameConfig();
}
@@ -779,10 +779,10 @@ export class HostLobbyModal extends LitElement {
...(this.gameMode === GameMode.Team &&
this.teamCount === HumansVsNations
? {
disableNPCs: false,
disableNations: false,
}
: {
disableNPCs: this.disableNPCs,
disableNations: this.disableNations,
}),
maxTimerValue:
this.maxTimer === true ? this.maxTimerValue : undefined,
+12 -10
View File
@@ -36,7 +36,7 @@ export class SinglePlayerModal extends LitElement {
};
@state() private selectedMap: GameMapType = GameMapType.World;
@state() private selectedDifficulty: Difficulty = Difficulty.Medium;
@state() private disableNPCs: boolean = false;
@state() private disableNations: boolean = false;
@state() private bots: number = 400;
@state() private infiniteGold: boolean = false;
@state() private infiniteTroops: boolean = false;
@@ -261,15 +261,17 @@ export class SinglePlayerModal extends LitElement {
)
? html`
<label
for="singleplayer-modal-disable-npcs"
class="option-card ${this.disableNPCs ? "selected" : ""}"
for="singleplayer-modal-disable-nations"
class="option-card ${this.disableNations
? "selected"
: ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="singleplayer-modal-disable-npcs"
@change=${this.handleDisableNPCsChange}
.checked=${this.disableNPCs}
id="singleplayer-modal-disable-nations"
@change=${this.handleDisableNationsChange}
.checked=${this.disableNations}
/>
<div class="option-card-title">
${translateText("single_modal.disable_nations")}
@@ -491,8 +493,8 @@ export class SinglePlayerModal extends LitElement {
this.maxTimerValue = value;
}
private handleDisableNPCsChange(e: Event) {
this.disableNPCs = Boolean((e.target as HTMLInputElement).checked);
private handleDisableNationsChange(e: Event) {
this.disableNations = Boolean((e.target as HTMLInputElement).checked);
}
private handleGameModeSelection(value: GameMode) {
@@ -591,10 +593,10 @@ export class SinglePlayerModal extends LitElement {
...(this.gameMode === GameMode.Team &&
this.teamCount === HumansVsNations
? {
disableNPCs: false,
disableNations: false,
}
: {
disableNPCs: this.disableNPCs,
disableNations: this.disableNations,
}),
},
lobbyCreatedAt: Date.now(), // ms; server should be authoritative in MP
@@ -260,11 +260,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
.map((a) => a.troops)
.reduce((a, b) => a + b, 0);
if (
player.type() === PlayerType.FakeHuman &&
myPlayer !== null &&
!isAllied
) {
if (player.type() === PlayerType.Nation && myPlayer !== null && !isAllied) {
const relation =
this.playerProfile?.relations[myPlayer.smallID()] ?? Relation.Neutral;
const relationClass = this.getRelationClass(relation);
@@ -299,7 +295,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
case PlayerType.Bot:
playerType = translateText("player_type.bot");
break;
case PlayerType.FakeHuman:
case PlayerType.Nation:
playerType = translateText("player_type.nation");
break;
case PlayerType.Human:
+2 -2
View File
@@ -276,7 +276,7 @@ export class PlayerPanel extends LitElement implements Layer {
private identityChipProps(type: PlayerType) {
switch (type) {
case PlayerType.FakeHuman:
case PlayerType.Nation:
return {
labelKey: "player_type.nation",
aria: "Nation player",
@@ -388,7 +388,7 @@ export class PlayerPanel extends LitElement implements Layer {
}
private renderRelationPillIfNation(other: PlayerView, my: PlayerView) {
if (other.type() !== PlayerType.FakeHuman) return html``;
if (other.type() !== PlayerType.Nation) return html``;
if (other.isTraitor()) return html``;
if (my?.isAlliedWith && my.isAlliedWith(other)) return html``;
if (!this.otherProfile || !my) return html``;
+5 -5
View File
@@ -55,7 +55,7 @@ export async function createGameRunner(
);
});
const nations = gameStart.config.disableNPCs
const nations = gameStart.config.disableNations
? []
: gameMap.nations.map(
(n) =>
@@ -63,7 +63,7 @@ export async function createGameRunner(
new Cell(n.coordinates[0], n.coordinates[1]),
new PlayerInfo(
n.name,
PlayerType.FakeHuman,
PlayerType.Nation,
null,
random.nextID(),
n.strength,
@@ -110,8 +110,8 @@ export class GameRunner {
...this.execManager.spawnBots(this.game.config().numBots()),
);
}
if (this.game.config().spawnNPCs()) {
this.game.addExecution(...this.execManager.fakeHumanExecutions());
if (this.game.config().spawnNations()) {
this.game.addExecution(...this.execManager.nationExecutions());
}
this.game.addExecution(new WinCheckExecution());
}
@@ -160,7 +160,7 @@ export class GameRunner {
.players()
.filter(
(p) =>
p.type() === PlayerType.Human || p.type() === PlayerType.FakeHuman,
p.type() === PlayerType.Human || p.type() === PlayerType.Nation,
)
.forEach(
(p) => (this.playerViewData[p.id()] = placeName(this.game, p)),
+1 -1
View File
@@ -164,7 +164,7 @@ export const GameConfigSchema = z.object({
gameType: z.enum(GameType),
gameMode: z.enum(GameMode),
gameMapSize: z.enum(GameMapSize),
disableNPCs: z.boolean(),
disableNations: z.boolean(),
bots: z.number().int().min(0).max(400),
infiniteGold: z.boolean(),
infiniteTroops: z.boolean(),
+1 -3
View File
@@ -1,7 +1,6 @@
import { Colord } from "colord";
import { JWK } from "jose";
import {
Difficulty,
Game,
GameMapType,
GameMode,
@@ -82,7 +81,7 @@ export interface Config {
theme(): Theme;
percentageTilesOwnedToWin(): number;
numBots(): number;
spawnNPCs(): boolean;
spawnNations(): boolean;
isUnitDisabled(unitType: UnitType): boolean;
bots(): number;
infiniteGold(): boolean;
@@ -159,7 +158,6 @@ export interface Config {
defensePostDefenseBonus(): number;
defensePostSpeedBonus(): number;
falloutDefenseModifier(percentOfFallout: number): number;
difficultyModifier(difficulty: Difficulty): number;
warshipPatrolRange(): number;
warshipShellAttackRate(): number;
warshipTargettingRange(): number;
+13 -20
View File
@@ -287,19 +287,6 @@ export class DefaultConfig implements Config {
return this._userSettings;
}
difficultyModifier(difficulty: Difficulty): number {
switch (difficulty) {
case Difficulty.Easy:
return 1;
case Difficulty.Medium:
return 3;
case Difficulty.Hard:
return 9;
case Difficulty.Impossible:
return 18;
}
}
cityTroopIncrease(): number {
return 250_000;
}
@@ -332,8 +319,8 @@ export class DefaultConfig implements Config {
return this._gameConfig.playerTeams ?? 0;
}
spawnNPCs(): boolean {
return !this._gameConfig.disableNPCs;
spawnNations(): boolean {
return !this._gameConfig.disableNations;
}
isUnitDisabled(unitType: UnitType): boolean {
@@ -712,7 +699,7 @@ export class DefaultConfig implements Config {
mag *= 0.8;
}
if (
attacker.type() === PlayerType.FakeHuman &&
attacker.type() === PlayerType.Nation &&
defender.type() === PlayerType.Bot
) {
mag *= 0.8;
@@ -816,9 +803,9 @@ export class DefaultConfig implements Config {
}
useNationStrengthForStartManpower(): boolean {
// Currently disabled: FakeHumans became harder to play against due to AI improvements
// Currently disabled: Nations became harder to play against due to AI improvements
// nation strength multiplier was unintentionally disabled during those AI improvements (playerInfo.nation was undefined),
// Re-enabling this without rebalancing FakeHuman difficulty elsewhere may make them overpowered
// Re-enabling this without rebalancing Nation difficulty elsewhere may make them overpowered
return false;
}
@@ -826,7 +813,7 @@ export class DefaultConfig implements Config {
if (playerInfo.playerType === PlayerType.Bot) {
return 10_000;
}
if (playerInfo.playerType === PlayerType.FakeHuman) {
if (playerInfo.playerType === PlayerType.Nation) {
const strength = this.useNationStrengthForStartManpower()
? (playerInfo.nationStrength ?? 1)
: 1;
@@ -840,6 +827,8 @@ export class DefaultConfig implements Config {
return 31_250 * strength;
case Difficulty.Impossible:
return 37_500 * strength;
default:
assertNever(this._gameConfig.difficulty);
}
}
return this.infiniteTroops() ? 1_000_000 : 25_000;
@@ -873,6 +862,8 @@ export class DefaultConfig implements Config {
return maxTroops * 1.25;
case Difficulty.Impossible:
return maxTroops * 1.5;
default:
assertNever(this._gameConfig.difficulty);
}
}
@@ -888,7 +879,7 @@ export class DefaultConfig implements Config {
toAdd *= 0.6;
}
if (player.type() === PlayerType.FakeHuman) {
if (player.type() === PlayerType.Nation) {
switch (this._gameConfig.difficulty) {
case Difficulty.Easy:
toAdd *= 0.95;
@@ -902,6 +893,8 @@ export class DefaultConfig implements Config {
case Difficulty.Impossible:
toAdd *= 1.1;
break;
default:
assertNever(this._gameConfig.difficulty);
}
}
+10 -10
View File
@@ -2,7 +2,7 @@ import { Execution, Game, Player } from "../game/Game";
import { PseudoRandom } from "../PseudoRandom";
import { simpleHash } from "../Util";
import { AllianceExtensionExecution } from "./alliance/AllianceExtensionExecution";
import { BotBehavior } from "./utils/BotBehavior";
import { AiAttackBehavior } from "./utils/AiAttackBehavior";
export class BotExecution implements Execution {
private active = true;
@@ -10,7 +10,7 @@ export class BotExecution implements Execution {
private mg: Game;
private neighborsTerraNullius = true;
private behavior: BotBehavior | null = null;
private attackBehavior: AiAttackBehavior | null = null;
private attackRate: number;
private attackTick: number;
private triggerRatio: number;
@@ -42,8 +42,8 @@ export class BotExecution implements Execution {
return;
}
if (this.behavior === null) {
this.behavior = new BotBehavior(
if (this.attackBehavior === null) {
this.attackBehavior = new AiAttackBehavior(
this.random,
this.mg,
this.bot,
@@ -53,7 +53,7 @@ export class BotExecution implements Execution {
);
// Send an attack on the first tick
this.behavior.sendAttack(this.mg.terraNullius());
this.attackBehavior.sendAttack(this.mg.terraNullius());
return;
}
@@ -81,10 +81,10 @@ export class BotExecution implements Execution {
}
private maybeAttack() {
if (this.behavior === null) {
if (this.attackBehavior === null) {
throw new Error("not initialized");
}
const toAttack = this.behavior.getNeighborTraitorToAttack();
const toAttack = this.attackBehavior.getNeighborTraitorToAttack();
if (toAttack !== null) {
const odds = this.bot.isFriendly(toAttack) ? 6 : 3;
if (this.random.chance(odds)) {
@@ -95,20 +95,20 @@ export class BotExecution implements Execution {
this.bot.breakAlliance(alliance);
}
this.behavior.sendAttack(toAttack);
this.attackBehavior.sendAttack(toAttack);
return;
}
}
if (this.neighborsTerraNullius) {
if (this.bot.sharesBorderWith(this.mg.terraNullius())) {
this.behavior.sendAttack(this.mg.terraNullius());
this.attackBehavior.sendAttack(this.mg.terraNullius());
return;
}
this.neighborsTerraNullius = false;
}
this.behavior.attackRandomTarget();
this.attackBehavior.attackRandomTarget();
}
isActive(): boolean {
+1 -1
View File
@@ -43,7 +43,7 @@ export class EmojiExecution implements Execution {
if (
emojiString === "🖕" &&
this.recipient !== AllPlayers &&
this.recipient.type() === PlayerType.FakeHuman
this.recipient.type() === PlayerType.Nation
) {
this.recipient.updateRelation(this.requestor, -100);
}
+3 -3
View File
@@ -16,9 +16,9 @@ import { DonateTroopsExecution } from "./DonateTroopExecution";
import { EmbargoAllExecution } from "./EmbargoAllExecution";
import { EmbargoExecution } from "./EmbargoExecution";
import { EmojiExecution } from "./EmojiExecution";
import { FakeHumanExecution } from "./FakeHumanExecution";
import { MarkDisconnectedExecution } from "./MarkDisconnectedExecution";
import { MoveWarshipExecution } from "./MoveWarshipExecution";
import { NationExecution } from "./NationExecution";
import { NoOpExecution } from "./NoOpExecution";
import { QuickChatExecution } from "./QuickChatExecution";
import { RetreatExecution } from "./RetreatExecution";
@@ -136,10 +136,10 @@ export class Executor {
return new PlayerSpawner(this.mg, this.gameID).spawnPlayers();
}
fakeHumanExecutions(): Execution[] {
nationExecutions(): Execution[] {
const execs: Execution[] = [];
for (const nation of this.mg.nations()) {
execs.push(new FakeHumanExecution(this.gameID, nation));
execs.push(new NationExecution(this.gameID, nation));
}
return execs;
}
@@ -20,19 +20,19 @@ import { GameID } from "../Schemas";
import { boundingBoxTiles, calculateBoundingBox, simpleHash } from "../Util";
import { ConstructionExecution } from "./ConstructionExecution";
import { MirvExecution } from "./MIRVExecution";
import { NationAllianceBehavior } from "./nation/NationAllianceBehavior";
import { structureSpawnTileValue } from "./nation/structureSpawnTileValue";
import { NukeExecution } from "./NukeExecution";
import { SpawnExecution } from "./SpawnExecution";
import { TransportShipExecution } from "./TransportShipExecution";
import { calculateTerritoryCenter, closestTwoTiles } from "./Util";
import { AllianceBehavior } from "./utils/AllianceBehavior";
import { BotBehavior } from "./utils/BotBehavior";
import { AiAttackBehavior } from "./utils/AiAttackBehavior";
export class FakeHumanExecution implements Execution {
export class NationExecution implements Execution {
private active = true;
private random: PseudoRandom;
private behavior: BotBehavior | null = null; // Shared behavior logic for both bots and fakehumans
private allianceBehavior: AllianceBehavior | null = null;
private attackBehavior: AiAttackBehavior | null = null;
private allianceBehavior: NationAllianceBehavior | null = null;
private mg: Game;
private player: Player | null = null;
@@ -73,7 +73,7 @@ export class FakeHumanExecution implements Execution {
constructor(
gameID: GameID,
private nation: Nation, // Nation contains PlayerInfo with PlayerType.FakeHuman
private nation: Nation, // Nation contains PlayerInfo with PlayerType.Nation
) {
this.random = new PseudoRandom(
simpleHash(nation.playerInfo.id) + simpleHash(gameID),
@@ -176,9 +176,9 @@ export class FakeHumanExecution implements Execution {
return;
}
if (this.behavior === null || this.allianceBehavior === null) {
if (this.attackBehavior === null || this.allianceBehavior === null) {
// Player is unavailable during init()
this.behavior = new BotBehavior(
this.attackBehavior = new AiAttackBehavior(
this.random,
this.mg,
this.player,
@@ -186,14 +186,14 @@ export class FakeHumanExecution implements Execution {
this.reserveRatio,
this.expandRatio,
);
this.allianceBehavior = new AllianceBehavior(
this.allianceBehavior = new NationAllianceBehavior(
this.random,
this.mg,
this.player,
);
// Send an attack on the first tick
this.behavior.forceSendAttack(this.mg.terraNullius());
this.attackBehavior.forceSendAttack(this.mg.terraNullius());
return;
}
@@ -209,7 +209,7 @@ export class FakeHumanExecution implements Execution {
private maybeAttack() {
if (
this.player === null ||
this.behavior === null ||
this.attackBehavior === null ||
this.allianceBehavior === null
) {
throw new Error("not initialized");
@@ -240,7 +240,7 @@ export class FakeHumanExecution implements Execution {
(t) => !this.mg.hasOwner(t) && !this.mg.hasFallout(t),
);
if (hasNonNukedTerraNullius) {
this.behavior.sendAttack(this.mg.terraNullius());
this.attackBehavior.sendAttack(this.mg.terraNullius());
return;
}
@@ -257,11 +257,13 @@ export class FakeHumanExecution implements Execution {
this.allianceBehavior.maybeSendAllianceRequests(borderingEnemies);
}
this.behavior.assistAllies();
this.attackBehavior.assistAllies();
this.behavior.attackBestTarget(borderingFriends, borderingEnemies);
this.attackBehavior.attackBestTarget(borderingFriends, borderingEnemies);
this.maybeSendNuke(this.behavior.findBestNukeTarget(borderingEnemies));
this.maybeSendNuke(
this.attackBehavior.findBestNukeTarget(borderingEnemies),
);
}
private maybeSendNuke(other: Player | null) {
@@ -271,7 +273,7 @@ export class FakeHumanExecution implements Execution {
silos.length === 0 ||
this.player.gold() < this.cost(UnitType.AtomBomb) ||
other === null ||
other.type() === PlayerType.Bot || // Don't nuke bots (as opposed to fakehumans and humans)
other.type() === PlayerType.Bot || // Don't nuke bots (as opposed to nations and humans)
this.player.isOnSameTeam(other)
) {
return;
@@ -340,7 +342,7 @@ export class FakeHumanExecution implements Execution {
const tick = this.mg.ticks();
this.lastNukeSent.push([tick, tile]);
this.mg.addExecution(new NukeExecution(nukeType, this.player, tile));
this.behavior?.maybeSendEmoji(targetPlayer);
this.attackBehavior?.maybeSendEmoji(targetPlayer);
}
private nukeTileScore(tile: TileRef, silos: Unit[], targets: Unit[]): number {
@@ -683,7 +685,7 @@ export class FakeHumanExecution implements Execution {
return false;
}
if (this.random.chance(FakeHumanExecution.MIRV_HESITATION_ODDS)) {
if (this.random.chance(NationExecution.MIRV_HESITATION_ODDS)) {
this.triggerMIRVCooldown();
return false;
}
@@ -735,7 +737,7 @@ export class FakeHumanExecution implements Execution {
.map((x) => x.numTilesOwned())
.reduce((a, b) => a + b, 0);
const teamShare = teamTerritory / totalLand;
if (teamShare >= FakeHumanExecution.VICTORY_DENIAL_TEAM_THRESHOLD) {
if (teamShare >= NationExecution.VICTORY_DENIAL_TEAM_THRESHOLD) {
// Only consider the largest team member as the target when team exceeds threshold
let largestMember: Player | null = null;
let largestTiles = -1;
@@ -754,7 +756,7 @@ export class FakeHumanExecution implements Execution {
}
} else {
const share = p.numTilesOwned() / totalLand;
if (share >= FakeHumanExecution.VICTORY_DENIAL_INDIVIDUAL_THRESHOLD)
if (share >= NationExecution.VICTORY_DENIAL_INDIVIDUAL_THRESHOLD)
severity = share;
}
if (severity > 0) {
@@ -780,13 +782,13 @@ export class FakeHumanExecution implements Execution {
const topPlayer = allPlayers[0];
if (topPlayer.cityCount <= FakeHumanExecution.STEAMROLL_MIN_LEADER_CITIES)
if (topPlayer.cityCount <= NationExecution.STEAMROLL_MIN_LEADER_CITIES)
return null;
const secondHighest = allPlayers[1].cityCount;
const threshold =
secondHighest * FakeHumanExecution.STEAMROLL_CITY_GAP_MULTIPLIER;
secondHighest * NationExecution.STEAMROLL_CITY_GAP_MULTIPLIER;
if (topPlayer.cityCount >= threshold) {
return validTargets.some((p) => p === topPlayer.p) ? topPlayer.p : null;
@@ -852,7 +854,7 @@ export class FakeHumanExecution implements Execution {
private maybeSendMIRV(enemy: Player): void {
if (this.player === null) throw new Error("not initialized");
this.behavior?.maybeSendEmoji(enemy);
this.attackBehavior?.maybeSendEmoji(enemy);
const centerTile = this.calculateTerritoryCenter(enemy);
if (centerTile && this.player.canBuild(UnitType.MIRV, centerTile)) {
@@ -878,7 +880,7 @@ export class FakeHumanExecution implements Execution {
}
private removeOldMIRVEvents() {
const maxAge = FakeHumanExecution.MIRV_COOLDOWN_TICKS;
const maxAge = NationExecution.MIRV_COOLDOWN_TICKS;
const tick = this.mg.ticks();
while (
this.lastMIRVSent.length > 0 &&
@@ -6,10 +6,11 @@ import {
Relation,
} from "../../game/Game";
import { PseudoRandom } from "../../PseudoRandom";
import { assertNever } from "../../Util";
import { AllianceExtensionExecution } from "../alliance/AllianceExtensionExecution";
import { AllianceRequestExecution } from "../alliance/AllianceRequestExecution";
export class AllianceBehavior {
export class NationAllianceBehavior {
constructor(
private random: PseudoRandom,
private game: Game,
@@ -52,8 +53,10 @@ export class AllianceBehavior {
return this.random.chance(30);
case Difficulty.Hard:
return this.random.chance(25);
default:
case Difficulty.Impossible:
return this.random.chance(20);
default:
assertNever(difficulty);
}
};
@@ -116,8 +119,10 @@ export class AllianceBehavior {
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:
case Difficulty.Impossible:
return false; // No confusion on impossible
default:
assertNever(difficulty);
}
}
@@ -137,7 +142,7 @@ export class AllianceBehavior {
this.game.config().maxTroops(otherPlayer) >
this.game.config().maxTroops(this.player) * 2
);
default: {
case Difficulty.Impossible: {
// 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;
@@ -150,6 +155,8 @@ export class AllianceBehavior {
otherPlayer.numTilesOwned() > this.player.numTilesOwned() * 1.5;
return otherHasMoreTroops || otherHasMoreMaxTroops || otherHasMoreTiles;
}
default:
assertNever(difficulty);
}
}
@@ -160,7 +167,8 @@ export class AllianceBehavior {
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: {
case Difficulty.Hard:
case Difficulty.Impossible: {
// On hard and impossible we try to not ally with all our neighbors (If we have 3+ neighbors)
const borderingPlayers = this.player
.neighbors()
@@ -181,6 +189,8 @@ export class AllianceBehavior {
}
return this.player.alliances().length >= this.random.nextInt(2, 5);
}
default:
assertNever(difficulty);
}
}
@@ -195,11 +205,13 @@ export class AllianceBehavior {
this.player.relation(otherPlayer) === Relation.Friendly &&
this.random.nextInt(0, 100) >= 17
);
default:
case Difficulty.Impossible:
return (
this.player.relation(otherPlayer) === Relation.Friendly &&
this.random.nextInt(0, 100) >= 33
);
default:
assertNever(difficulty);
}
}
@@ -222,11 +234,13 @@ export class AllianceBehavior {
otherPlayer.troops() >
this.player.troops() * (this.random.nextInt(75, 85) / 100)
);
default:
case Difficulty.Impossible:
return (
otherPlayer.troops() >
this.player.troops() * (this.random.nextInt(80, 90) / 100)
);
default:
assertNever(difficulty);
}
}
}
@@ -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 -1
View File
@@ -350,7 +350,7 @@ export enum TerrainType {
export enum PlayerType {
Bot = "BOT",
Human = "HUMAN",
FakeHuman = "FAKEHUMAN",
Nation = "NATION",
}
export interface Execution {
+1 -1
View File
@@ -444,7 +444,7 @@ export class PlayerImpl implements Player {
markTraitor(): void {
this.markedTraitorTick = this.mg.ticks();
this._betrayalCount++; // Keep count for FakeHumans too
this._betrayalCount++; // Keep count for Nations too
// Record stats (only for real Humans)
this.mg.stats().betray(this);
+2 -2
View File
@@ -59,7 +59,7 @@ export function assignTeams(
// Then, assign non-clan players to balance teams
let nationPlayers = noClanPlayers.filter(
(player) => player.playerType === PlayerType.FakeHuman,
(player) => player.playerType === PlayerType.Nation,
);
if (nationPlayers.length > 0) {
// Shuffle only nations to randomize their team assignment
@@ -67,7 +67,7 @@ export function assignTeams(
nationPlayers = random.shuffleArray(nationPlayers);
}
const otherPlayers = noClanPlayers.filter(
(player) => player.playerType !== PlayerType.FakeHuman,
(player) => player.playerType !== PlayerType.Nation,
);
for (const player of otherPlayers.concat(nationPlayers)) {
+1 -1
View File
@@ -65,7 +65,7 @@ export class GameManager {
gameType: GameType.Private,
gameMapSize: GameMapSize.Normal,
difficulty: Difficulty.Medium,
disableNPCs: false,
disableNations: false,
infiniteGold: false,
infiniteTroops: false,
maxTimerValue: undefined,
+2 -2
View File
@@ -90,8 +90,8 @@ export class GameServer {
if (gameConfig.difficulty !== undefined) {
this.gameConfig.difficulty = gameConfig.difficulty;
}
if (gameConfig.disableNPCs !== undefined) {
this.gameConfig.disableNPCs = gameConfig.disableNPCs;
if (gameConfig.disableNations !== undefined) {
this.gameConfig.disableNations = gameConfig.disableNations;
}
if (gameConfig.bots !== undefined) {
this.gameConfig.bots = gameConfig.bots;
+1 -1
View File
@@ -99,7 +99,7 @@ export class MapPlaylist {
maxTimerValue: undefined,
instantBuild: false,
randomSpawn: false,
disableNPCs: mode === GameMode.Team && playerTeams !== HumansVsNations,
disableNations: mode === GameMode.Team && playerTeams !== HumansVsNations,
gameMode: mode,
playerTeams,
bots: 400,
@@ -1,13 +1,13 @@
import { BotBehavior } from "../src/core/execution/utils/BotBehavior";
import { AiAttackBehavior } from "../src/core/execution/utils/AiAttackBehavior";
import { Game, Player, PlayerInfo, PlayerType } from "../src/core/game/Game";
import { PseudoRandom } from "../src/core/PseudoRandom";
import { setup } from "./util/Setup";
describe("BotBehavior Attack Behavior", () => {
describe("Ai Attack Behavior", () => {
let game: Game;
let bot: Player;
let human: Player;
let botBehavior: BotBehavior;
let attackBehavior: AiAttackBehavior;
// Helper function for basic test setup
async function setupTestEnvironment() {
@@ -52,7 +52,7 @@ describe("BotBehavior Attack Behavior", () => {
testGame.executeNextTick();
}
const behavior = new BotBehavior(
const behavior = new AiAttackBehavior(
new PseudoRandom(42),
testGame,
testBot,
@@ -85,7 +85,7 @@ describe("BotBehavior Attack Behavior", () => {
game = env.testGame;
bot = env.testBot;
human = env.testHuman;
botBehavior = env.behavior;
attackBehavior = env.behavior;
});
test("bot cannot attack allied player", () => {
@@ -99,7 +99,7 @@ describe("BotBehavior Attack Behavior", () => {
const attacksBefore = bot.outgoingAttacks().length;
// Attempt attack (should be blocked)
botBehavior.sendAttack(human);
attackBehavior.sendAttack(human);
// Execute a few ticks to process the attacks
for (let i = 0; i < 5; i++) {
@@ -116,7 +116,7 @@ describe("BotBehavior Attack Behavior", () => {
// Create nation
const nationInfo = new PlayerInfo(
"nation_test",
PlayerType.FakeHuman,
PlayerType.Nation,
null,
"nation_test",
);
@@ -128,7 +128,7 @@ describe("BotBehavior Attack Behavior", () => {
nation.addTroops(1000);
const nationBehavior = new BotBehavior(
const nationBehavior = new AiAttackBehavior(
new PseudoRandom(42),
game,
nation,
+10 -6
View File
@@ -1,4 +1,4 @@
import { AllianceBehavior } from "../src/core/execution/utils/AllianceBehavior";
import { NationAllianceBehavior } from "../src/core/execution/nation/NationAllianceBehavior";
import {
AllianceRequest,
Game,
@@ -13,7 +13,7 @@ import { setup } from "./util/Setup";
let game: Game;
let player: Player;
let requestor: Player;
let allianceBehavior: AllianceBehavior;
let allianceBehavior: NationAllianceBehavior;
describe("AllianceBehavior.handleAllianceRequests", () => {
beforeEach(async () => {
@@ -44,7 +44,7 @@ describe("AllianceBehavior.handleAllianceRequests", () => {
// Use a fixed random seed for deterministic behavior
const random = new PseudoRandom(46);
allianceBehavior = new AllianceBehavior(random, game, player);
allianceBehavior = new NationAllianceBehavior(random, game, player);
});
function setupAllianceRequest({
@@ -142,7 +142,7 @@ describe("AllianceBehavior.handleAllianceExtensionRequests", () => {
let mockAlliance: any;
let mockHuman: any;
let mockRandom: any;
let allianceBehavior: AllianceBehavior;
let allianceBehavior: NationAllianceBehavior;
beforeEach(() => {
mockGame = { addExecution: jest.fn() };
@@ -157,10 +157,14 @@ describe("AllianceBehavior.handleAllianceExtensionRequests", () => {
alliances: jest.fn(() => [mockAlliance]),
relation: jest.fn(),
id: jest.fn(() => "bot_id"),
type: jest.fn(() => PlayerType.FakeHuman),
type: jest.fn(() => PlayerType.Nation),
};
allianceBehavior = new AllianceBehavior(mockRandom, mockGame, mockPlayer);
allianceBehavior = new NationAllianceBehavior(
mockRandom,
mockGame,
mockPlayer,
);
});
it("should NOT request extension if onlyOneAgreedToExtend is false (no expiration yet or both already agreed)", () => {
+1 -2
View File
@@ -21,7 +21,7 @@ describe("AllianceExtensionExecution", () => {
[
playerInfo("player1", PlayerType.Human),
playerInfo("player2", PlayerType.Human),
playerInfo("player3", PlayerType.FakeHuman),
playerInfo("player3", PlayerType.Nation),
],
);
@@ -82,7 +82,6 @@ describe("AllianceExtensionExecution", () => {
});
test("Successfully extends existing alliance between Human and non-Human", () => {
//test of handleAllianceExtensions is done in BotBehavior tests
jest.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true);
jest.spyOn(player3, "isAlive").mockReturnValue(true);
jest.spyOn(player1, "isAlive").mockReturnValue(true);
+1 -1
View File
@@ -21,7 +21,7 @@ describe("AllianceRequestExecution", () => {
[
playerInfo("player1", PlayerType.Human),
playerInfo("player2", PlayerType.Human),
playerInfo("player3", PlayerType.FakeHuman),
playerInfo("player3", PlayerType.Nation),
],
);
@@ -1,5 +1,5 @@
import { FakeHumanExecution } from "../src/core/execution/FakeHumanExecution";
import { MirvExecution } from "../src/core/execution/MIRVExecution";
import { NationExecution } from "../src/core/execution/NationExecution";
import {
Cell,
GameMode,
@@ -11,8 +11,8 @@ import {
import { setup } from "./util/Setup";
import { executeTicks } from "./util/utils";
describe("FakeHuman MIRV Retaliation", () => {
test("fakehuman retaliates with MIRV when attacked by MIRV", async () => {
describe("Nation MIRV Retaliation", () => {
test("nation retaliates with MIRV when attacked by MIRV", async () => {
const game = await setup("big_plains", {
infiniteGold: true,
instantBuild: true,
@@ -25,15 +25,15 @@ describe("FakeHuman MIRV Retaliation", () => {
null,
"attacker_id",
);
const fakehumanInfo = new PlayerInfo(
"defender_fakehuman",
PlayerType.FakeHuman,
const nationInfo = new PlayerInfo(
"defender_nation",
PlayerType.Nation,
null,
"fakehuman_id",
"nation_id",
);
game.addPlayer(attackerInfo);
game.addPlayer(fakehumanInfo);
game.addPlayer(nationInfo);
// Skip spawn phase
while (game.inSpawnPhase()) {
@@ -41,7 +41,7 @@ describe("FakeHuman MIRV Retaliation", () => {
}
const attacker = game.player("attacker_id");
const fakehuman = game.player("fakehuman_id");
const nation = game.player("nation_id");
// Give attacker territory and missile silo
for (let x = 5; x < 15; x++) {
@@ -54,45 +54,45 @@ describe("FakeHuman MIRV Retaliation", () => {
}
attacker.buildUnit(UnitType.MissileSilo, game.ref(10, 10), {});
// Give fakehuman territory and missile silo
// Give nation territory and missile silo
for (let x = 25; x < 75; x++) {
for (let y = 25; y < 75; y++) {
const tile = game.ref(x, y);
if (game.map().isLand(tile)) {
fakehuman.conquer(tile);
nation.conquer(tile);
}
}
}
fakehuman.buildUnit(UnitType.MissileSilo, game.ref(50, 50), {});
nation.buildUnit(UnitType.MissileSilo, game.ref(50, 50), {});
// Give both players enough gold for MIRVs
attacker.addGold(1_000_000_000n);
fakehuman.addGold(1_000_000_000n);
nation.addGold(1_000_000_000n);
// Verify preconditions
expect(attacker.units(UnitType.MissileSilo)).toHaveLength(1);
expect(fakehuman.units(UnitType.MissileSilo)).toHaveLength(1);
expect(nation.units(UnitType.MissileSilo)).toHaveLength(1);
expect(attacker.gold()).toBeGreaterThan(35_000_000n);
expect(fakehuman.gold()).toBeGreaterThan(35_000_000n);
expect(nation.gold()).toBeGreaterThan(35_000_000n);
// Track MIRVs before fakehuman retaliates
const mirvCountBefore = fakehuman.units(UnitType.MIRV).length;
// Track MIRVs before nation retaliates
const mirvCountBefore = nation.units(UnitType.MIRV).length;
// Initialize fakehuman with FakeHumanExecution to enable retaliation logic
const fakehumanNation = new Nation(new Cell(50, 50), fakehuman.info());
// Initialize nation with NationExecution to enable retaliation logic
const testExecutionNation = new Nation(new Cell(50, 50), nation.info());
// Try different game IDs to account for hesitation odds
const gameIds = Array.from({ length: 20 }, (_, i) => `game_${i}`);
let retaliationAttempted = false;
for (const gameId of gameIds) {
const testExecution = new FakeHumanExecution(gameId, fakehumanNation);
const testExecution = new NationExecution(gameId, testExecutionNation);
testExecution.init(game);
// Launch MIRV from attacker to fakehuman
const targetTile = Array.from(fakehuman.tiles())[0];
// Launch MIRV from attacker to nation
const targetTile = Array.from(nation.tiles())[0];
game.addExecution(new MirvExecution(attacker, targetTile));
// Execute fakehuman's tick logic
// Execute nation's tick logic
for (let tick = 0; tick < 200; tick++) {
testExecution.tick(tick);
// Allow the game to process executions
@@ -100,8 +100,8 @@ describe("FakeHuman MIRV Retaliation", () => {
game.executeNextTick();
}
// Check if fakehuman attempted retaliation
if (fakehuman.units(UnitType.MIRV).length > mirvCountBefore) {
// Check if nation attempted retaliation
if (nation.units(UnitType.MIRV).length > mirvCountBefore) {
retaliationAttempted = true;
break;
}
@@ -116,15 +116,15 @@ describe("FakeHuman MIRV Retaliation", () => {
// Process the retaliation
executeTicks(game, 2);
// Assert: Fakehuman launched a retaliatory MIRV
const mirvCountAfter = fakehuman.units(UnitType.MIRV).length;
// Assert: Nation launched a retaliatory MIRV
const mirvCountAfter = nation.units(UnitType.MIRV).length;
expect(mirvCountAfter).toBeGreaterThan(mirvCountBefore);
// Verify the retaliatory MIRV targets the attacker's territory
const fakehumanMirvs = fakehuman.units(UnitType.MIRV);
expect(fakehumanMirvs.length).toBeGreaterThan(0);
const nationMirvs = nation.units(UnitType.MIRV);
expect(nationMirvs.length).toBeGreaterThan(0);
const retaliationMirv = fakehumanMirvs[fakehumanMirvs.length - 1];
const retaliationMirv = nationMirvs[nationMirvs.length - 1];
const retaliationTarget = retaliationMirv.targetTile();
expect(retaliationTarget).toBeDefined();
@@ -134,7 +134,7 @@ describe("FakeHuman MIRV Retaliation", () => {
}
});
test("fakehuman launches MIRV to prevent victory when player approaches win condition", async () => {
test("nation launches MIRV to prevent victory when player approaches win condition", async () => {
// Setup game
const game = await setup("big_plains", {
infiniteGold: true,
@@ -148,15 +148,15 @@ describe("FakeHuman MIRV Retaliation", () => {
null,
"dominant_id",
);
const fakehumanInfo = new PlayerInfo(
"defender_fakehuman",
PlayerType.FakeHuman,
const nationInfo = new PlayerInfo(
"defender_nation",
PlayerType.Nation,
null,
"fakehuman_id",
"nation_id",
);
game.addPlayer(dominantPlayerInfo);
game.addPlayer(fakehumanInfo);
game.addPlayer(nationInfo);
// Skip spawn phase
while (game.inSpawnPhase()) {
@@ -164,39 +164,39 @@ describe("FakeHuman MIRV Retaliation", () => {
}
const dominantPlayer = game.player("dominant_id");
const fakehuman = game.player("fakehuman_id");
const nation = game.player("nation_id");
// First, give fakehuman a small territory and missile silo
let fakehumanTiles = 0;
// First, give nation a small territory and missile silo
let nationTiles = 0;
for (let x = 45; x < 55; x++) {
for (let y = 45; y < 55; y++) {
const tile = game.ref(x, y);
if (game.map().isLand(tile) && !game.map().hasOwner(tile)) {
fakehuman.conquer(tile);
fakehumanTiles++;
nation.conquer(tile);
nationTiles++;
}
}
}
// If we didn't find enough tiles, try a different area
if (fakehumanTiles === 0) {
if (nationTiles === 0) {
for (let x = 60; x < 70; x++) {
for (let y = 60; y < 70; y++) {
const tile = game.ref(x, y);
if (game.map().isLand(tile) && !game.map().hasOwner(tile)) {
fakehuman.conquer(tile);
fakehumanTiles++;
if (fakehumanTiles >= 10) break; // Need at least some territory
nation.conquer(tile);
nationTiles++;
if (nationTiles >= 10) break; // Need at least some territory
}
}
if (fakehumanTiles >= 10) break;
if (nationTiles >= 10) break;
}
}
// Build missile silo on one of the fakehuman's tiles
const fakehumanTile = Array.from(fakehuman.tiles())[0];
if (fakehumanTile) {
fakehuman.buildUnit(UnitType.MissileSilo, fakehumanTile, {});
// Build missile silo on one of the nation's tiles
const nationTile = Array.from(nation.tiles())[0];
if (nationTile) {
nation.buildUnit(UnitType.MissileSilo, nationTile, {});
}
// Then give dominant player a large amount of territory
@@ -225,35 +225,35 @@ describe("FakeHuman MIRV Retaliation", () => {
// Give both players enough gold for MIRVs
dominantPlayer.addGold(100_000_000n);
fakehuman.addGold(100_000_000n);
nation.addGold(100_000_000n);
// Verify preconditions
expect(dominantPlayer.units(UnitType.MissileSilo)).toHaveLength(0);
expect(fakehuman.units(UnitType.MissileSilo)).toHaveLength(1);
expect(fakehuman.units(UnitType.MIRV)).toHaveLength(0);
expect(nation.units(UnitType.MissileSilo)).toHaveLength(1);
expect(nation.units(UnitType.MIRV)).toHaveLength(0);
expect(dominantPlayer.units(UnitType.MIRV)).toHaveLength(0);
expect(dominantPlayer.gold()).toBeGreaterThan(35_000_000n);
expect(fakehuman.gold()).toBeGreaterThan(35_000_000n);
expect(fakehuman.isAlive()).toBe(true);
expect(fakehuman.numTilesOwned()).toBeGreaterThan(0);
expect(nation.gold()).toBeGreaterThan(35_000_000n);
expect(nation.isAlive()).toBe(true);
expect(nation.numTilesOwned()).toBeGreaterThan(0);
// Verify dominant player has enough territory to trigger victory denial
const dominantTerritoryShare =
dominantPlayer.numTilesOwned() / game.map().numLandTiles();
expect(dominantTerritoryShare).toBeGreaterThan(0.65);
// Track MIRVs before fakehuman considers victory denial
const mirvCountBefore = fakehuman.units(UnitType.MIRV).length;
// Track MIRVs before nation considers victory denial
const mirvCountBefore = nation.units(UnitType.MIRV).length;
// Initialize fakehuman with FakeHumanExecution to enable victory denial logic
const fakehumanNation = new Nation(new Cell(50, 50), fakehuman.info());
// Initialize nation with NationExecution to enable victory denial logic
const testExecutionNation = new Nation(new Cell(50, 50), nation.info());
// Try different game IDs to account for hesitation odds
const gameIds = Array.from({ length: 20 }, (_, i) => `game_${i}`);
let victoryDenialSuccessful = false;
for (const gameId of gameIds) {
const testExecution = new FakeHumanExecution(gameId, fakehumanNation);
const testExecution = new NationExecution(gameId, testExecutionNation);
testExecution.init(game);
for (let tick = 0; tick < 200; tick++) {
@@ -262,7 +262,7 @@ describe("FakeHuman MIRV Retaliation", () => {
if (tick % 10 === 0) {
game.executeNextTick();
}
if (fakehuman.units(UnitType.MIRV).length > mirvCountBefore) {
if (nation.units(UnitType.MIRV).length > mirvCountBefore) {
victoryDenialSuccessful = true;
break;
}
@@ -277,15 +277,15 @@ describe("FakeHuman MIRV Retaliation", () => {
// Process the victory denial MIRV
executeTicks(game, 2);
// Assert: Fakehuman launched a victory denial MIRV
const mirvCountAfter = fakehuman.units(UnitType.MIRV).length;
// Assert: Nation launched a victory denial MIRV
const mirvCountAfter = nation.units(UnitType.MIRV).length;
expect(mirvCountAfter).toBeGreaterThan(mirvCountBefore);
// Verify the victory denial MIRV targets the dominant player's territory
const fakehumanMirvs = fakehuman.units(UnitType.MIRV);
expect(fakehumanMirvs.length).toBeGreaterThan(0);
const nationMirvs = nation.units(UnitType.MIRV);
expect(nationMirvs.length).toBeGreaterThan(0);
const victoryDenialMirv = fakehumanMirvs[fakehumanMirvs.length - 1];
const victoryDenialMirv = nationMirvs[nationMirvs.length - 1];
const victoryDenialTarget = victoryDenialMirv.targetTile();
expect(victoryDenialTarget).toBeDefined();
@@ -295,7 +295,7 @@ describe("FakeHuman MIRV Retaliation", () => {
}
});
test("fakehuman launches MIRV to stop steamrolling player with excessive cities", async () => {
test("nation launches MIRV to stop steamrolling player with excessive cities", async () => {
// Setup game
const game = await setup("big_plains", {
infiniteGold: true,
@@ -315,16 +315,16 @@ describe("FakeHuman MIRV Retaliation", () => {
null,
"second_id",
);
const fakehumanInfo = new PlayerInfo(
"defender_fakehuman",
PlayerType.FakeHuman,
const nationInfo = new PlayerInfo(
"defender_nation",
PlayerType.Nation,
null,
"fakehuman_id",
"nation_id",
);
game.addPlayer(steamrollerInfo);
game.addPlayer(secondPlayerInfo);
game.addPlayer(fakehumanInfo);
game.addPlayer(nationInfo);
// Skip spawn phase
while (game.inSpawnPhase()) {
@@ -333,20 +333,20 @@ describe("FakeHuman MIRV Retaliation", () => {
const steamroller = game.player("steamroller_id");
const secondPlayer = game.player("second_id");
const fakehuman = game.player("fakehuman_id");
const nation = game.player("nation_id");
// Give fakehuman a small territory and missile silo
// Give nation a small territory and missile silo
for (let x = 45; x < 55; x++) {
for (let y = 45; y < 55; y++) {
const tile = game.ref(x, y);
if (game.map().isLand(tile) && !game.map().hasOwner(tile)) {
fakehuman.conquer(tile);
nation.conquer(tile);
}
}
}
const fakehumanTile = Array.from(fakehuman.tiles())[0];
if (fakehumanTile) {
fakehuman.buildUnit(UnitType.MissileSilo, fakehumanTile, {});
const nationTile = Array.from(nation.tiles())[0];
if (nationTile) {
nation.buildUnit(UnitType.MissileSilo, nationTile, {});
}
// Give second player some territory and cities
@@ -387,26 +387,26 @@ describe("FakeHuman MIRV Retaliation", () => {
// Give all players enough gold for MIRVs
steamroller.addGold(100_000_000n);
secondPlayer.addGold(100_000_000n);
fakehuman.addGold(100_000_000n);
nation.addGold(100_000_000n);
// Verify preconditions
expect(fakehuman.units(UnitType.MissileSilo)).toHaveLength(1);
expect(nation.units(UnitType.MissileSilo)).toHaveLength(1);
expect(steamroller.unitCount(UnitType.City)).toBe(minLeaderCities + 2);
expect(secondPlayer.unitCount(UnitType.City)).toBe(5);
expect(fakehuman.units(UnitType.MIRV)).toHaveLength(0);
expect(nation.units(UnitType.MIRV)).toHaveLength(0);
// Track MIRVs before fakehuman considers steamroll stop
const mirvCountBefore = fakehuman.units(UnitType.MIRV).length;
// Track MIRVs before nation considers steamroll stop
const mirvCountBefore = nation.units(UnitType.MIRV).length;
// Initialize fakehuman with FakeHumanExecution to enable steamroll stop logic
const fakehumanNation = new Nation(new Cell(50, 50), fakehuman.info());
// Initialize nation with NationExecution to enable steamroll stop logic
const testExecutionNation = new Nation(new Cell(50, 50), nation.info());
// Try different game IDs to account for hesitation odds
const gameIds = Array.from({ length: 20 }, (_, i) => `game_${i}`);
let steamrollStopSuccessful = false;
for (const gameId of gameIds) {
const testExecution = new FakeHumanExecution(gameId, fakehumanNation);
const testExecution = new NationExecution(gameId, testExecutionNation);
testExecution.init(game);
for (let tick = 0; tick < 200; tick++) {
@@ -415,7 +415,7 @@ describe("FakeHuman MIRV Retaliation", () => {
if (tick % 10 === 0) {
game.executeNextTick();
}
if (fakehuman.units(UnitType.MIRV).length > mirvCountBefore) {
if (nation.units(UnitType.MIRV).length > mirvCountBefore) {
steamrollStopSuccessful = true;
break;
}
@@ -430,15 +430,15 @@ describe("FakeHuman MIRV Retaliation", () => {
// Process the steamroll stop MIRV
executeTicks(game, 2);
// Assert: Fakehuman launched a steamroll stop MIRV
const mirvCountAfter = fakehuman.units(UnitType.MIRV).length;
// Assert: Nation launched a steamroll stop MIRV
const mirvCountAfter = nation.units(UnitType.MIRV).length;
expect(mirvCountAfter).toBeGreaterThan(mirvCountBefore);
// Verify the steamroll stop MIRV targets the steamroller's territory
const fakehumanMirvs = fakehuman.units(UnitType.MIRV);
expect(fakehumanMirvs.length).toBeGreaterThan(0);
const nationMirvs = nation.units(UnitType.MIRV);
expect(nationMirvs.length).toBeGreaterThan(0);
const steamrollStopMirv = fakehumanMirvs[fakehumanMirvs.length - 1];
const steamrollStopMirv = nationMirvs[nationMirvs.length - 1];
const steamrollStopTarget = steamrollStopMirv.targetTile();
expect(steamrollStopTarget).toBeDefined();
@@ -448,7 +448,7 @@ describe("FakeHuman MIRV Retaliation", () => {
}
});
test("fakehuman does not launch MIRV for steamroll when leader has <= 10 cities", async () => {
test("nation does not launch MIRV for steamroll when leader has <= 10 cities", async () => {
// Setup game
const game = await setup("big_plains", {
infiniteGold: true,
@@ -468,16 +468,16 @@ describe("FakeHuman MIRV Retaliation", () => {
null,
"second_id",
);
const fakehumanInfo = new PlayerInfo(
"defender_fakehuman",
PlayerType.FakeHuman,
const nationInfo = new PlayerInfo(
"defender_nation",
PlayerType.Nation,
null,
"fakehuman_id",
"nation_id",
);
game.addPlayer(steamrollerInfo);
game.addPlayer(secondPlayerInfo);
game.addPlayer(fakehumanInfo);
game.addPlayer(nationInfo);
// Skip spawn phase
while (game.inSpawnPhase()) {
@@ -486,20 +486,20 @@ describe("FakeHuman MIRV Retaliation", () => {
const steamroller = game.player("steamroller_id");
const secondPlayer = game.player("second_id");
const fakehuman = game.player("fakehuman_id");
const nation = game.player("nation_id");
// Give fakehuman a small territory and missile silo
// Give nation a small territory and missile silo
for (let x = 45; x < 55; x++) {
for (let y = 45; y < 55; y++) {
const tile = game.ref(x, y);
if (game.map().isLand(tile) && !game.map().hasOwner(tile)) {
fakehuman.conquer(tile);
nation.conquer(tile);
}
}
}
const fakehumanTile = Array.from(fakehuman.tiles())[0];
if (fakehumanTile) {
fakehuman.buildUnit(UnitType.MissileSilo, fakehumanTile, {});
const nationTile = Array.from(nation.tiles())[0];
if (nationTile) {
nation.buildUnit(UnitType.MissileSilo, nationTile, {});
}
// Give second player territory and cities (5 cities)
@@ -538,26 +538,26 @@ describe("FakeHuman MIRV Retaliation", () => {
// Give all players enough gold for MIRVs
steamroller.addGold(100_000_000n);
secondPlayer.addGold(100_000_000n);
fakehuman.addGold(100_000_000n);
nation.addGold(100_000_000n);
// Verify preconditions
expect(fakehuman.units(UnitType.MissileSilo)).toHaveLength(1);
expect(nation.units(UnitType.MissileSilo)).toHaveLength(1);
expect(steamroller.unitCount(UnitType.City)).toBe(minLeaderCities);
expect(secondPlayer.unitCount(UnitType.City)).toBe(5);
expect(fakehuman.units(UnitType.MIRV)).toHaveLength(0);
expect(nation.units(UnitType.MIRV)).toHaveLength(0);
// Track MIRVs before fakehuman considers steamroll stop
const mirvCountBefore = fakehuman.units(UnitType.MIRV).length;
// Track MIRVs before nation considers steamroll stop
const mirvCountBefore = nation.units(UnitType.MIRV).length;
// Initialize fakehuman with FakeHumanExecution to enable steamroll stop logic
const fakehumanNation = new Nation(new Cell(50, 50), fakehuman.info());
// Initialize nation with NationExecution to enable steamroll stop logic
const testExecutionNation = new Nation(new Cell(50, 50), nation.info());
// Try different game IDs to account for hesitation odds
const gameIds = Array.from({ length: 20 }, (_, i) => `game_${i}`);
let steamrollStopAttempted = false;
for (const gameId of gameIds) {
const testExecution = new FakeHumanExecution(gameId, fakehumanNation);
const testExecution = new NationExecution(gameId, testExecutionNation);
testExecution.init(game);
for (let tick = 0; tick < 200; tick++) {
@@ -566,8 +566,8 @@ describe("FakeHuman MIRV Retaliation", () => {
}
// Check if any MIRVs were launched for steamroll stop
const fakehumanMirvs = fakehuman.units(UnitType.MIRV);
if (fakehumanMirvs.length > mirvCountBefore) {
const nationMirvs = nation.units(UnitType.MIRV);
if (nationMirvs.length > mirvCountBefore) {
steamrollStopAttempted = true;
break;
}
@@ -577,7 +577,7 @@ describe("FakeHuman MIRV Retaliation", () => {
expect(steamrollStopAttempted).toBe(false);
});
test("fakehuman launches MIRV to prevent team victory when team approaches victory denial threshold (targets biggest team member)", async () => {
test("nation launches MIRV to prevent team victory when team approaches victory denial threshold (targets biggest team member)", async () => {
// Setup game
const teamPlayer1Info = new PlayerInfo(
"[ALPHA]team_player_1",
@@ -591,11 +591,11 @@ describe("FakeHuman MIRV Retaliation", () => {
null,
"team2_id",
);
const fakehumanInfo = new PlayerInfo(
"defender_fakehuman",
PlayerType.FakeHuman,
const nationInfo = new PlayerInfo(
"defender_nation",
PlayerType.Nation,
null,
"fakehuman_id",
"nation_id",
);
const game = await setup(
"big_plains",
@@ -605,7 +605,7 @@ describe("FakeHuman MIRV Retaliation", () => {
gameMode: GameMode.Team,
playerTeams: 2,
},
[teamPlayer1Info, teamPlayer2Info, fakehumanInfo],
[teamPlayer1Info, teamPlayer2Info, nationInfo],
);
// Players already added via setup() with Team mode and shared clan for humans
@@ -617,20 +617,20 @@ describe("FakeHuman MIRV Retaliation", () => {
const teamPlayer1 = game.player("team1_id");
const teamPlayer2 = game.player("team2_id");
const fakehuman = game.player("fakehuman_id");
const nation = game.player("nation_id");
// Give fakehuman a small territory and missile silo
// Give nation a small territory and missile silo
for (let x = 45; x < 55; x++) {
for (let y = 45; y < 55; y++) {
const tile = game.ref(x, y);
if (game.map().isLand(tile) && !game.map().hasOwner(tile)) {
fakehuman.conquer(tile);
nation.conquer(tile);
}
}
}
const fakehumanTile = Array.from(fakehuman.tiles())[0];
if (fakehumanTile) {
fakehuman.buildUnit(UnitType.MissileSilo, fakehumanTile, {});
const nationTile = Array.from(nation.tiles())[0];
if (nationTile) {
nation.buildUnit(UnitType.MissileSilo, nationTile, {});
}
// Give team players a large amount of territory to exceed team threshold,
@@ -663,16 +663,16 @@ describe("FakeHuman MIRV Retaliation", () => {
// Give all players enough gold for MIRVs
teamPlayer1.addGold(100_000_000n);
teamPlayer2.addGold(100_000_000n);
fakehuman.addGold(100_000_000n);
nation.addGold(100_000_000n);
// Verify preconditions
expect(fakehuman.units(UnitType.MissileSilo)).toHaveLength(1);
expect(fakehuman.units(UnitType.MIRV)).toHaveLength(0);
expect(nation.units(UnitType.MissileSilo)).toHaveLength(1);
expect(nation.units(UnitType.MIRV)).toHaveLength(0);
expect(teamPlayer1.gold()).toBeGreaterThan(35_000_000n);
expect(teamPlayer2.gold()).toBeGreaterThan(35_000_000n);
expect(fakehuman.gold()).toBeGreaterThan(35_000_000n);
expect(fakehuman.isAlive()).toBe(true);
expect(fakehuman.numTilesOwned()).toBeGreaterThan(0);
expect(nation.gold()).toBeGreaterThan(35_000_000n);
expect(nation.isAlive()).toBe(true);
expect(nation.numTilesOwned()).toBeGreaterThan(0);
// Verify team has enough territory to trigger team victory denial
const teamTerritory =
@@ -680,18 +680,18 @@ describe("FakeHuman MIRV Retaliation", () => {
const teamShare = teamTerritory / game.map().numLandTiles();
expect(teamShare).toBeGreaterThan(0.8); //
// Track MIRVs before fakehuman considers team victory denial
const mirvCountBefore = fakehuman.units(UnitType.MIRV).length;
// Track MIRVs before nation considers team victory denial
const mirvCountBefore = nation.units(UnitType.MIRV).length;
// Initialize fakehuman with FakeHumanExecution to enable team victory denial logic
const fakehumanNation = new Nation(new Cell(50, 50), fakehuman.info());
// Initialize nation with NationExecution to enable team victory denial logic
const testExecutionNation = new Nation(new Cell(50, 50), nation.info());
// Try different game IDs to account for hesitation odds
const gameIds = Array.from({ length: 20 }, (_, i) => `game_${i}`);
let teamVictoryDenialSuccessful = false;
for (const gameId of gameIds) {
const testExecution = new FakeHumanExecution(gameId, fakehumanNation);
const testExecution = new NationExecution(gameId, testExecutionNation);
testExecution.init(game);
for (let tick = 0; tick < 200; tick++) {
@@ -700,7 +700,7 @@ describe("FakeHuman MIRV Retaliation", () => {
if (tick % 10 === 0) {
game.executeNextTick();
}
if (fakehuman.units(UnitType.MIRV).length > mirvCountBefore) {
if (nation.units(UnitType.MIRV).length > mirvCountBefore) {
teamVictoryDenialSuccessful = true;
break;
}
@@ -715,15 +715,15 @@ describe("FakeHuman MIRV Retaliation", () => {
// Process the team victory denial MIRV
executeTicks(game, 2);
// Assert: Fakehuman launched a team victory denial MIRV
const mirvCountAfter = fakehuman.units(UnitType.MIRV).length;
// Assert: Nation launched a team victory denial MIRV
const mirvCountAfter = nation.units(UnitType.MIRV).length;
expect(mirvCountAfter).toBeGreaterThan(mirvCountBefore);
// Verify the team victory denial MIRV targets the largest member of the team
const fakehumanMirvs = fakehuman.units(UnitType.MIRV);
expect(fakehumanMirvs.length).toBeGreaterThan(0);
const nationMirvs = nation.units(UnitType.MIRV);
expect(nationMirvs.length).toBeGreaterThan(0);
const teamVictoryDenialMirv = fakehumanMirvs[fakehumanMirvs.length - 1];
const teamVictoryDenialMirv = nationMirvs[nationMirvs.length - 1];
const teamVictoryDenialTarget = teamVictoryDenialMirv.targetTile();
expect(teamVictoryDenialTarget).toBeDefined();
+1 -1
View File
@@ -61,7 +61,7 @@ export async function setup(
gameMode: GameMode.FFA,
gameType: GameType.Singleplayer,
difficulty: Difficulty.Medium,
disableNPCs: false,
disableNations: false,
donateGold: false,
donateTroops: false,
bots: 0,