mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-27 16:55:06 +00:00
Feature: Enable FakeHumans ("Nation Bots") to Launch MIRVs Strategically (#2225)
## Description: > [!IMPORTANT] > Try here: https://mirv-test.openfront.dev/ > [!NOTE] > Blocks PRs: > - #2244 > - #2263 ### Summary Implements intelligent MIRV usage for fakehuman players, enabling them to make strategic nuclear strikes based on game state analysis. ### Changes #### Core FakeHuman Strategy (`FakeHumanExecution.ts`) - **MIRV Decision System**: Added `considerMIRV()` method that evaluates game state and determines optimal MIRV usage - **Three Strategic Targeting Modes**: 1. **Counter-MIRV**: Retaliatory strikes against players actively launching MIRVs at the fakehuman 2. **Victory Denial**: Preemptive strikes against players approaching win conditions - Team threshold: n% of total land (configurable) - Individual threshold: n% of total land (configurable) 3. **Steamroll Prevention**: Strikes against players with dominant city counts (n% ahead of next competitor) #### FakeHuman Behavior Tuning - **Cooldown System**: n-minute cooldown between MIRV attempts (configurable) - **Failure Rate**: ~n% chance of cooldown trigger without launch (simulates human hesitation/resource management; configurable) - **Territory Targeting**: Centers MIRV strikes on enemy territory center-of-mass for maximum impact #### Technical Improvements - **Type Safety**: Updated `UnitParamsMap` to include `targetTile` parameter for MIRV units - **Execution Flow**: Integrated MIRV consideration into fakehuman tick cycle outside of standard attack logic, due to its holistic strategic nature ### Game Balance Impact - **FakeHuman Threat Level**: Increases late-game fakehuman competitiveness - **Endgame Dynamics**: Prevents runaway victories, extends game tension ### Breaking Changes None - purely additive feature ### Related GitHub Issues: - #2205 ------ ## Other - [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 Discord Username: samsammiliah --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Evan <evanpelle@gmail.com>
This commit is contained in:
@@ -20,17 +20,18 @@ import { boundingBoxTiles, calculateBoundingBox, simpleHash } from "../Util";
|
||||
import { AllianceRequestExecution } from "./alliance/AllianceRequestExecution";
|
||||
import { ConstructionExecution } from "./ConstructionExecution";
|
||||
import { EmojiExecution } from "./EmojiExecution";
|
||||
import { MirvExecution } from "./MIRVExecution";
|
||||
import { structureSpawnTileValue } from "./nation/structureSpawnTileValue";
|
||||
import { NukeExecution } from "./NukeExecution";
|
||||
import { SpawnExecution } from "./SpawnExecution";
|
||||
import { TransportShipExecution } from "./TransportShipExecution";
|
||||
import { closestTwoTiles } from "./Util";
|
||||
import { calculateTerritoryCenter, closestTwoTiles } from "./Util";
|
||||
import { BotBehavior, EMOJI_HECKLE } from "./utils/BotBehavior";
|
||||
|
||||
export class FakeHumanExecution implements Execution {
|
||||
private active = true;
|
||||
private random: PseudoRandom;
|
||||
private behavior: BotBehavior | null = null;
|
||||
private behavior: BotBehavior | null = null; // Shared behavior logic for both bots and fakehumans
|
||||
private mg: Game;
|
||||
private player: Player | null = null;
|
||||
|
||||
@@ -42,11 +43,32 @@ export class FakeHumanExecution implements Execution {
|
||||
|
||||
private readonly lastEmojiSent = new Map<Player, Tick>();
|
||||
private readonly lastNukeSent: [Tick, TileRef][] = [];
|
||||
private readonly lastMIRVSent: [Tick, TileRef][] = [];
|
||||
private readonly embargoMalusApplied = new Set<PlayerID>();
|
||||
|
||||
/** MIRV Strategy Constants */
|
||||
|
||||
/** Ticks until MIRV can be attempted again */
|
||||
private static readonly MIRV_COOLDOWN_TICKS = 20;
|
||||
|
||||
/** Odds of aborting a MIRV attempt */
|
||||
private static readonly MIRV_HESITATION_ODDS = 7;
|
||||
|
||||
/** Threshold for team victory denial */
|
||||
private static readonly VICTORY_DENIAL_TEAM_THRESHOLD = 0.8;
|
||||
|
||||
/** Threshold for individual victory denial */
|
||||
private static readonly VICTORY_DENIAL_INDIVIDUAL_THRESHOLD = 0.65;
|
||||
|
||||
/** Multiplier for steamroll city gap threshold */
|
||||
private static readonly STEAMROLL_CITY_GAP_MULTIPLIER = 1.3;
|
||||
|
||||
/** Minimum city count for leader to trigger steam roll detection */
|
||||
private static readonly STEAMROLL_MIN_LEADER_CITIES = 10;
|
||||
|
||||
constructor(
|
||||
gameID: GameID,
|
||||
private nation: Nation,
|
||||
private nation: Nation, // Nation contains PlayerInfo with PlayerType.FakeHuman
|
||||
) {
|
||||
this.random = new PseudoRandom(
|
||||
simpleHash(nation.playerInfo.id) + simpleHash(gameID),
|
||||
@@ -111,7 +133,9 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (ticks % this.attackRate !== this.attackTick) return;
|
||||
if (ticks % this.attackRate !== this.attackTick) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mg.inSpawnPhase()) {
|
||||
const rl = this.randomSpawnLand();
|
||||
@@ -158,6 +182,7 @@ export class FakeHumanExecution implements Execution {
|
||||
this.behavior.handleAllianceExtensionRequests();
|
||||
this.handleUnits();
|
||||
this.handleEmbargoesToHostileNations();
|
||||
this.considerMIRV();
|
||||
this.maybeAttack();
|
||||
}
|
||||
|
||||
@@ -230,6 +255,7 @@ export class FakeHumanExecution implements Execution {
|
||||
|
||||
this.behavior.forgetOldEnemies();
|
||||
this.behavior.assistAllies();
|
||||
|
||||
const enemy = this.behavior.selectEnemy(enemies);
|
||||
if (!enemy) return;
|
||||
this.maybeSendEmoji(enemy);
|
||||
@@ -262,7 +288,7 @@ export class FakeHumanExecution implements Execution {
|
||||
if (
|
||||
silos.length === 0 ||
|
||||
this.player.gold() < this.cost(UnitType.AtomBomb) ||
|
||||
other.type() === PlayerType.Bot ||
|
||||
other.type() === PlayerType.Bot || // Don't nuke bots (as opposed to fakehumans and humans)
|
||||
this.player.isOnSameTeam(other)
|
||||
) {
|
||||
return;
|
||||
@@ -656,6 +682,226 @@ export class FakeHumanExecution implements Execution {
|
||||
return null;
|
||||
}
|
||||
|
||||
// MIRV Strategy Methods
|
||||
private considerMIRV(): boolean {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (this.player.units(UnitType.MissileSilo).length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (this.player.gold() < this.cost(UnitType.MIRV)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.removeOldMIRVEvents();
|
||||
if (this.lastMIRVSent.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.random.chance(FakeHumanExecution.MIRV_HESITATION_ODDS)) {
|
||||
this.triggerMIRVCooldown();
|
||||
return false;
|
||||
}
|
||||
|
||||
const inboundMIRVSender = this.selectCounterMirvTarget();
|
||||
if (inboundMIRVSender) {
|
||||
this.maybeSendMIRV(inboundMIRVSender);
|
||||
return true;
|
||||
}
|
||||
|
||||
const victoryDenialTarget = this.selectVictoryDenialTarget();
|
||||
if (victoryDenialTarget) {
|
||||
this.maybeSendMIRV(victoryDenialTarget);
|
||||
return true;
|
||||
}
|
||||
|
||||
const steamrollStopTarget = this.selectSteamrollStopTarget();
|
||||
if (steamrollStopTarget) {
|
||||
this.maybeSendMIRV(steamrollStopTarget);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private selectCounterMirvTarget(): Player | null {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const attackers = this.getValidMirvTargetPlayers().filter((p) =>
|
||||
this.isInboundMIRVFrom(p),
|
||||
);
|
||||
if (attackers.length === 0) return null;
|
||||
attackers.sort((a, b) => b.numTilesOwned() - a.numTilesOwned());
|
||||
return attackers[0];
|
||||
}
|
||||
|
||||
private selectVictoryDenialTarget(): Player | null {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const totalLand = this.mg.numLandTiles();
|
||||
if (totalLand === 0) return null;
|
||||
let best: { p: Player; severity: number } | null = null;
|
||||
for (const p of this.getValidMirvTargetPlayers()) {
|
||||
let severity = 0;
|
||||
const team = p.team();
|
||||
if (team !== null) {
|
||||
const teamMembers = this.mg
|
||||
.players()
|
||||
.filter((x) => x.team() === team && x.isPlayer());
|
||||
const teamTerritory = teamMembers
|
||||
.map((x) => x.numTilesOwned())
|
||||
.reduce((a, b) => a + b, 0);
|
||||
const teamShare = teamTerritory / totalLand;
|
||||
if (teamShare >= FakeHumanExecution.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;
|
||||
for (const member of teamMembers) {
|
||||
const tiles = member.numTilesOwned();
|
||||
if (tiles > largestTiles) {
|
||||
largestTiles = tiles;
|
||||
largestMember = member;
|
||||
}
|
||||
}
|
||||
if (largestMember === p) {
|
||||
severity = teamShare;
|
||||
} else {
|
||||
severity = 0; // Skip non-largest members
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const share = p.numTilesOwned() / totalLand;
|
||||
if (share >= FakeHumanExecution.VICTORY_DENIAL_INDIVIDUAL_THRESHOLD)
|
||||
severity = share;
|
||||
}
|
||||
if (severity > 0) {
|
||||
if (best === null || severity > best.severity) best = { p, severity };
|
||||
}
|
||||
}
|
||||
return best ? best.p : null;
|
||||
}
|
||||
|
||||
private selectSteamrollStopTarget(): Player | null {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const validTargets = this.getValidMirvTargetPlayers();
|
||||
|
||||
if (validTargets.length === 0) return null;
|
||||
|
||||
const allPlayers = this.mg
|
||||
.players()
|
||||
.filter((p) => p.isPlayer())
|
||||
.map((p) => ({ p, cityCount: this.countCities(p) }))
|
||||
.sort((a, b) => b.cityCount - a.cityCount);
|
||||
|
||||
if (allPlayers.length < 2) return null;
|
||||
|
||||
const topPlayer = allPlayers[0];
|
||||
|
||||
if (topPlayer.cityCount <= FakeHumanExecution.STEAMROLL_MIN_LEADER_CITIES)
|
||||
return null;
|
||||
|
||||
const secondHighest = allPlayers[1].cityCount;
|
||||
|
||||
const threshold =
|
||||
secondHighest * FakeHumanExecution.STEAMROLL_CITY_GAP_MULTIPLIER;
|
||||
|
||||
if (topPlayer.cityCount >= threshold) {
|
||||
return validTargets.some((p) => p === topPlayer.p) ? topPlayer.p : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// MIRV Helper Methods
|
||||
private mirvTargetsCache: {
|
||||
tick: number;
|
||||
players: Player[];
|
||||
} | null = null;
|
||||
|
||||
private getValidMirvTargetPlayers(): Player[] {
|
||||
const MIRV_TARGETS_CACHE_TICKS = 2 * 10; // 2 seconds
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
|
||||
if (
|
||||
this.mirvTargetsCache &&
|
||||
this.mg.ticks() - this.mirvTargetsCache.tick < MIRV_TARGETS_CACHE_TICKS
|
||||
) {
|
||||
return this.mirvTargetsCache.players;
|
||||
}
|
||||
|
||||
const players = this.mg.players().filter((p) => {
|
||||
return (
|
||||
p !== this.player &&
|
||||
p.isPlayer() &&
|
||||
p.type() !== PlayerType.Bot &&
|
||||
!this.player!.isOnSameTeam(p)
|
||||
);
|
||||
});
|
||||
|
||||
this.mirvTargetsCache = { tick: this.mg.ticks(), players };
|
||||
return players;
|
||||
}
|
||||
|
||||
private isInboundMIRVFrom(attacker: Player): boolean {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const enemyMirvs = attacker.units(UnitType.MIRV);
|
||||
for (const mirv of enemyMirvs) {
|
||||
const dst = mirv.targetTile();
|
||||
if (!dst) continue;
|
||||
if (!this.mg.hasOwner(dst)) continue;
|
||||
const owner = this.mg.owner(dst);
|
||||
if (owner === this.player) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private countCities(p: Player): number {
|
||||
return p.unitCount(UnitType.City);
|
||||
}
|
||||
|
||||
private calculateTerritoryCenter(target: Player): TileRef | null {
|
||||
return calculateTerritoryCenter(this.mg, target);
|
||||
}
|
||||
|
||||
// MIRV Execution Methods
|
||||
private maybeSendMIRV(enemy: Player): void {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
|
||||
this.maybeSendEmoji(enemy);
|
||||
|
||||
const centerTile = this.calculateTerritoryCenter(enemy);
|
||||
if (centerTile && this.player.canBuild(UnitType.MIRV, centerTile)) {
|
||||
this.sendMIRV(centerTile);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private sendMIRV(tile: TileRef): void {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
this.triggerMIRVCooldown(tile);
|
||||
this.mg.addExecution(new MirvExecution(this.player, tile));
|
||||
}
|
||||
|
||||
private triggerMIRVCooldown(tile?: TileRef): void {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
this.removeOldMIRVEvents();
|
||||
const tick = this.mg.ticks();
|
||||
// Use provided tile or any tile from player's territory for cooldown tracking
|
||||
const cooldownTile =
|
||||
tile ?? Array.from(this.player.tiles())[0] ?? this.mg.ref(0, 0);
|
||||
this.lastMIRVSent.push([tick, cooldownTile]);
|
||||
}
|
||||
|
||||
private removeOldMIRVEvents() {
|
||||
const maxAge = FakeHumanExecution.MIRV_COOLDOWN_TICKS;
|
||||
const tick = this.mg.ticks();
|
||||
while (
|
||||
this.lastMIRVSent.length > 0 &&
|
||||
this.lastMIRVSent[0][0] + maxAge <= tick
|
||||
) {
|
||||
this.lastMIRVSent.shift();
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,9 @@ export class MirvExecution implements Execution {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.nuke = this.player.buildUnit(UnitType.MIRV, spawn, {});
|
||||
this.nuke = this.player.buildUnit(UnitType.MIRV, spawn, {
|
||||
targetTile: this.dst,
|
||||
});
|
||||
const x = Math.floor(
|
||||
(this.mg.x(this.dst) + this.mg.x(this.mg.x(this.nuke.tile()))) / 2,
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Game, Player } from "../game/Game";
|
||||
import { euclDistFN, GameMap, TileRef } from "../game/GameMap";
|
||||
|
||||
export function getSpawnTiles(gm: GameMap, tile: TileRef): TileRef[] {
|
||||
@@ -71,3 +72,61 @@ export function closestTwoTiles(
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the center of a player's territory using geometric approach.
|
||||
* Uses the bounding box center and verifies ownership, falling back to nearest border tile if necessary.
|
||||
*
|
||||
* @param game - The game instance
|
||||
* @param target - The player whose territory center to calculate
|
||||
* @returns The tile reference for the territory center, or null if no valid center found
|
||||
*/
|
||||
export function calculateTerritoryCenter(
|
||||
game: Game,
|
||||
target: Player,
|
||||
): TileRef | null {
|
||||
const borderTiles = target.borderTiles();
|
||||
if (borderTiles.size === 0) return null;
|
||||
|
||||
// Calculate bounding box center in a single pass through border tiles
|
||||
let minX = Infinity,
|
||||
maxX = -Infinity;
|
||||
let minY = Infinity,
|
||||
maxY = -Infinity;
|
||||
|
||||
for (const tile of borderTiles) {
|
||||
const x = game.x(tile);
|
||||
const y = game.y(tile);
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (y > maxY) maxY = y;
|
||||
}
|
||||
|
||||
const centerX = Math.floor((minX + maxX) / 2);
|
||||
const centerY = Math.floor((minY + maxY) / 2);
|
||||
|
||||
const centerTile = game.ref(centerX, centerY);
|
||||
|
||||
// Verify ownership of the center tile
|
||||
if (game.owner(centerTile) === target) {
|
||||
return centerTile;
|
||||
}
|
||||
|
||||
// Fall back to nearest border tile if center is not owned
|
||||
let closestTile: TileRef | null = null;
|
||||
let closestDistanceSquared = Infinity;
|
||||
|
||||
for (const tile of borderTiles) {
|
||||
const dx = game.x(tile) - centerX;
|
||||
const dy = game.y(tile) - centerY;
|
||||
const distSquared = dx * dx + dy * dy;
|
||||
|
||||
if (distSquared < closestDistanceSquared) {
|
||||
closestDistanceSquared = distSquared;
|
||||
closestTile = tile;
|
||||
}
|
||||
}
|
||||
|
||||
return closestTile;
|
||||
}
|
||||
|
||||
@@ -270,7 +270,9 @@ export interface UnitParamsMap {
|
||||
|
||||
[UnitType.City]: Record<string, never>;
|
||||
|
||||
[UnitType.MIRV]: Record<string, never>;
|
||||
[UnitType.MIRV]: {
|
||||
targetTile?: number;
|
||||
};
|
||||
|
||||
[UnitType.MIRVWarhead]: {
|
||||
targetTile?: number;
|
||||
|
||||
@@ -0,0 +1,741 @@
|
||||
import { FakeHumanExecution } from "../src/core/execution/FakeHumanExecution";
|
||||
import { MirvExecution } from "../src/core/execution/MIRVExecution";
|
||||
import {
|
||||
Cell,
|
||||
GameMode,
|
||||
Nation,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
UnitType,
|
||||
} from "../src/core/game/Game";
|
||||
import { setup } from "./util/Setup";
|
||||
import { executeTicks } from "./util/utils";
|
||||
|
||||
describe("FakeHuman MIRV Retaliation", () => {
|
||||
test("fakehuman retaliates with MIRV when attacked by MIRV", async () => {
|
||||
const game = await setup("big_plains", {
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
});
|
||||
|
||||
// Create two players
|
||||
const attackerInfo = new PlayerInfo(
|
||||
"attacker",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"attacker_id",
|
||||
);
|
||||
const fakehumanInfo = new PlayerInfo(
|
||||
"defender_fakehuman",
|
||||
PlayerType.FakeHuman,
|
||||
null,
|
||||
"fakehuman_id",
|
||||
);
|
||||
|
||||
game.addPlayer(attackerInfo);
|
||||
game.addPlayer(fakehumanInfo);
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
const attacker = game.player("attacker_id");
|
||||
const fakehuman = game.player("fakehuman_id");
|
||||
|
||||
// Give attacker territory and missile silo
|
||||
for (let x = 5; x < 15; x++) {
|
||||
for (let y = 5; y < 15; y++) {
|
||||
const tile = game.ref(x, y);
|
||||
if (game.map().isLand(tile)) {
|
||||
attacker.conquer(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
attacker.buildUnit(UnitType.MissileSilo, game.ref(10, 10), {});
|
||||
|
||||
// Give fakehuman 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
fakehuman.buildUnit(UnitType.MissileSilo, game.ref(50, 50), {});
|
||||
|
||||
// Give both players enough gold for MIRVs
|
||||
attacker.addGold(100_000_000n);
|
||||
fakehuman.addGold(100_000_000n);
|
||||
|
||||
// Verify preconditions
|
||||
expect(attacker.units(UnitType.MissileSilo)).toHaveLength(1);
|
||||
expect(fakehuman.units(UnitType.MissileSilo)).toHaveLength(1);
|
||||
expect(attacker.gold()).toBeGreaterThan(35_000_000n);
|
||||
expect(fakehuman.gold()).toBeGreaterThan(35_000_000n);
|
||||
|
||||
// Track MIRVs before fakehuman retaliates
|
||||
const mirvCountBefore = fakehuman.units(UnitType.MIRV).length;
|
||||
|
||||
// Initialize fakehuman with FakeHumanExecution to enable retaliation logic
|
||||
const fakehumanNation = new Nation(new Cell(50, 50), 1, fakehuman.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);
|
||||
testExecution.init(game);
|
||||
|
||||
// Launch MIRV from attacker to fakehuman
|
||||
const targetTile = Array.from(fakehuman.tiles())[0];
|
||||
game.addExecution(new MirvExecution(attacker, targetTile));
|
||||
|
||||
// Execute fakehuman's tick logic
|
||||
for (let tick = 0; tick < 200; tick++) {
|
||||
testExecution.tick(tick);
|
||||
// Allow the game to process executions
|
||||
if (tick % 10 === 0) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// Check if fakehuman attempted retaliation
|
||||
if (fakehuman.units(UnitType.MIRV).length > mirvCountBefore) {
|
||||
retaliationAttempted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (retaliationAttempted) break;
|
||||
}
|
||||
|
||||
// Assert that retaliation was attempted
|
||||
expect(retaliationAttempted).toBe(true);
|
||||
|
||||
// Process the retaliation
|
||||
executeTicks(game, 2);
|
||||
|
||||
// Assert: Fakehuman launched a retaliatory MIRV
|
||||
const mirvCountAfter = fakehuman.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 retaliationMirv = fakehumanMirvs[fakehumanMirvs.length - 1];
|
||||
const retaliationTarget = retaliationMirv.targetTile();
|
||||
expect(retaliationTarget).toBeDefined();
|
||||
|
||||
if (retaliationTarget) {
|
||||
const targetOwner = game.owner(retaliationTarget);
|
||||
expect(targetOwner).toBe(attacker);
|
||||
}
|
||||
});
|
||||
|
||||
test("fakehuman launches MIRV to prevent victory when player approaches win condition", async () => {
|
||||
// Setup game
|
||||
const game = await setup("big_plains", {
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
});
|
||||
|
||||
// Create two players
|
||||
const dominantPlayerInfo = new PlayerInfo(
|
||||
"dominant_player",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"dominant_id",
|
||||
);
|
||||
const fakehumanInfo = new PlayerInfo(
|
||||
"defender_fakehuman",
|
||||
PlayerType.FakeHuman,
|
||||
null,
|
||||
"fakehuman_id",
|
||||
);
|
||||
|
||||
game.addPlayer(dominantPlayerInfo);
|
||||
game.addPlayer(fakehumanInfo);
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
const dominantPlayer = game.player("dominant_id");
|
||||
const fakehuman = game.player("fakehuman_id");
|
||||
|
||||
// First, give fakehuman a small territory and missile silo
|
||||
let fakehumanTiles = 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find enough tiles, try a different area
|
||||
if (fakehumanTiles === 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
|
||||
}
|
||||
}
|
||||
if (fakehumanTiles >= 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, {});
|
||||
}
|
||||
|
||||
// Then give dominant player a large amount of territory
|
||||
// This should trigger the victory denial threshold
|
||||
const totalLandTiles = game.map().numLandTiles();
|
||||
const targetTiles = Math.floor(totalLandTiles * 0.66);
|
||||
|
||||
let conqueredTiles = 0;
|
||||
for (
|
||||
let x = 0;
|
||||
x < game.map().width() && conqueredTiles < targetTiles;
|
||||
x++
|
||||
) {
|
||||
for (
|
||||
let y = 0;
|
||||
y < game.map().height() && conqueredTiles < targetTiles;
|
||||
y++
|
||||
) {
|
||||
const tile = game.ref(x, y);
|
||||
if (game.map().isLand(tile) && !game.map().hasOwner(tile)) {
|
||||
dominantPlayer.conquer(tile);
|
||||
conqueredTiles++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Give both players enough gold for MIRVs
|
||||
dominantPlayer.addGold(100_000_000n);
|
||||
fakehuman.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(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);
|
||||
|
||||
// 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;
|
||||
|
||||
// Initialize fakehuman with FakeHumanExecution to enable victory denial logic
|
||||
const fakehumanNation = new Nation(new Cell(50, 50), 1, fakehuman.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);
|
||||
testExecution.init(game);
|
||||
|
||||
for (let tick = 0; tick < 200; tick++) {
|
||||
testExecution.tick(game.ticks());
|
||||
// Allow the game to process executions
|
||||
if (tick % 10 === 0) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
if (fakehuman.units(UnitType.MIRV).length > mirvCountBefore) {
|
||||
victoryDenialSuccessful = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (victoryDenialSuccessful) break;
|
||||
}
|
||||
|
||||
// Assert that victory denial was successful
|
||||
expect(victoryDenialSuccessful).toBe(true);
|
||||
|
||||
// Process the victory denial MIRV
|
||||
executeTicks(game, 2);
|
||||
|
||||
// Assert: Fakehuman launched a victory denial MIRV
|
||||
const mirvCountAfter = fakehuman.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 victoryDenialMirv = fakehumanMirvs[fakehumanMirvs.length - 1];
|
||||
const victoryDenialTarget = victoryDenialMirv.targetTile();
|
||||
expect(victoryDenialTarget).toBeDefined();
|
||||
|
||||
if (victoryDenialTarget) {
|
||||
const targetOwner = game.owner(victoryDenialTarget);
|
||||
expect(targetOwner).toBe(dominantPlayer);
|
||||
}
|
||||
});
|
||||
|
||||
test("fakehuman launches MIRV to stop steamrolling player with excessive cities", async () => {
|
||||
// Setup game
|
||||
const game = await setup("big_plains", {
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
});
|
||||
|
||||
// Create three players
|
||||
const steamrollerInfo = new PlayerInfo(
|
||||
"steamroller",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"steamroller_id",
|
||||
);
|
||||
const secondPlayerInfo = new PlayerInfo(
|
||||
"second_player",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"second_id",
|
||||
);
|
||||
const fakehumanInfo = new PlayerInfo(
|
||||
"defender_fakehuman",
|
||||
PlayerType.FakeHuman,
|
||||
null,
|
||||
"fakehuman_id",
|
||||
);
|
||||
|
||||
game.addPlayer(steamrollerInfo);
|
||||
game.addPlayer(secondPlayerInfo);
|
||||
game.addPlayer(fakehumanInfo);
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
const steamroller = game.player("steamroller_id");
|
||||
const secondPlayer = game.player("second_id");
|
||||
const fakehuman = game.player("fakehuman_id");
|
||||
|
||||
// Give fakehuman 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
const fakehumanTile = Array.from(fakehuman.tiles())[0];
|
||||
if (fakehumanTile) {
|
||||
fakehuman.buildUnit(UnitType.MissileSilo, fakehumanTile, {});
|
||||
}
|
||||
|
||||
// Give second player some territory and cities
|
||||
for (let x = 20; x < 30; x++) {
|
||||
for (let y = 20; y < 30; y++) {
|
||||
const tile = game.ref(x, y);
|
||||
if (game.map().isLand(tile) && !game.map().hasOwner(tile)) {
|
||||
secondPlayer.conquer(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Give second player 5 cities
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const secondPlayerTile = Array.from(secondPlayer.tiles())[0];
|
||||
if (secondPlayerTile) {
|
||||
secondPlayer.buildUnit(UnitType.City, secondPlayerTile, {});
|
||||
}
|
||||
}
|
||||
|
||||
// Give steamroller territory and many cities
|
||||
for (let x = 5; x < 25; x++) {
|
||||
for (let y = 5; y < 25; y++) {
|
||||
const tile = game.ref(x, y);
|
||||
if (game.map().isLand(tile) && !game.map().hasOwner(tile)) {
|
||||
steamroller.conquer(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Give steamroller cities
|
||||
const minLeaderCities = 10;
|
||||
for (let i = 0; i < minLeaderCities + 2; i++) {
|
||||
const steamrollerTile = Array.from(steamroller.tiles())[0];
|
||||
if (steamrollerTile) {
|
||||
steamroller.buildUnit(UnitType.City, steamrollerTile, {});
|
||||
}
|
||||
}
|
||||
|
||||
// Give all players enough gold for MIRVs
|
||||
steamroller.addGold(100_000_000n);
|
||||
secondPlayer.addGold(100_000_000n);
|
||||
fakehuman.addGold(100_000_000n);
|
||||
|
||||
// Verify preconditions
|
||||
expect(fakehuman.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);
|
||||
|
||||
// Track MIRVs before fakehuman considers steamroll stop
|
||||
const mirvCountBefore = fakehuman.units(UnitType.MIRV).length;
|
||||
|
||||
// Initialize fakehuman with FakeHumanExecution to enable steamroll stop logic
|
||||
const fakehumanNation = new Nation(new Cell(50, 50), 1, fakehuman.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);
|
||||
testExecution.init(game);
|
||||
|
||||
for (let tick = 0; tick < 200; tick++) {
|
||||
testExecution.tick(game.ticks());
|
||||
// Allow the game to process executions
|
||||
if (tick % 10 === 0) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
if (fakehuman.units(UnitType.MIRV).length > mirvCountBefore) {
|
||||
steamrollStopSuccessful = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (steamrollStopSuccessful) break;
|
||||
}
|
||||
|
||||
// Assert that steamroll stop was successful
|
||||
expect(steamrollStopSuccessful).toBe(true);
|
||||
|
||||
// Process the steamroll stop MIRV
|
||||
executeTicks(game, 2);
|
||||
|
||||
// Assert: Fakehuman launched a steamroll stop MIRV
|
||||
const mirvCountAfter = fakehuman.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 steamrollStopMirv = fakehumanMirvs[fakehumanMirvs.length - 1];
|
||||
const steamrollStopTarget = steamrollStopMirv.targetTile();
|
||||
expect(steamrollStopTarget).toBeDefined();
|
||||
|
||||
if (steamrollStopTarget) {
|
||||
const targetOwner = game.owner(steamrollStopTarget);
|
||||
expect(targetOwner).toBe(steamroller);
|
||||
}
|
||||
});
|
||||
|
||||
test("fakehuman does not launch MIRV for steamroll when leader has <= 10 cities", async () => {
|
||||
// Setup game
|
||||
const game = await setup("big_plains", {
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
});
|
||||
|
||||
// Create three players
|
||||
const steamrollerInfo = new PlayerInfo(
|
||||
"steamroller",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"steamroller_id",
|
||||
);
|
||||
const secondPlayerInfo = new PlayerInfo(
|
||||
"second_player",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"second_id",
|
||||
);
|
||||
const fakehumanInfo = new PlayerInfo(
|
||||
"defender_fakehuman",
|
||||
PlayerType.FakeHuman,
|
||||
null,
|
||||
"fakehuman_id",
|
||||
);
|
||||
|
||||
game.addPlayer(steamrollerInfo);
|
||||
game.addPlayer(secondPlayerInfo);
|
||||
game.addPlayer(fakehumanInfo);
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
const steamroller = game.player("steamroller_id");
|
||||
const secondPlayer = game.player("second_id");
|
||||
const fakehuman = game.player("fakehuman_id");
|
||||
|
||||
// Give fakehuman 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
const fakehumanTile = Array.from(fakehuman.tiles())[0];
|
||||
if (fakehumanTile) {
|
||||
fakehuman.buildUnit(UnitType.MissileSilo, fakehumanTile, {});
|
||||
}
|
||||
|
||||
// Give second player territory and cities (5 cities)
|
||||
for (let x = 25; x < 45; x++) {
|
||||
for (let y = 25; y < 45; y++) {
|
||||
const tile = game.ref(x, y);
|
||||
if (game.map().isLand(tile) && !game.map().hasOwner(tile)) {
|
||||
secondPlayer.conquer(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const secondPlayerTile = Array.from(secondPlayer.tiles())[0];
|
||||
if (secondPlayerTile) {
|
||||
secondPlayer.buildUnit(UnitType.City, secondPlayerTile, {});
|
||||
}
|
||||
}
|
||||
|
||||
// Give steamroller territory and cities
|
||||
const minLeaderCities = 10;
|
||||
for (let x = 5; x < 25; x++) {
|
||||
for (let y = 5; y < 25; y++) {
|
||||
const tile = game.ref(x, y);
|
||||
if (game.map().isLand(tile) && !game.map().hasOwner(tile)) {
|
||||
steamroller.conquer(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < minLeaderCities; i++) {
|
||||
const steamrollerTile = Array.from(steamroller.tiles())[0];
|
||||
if (steamrollerTile) {
|
||||
steamroller.buildUnit(UnitType.City, steamrollerTile, {});
|
||||
}
|
||||
}
|
||||
|
||||
// Give all players enough gold for MIRVs
|
||||
steamroller.addGold(100_000_000n);
|
||||
secondPlayer.addGold(100_000_000n);
|
||||
fakehuman.addGold(100_000_000n);
|
||||
|
||||
// Verify preconditions
|
||||
expect(fakehuman.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);
|
||||
|
||||
// Track MIRVs before fakehuman considers steamroll stop
|
||||
const mirvCountBefore = fakehuman.units(UnitType.MIRV).length;
|
||||
|
||||
// Initialize fakehuman with FakeHumanExecution to enable steamroll stop logic
|
||||
const fakehumanNation = new Nation(new Cell(50, 50), 1, fakehuman.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);
|
||||
testExecution.init(game);
|
||||
|
||||
for (let tick = 0; tick < 200; tick++) {
|
||||
testExecution.tick(game.ticks());
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// Check if any MIRVs were launched for steamroll stop
|
||||
const fakehumanMirvs = fakehuman.units(UnitType.MIRV);
|
||||
if (fakehumanMirvs.length > mirvCountBefore) {
|
||||
steamrollStopAttempted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that steamroll stop was NOT attempted
|
||||
expect(steamrollStopAttempted).toBe(false);
|
||||
});
|
||||
|
||||
test("fakehuman 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",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"team1_id",
|
||||
);
|
||||
const teamPlayer2Info = new PlayerInfo(
|
||||
"[ALPHA]team_player_2",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"team2_id",
|
||||
);
|
||||
const fakehumanInfo = new PlayerInfo(
|
||||
"defender_fakehuman",
|
||||
PlayerType.FakeHuman,
|
||||
null,
|
||||
"fakehuman_id",
|
||||
);
|
||||
const game = await setup(
|
||||
"big_plains",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
gameMode: GameMode.Team,
|
||||
playerTeams: 2,
|
||||
},
|
||||
[teamPlayer1Info, teamPlayer2Info, fakehumanInfo],
|
||||
);
|
||||
|
||||
// Players already added via setup() with Team mode and shared clan for humans
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
const teamPlayer1 = game.player("team1_id");
|
||||
const teamPlayer2 = game.player("team2_id");
|
||||
const fakehuman = game.player("fakehuman_id");
|
||||
|
||||
// Give fakehuman 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
const fakehumanTile = Array.from(fakehuman.tiles())[0];
|
||||
if (fakehumanTile) {
|
||||
fakehuman.buildUnit(UnitType.MissileSilo, fakehumanTile, {});
|
||||
}
|
||||
|
||||
// Give team players a large amount of territory to exceed team threshold,
|
||||
// but skew so teamPlayer1 is clearly the largest member
|
||||
const totalLandTiles = game.map().numLandTiles();
|
||||
const teamTargetTiles = Math.floor(totalLandTiles * 0.82);
|
||||
|
||||
let conqueredTiles = 0;
|
||||
for (
|
||||
let x = 0;
|
||||
x < game.map().width() && conqueredTiles < teamTargetTiles;
|
||||
x++
|
||||
) {
|
||||
for (
|
||||
let y = 0;
|
||||
y < game.map().height() && conqueredTiles < teamTargetTiles;
|
||||
y++
|
||||
) {
|
||||
const tile = game.ref(x, y);
|
||||
if (game.map().isLand(tile) && !game.map().hasOwner(tile)) {
|
||||
// 3:1 bias towards teamPlayer1 to ensure largest-member targeting is well-defined
|
||||
const teamPlayer =
|
||||
conqueredTiles % 4 === 0 ? teamPlayer2 : teamPlayer1;
|
||||
teamPlayer.conquer(tile);
|
||||
conqueredTiles++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Give all players enough gold for MIRVs
|
||||
teamPlayer1.addGold(100_000_000n);
|
||||
teamPlayer2.addGold(100_000_000n);
|
||||
fakehuman.addGold(100_000_000n);
|
||||
|
||||
// Verify preconditions
|
||||
expect(fakehuman.units(UnitType.MissileSilo)).toHaveLength(1);
|
||||
expect(fakehuman.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);
|
||||
|
||||
// Verify team has enough territory to trigger team victory denial
|
||||
const teamTerritory =
|
||||
teamPlayer1.numTilesOwned() + teamPlayer2.numTilesOwned();
|
||||
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;
|
||||
|
||||
// Initialize fakehuman with FakeHumanExecution to enable team victory denial logic
|
||||
const fakehumanNation = new Nation(new Cell(50, 50), 1, fakehuman.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);
|
||||
testExecution.init(game);
|
||||
|
||||
for (let tick = 0; tick < 200; tick++) {
|
||||
testExecution.tick(game.ticks());
|
||||
// Allow the game to process executions
|
||||
if (tick % 10 === 0) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
if (fakehuman.units(UnitType.MIRV).length > mirvCountBefore) {
|
||||
teamVictoryDenialSuccessful = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (teamVictoryDenialSuccessful) break;
|
||||
}
|
||||
|
||||
// Assert that team victory denial was successful
|
||||
expect(teamVictoryDenialSuccessful).toBe(true);
|
||||
|
||||
// Process the team victory denial MIRV
|
||||
executeTicks(game, 2);
|
||||
|
||||
// Assert: Fakehuman launched a team victory denial MIRV
|
||||
const mirvCountAfter = fakehuman.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 teamVictoryDenialMirv = fakehumanMirvs[fakehumanMirvs.length - 1];
|
||||
const teamVictoryDenialTarget = teamVictoryDenialMirv.targetTile();
|
||||
expect(teamVictoryDenialTarget).toBeDefined();
|
||||
|
||||
if (teamVictoryDenialTarget) {
|
||||
const targetOwner = game.owner(teamVictoryDenialTarget);
|
||||
// Should target the biggest member of the team
|
||||
const biggest =
|
||||
teamPlayer1.numTilesOwned() >= teamPlayer2.numTilesOwned()
|
||||
? teamPlayer1
|
||||
: teamPlayer2;
|
||||
expect(targetOwner).toBe(biggest);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user