Files
Battle-for-Tismo/tests/warfare_test.js

337 lines
14 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import { smallState, cityOf, stripPorts } from "./framework/helpers.js";
import { key } from "../shared/hex.js";
import { TRENCH } from "../shared/data/combat.js";
const BIG_BUDGET = 1.0e15;
function emptyLandPair(state) {
for (const cell of state.landCells) {
if (state.unitAt(cell) || state.cityAt(cell)) continue;
for (const neighbour of state.topology.neighbours(cell.x, cell.y)) {
if (!state._isLand(neighbour)) continue;
if (state.unitAt(neighbour) || state.cityAt(neighbour)) continue;
return { a: { x: cell.x, y: cell.y }, b: { x: neighbour.x, y: neighbour.y } };
}
}
return null;
}
function ownLand(state, civ = 0) {
for (const cell of state.landCells) {
if (state.civAt(cell) !== civ) continue;
if (state.unitAt(cell) || state.cityAt(cell)) continue;
return cell;
}
return null;
}
export class TrenchTest extends TestCase {
test_infantry_dig_in_on_their_own_land() {
const state = smallState();
const coords = ownLand(state);
this.assertNotNull(coords, "the nation holds land");
const infantry = state._spawnUnit(coords, 0, state.protoUnits[0]);
this.assertTrue(state.requestDigTrench([infantry.id]));
this.assertTrue(state.trenches.has(key(coords.x, coords.y)));
this.assertFalse(state.requestDigTrench([infantry.id]), "already dug in");
}
test_only_infantry_can_dig() {
const state = smallState();
const coords = ownLand(state);
const artillery = state._spawnUnit(coords, 0, state.protoUnits[2]);
this.assertFalse(state.requestDigTrench([artillery.id]));
this.assertFalse(state.trenches.has(key(coords.x, coords.y)));
}
test_trenches_multiply_defense_and_cut_gdp() {
const state = smallState();
// A seeded port adds flat GDP that the trench multiplier would not scale.
stripPorts(state, 0);
const coords = ownLand(state);
const k = key(coords.x, coords.y);
const before = state.getTileGdpPerCapita(coords);
state.trenches.add(k);
state._gdpPerCapitaCache.delete(k);
this.assertApprox(
state.getTileGdpPerCapita(coords),
before * TRENCH.productionMultiplier,
1e-6,
"a trench costs the tile a fifth of its GDP per capita"
);
// Infantry attackers are blunted partly; artillery far more.
const infantryProto = state.protoUnits[0];
const artilleryProto = state.protoUnits[2];
this.assertApprox(
state._tileFortificationMultiplier(coords, infantryProto),
TRENCH.meleeDefenseMultiplier,
1e-9
);
this.assertApprox(
state._tileFortificationMultiplier(coords, artilleryProto),
TRENCH.artilleryDefenseMultiplier,
1e-9
);
}
}
export class CaptureTest extends TestCase {
test_infantry_captures_unaccompanied_artillery() {
const state = smallState();
const pair = emptyLandPair(state);
const artillery = state._spawnUnit(pair.a, 0, state.protoUnits[2]);
const infantry = state._spawnUnit(pair.b, 1, state.protoUnits[0]);
// A gun is usually spiked when taken; force the one-in-five capture.
state._random = () => 0.9;
this.assertTrue(state.requestMove(infantry.id, pair.a));
state.advanceMovement(1000);
this.assertEqual(artillery.civ, 1, "the battery changed hands");
}
test_infantry_beside_the_artillery_protects_it() {
const state = smallState();
const pair = emptyLandPair(state);
const artillery = state._spawnUnit(pair.a, 0, state.protoUnits[2]);
state._spawnUnit(pair.a, 0, state.protoUnits[0]);
const infantry = state._spawnUnit(pair.b, 1, state.protoUnits[0]);
this.assertTrue(state.requestMove(infantry.id, pair.a));
state.advanceMovement(1000);
this.assertEqual(artillery.civ, 0, "the artillery is not captured while sheltered");
}
test_walking_onto_a_radar_tower_captures_it() {
const state = smallState();
const pair = emptyLandPair(state);
const k = key(pair.a.x, pair.a.y);
state.tileImprovements.set(k, "radar_tower");
state.tileImprovementOwner.set(k, 0);
state.tileImprovementHp.set(k, 500);
const infantry = state._spawnUnit(pair.b, 1, state.protoUnits[0]);
this.assertTrue(state.requestMove(infantry.id, pair.a));
state.advanceMovement(1000);
this.assertEqual(state.tileImprovementOwner.get(k), 1, "the radar tower changed hands");
this.assertTrue(state.tileImprovements.has(k), "the tower is captured, not destroyed");
}
test_capturing_a_city_takes_its_aircraft() {
const state = smallState();
const city = cityOf(state, 1);
const airport = state.protoBuildings.findIndex((b) => b.id === "airport");
city.buildings[airport] = 1;
const jet = state._spawnUnit(
city.coords,
1,
state.protoUnits.find((u) => u.id === "jet_fighter"),
city.id
);
// Aircraft are usually destroyed with the field; force the one-in-five
// chance that the plane is taken intact.
state._random = () => 0.9;
state._captureCity(city, 0);
this.assertEqual(jet.civ, 0, "the aircraft is captured with the city");
this.assertEqual(jet.homeCityId, city.id);
}
}
export class TileImprovementTest extends TestCase {
test_a_radar_tower_takes_time_to_build() {
const state = smallState();
state.budgets.set(0, BIG_BUDGET);
const coords = ownLand(state);
const k = key(coords.x, coords.y);
this.assertTrue(state.requestBuildTileImprovement(0, coords, "radar_tower"));
this.assertFalse(state.tileImprovements.has(k), "not raised instantly");
for (let i = 0; i < 100000 && !state.tileImprovements.has(k); i++) state.advanceHour();
this.assertEqual(state.tileImprovements.get(k), "radar_tower", "the work completes");
this.assertEqual(state.tileImprovementOwner.get(k), 0);
}
test_a_coastal_cannon_needs_the_sea() {
const state = smallState();
state.budgets.set(0, BIG_BUDGET);
// A coastal preference can put every owned tile on the shore, so reach for
// a landlocked tile explicitly.
const inland = state.landCells.find((cell) =>
state.civAt(cell) === 0 &&
!state.unitAt(cell) &&
!state.cityAt(cell) &&
!state.isCoastalCoords(cell)
);
this.assertNotNull(inland, "the fixture has an inland tile");
this.assertFalse(
state.requestBuildTileImprovement(0, inland, "coastal_cannon"),
"an inland tile cannot host a coastal cannon"
);
}
test_improvements_cost_an_hourly_maintenance() {
const state = smallState();
state.budgets.set(0, BIG_BUDGET);
const coords = ownLand(state);
const k = key(coords.x, coords.y);
const before = state.getTileImprovementUpkeep(0);
state.tileImprovements.set(k, "fortifications");
state.tileImprovementOwner.set(k, 0);
state.tileImprovementHp.set(k, 2000);
this.assertGreater(state.getTileImprovementUpkeep(0), before, "forts carry upkeep");
}
test_fortifications_hold_permanent_control() {
const state = smallState();
const city = cityOf(state, 0);
// A tile right beside the city: without the fort it would fall with it.
const coords = state.topology
.neighbours(city.coords.x, city.coords.y)
.find((n) => state._isLand(n) && state.civAt(n) === 0 && !state.unitAt(n) && !state.cityAt(n));
this.assertNotNull(coords, "the city borders a tile of its own land");
const k = key(coords.x, coords.y);
state.tileImprovements.set(k, "fortifications");
state.tileImprovementOwner.set(k, 0);
state.tileImprovementHp.set(k, 2000);
state._captureCity(city, 1);
this.assertEqual(state.civAt(coords), 0, "the fort's area of control never changes hands");
}
test_a_coastal_cannon_fires_on_a_warship() {
const state = smallState();
const coastal = state.landCells.find((cell) =>
state.civAt(cell) === 0 &&
state.topology.neighbours(cell.x, cell.y).some(
(n) => state.tiles[key(n.x, n.y)].terrainClass === "Sea"
)
);
this.assertNotNull(coastal, "the nation holds a coastal tile");
const sea = state.topology.neighbours(coastal.x, coastal.y).find(
(n) => state.tiles[key(n.x, n.y)].terrainClass === "Sea"
);
const k = key(coastal.x, coastal.y);
state.tileImprovements.set(k, "coastal_cannon");
state.tileImprovementOwner.set(k, 0);
state.tileImprovementHp.set(k, 800);
const warship = state._spawnUnit(
sea,
1,
state.protoUnits.find((u) => u.id === "battleship")
);
const hp = warship.hp;
state._tickCoastalCannons();
this.assertLess(warship.hp, hp, "the cannon hits the warship");
this.assertTrue(state.tileImprovements.has(k), "the cannon survives (only fires)");
}
}
export class TransportTest extends TestCase {
coastalSetup(state) {
for (const cell of state.landCells) {
if (state.civAt(cell) !== 0) continue;
for (const neighbour of state.topology.neighbours(cell.x, cell.y)) {
if (state.tiles[key(neighbour.x, neighbour.y)].terrainClass !== "Sea") continue;
if (state.unitAt(cell) || state.unitAt(neighbour)) continue;
return { land: { x: cell.x, y: cell.y }, sea: { x: neighbour.x, y: neighbour.y } };
}
}
return null;
}
test_infantry_board_a_launch_and_are_carried() {
const state = smallState();
const spot = this.coastalSetup(state);
this.assertNotNull(spot, "the nation has a coastal tile");
const launchProto = state.protoUnits.find((u) => u.id === "launch");
const launch = state._spawnUnit(spot.sea, 0, launchProto);
const infantry = state._spawnUnit(spot.land, 0, state.protoUnits[0]);
this.assertTrue(state.requestEmbark([infantry.id], launch.id), "boarding begins");
for (let i = 0; i < 24; i++) state.advanceHour();
this.assertNull(state.findUnit(infantry.id), "the infantry left the map");
this.assertEqual(launch.cargo.length, 1, "the infantry is cargo");
}
test_a_spy_can_board_a_launch_and_go_ashore() {
const state = smallState();
const spot = this.coastalSetup(state);
this.assertNotNull(spot, "the nation has a coastal tile");
const launch = state._spawnUnit(spot.sea, 0, state.protoUnits.find((u) => u.id === "launch"));
const spyProto = state.protoUnits.find((u) => u.id === "spy");
const spy = state._spawnUnit(spot.land, 0, spyProto);
this.assertTrue(state.requestEmbark([spy.id], launch.id), "a spy may board");
for (let i = 0; i < 24; i++) state.advanceHour();
this.assertNull(state.findUnit(spy.id), "the spy left the map");
this.assertEqual(launch.cargo.length, 1, "the spy is cargo");
this.assertTrue(state.requestDisembark([launch.id], spot.land), "the spy goes ashore");
this.assertTrue(
state.unitsAt(spot.land).some((unit) => unit.proto === state.protoUnits.indexOf(spyProto)),
"the spy stands on the shore"
);
}
test_a_worker_stays_ashore() {
const state = smallState();
const spot = this.coastalSetup(state);
const launch = state._spawnUnit(spot.sea, 0, state.protoUnits.find((u) => u.id === "launch"));
const worker = state._spawnUnit(spot.land, 0, state.protoUnits.find((u) => u.id === "worker"));
this.assertFalse(state.requestEmbark([worker.id], launch.id), "only infantry and spies board");
}
test_a_loaded_launch_disembarks_its_stack() {
const state = smallState();
const spot = this.coastalSetup(state);
const launchProto = state.protoUnits.find((u) => u.id === "launch");
const launch = state._spawnUnit(spot.sea, 0, launchProto);
const infantry = state._spawnUnit(spot.land, 0, state.protoUnits[0]);
state.requestEmbark([infantry.id], launch.id);
for (let i = 0; i < 24; i++) state.advanceHour();
this.assertTrue(state.requestDisembark([launch.id], spot.land), "the troops go ashore");
this.assertEqual(launch.cargo.length, 0, "the launch is empty");
this.assertGreater(state.unitsAt(spot.land).length, 0, "a unit stands on the shore");
}
}
export class ZoneOfControlTest extends TestCase {
// Two neighbouring, unclaimed land tiles, so terrain and ownership never
// interfere with the zone-of-control check under test.
neutralPair(state) {
for (const cell of state.landCells) {
if (state.civAt(cell) !== 0) continue;
for (const neighbour of state.topology.neighbours(cell.x, cell.y)) {
if (!state._isLand(neighbour) || state.civAt(neighbour) !== 0) continue;
if (state.unitAt(cell) || state.unitAt(neighbour)) continue;
return { a: { x: cell.x, y: cell.y }, b: { x: neighbour.x, y: neighbour.y } };
}
}
return null;
}
test_a_unit_cannot_cross_between_two_denied_tiles() {
const state = smallState();
const pair = this.neutralPair(state);
this.assertNotNull(pair, "the map has two adjacent neutral land tiles");
const mover = state._spawnUnit(pair.a, 0, state.protoUnits[0]);
// The hostile is only denied by the artificial zone, not by occupancy.
const zoc = new Set([key(pair.a.x, pair.a.y), key(pair.b.x, pair.b.y)]);
this.assertFalse(
state._canUnitEnter(mover, pair.b, false, -1, null, zoc, pair.a),
"denied to denied is blocked"
);
this.assertTrue(
state._canUnitEnter(mover, pair.b, false, -1, null, new Set([key(pair.b.x, pair.b.y)]), pair.a),
"entering the ring from outside is allowed"
);
this.assertTrue(
state._canUnitEnter(mover, pair.b, true, -1, null, zoc, pair.a),
"a deliberate attack on the goal is allowed"
);
}
test_a_hostile_unit_denies_its_tile_and_neighbours() {
const state = smallState();
const pair = emptyLandPair(state);
state._spawnUnit(pair.a, 1, state.protoUnits[0]);
const zoc = state._zoneOfControl(0);
this.assertTrue(zoc.has(key(pair.a.x, pair.a.y)), "the hostile's tile is denied");
for (const neighbour of state.topology.neighbours(pair.a.x, pair.a.y)) {
this.assertTrue(zoc.has(key(neighbour.x, neighbour.y)), "each neighbour is denied");
}
}
}