Files
OpenFrontIO/tests/InvasionConfig.test.ts
T
2026-06-15 17:39:15 -04:00

179 lines
5.9 KiB
TypeScript

import {
boatIntervalTicks,
boatTroops,
bombIntervalTicks,
maxInvaderNations,
nukeTier,
selectInvasionNuke,
warshipCount,
} from "../src/core/execution/invasion/InvasionConfig";
import { Difficulty } from "../src/core/game/Game";
import { PseudoRandom } from "../src/core/PseudoRandom";
const MIN = 600; // ticks per minute
describe("InvasionConfig.boatIntervalTicks", () => {
test("starts around 15s and decreases monotonically to the 2s floor", () => {
let prev = Infinity;
for (let m = 0; m <= 20; m++) {
const interval = boatIntervalTicks(m * MIN, Difficulty.Medium);
expect(interval).toBeLessThanOrEqual(prev);
expect(interval).toBeGreaterThanOrEqual(20);
prev = interval;
}
expect(boatIntervalTicks(0, Difficulty.Medium)).toBe(150);
expect(boatIntervalTicks(20 * MIN, Difficulty.Medium)).toBe(20);
});
test("never drops below the 2s (20 tick) floor, even past 20 min", () => {
expect(
boatIntervalTicks(40 * MIN, Difficulty.Impossible),
).toBeGreaterThanOrEqual(20);
});
test("higher difficulty sends boats at least as often", () => {
const easy = boatIntervalTicks(5 * MIN, Difficulty.Easy);
const impossible = boatIntervalTicks(5 * MIN, Difficulty.Impossible);
expect(impossible).toBeLessThan(easy);
});
});
describe("InvasionConfig.boatTroops", () => {
test("starts at 30k for every difficulty and grows over time", () => {
for (const d of [
Difficulty.Easy,
Difficulty.Medium,
Difficulty.Hard,
Difficulty.Impossible,
]) {
expect(boatTroops(0, d)).toBe(30_000);
}
expect(boatTroops(5 * MIN, Difficulty.Medium)).toBeGreaterThan(30_000);
expect(boatTroops(10 * MIN, Difficulty.Medium)).toBeGreaterThan(
boatTroops(5 * MIN, Difficulty.Medium),
);
});
test("harder difficulties field larger waves and the count is capped", () => {
expect(boatTroops(10 * MIN, Difficulty.Impossible)).toBeGreaterThan(
boatTroops(10 * MIN, Difficulty.Easy),
);
expect(boatTroops(1000 * MIN, Difficulty.Impossible)).toBeLessThanOrEqual(
350_000,
);
});
test("each successive boat carries slightly more troops", () => {
const first = boatTroops(0, Difficulty.Medium, 0);
const later = boatTroops(0, Difficulty.Medium, 10);
expect(later).toBeGreaterThan(first);
// Still bounded by the cap even for huge wave indices.
expect(boatTroops(0, Difficulty.Medium, 100_000)).toBeLessThanOrEqual(
350_000,
);
});
});
describe("InvasionConfig.maxInvaderNations", () => {
test("caps fewer nations on easier difficulties (5 easy, 20 impossible)", () => {
expect(maxInvaderNations(Difficulty.Easy)).toBe(5);
expect(maxInvaderNations(Difficulty.Impossible)).toBe(20);
expect(maxInvaderNations(Difficulty.Easy)).toBeLessThan(
maxInvaderNations(Difficulty.Medium),
);
expect(maxInvaderNations(Difficulty.Medium)).toBeLessThan(
maxInvaderNations(Difficulty.Hard),
);
expect(maxInvaderNations(Difficulty.Hard)).toBeLessThan(
maxInvaderNations(Difficulty.Impossible),
);
});
});
describe("InvasionConfig.warshipCount", () => {
test("is always 0 before minute 2", () => {
const rng = new PseudoRandom(1);
for (let m = 0; m < 2; m++) {
expect(warshipCount(m * MIN, rng, Difficulty.Medium)).toBe(0);
}
});
test("stays within 0-3 once active", () => {
const rng = new PseudoRandom(42);
for (let i = 0; i < 200; i++) {
const n = warshipCount(10 * MIN, rng, Difficulty.Hard);
expect(n).toBeGreaterThanOrEqual(0);
expect(n).toBeLessThanOrEqual(3);
}
});
test("averages higher later in the game (weighted toward 3)", () => {
const avg = (elapsed: number) => {
const rng = new PseudoRandom(7);
let sum = 0;
const samples = 400;
for (let i = 0; i < samples; i++) {
sum += warshipCount(elapsed, rng, Difficulty.Medium);
}
return sum / samples;
};
expect(avg(15 * MIN)).toBeGreaterThan(avg(3 * MIN));
});
});
describe("InvasionConfig.nukeTier", () => {
test("unlocks atom/hydrogen/mirv at 4/10/20 min on Medium", () => {
expect(nukeTier(3 * MIN, Difficulty.Medium)).toBe("none");
expect(nukeTier(4 * MIN, Difficulty.Medium)).toBe("atom");
expect(nukeTier(9 * MIN, Difficulty.Medium)).toBe("atom");
expect(nukeTier(10 * MIN, Difficulty.Medium)).toBe("hydrogen");
expect(nukeTier(20 * MIN, Difficulty.Medium)).toBe("mirv");
});
test("higher difficulty unlocks tiers earlier", () => {
// Atom unlocks at minute 4 on Medium, minute 2 on Impossible.
expect(nukeTier(2 * MIN, Difficulty.Medium)).toBe("none");
expect(nukeTier(2 * MIN, Difficulty.Impossible)).toBe("atom");
});
});
describe("InvasionConfig.selectInvasionNuke", () => {
test("returns null before nukes unlock", () => {
const rng = new PseudoRandom(3);
expect(selectInvasionNuke(0, rng, Difficulty.Medium)).toBeNull();
});
test("only atoms in the atom tier", () => {
const rng = new PseudoRandom(3);
for (let i = 0; i < 50; i++) {
expect(selectInvasionNuke(5 * MIN, rng, Difficulty.Medium)).toBe("atom");
}
});
test("MIRVs appear (~10%) only once the mirv tier is reached", () => {
const rng = new PseudoRandom(99);
let mirvs = 0;
let total = 0;
for (let i = 0; i < 2000; i++) {
const pick = selectInvasionNuke(20 * MIN, rng, Difficulty.Medium);
expect(pick).not.toBeNull();
if (pick === "mirv") mirvs++;
total++;
}
const ratio = mirvs / total;
expect(ratio).toBeGreaterThan(0.03);
expect(ratio).toBeLessThan(0.2);
});
});
describe("InvasionConfig.bombIntervalTicks", () => {
test("ramps down with time and difficulty but respects the floor", () => {
expect(bombIntervalTicks(20 * MIN, Difficulty.Medium)).toBeLessThan(
bombIntervalTicks(4 * MIN, Difficulty.Medium),
);
expect(
bombIntervalTicks(20 * MIN, Difficulty.Impossible),
).toBeGreaterThanOrEqual(120);
});
});