181 lines
7.1 KiB
JavaScript
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]));
|
|
}
|
|
}
|