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

181 lines
7.1 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import { smallState } 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_can_share_a_tile() {
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");
}
test_a_stack_moves_at_its_slowest_speed() {
const state = smallState();
const pair = emptyLandPair(state);
const infantry = state._spawnUnit(pair.a, 0, state.protoUnits[0]);
const artillery = state._spawnUnit(pair.a, 0, state.protoUnits[2]);
this.assertTrue(state.requestGroupMove([infantry.id, artillery.id], pair.b));
this.assertApprox(infantry.moveSpeed, 0.25, 1e-9, "pinned to the slowest member");
this.assertApprox(artillery.moveSpeed, 0.25, 1e-9);
state.advanceMovement(1000);
this.assertEqual(infantry.coords, pair.b);
this.assertEqual(artillery.coords, pair.b);
this.assertEqual(state.unitsAt(pair.b).length, 2, "the stack stays together");
}
test_a_stack_of_one_type_pathfinds_once_and_shares_the_route() {
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");
}
}
test_a_mixed_stack_pathfinds_once_per_unit_type() {
const state = smallState();
const pair = emptyLandPair(state);
const ids = [
state._spawnUnit(pair.a, 0, state.protoUnits[0]).id,
state._spawnUnit(pair.a, 0, state.protoUnits[0]).id,
state._spawnUnit(pair.a, 0, state.protoUnits[2]).id,
];
const searches = this.countPathSearches(state);
this.assertTrue(state.requestGroupMove(ids, pair.b));
this.assertEqual(searches.count, 2, "one search per type, not per unit");
}
test_a_lone_unit_keeps_its_own_speed() {
const state = smallState();
const pair = emptyLandPair(state);
const infantry = state._spawnUnit(pair.a, 0, state.protoUnits[0]);
state.requestMove(infantry.id, pair.b);
this.assertNull(infantry.moveSpeed, "a solo move is not slowed by neighbours");
}
test_garrisoned_units_heal_one_hp_per_hour() {
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");
for (let i = 0; i < 1000; i++) state.tickHour();
this.assertEqual(unit.hp, unit.maxHp, "never exceeds maximum");
}
test_units_in_the_field_do_not_heal() {
const state = smallState();
const pair = emptyLandPair(state);
const unit = state._spawnUnit(pair.a, 0, state.protoUnits[0]);
unit.hp = 100;
state.tickHour();
this.assertEqual(unit.hp, 100);
}
}
export class PillageTest extends TestCase {
test_pillaging_removes_population_and_heals() {
const state = smallState();
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.tileGdpPenalty.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");
}
test_a_stack_pillages_once_per_unit_weakest_first() {
const state = smallState();
const coords = tileOwnedBy(state, 1);
const k = `${coords.x},${coords.y}`;
state.tilePopulation.set(k, 100000);
const strong = state._spawnUnit(coords, 0, state.protoUnits[0]);
strong.hp = strong.maxHp - 100;
const weak = state._spawnUnit(coords, 0, state.protoUnits[0]);
weak.hp = strong.maxHp / 2;
this.assertTrue(state.requestPillage([strong.id, weak.id]));
this.assertApprox(state.tilePopulation.get(k), 81000, 1, "90% of 90% remains");
this.assertApprox(state.tileGdpPenalty.get(k), 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_needs_enemy_territory() {
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");
}
}
test_pillaging_rejects_an_enemy_unit() {
const state = smallState();
const coords = tileOwnedBy(state, 1);
const enemy = state._spawnUnit(coords, 1, state.protoUnits[0]);
this.assertFalse(state.requestPillage([enemy.id]));
}
}