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( "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 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); } }); });