mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 21:14:14 +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
125 lines
4.2 KiB
TypeScript
125 lines
4.2 KiB
TypeScript
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);
|
||
}
|
||
});
|
||
});
|