Files
OpenFrontIO/tests/NationNukeSamOverwhelm.test.ts
T
Aotumuri f1d162825e feat: remove spawn timer on singleplayer (#3199)
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
2026-05-11 12:44:44 -07:00

125 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { MissileSiloExecution } from "../src/core/execution/MissileSiloExecution";
import { NationExecution } from "../src/core/execution/NationExecution";
import { SAMLauncherExecution } from "../src/core/execution/SAMLauncherExecution";
import {
Cell,
Difficulty,
Nation,
PlayerInfo,
PlayerType,
UnitType,
} from "../src/core/game/Game";
import { setup } from "./util/Setup";
import { executeTicks } from "./util/utils";
describe("NationNukeBehavior - maybeDestroyEnemySam", () => {
test("nation overwhelms enemy SAM with atom bomb salvo on Impossible difficulty", async () => {
// Impossible difficulty with 2 players forces findBestNukeTarget to
// return the human. The SAM covers all human territory so every nuke
// trajectory is interceptable, keeping bestValue ≤ 0 and triggering
// maybeDestroyEnemySam.
const game = await setup("big_plains", {
difficulty: Difficulty.Impossible,
infiniteGold: true,
instantBuild: true,
});
const nationInfo = new PlayerInfo(
"nation",
PlayerType.Nation,
null,
"nation_id",
);
const humanInfo = new PlayerInfo(
"human",
PlayerType.Human,
null,
"human_id",
);
game.addPlayer(nationInfo);
game.addPlayer(humanInfo);
const nation = game.player("nation_id");
const human = game.player("human_id");
// Assign territory blocks (30×30 each, well separated)
for (let x = 10; x < 40; x++) {
for (let y = 10; y < 40; y++) {
const tile = game.ref(x, y);
if (game.map().isLand(tile)) nation.conquer(tile);
}
}
for (let x = 60; x < 90; x++) {
for (let y = 60; y < 90; y++) {
const tile = game.ref(x, y);
if (game.map().isLand(tile)) human.conquer(tile);
}
}
// Level-1 SAM at center of human territory (samRange = 20 in TestConfig,
// covering the entire 60-90 block and intercepting all trajectories).
const samTile = game.ref(75, 75);
const sam = human.buildUnit(UnitType.SAMLauncher, samTile, {});
game.addExecution(new SAMLauncherExecution(human, null, sam));
// 3 level-1 missile silos (1 slot each). Overwhelming a level-1 SAM
// requires 2 bombs (1 intercepted + 1 passes through).
for (const [x, y] of [
[20, 20],
[25, 25],
[30, 30],
] as const) {
const silo = nation.buildUnit(UnitType.MissileSilo, game.ref(x, y), {});
game.addExecution(new MissileSiloExecution(silo));
}
// infiniteGold only applies to Human players, so the nation needs gold
nation.addGold(1_000_000_000n);
nation.addTroops(100_000);
human.addTroops(100_000);
expect(nation.units(UnitType.MissileSilo)).toHaveLength(3);
expect(human.units(UnitType.SAMLauncher)).toHaveLength(1);
expect(nation.units(UnitType.AtomBomb)).toHaveLength(0);
// Try multiple game IDs to account for random attack-tick alignment
// (attackRate ∈ [30,50] on Impossible). 150 inner ticks guarantees ≥2
// attack ticks for the worst-case seed: 1st initializes behaviors, 2nd
// fires maybeSendNuke → maybeDestroyEnemySam.
const testNation = new Nation(new Cell(25, 25), nation.info());
let salvoLaunched = false;
for (let i = 0; i < 10 && !salvoLaunched; i++) {
// Let any executions from a prior iteration settle
if (i > 0) executeTicks(game, 50);
const exec = new NationExecution(`game_${i}`, testNation);
exec.init(game);
for (let tick = 0; tick < 150; tick++) {
exec.tick(tick);
// Advance the game sparingly so NukeExecution creates atom-bomb units
// but they don't complete their flight before we detect them.
if (tick % 10 === 0) game.executeNextTick();
if (nation.units(UnitType.AtomBomb).length > 0) {
salvoLaunched = true;
break;
}
}
}
expect(salvoLaunched).toBe(true);
// At least 2 atom bombs to overwhelm the level-1 SAM
const atomBombs = nation.units(UnitType.AtomBomb);
expect(atomBombs.length).toBeGreaterThanOrEqual(2);
// All bombs should target the SAM tile
for (const bomb of atomBombs) {
expect(bomb.targetTile()).toBe(samTile);
}
});
});