Files
Battle-for-Tismo/tests/data_test.js
T
adrien 7ea5373b60 Moved the economy onto resources and surfaced each building's material bill
Rebalanced the resource rules so nothing is priced primarily in money: steel, electricity and high-tech lead every construction bill, chip foundries are seeded everywhere instead of being hand-placed, and cities open with stores of steel and high-tech. City building rows now lead with the resource bill and keep the money as an aside, and the region's resources expand into the buildings that demand them. Pulled the graph and cost maths into a framework-free shared/economy_graph.js, quoted currency through one helper, and gave every data table the same striping and row highlight.
2026-09-22 23:31:36 +02:00

213 lines
8.4 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,
RESOURCE_BUILDINGS,
} 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("barracks"), BUILDINGS.find((b) => b.id === "barracks"));
this.assertEqual(governmentById("republic"), GOVERNMENTS[0]);
this.assertEqual(technologyById("scientific_method"), TECHNOLOGIES[0]);
}
test_resource_building_output_is_scaled_up() {
// One work stands for a whole industry: the authored output is multiplied
// tenfold so fewer of them stand on the map.
const mill = RESOURCE_BUILDINGS.find((b) => b.id === "steel_mill");
this.assertApprox(mill.outputPerDay, 200_000, 1e-6);
}
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.productionMultiplier, PILLAGE.productionPenalty, 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.productionMultiplier, 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_gone() {
// The Market was removed and the tax-raising Bank is gone too: the state
// lives on trade taxes alone.
this.assertEqual(buildingById("market"), null);
this.assertEqual(buildingById("bank"), null);
}
test_new_buildings_declare_their_mechanics() {
const expected = {
barracks: "parallel_training",
shelters: "civilian_shelter",
aa_building: "air_defense",
shipyard: "parallel_building",
port: "port_production",
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.productionMultiplier, 0.2, 1e-9);
this.assertApprox(tundra.movementCostMultiplier, 2.0, 1e-9);
}
}