mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:30:45 +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:
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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``;
|
||||
|
||||
@@ -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
@@ -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,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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 &&
|
||||
+21
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+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) {
|
||||
@@ -350,7 +350,7 @@ export enum TerrainType {
|
||||
export enum PlayerType {
|
||||
Bot = "BOT",
|
||||
Human = "HUMAN",
|
||||
FakeHuman = "FAKEHUMAN",
|
||||
Nation = "NATION",
|
||||
}
|
||||
|
||||
export interface Execution {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
@@ -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)", () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user