Files
Battle-for-Tismo/tests/data_test.js
T
adrien bd9b206708 Gave the new buildings real effects
The catalogue had grown a barracks, wartime shelters, an air-defense
battery, a warm-water port and a shipyard, but only as data: infantry
still trained without a barracks, shelters did nothing, and an airport was
just an airbase. Each building now names the rule it grants with a
`mechanic` id, so the simulation and the city panel describe the same
thing without magic strings.

- Barracks and shipyards open one more production line each. A line sticks
  to one entry at full rate, so two units build at once rather than the
  head absorbing every line; the base behaviour (one queue, overflow
  carrying into the next entry) is unchanged.
- Wartime shelters keep 10% of civilians per level safe from bombardment
  and are very hard to knock down, and ranged fire can now destroy a
  building level outright, with no refund. Each building rolls its own
  chance per point of damage, from a generator seeded off the world so a
  bombardment replays identically.
- Air defense fires one volley at a hostile aircraft the moment it enters
  the city tile, scaled by its level; a plane shot down never drops its
  bombs.
- A port adds 1000 GDP per capita per level, per adjacent sea tile, to
  every tile in its region.
- An airport draws a daily trickle of people from every other airport city
  in the world, scaled by the destination's level.
- Wired the new effects into the city panel and taught the repeatable
  Industrial Automation to satisfy other technologies' prerequisites.

Added tests for each mechanic and for the parallel lines.
2026-09-21 11:38:01 +02:00

