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

175 lines
7.3 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import { smallState, stripPorts } from "./framework/helpers.js";
function tileOwnedBy(state, owner) {
for (const [k, tileOwner] of state.territory) {
if (tileOwner !== owner) continue;
const i = k.indexOf(",");
const coords = { x: Number(k.slice(0, i)), y: Number(k.slice(i + 1)) };
if (state.unitAt(coords) || state.cityAt(coords)) continue;
return coords;
}
return null;
}
function emptyLandPair(state) {
let fallback = null;
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;
const pair = { a: { x: cell.x, y: cell.y }, b: { x: neighbour.x, y: neighbour.y } };
if (!fallback) fallback = pair;
const ownA = state.civAt(cell) < 0 || state.civAt(cell) === 0;
const ownB = state.civAt(neighbour) < 0 || state.civAt(neighbour) === 0;
if (ownA && ownB) return pair;
}
}
return fallback;
}
export class StackingTest extends TestCase {
// Counts actual A* runs by wrapping this state's pathfinder.
countPathSearches(state) {
const counter = { count: 0 };
const pathfinder = state._pathfinder;
const original = pathfinder.findPath.bind(pathfinder);
pathfinder.findPath = (...args) => {
counter.count += 1;
return original(...args);
};
return counter;
}
test_friendly_units_share_a_tile_and_move_at_the_slowest_speed() {
const state = smallState();
const pair = emptyLandPair(state);
const first = state._spawnUnit(pair.a, 0, state.protoUnits[0]);
state._spawnUnit(pair.a, 0, state.protoUnits[0]);
this.assertEqual(state.unitsAt(pair.a).length, 2, "both units stack");
this.assertTrue(state._canUnitEnter(first, pair.a), "a unit may join its own stack");
const slow = smallState();
const slowPair = emptyLandPair(slow);
const infantry = slow._spawnUnit(slowPair.a, 0, slow.protoUnits[0]);
const artillery = slow._spawnUnit(slowPair.a, 0, slow.protoUnits[2]);
this.assertTrue(slow.requestGroupMove([infantry.id, artillery.id], slowPair.b));
this.assertApprox(infantry.moveSpeed, 0.25, 1e-9, "pinned to the slowest member");
this.assertApprox(artillery.moveSpeed, 0.25, 1e-9);
slow.advanceMovement(1000);
this.assertEqual(infantry.coords, slowPair.b);
this.assertEqual(artillery.coords, slowPair.b);
this.assertEqual(slow.unitsAt(slowPair.b).length, 2, "the stack stays together");
const lone = smallState();
const lonePair = emptyLandPair(lone);
const solo = lone._spawnUnit(lonePair.a, 0, lone.protoUnits[0]);
lone.requestMove(solo.id, lonePair.b);
this.assertNull(solo.moveSpeed, "a solo move is not slowed by neighbours");
}
test_group_pathfinding_is_shared_per_unit_type() {
const state = smallState();
const pair = emptyLandPair(state);
const ids = [];
for (let i = 0; i < 5; i++) ids.push(state._spawnUnit(pair.a, 0, state.protoUnits[0]).id);
const searches = this.countPathSearches(state);
this.assertTrue(state.requestGroupMove(ids, pair.b));
this.assertEqual(searches.count, 1, "one search covers the whole stack");
const first = state.findUnit(ids[0]).path;
for (const id of ids) {
this.assertTrue(state.findUnit(id).path === first, "the stack shares one route");
}
const mixed = smallState();
const mixedPair = emptyLandPair(mixed);
const mixedIds = [
mixed._spawnUnit(mixedPair.a, 0, mixed.protoUnits[0]).id,
mixed._spawnUnit(mixedPair.a, 0, mixed.protoUnits[0]).id,
mixed._spawnUnit(mixedPair.a, 0, mixed.protoUnits[2]).id,
];
const mixedSearches = this.countPathSearches(mixed);
this.assertTrue(mixed.requestGroupMove(mixedIds, mixedPair.b));
this.assertEqual(mixedSearches.count, 2, "one search per type, not per unit");
}
test_units_heal_only_when_garrisoned_and_never_past_maximum() {
const state = smallState();
const city = state.cities.find((c) => c.civ === 0);
const unit = state._spawnUnit(city.coords, 0, state.protoUnits[0]);
unit.hp = 100;
state.tickHour();
this.assertEqual(unit.hp, 101, "heals inside a friendly city");
unit.hp = unit.maxHp - 1;
state.tickHour();
this.assertEqual(unit.hp, unit.maxHp, "reaches full health");
state.tickHour();
this.assertEqual(unit.hp, unit.maxHp, "never exceeds maximum");
const field = smallState();
const pair = emptyLandPair(field);
const wounded = field._spawnUnit(pair.a, 0, field.protoUnits[0]);
wounded.hp = 100;
field.tickHour();
this.assertEqual(wounded.hp, 100, "units in the field do not heal");
}
}
export class PillageTest extends TestCase {
test_pillaging_removes_population_heals_and_compounds() {
const state = smallState();
// A port adds a flat production figure the pillage does not scale, which
// would keep the ratio above 0.7; drop the ports so the test isolates the
// pillage penalty.
stripPorts(state);
const coords = tileOwnedBy(state, 1);
this.assertNotNull(coords, "found an enemy tile");
const k = `${coords.x},${coords.y}`;
state.tilePopulation.set(k, 100000);
const unit = state._spawnUnit(coords, 0, state.protoUnits[0]);
unit.hp = 100;
const gdpBefore = state.getTileGdpPerCapita(coords);
this.assertTrue(state.requestPillage([unit.id]));
this.assertApprox(state.tilePopulation.get(k), 90000, 1);
this.assertApprox(state.tileProductionPenalty.get(k), 0.7, 1e-9);
this.assertApprox(state.getTileGdpPerCapita(coords), gdpBefore * 0.7, 1e-6);
this.assertApprox(unit.hp, 110, 1e-9, "heals 1 HP per 1000 people removed");
const stack = smallState();
const stackCoords = tileOwnedBy(stack, 1);
const stackKey = `${stackCoords.x},${stackCoords.y}`;
stack.tilePopulation.set(stackKey, 100000);
const strong = stack._spawnUnit(stackCoords, 0, stack.protoUnits[0]);
strong.hp = strong.maxHp - 100;
const weak = stack._spawnUnit(stackCoords, 0, stack.protoUnits[0]);
weak.hp = strong.maxHp / 2;
this.assertTrue(stack.requestPillage([strong.id, weak.id]));
this.assertApprox(stack.tilePopulation.get(stackKey), 81000, 1, "90% of 90% remains");
this.assertApprox(stack.tileProductionPenalty.get(stackKey), 0.49, 1e-9, "70% of 70% remains");
this.assertApprox(weak.hp, strong.maxHp / 2 + 10, 1e-9, "the weakest pillages first");
this.assertApprox(strong.hp, strong.maxHp - 100 + 9, 1e-9, "the second heals for the 9000 it removed");
}
test_pillaging_requires_enemy_territory_and_an_own_unit() {
const state = smallState();
const city = state.cities.find((c) => c.civ === 0);
const unit = state._spawnUnit(city.coords, 0, state.protoUnits[0]);
this.assertFalse(state.canPillage([unit.id]), "cannot pillage your own land");
const pair = emptyLandPair(state);
const neutral = state._spawnUnit(pair.a, 0, state.protoUnits[0]);
if (state.civAt(pair.a) < 0) {
this.assertFalse(state.canPillage([neutral.id]), "cannot pillage unclaimed land");
}
const enemyCoords = tileOwnedBy(state, 1);
const enemy = state._spawnUnit(enemyCoords, 1, state.protoUnits[0]);
this.assertFalse(state.requestPillage([enemy.id]));
}
}