218 lines
8.5 KiB
JavaScript
218 lines
8.5 KiB
JavaScript
import { TestCase } from "./framework/test_case.js";
|
|
import { smallState, defaultState, grantBuilding } from "./framework/helpers.js";
|
|
import { key, parseKey } from "../shared/hex.js";
|
|
|
|
const BIG_BUDGET = 1.0e15;
|
|
|
|
function protoOf(state, id) {
|
|
return state.protoUnits.find((unit) => unit.id === id);
|
|
}
|
|
|
|
function protoIndex(state, id) {
|
|
return state.protoUnits.findIndex((unit) => unit.id === id);
|
|
}
|
|
|
|
function techIndex(state, id) {
|
|
return state.technologies.findIndex((tech) => tech.id === id);
|
|
}
|
|
|
|
// Grants a nation a technology directly, the way a long research project would.
|
|
function research(state, civ, id) {
|
|
const index = techIndex(state, id);
|
|
if (!state.researched.has(civ)) state.researched.set(civ, new Set());
|
|
state.researched.get(civ).add(index);
|
|
}
|
|
|
|
function freeLandNear(state, center, maxDistance) {
|
|
let best = null;
|
|
let bestDistance = Infinity;
|
|
for (const k in state.tiles) {
|
|
const coords = parseKey(k);
|
|
if (state.tiles[k].terrainClass !== "Land") continue;
|
|
if (state.cityAt(coords) || state.unitAt(coords)) continue;
|
|
const distance = state.topology.tileDistance(center, coords);
|
|
if (distance <= 0 || distance > maxDistance) continue;
|
|
if (distance < bestDistance) {
|
|
bestDistance = distance;
|
|
best = coords;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
function farLand(state, from, minDistance) {
|
|
for (const k in state.tiles) {
|
|
const coords = parseKey(k);
|
|
if (state.tiles[k].terrainClass !== "Land") continue;
|
|
if (state.cityAt(coords) || state.unitAt(coords)) continue;
|
|
if (state.topology.tileDistance(from, coords) <= minDistance) continue;
|
|
return coords;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function freeLand(state) {
|
|
for (const cell of state.landCells) {
|
|
if (state.cityAt(cell) || state.unitAt(cell)) continue;
|
|
return cell;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// A missile is no longer consumed the instant it is fired: it flies a straight
|
|
// line to its target and only then detonates. Fly the clock until it is gone.
|
|
function flyUntilGone(state, id, maxHours = 200) {
|
|
for (let hour = 0; hour < maxHours && state.findUnit(id); hour++) state.advanceHour();
|
|
}
|
|
|
|
export class WmdTest extends TestCase {
|
|
test_nuclear_weapons_research_needs_an_enrichment_centre() {
|
|
const state = smallState();
|
|
const index = techIndex(state, "nuclear_weapons");
|
|
this.assertFalse(
|
|
state.canResearchTechnology(0, index),
|
|
"the bomb is locked without a uranium enrichment centre"
|
|
);
|
|
const city = state.cities.find((entry) => entry.civ === 0);
|
|
grantBuilding(state, city, "uranium_enrichment_center");
|
|
this.assertTrue(
|
|
state.canResearchTechnology(0, index),
|
|
"the centre unlocks the research"
|
|
);
|
|
}
|
|
|
|
test_a_missile_is_one_use() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const capital = state.cities.find((city) => city.civ === 0 && city.isCapital);
|
|
const missile = state._spawnUnit(capital.coords, 0, protoOf(state, "missile"));
|
|
const targetCoords = freeLandNear(state, capital.coords, 29);
|
|
this.assertNotNull(targetCoords, "a free tile within missile range exists");
|
|
const enemy = state._spawnUnit(targetCoords, 1, protoOf(state, "modern_infantry"));
|
|
|
|
this.assertTrue(
|
|
state.requestMissileStrike([missile.id], targetCoords),
|
|
"the strike is accepted"
|
|
);
|
|
this.assertNotNull(state.findUnit(missile.id), "the rocket is in flight");
|
|
flyUntilGone(state, missile.id);
|
|
this.assertNull(state.findUnit(missile.id), "the rocket is consumed on arrival");
|
|
this.assertNull(state.findUnit(enemy.id), "the infantry is destroyed by the blast");
|
|
}
|
|
|
|
test_a_theatre_missile_cannot_reach_anywhere_but_an_icbm_can() {
|
|
const state = defaultState();
|
|
const capital = state.cities.find((city) => city.civ === 0 && city.isCapital);
|
|
const missile = state._spawnUnit(capital.coords, 0, protoOf(state, "missile"));
|
|
const icbm = state._spawnUnit(capital.coords, 0, protoOf(state, "icbm"));
|
|
const far = farLand(state, capital.coords, 30);
|
|
if (!far) return; // the default map is wide enough, but never assume it.
|
|
const enemy = state._spawnUnit(far, 1, protoOf(state, "modern_infantry"));
|
|
this.assertFalse(
|
|
state.requestMissileStrike([missile.id], far),
|
|
"a theatre missile stops at 30 tiles"
|
|
);
|
|
this.assertNotNull(state.findUnit(missile.id), "the refused rocket is not lost");
|
|
// An ICBM is nuclear by design, so grant the bomb and launch it.
|
|
research(state, 0, "nuclear_weapons");
|
|
this.assertTrue(
|
|
state.requestMissileStrike([icbm.id], far),
|
|
"an ICBM reaches any tile"
|
|
);
|
|
flyUntilGone(state, icbm.id);
|
|
this.assertNull(state.findUnit(icbm.id), "the ICBM is consumed on arrival");
|
|
this.assertNull(state.findUnit(enemy.id), "the target is destroyed");
|
|
}
|
|
|
|
test_a_nuclear_payload_needs_the_bomb() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const capital = state.cities.find((city) => city.civ === 0 && city.isCapital);
|
|
const missile = state._spawnUnit(capital.coords, 0, protoOf(state, "missile"));
|
|
const targetCoords = freeLandNear(state, capital.coords, 29);
|
|
const enemy = state._spawnUnit(targetCoords, 1, protoOf(state, "modern_infantry"));
|
|
|
|
this.assertFalse(
|
|
state.requestMissileStrike([missile.id], targetCoords, { nuclear: true }),
|
|
"a nation without the bomb cannot arm a nuclear warhead"
|
|
);
|
|
this.assertNotNull(state.findUnit(missile.id), "the missile is still there");
|
|
this.assertNotNull(state.findUnit(enemy.id), "and no one died");
|
|
|
|
research(state, 0, "nuclear_weapons");
|
|
this.assertTrue(
|
|
state.requestMissileStrike([missile.id], targetCoords, { nuclear: true }),
|
|
"with the bomb the strike goes ahead"
|
|
);
|
|
flyUntilGone(state, missile.id);
|
|
this.assertNull(state.findUnit(missile.id), "the missile flew and detonated");
|
|
this.assertNull(state.findUnit(enemy.id), "the garrison is wiped out");
|
|
}
|
|
|
|
test_the_icbm_needs_the_bomb_and_the_space_program() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const capital = state.cities.find((city) => city.civ === 0 && city.isCapital);
|
|
const icbm = protoIndex(state, "icbm");
|
|
this.assertFalse(state.requestTrain(capital.id, icbm), "an ICBM needs the bomb first");
|
|
research(state, 0, "nuclear_weapons");
|
|
this.assertFalse(
|
|
state.requestTrain(capital.id, icbm),
|
|
"the bomb alone does not build a rocket that reaches orbit"
|
|
);
|
|
research(state, 0, "space_program");
|
|
this.assertTrue(state.requestTrain(capital.id, icbm), "both unlock the ICBM");
|
|
}
|
|
|
|
test_a_nuclear_blast_destroys_the_tile() {
|
|
const state = smallState();
|
|
const city = state.cities.find((entry) => entry.civ === 0);
|
|
const k = key(city.coords.x, city.coords.y);
|
|
state.tilePopulation.set(k, 10_000);
|
|
grantBuilding(state, city, "barracks");
|
|
grantBuilding(state, city, "shelters");
|
|
const barracks = state.protoBuildings.findIndex((b) => b.id === "barracks");
|
|
const shelters = state.protoBuildings.findIndex((b) => b.id === "shelters");
|
|
|
|
state._applyNuclearBlast(city.coords, 0);
|
|
|
|
this.assertApprox(state.tilePopulation.get(k), 1_000, 1, "nine in ten died");
|
|
this.assertEqual(city.buildings[barracks], undefined, "the barracks is flattened");
|
|
this.assertEqual(city.buildings[shelters], 1, "the bunker survives");
|
|
}
|
|
|
|
test_a_nuclear_blast_spares_only_bunkers_and_fortifications() {
|
|
const state = smallState();
|
|
const fort = freeLand(state);
|
|
const mill = freeLandNear(state, fort, 5);
|
|
this.assertNotNull(fort, "a free tile for the fort");
|
|
this.assertNotNull(mill, "a free tile for the mill");
|
|
const fortKey = key(fort.x, fort.y);
|
|
const millKey = key(mill.x, mill.y);
|
|
state.tileImprovements.set(fortKey, "fortifications");
|
|
state.tileImprovements.set(millKey, "steel_mill");
|
|
state.roads.add(millKey);
|
|
|
|
state._applyNuclearBlast(fort, 0);
|
|
state._applyNuclearBlast(mill, 0);
|
|
|
|
this.assertEqual(state.tileImprovements.get(fortKey), "fortifications", "the fort stands");
|
|
this.assertEqual(state.tileImprovements.get(millKey), undefined, "the mill is gone");
|
|
this.assertFalse(state.roads.has(millKey), "and its road with it");
|
|
}
|
|
|
|
test_a_nuclear_blast_costs_every_nation_standing() {
|
|
const state = smallState();
|
|
const city = state.cities.find((entry) => entry.civ === 0);
|
|
const before = state.civilisations.map((_, civ) => state.getCivPopularity(civ));
|
|
state._applyNuclearBlast(city.coords, 0);
|
|
for (let civ = 0; civ < state.civilisations.length; civ++) {
|
|
this.assertLess(
|
|
state.getCivPopularity(civ),
|
|
before[civ],
|
|
`nation ${civ} loses standing`
|
|
);
|
|
}
|
|
}
|
|
}
|