mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 16:30:16 +00:00
86e51ab790
## Description: As far as I can remember, in v28 the spawn immunity applied to both humans and nations. With the configurable spawn immunity (added for v29) the spawn immunity no longer applies to nations... Because its called PVP immunity now. So right now it's possible to spawnkill nations. This is a big problem for the 5M gold modifier games... And you can "cheat" in singleplayer. This PR changes two things: - Nations always have 5 seconds spawn immunity now, no matter whats configured for the PVP immunity - Nations attack TerraNullius earlier (Otherwise the easy nations would sometimes do their first attack after the 5 seconds are over, spawnkills would still be possible) ## 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: Ryan <7389646+ryanbarlow97@users.noreply.github.com>
770 lines
24 KiB
TypeScript
770 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
|
|
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), {});
|
|
// 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
|
|
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, {});
|
|
// 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
|
|
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, {});
|
|
// 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
|
|
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, {});
|
|
// 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);
|
|
}
|
|
});
|
|
});
|