Files
Battle-for-Tismo/tests/wmd_test.js
T

219 lines
8.6 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");
research(state, 0, "scientific_method");
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`
);
}
}
}