Files
OpenFrontIO/tests/NationMIRV.test.ts
T
FloPinguin 23e4bf6725 ☢️ Nations send much better nukes now (Part 1) ☢️ (#2756)
This is a very important PR for HumansVsNations (But also for
singleplayer).
Humans will throw lots of nukes onto nations, but nations didn't do
that. Until now :)

## Refactor

- Moved all the nuking logic to the new file `NationNukeBehavior.ts`
- Moved `randTerritoryTileArray()` and `randTerritoryTile()` to the new
file `NationUtils.ts` because we need that method in multiple places now
- Because we already have an `NationUtils.ts` (It contains the method
`createNationsForGame` for HumansVsNations) I renamed the old one to
`NationCreation.ts` to avoid confusion

## Bug fixed

- `allRelationsSorted()` in `PlayerImpl` returned dead players all the
time... Which caused nations to not attack / send nukes in some cases...

## Nuke-sending features / improvements

- On hard and impossible difficulty, nations no longer make sure that
nukes will only hit inside of their targets border. This logic very
often stopped nations from throwing nukes. Now their nukes are allowed
to hit TerraNullius (=> ocean!). And in team games, it's even allowed
that their nukes hit other non-friendly players as well! This is very
important for HumansVsNations.
- The basic check for SAMs now gets skipped if we are on easy difficulty
(easy nations are not smart enough to do that)
- I improved the basic check for SAMs (medium difficulty) a bit (nations
send less nukes into SAMs)
- On hard and impossible difficulty, we now use the new method
`isTrajectoryInterceptableBySam()` to avoid SAMs completely. It's
mirroring `NukeTrajectoryPreviewLayer.ts` logic a bit.
- I added "perceived cost" to simulate nations saving up for a MIRV
(Otherwise most hard/impossible nations will spend all their gold on
nukes). But if we are in a team game (MIRVs are not relevant) or if we
already saved up for a MIRV, the "perceived cost" gets ignored.
- Updated the "most hated player" selection in `findBestNukeTarget()` to
ignore very weak players. We don't need to throw nukes at players which
we can easily steamroll by land.
- Added `findFFACrownTarget()` to nuke the crown (based on difficulty).
- Added `findStrongestTeamTarget()` to nuke the strongest team.
- Updated `randTerritoryTile()` so that it has a higher chance of
returning the tiles of a
"leftover-nuked-to-death-player-with-some-tiles-left": `if
(p.numTilesOwned() <= 100) {return
random.randElement(Array.from(p.tiles()));}`.
- Changed `const range = nukeType === UnitType.HydrogenBomb ? 60 : 15`
to `config().nukeMagnitudes(nukeType).inner`. Should make more sense.
- Adjusted `nukeTileScore()` to search for units in
`this.mg.config().nukeMagnitudes(nukeType).inner` instead of fixed 25
- Adjusted `nukeTileScore()` to account for unit levels (levels got
ignored previously). Also increased score for ports from 10_000 to
15_000.
- I made sure that nations can nuke EVERY SINGLE TILE from an enemy,
even if the enemy has no structures ("Prefer tiles that are closer to a
silo" can no longer make the `nukeTileScore()` drop too much,
`bestValue` in `maybeSendNuke()` starts at -1 now)
- In the entire nuking logic, factories were missing. Now they are
added.

## Media

Nation team vs. nation team: They are nuking the very last pixels of
red, just like humans would do it 😀

<img width="915" height="683" alt="image"
src="https://github.com/user-attachments/assets/109c7921-b959-4aa9-a971-0d7742971686"
/>

Hard difficulty FFA game: Nations throwing much more nukes. And they are
nuking the crown.


https://github.com/user-attachments/assets/a6e43924-a6ca-4b1a-a578-4e4f8252e383

Lots of nukes flying:


https://github.com/user-attachments/assets/8fc4edad-a6e6-4476-8a86-08cdef58169e

## Please complete the following:

- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

FloPinguin

---------

Co-authored-by: iamlewis <lewismmmm@gmail.com>
2026-01-01 13:29:46 -08:00

741 lines
23 KiB
TypeScript