203 lines
8.0 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import {
CIVILISATIONS,
PROTO_UNITS,
BUILDINGS,
GOVERNMENTS,
TECHNOLOGIES,
TRAINABLE_UNIT_IDS,
TERRAIN_TILES,
protoUnitById,
buildingById,
governmentById,
technologyById,
effectValue,
statusById,
STATUS_BATTLE,
PILLAGE,
pillageSteps,
POLICIES,
} from "../shared/data.js";
export class DataTest extends TestCase {
test_lookups_return_the_matching_entry() {
this.assertEqual(protoUnitById("modern_infantry"), PROTO_UNITS.find((u) => u.id === "modern_infantry"));
this.assertEqual(buildingById("market"), BUILDINGS.find((b) => b.id === "market"));
this.assertEqual(governmentById("republic"), GOVERNMENTS[0]);
this.assertEqual(technologyById("scientific_method"), TECHNOLOGIES[0]);
}
test_unknown_lookups_return_null() {
this.assertNull(protoUnitById("nope"));
this.assertNull(buildingById("nope"));
this.assertNull(governmentById("nope"));
this.assertNull(technologyById("nope"));
}
test_every_civilisation_is_well_formed() {
this.assertGreaterOrEqual(CIVILISATIONS.length, 2);
for (const civ of CIVILISATIONS) {
this.assertTrue(civ.id.length > 0, "id");
this.assertTrue(civ.name.length > 0, "name");
this.assertTrue(civ.primaryColour.startsWith("#"), `primary colour of ${civ.id}`);
this.assertTrue(civ.flag.endsWith(".png"), `flag of ${civ.id}`);
this.assertTrue(civ.demonym.length > 0, `demonym of ${civ.id}`);
this.assertNotEmpty(civ.cityNames, `city names of ${civ.id}`);
}
}
test_battle_status_is_rendered_and_triples_upkeep() {
const battle = statusById(STATUS_BATTLE);
this.assertNotNull(battle, "the battle status exists");
this.assertEqual(battle.icon, "icon_battle.svg");
this.assertEqual(battle.upkeepMultiplier, 3);
}
test_pillage_steps_compound_per_unit() {
const one = pillageSteps(100000, 1);
this.assertApprox(one.steps[0].removed, 10000, 1e-6);
this.assertApprox(one.steps[0].heal, 10, 1e-9);
this.assertApprox(one.gdpMultiplier, PILLAGE.gdpPenalty, 1e-9);
const two = pillageSteps(100000, 2);
this.assertApprox(two.steps[0].removed, 10000, 1e-6);
this.assertApprox(two.steps[1].removed, 9000, 1e-6);
this.assertApprox(two.steps[1].heal, 9, 1e-9);
this.assertApprox(two.steps[1].remaining, 81000, 1e-6);
this.assertApprox(two.gdpMultiplier, 0.49, 1e-9);
}
test_trainable_units_exist_in_the_catalogue() {
this.assertNotEmpty(TRAINABLE_UNIT_IDS);
for (const id of TRAINABLE_UNIT_IDS) {
this.assertNotNull(protoUnitById(id), `trainable unit ${id}`);
}
}
test_artillery_is_expensive_slow_and_long_ranged() {
const infantry = protoUnitById("modern_infantry");
const artillery = protoUnitById("artillery");
this.assertNotNull(artillery, "artillery exists");
this.assertTrue(artillery.military, "artillery is military");
this.assertGreater(artillery.cost, infantry.cost * 2, "much more expensive");
this.assertGreater(artillery.upkeep, infantry.upkeep, "costlier to keep");
this.assertLess(artillery.speed, infantry.speed, "slower than infantry");
this.assertLess(artillery.defense, infantry.defense / 4, "nearly no defense");
this.assertGreater(artillery.attack, infantry.attack, "hits harder");
this.assertGreater(artillery.range, infantry.range, "outranges infantry");
this.assertEqual(artillery.range, 2);
this.assertTrue(TRAINABLE_UNIT_IDS.includes("artillery"), "artillery is trainable");
this.assertTrue(artillery.icon.endsWith(".svg"));
}
test_buildings_and_governments_carry_effects() {
for (const building of BUILDINGS) {
// A building carries either stat effects or a rule-based mechanic.
this.assertTrue(
building.effects.length > 0 || typeof building.mechanic === "string",
`${building.id} effects or mechanic`
);
this.assertGreater(building.costAlpha, 1, `${building.id} cost growth`);
this.assertTrue(building.icon && building.icon.endsWith(".svg"), `${building.id} icon`);
for (const effect of building.effects) {
this.assertTrue(effect.stat.length > 0);
this.assertGreater(effect.baseValue, 0);
}
}
for (const government of GOVERNMENTS) {
this.assertNotEmpty(government.effects, `${government.id} effects`);
}
}
test_policy_effects_are_popularity_points_not_percentages() {
for (const policy of POLICIES) {
this.assertFalse(
policy.targetEffect.includes("%"),
`${policy.id} target effect is stated in popularity points`
);
this.assertFalse(
policy.politicalCost.includes("%"),
`${policy.id} political cost is stated in popularity points`
);
}
}
test_market_and_bank_are_cheap_starter_buildings() {
this.assertEqual(buildingById("market").baseCost, 500_000);
this.assertEqual(buildingById("bank").baseCost, 1_000_000);
}
test_new_buildings_declare_their_mechanics() {
const expected = {
barracks: "parallel_training",
shelters: "civilian_shelter",
aa_building: "air_defense",
shipyard: "parallel_building",
port: "port_gdp",
airport: "air_immigration",
};
for (const [id, mechanic] of Object.entries(expected)) {
this.assertEqual(buildingById(id).mechanic, mechanic, `${id} mechanic`);
}
}
test_technologies_form_a_tree() {
const ids = new Set(TECHNOLOGIES.map((t) => t.id));
for (const tech of TECHNOLOGIES) {
this.assertGreater(tech.cost, 0, `${tech.id} cost`);
this.assertNotEmpty(tech.effects, `${tech.id} effects`);
for (const prerequisite of tech.prerequisites) {
this.assertTrue(ids.has(prerequisite), `${tech.id} prerequisite ${prerequisite} exists`);
}
}
}
test_ports_airports_and_shipyards_declare_their_requirements() {
const port = buildingById("port");
const airport = buildingById("airport");
const shipyard = buildingById("shipyard");
this.assertTrue(port.coastal, "a port needs the coast");
this.assertNull(port.requiresBuilding, "a port stands on its own");
this.assertTrue(shipyard.requiresBuilding === "port", "a shipyard needs a port");
this.assertTrue(shipyard.coastal, "a shipyard needs the coast too");
this.assertNull(airport.requiresBuilding, "an airport stands on its own");
this.assertTrue(port.icon.endsWith(".svg"));
this.assertTrue(airport.icon.endsWith(".svg"));
this.assertTrue(shipyard.icon.endsWith(".svg"));
}
test_aircraft_and_warships_are_gated_and_fuelled() {
for (const id of ["jet_fighter", "bomber"]) {
const plane = protoUnitById(id);
this.assertTrue(plane.air, `${id} is an aircraft`);
this.assertEqual(plane.requiresBuilding, "airport", `${id} needs an airport`);
this.assertGreater(plane.speed, 1, `${id} is fast`);
this.assertGreater(plane.enduranceHours, 0, `${id} has endurance`);
this.assertGreater(plane.missionRange, 0, `${id} has a mission radius`);
this.assertTrue(plane.icon.endsWith(".svg"));
}
this.assertTrue(protoUnitById("bomber").strike, "the bomber strikes from afar");
const battleship = protoUnitById("battleship");
this.assertEqual(battleship.requiresBuilding, "shipyard");
this.assertEqual(battleship.traversableTerrains, ["Sea"]);
this.assertTrue(battleship.icon.endsWith(".svg"));
}
test_effect_value_scales_with_level() {
const effect = { baseValue: 10, exponent: 1 };
this.assertApprox(effectValue(effect, 1), 10);
this.assertApprox(effectValue(effect, 3), 30);
this.assertEqual(effectValue(effect, 0), 0);
}
test_tundra_is_rocky_but_flat_and_unproductive() {
const tundra = TERRAIN_TILES.find((t) => t.terrainType === "Tundra");
this.assertNotNull(tundra, "tundra exists");
this.assertEqual(tundra.terrainClass, "Land", "tundra is land");
this.assertEqual(tundra.defenseBonus, 0, "tundra stays flat");
this.assertApprox(tundra.populationMultiplier, 0.05, 1e-9);
this.assertApprox(tundra.gdpMultiplier, 0.2, 1e-9);
this.assertApprox(tundra.movementCostMultiplier, 2.0, 1e-9);
}
}