Files
OpenFrontIO/tests/NationNukeSamOverwhelm.test.ts
Evan Pelle a7f992e9b0 refactor: convert to npm-workspaces monorepo (engine/core-public/shared/client/server)
Restructure the single src/ tree into an npm-workspaces monorepo under
packages/, rename core -> engine, extract a types-only core-public layer,
and break the pre-existing engine -> client dependency cycle.

Structure (packages/):
  core-public  public API/wire schemas + shared enums (clean leaf)
  shared       framework-agnostic helpers (clean leaf)
  engine       deterministic simulation (was src/core)
  client       rendering/UI (was src/client)
  server       coordination (was src/server)

Dependency DAG: engine -> {core-public, shared}; client -> {core-public,
shared, engine}; server -> {core-public, engine}.

- npm workspaces: root package.json workspaces + per-package package.json;
  tsconfig.base.json holds shared options + path aliases
  (core-public/* shared/* engine/* client/* server/*) resolved uniformly by
  tsc, Vite (resolve.tsconfigPaths), Vitest, and tsx. Lockfile regenerated.
- core-public: moved Schemas/ApiSchemas/CosmeticSchemas/StatsSchemas/
  ClanApiSchemas/WorkerSchemas/Base64/PatternDecoder; extracted the enums
  (GameTypes), GameEvent type, emoji table, and GraphicsOverrides schema.
  Engine re-exports the moved enums/types so existing imports keep working.
- Broke engine -> client cycle:
  - renderNumber/renderTroops -> shared/format
  - NameBoxCalculator moved into engine
  - username validation returns translation key + params; client translates
  - applyStateUpdate moved to client (operates on the render-only PlayerState)
  - Config/UnitGrid/execution-Util/GameImpl now use structural read
    interfaces (engine/game/ReadViews: PlayerLike/UnitLike/GameLike) instead
    of importing client view classes; client imports view classes from a new
    client/view barrel; deleted the engine/game/GameView re-export shim.
- Build/deploy updated: vite.config, index.html, eslint, Dockerfile
  (copies packages/ + tsconfig.base.json before npm ci), .vscode, tests.

Verified: tsc --noEmit clean; 1364 + 65 tests pass; production vite build
succeeds; engine has zero client/server imports; core-public and shared are
dependency leaves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:51:03 +00:00

125 lines
4.1 KiB
TypeScript
Raw Permalink 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 "engine/execution/MissileSiloExecution";
import { NationExecution } from "engine/execution/NationExecution";
import { SAMLauncherExecution } from "engine/execution/SAMLauncherExecution";
import {
Cell,
Difficulty,
Nation,
PlayerInfo,
PlayerType,
UnitType,
} from "engine/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);
}
});
});