import { MirvExecution } from "../src/core/execution/MIRVExecution";
import { NationExecution } from "../src/core/execution/NationExecution";
import {
Cell,
GameMode,
Nation,
PlayerInfo,
PlayerType,
UnitType,
} from "../src/core/game/Game";
import { setup } from "./util/Setup";
import { executeTicks } from "./util/utils";
describe("Nation MIRV Retaliation", () => {
test("nation 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 nationInfo = new PlayerInfo(
"defender_nation",
PlayerType.Nation,
null,
"nation_id",
);
game.addPlayer(attackerInfo);
game.addPlayer(nationInfo);
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
const attacker = game.player("attacker_id");
const nation = game.player("nation_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 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)) {
nation.conquer(tile);
}
}
}
nation.buildUnit(UnitType.MissileSilo, game.ref(50, 50), {});
// Give both players enough gold for MIRVs
attacker.addGold(1_000_000_000n);
nation.addGold(1_000_000_000n);
// Verify preconditions
expect(attacker.units(UnitType.MissileSilo)).toHaveLength(1);
expect(nation.units(UnitType.MissileSilo)).toHaveLength(1);
expect(attacker.gold()).toBeGreaterThan(35_000_000n);
expect(nation.gold()).toBeGreaterThan(35_000_000n);
// Track MIRVs before nation retaliates
const mirvCountBefore = nation.units(UnitType.MIRV).length;
// 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 NationExecution(gameId, testExecutionNation);
testExecution.init(game);
// Launch MIRV from attacker to nation
const targetTile = Array.from(nation.tiles())[0];
game.addExecution(new MirvExecution(attacker, targetTile));
// Execute nation'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 nation attempted retaliation
if (nation.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: 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 nationMirvs = nation.units(UnitType.MIRV);
expect(nationMirvs.length).toBeGreaterThan(0);
const retaliationMirv = nationMirvs[nationMirvs.length - 1];
const retaliationTarget = retaliationMirv.targetTile();
expect(retaliationTarget).toBeDefined();
if (retaliationTarget) {
const targetOwner = game.owner(retaliationTarget);
expect(targetOwner).toBe(attacker);
}
});
test("nation 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 nationInfo = new PlayerInfo(
"defender_nation",
PlayerType.Nation,
null,
"nation_id",
);
game.addPlayer(dominantPlayerInfo);
game.addPlayer(nationInfo);
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
const dominantPlayer = game.player("dominant_id");
const nation = game.player("nation_id");
// 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)) {
nation.conquer(tile);
nationTiles++;
}
}
}
// If we didn't find enough tiles, try a different area
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)) {
nation.conquer(tile);
nationTiles++;
if (nationTiles >= 10) break; // Need at least some territory
}
}
if (nationTiles >= 10) break;
}
}
// 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
// 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);
nation.addGold(100_000_000n);
// Verify preconditions
expect(dominantPlayer.units(UnitType.MissileSilo)).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(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 nation considers victory denial
const mirvCountBefore = nation.units(UnitType.MIRV).length;
// 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 NationExecution(gameId, testExecutionNation);
testExecution.init(game);
for (let tick = 0; tick < 200; tick++) {
testExecution.tick(tick);
// Allow the game to process executions
if (tick % 10 === 0) {
game.executeNextTick();
}
if (nation.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: 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 nationMirvs = nation.units(UnitType.MIRV);
expect(nationMirvs.length).toBeGreaterThan(0);
const victoryDenialMirv = nationMirvs[nationMirvs.length - 1];
const victoryDenialTarget = victoryDenialMirv.targetTile();
expect(victoryDenialTarget).toBeDefined();
if (victoryDenialTarget) {
const targetOwner = game.owner(victoryDenialTarget);
expect(targetOwner).toBe(dominantPlayer);
}
});
test("nation 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 nationInfo = new PlayerInfo(
"defender_nation",
PlayerType.Nation,
null,
"nation_id",
);
game.addPlayer(steamrollerInfo);
game.addPlayer(secondPlayerInfo);
game.addPlayer(nationInfo);
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
const steamroller = game.player("steamroller_id");
const secondPlayer = game.player("second_id");
const nation = game.player("nation_id");
// 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)) {
nation.conquer(tile);
}
}
}
const nationTile = Array.from(nation.tiles())[0];
if (nationTile) {
nation.buildUnit(UnitType.MissileSilo, nationTile, {});
}
// 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);
nation.addGold(100_000_000n);
// Verify preconditions
expect(nation.units(UnitType.MissileSilo)).toHaveLength(1);
expect(steamroller.unitCount(UnitType.City)).toBe(minLeaderCities + 2);
expect(secondPlayer.unitCount(UnitType.City)).toBe(5);
expect(nation.units(UnitType.MIRV)).toHaveLength(0);
// Track MIRVs before nation considers steamroll stop
const mirvCountBefore = nation.units(UnitType.MIRV).length;
// 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 NationExecution(gameId, testExecutionNation);
testExecution.init(game);
for (let tick = 0; tick < 200; tick++) {
testExecution.tick(tick);
// Allow the game to process executions
if (tick % 10 === 0) {
game.executeNextTick();
}
if (nation.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: 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 nationMirvs = nation.units(UnitType.MIRV);
expect(nationMirvs.length).toBeGreaterThan(0);
const steamrollStopMirv = nationMirvs[nationMirvs.length - 1];
const steamrollStopTarget = steamrollStopMirv.targetTile();
expect(steamrollStopTarget).toBeDefined();
if (steamrollStopTarget) {
const targetOwner = game.owner(steamrollStopTarget);
expect(targetOwner).toBe(steamroller);
}
});
test("nation 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 nationInfo = new PlayerInfo(
"defender_nation",
PlayerType.Nation,
null,
"nation_id",
);
game.addPlayer(steamrollerInfo);
game.addPlayer(secondPlayerInfo);
game.addPlayer(nationInfo);
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
const steamroller = game.player("steamroller_id");
const secondPlayer = game.player("second_id");
const nation = game.player("nation_id");
// 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)) {
nation.conquer(tile);
}
}
}
const nationTile = Array.from(nation.tiles())[0];
if (nationTile) {
nation.buildUnit(UnitType.MissileSilo, nationTile, {});
}
// 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);
nation.addGold(100_000_000n);
// Verify preconditions
expect(nation.units(UnitType.MissileSilo)).toHaveLength(1);
expect(steamroller.unitCount(UnitType.City)).toBe(minLeaderCities);
expect(secondPlayer.unitCount(UnitType.City)).toBe(5);
expect(nation.units(UnitType.MIRV)).toHaveLength(0);
// Track MIRVs before nation considers steamroll stop
const mirvCountBefore = nation.units(UnitType.MIRV).length;
// 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 NationExecution(gameId, testExecutionNation);
testExecution.init(game);
for (let tick = 0; tick < 200; tick++) {
testExecution.tick(tick);
game.executeNextTick();
}
// Check if any MIRVs were launched for steamroll stop
const nationMirvs = nation.units(UnitType.MIRV);
if (nationMirvs.length > mirvCountBefore) {
steamrollStopAttempted = true;
break;
}
}
// Assert that steamroll stop was NOT attempted
expect(steamrollStopAttempted).toBe(false);
});
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",
PlayerType.Human,
null,
"team1_id",
);
const teamPlayer2Info = new PlayerInfo(
"[ALPHA]team_player_2",
PlayerType.Human,
null,
"team2_id",
);
const nationInfo = new PlayerInfo(
"defender_nation",
PlayerType.Nation,
null,
"nation_id",
);
const game = await setup(
"big_plains",
{
infiniteGold: true,
instantBuild: true,
gameMode: GameMode.Team,
playerTeams: 2,
},
[teamPlayer1Info, teamPlayer2Info, nationInfo],
);
// 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 nation = game.player("nation_id");
// 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)) {
nation.conquer(tile);
}
}
}
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,
// 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);
nation.addGold(100_000_000n);
// Verify preconditions
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(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 =
teamPlayer1.numTilesOwned() + teamPlayer2.numTilesOwned();
const teamShare = teamTerritory / game.map().numLandTiles();
expect(teamShare).toBeGreaterThan(0.8); //
// Track MIRVs before nation considers team victory denial
const mirvCountBefore = nation.units(UnitType.MIRV).length;
// 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 NationExecution(gameId, testExecutionNation);
testExecution.init(game);
for (let tick = 0; tick < 200; tick++) {
testExecution.tick(tick);
// Allow the game to process executions
if (tick % 10 === 0) {
game.executeNextTick();
}
if (nation.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: 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 nationMirvs = nation.units(UnitType.MIRV);
expect(nationMirvs.length).toBeGreaterThan(0);
const teamVictoryDenialMirv = nationMirvs[nationMirvs.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);
}
});
});