mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 10:00:20 +00:00
f1d162825e
Resolves #1041 ## Description: Remove the singleplayer spawn countdown so the game starts when the player spawns, spawn nations immediately after player spawn, and align game timer/max-timer timing with the new start point. Added a singleplayer regression test for spawn-immunity timing (GameImpl.test.ts) and updated spawn-phase loop tests to use gameType: GameType.Public where singleplayer behavior is not under test (e.g. MIRV/AI/Spawn/WinCheck-related suites), eliminating inSpawnPhase() timeout hangs after the new singleplayer start logic. https://github.com/user-attachments/assets/c07a585f-1153-490e-88ca-a91fc7ae5756 ## 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: aotumuri
759 lines
24 KiB
TypeScript
759 lines
24 KiB
TypeScript
import { MirvExecution } from "../src/core/execution/MIRVExecution";
|
|
import { MissileSiloExecution } from "../src/core/execution/MissileSiloExecution";
|
|
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
|
|
|
|
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), {});
|
|
// Register MissileSiloExecution so the silo can reload after firing
|
|
const nationSilo = nation.units(UnitType.MissileSilo)[0];
|
|
if (nationSilo) {
|
|
game.addExecution(new MissileSiloExecution(nationSilo));
|
|
}
|
|
|
|
// 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) {
|
|
// Advance game to clear any silo cooldowns from previous iteration
|
|
executeTicks(game, 100);
|
|
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
|
|
|
|
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, {});
|
|
// Register MissileSiloExecution so the silo can reload after firing
|
|
const silo = nation.units(UnitType.MissileSilo)[0];
|
|
if (silo) {
|
|
game.addExecution(new MissileSiloExecution(silo));
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
// Advance game to clear any silo cooldowns from previous iteration
|
|
executeTicks(game, 100);
|
|
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
|
|
|
|
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, {});
|
|
// Register MissileSiloExecution so the silo can reload after firing
|
|
const silo = nation.units(UnitType.MissileSilo)[0];
|
|
if (silo) {
|
|
game.addExecution(new MissileSiloExecution(silo));
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
// Advance game to clear any silo cooldowns from previous iteration
|
|
executeTicks(game, 100);
|
|
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
|
|
|
|
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(
|
|
"team_player_1",
|
|
PlayerType.Human,
|
|
null,
|
|
"team1_id",
|
|
false,
|
|
"ALPHA",
|
|
);
|
|
const teamPlayer2Info = new PlayerInfo(
|
|
"team_player_2",
|
|
PlayerType.Human,
|
|
null,
|
|
"team2_id",
|
|
false,
|
|
"ALPHA",
|
|
);
|
|
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
|
|
|
|
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, {});
|
|
// Register MissileSiloExecution so the silo can reload after firing
|
|
const silo = nation.units(UnitType.MissileSilo)[0];
|
|
if (silo) {
|
|
game.addExecution(new MissileSiloExecution(silo));
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
// Advance game to clear any silo cooldowns from previous iteration
|
|
executeTicks(game, 100);
|
|
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);
|
|
}
|
|
});
|
|
});
|