Files
Battle-for-Tismo/tests/industry_test.js
T
adrien 3e429ccb64 Gave production buildings their own economy and fed island regions
Production buildings are now independent private-sector agents: they keep what their output sells for, run only to the demand they can actually sell, and spend that cash on steel and high-tech upgrades when their demand grows. Grid power is a paid private flow between plants and their consumers, and regions synthesise the food their own harvest and the reachable market cannot supply.

Island regions no longer famine: a region with no reachable supplier grows its whole shortfall from energy, and grid power now feeds cities and food before the converters, so a famine never waits on a factory.

Bundles the in-progress tree cleanup that was already present in the working tree.
2026-09-22 14:34:14 +02:00

249 lines
10 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import { smallState, cityOf } from "./framework/helpers.js";
import { key } from "../shared/hex.js";
import {
RESOURCE_BUILDING_UPGRADE,
resourceBuildingOutputAt,
resourceBuildingInputAt,
resourceBuildingUpgradeMoneyCost,
tileImprovementById,
} from "../shared/data.js";
// A production building of `id` on the first land tile of `civ`'s that is not a
// city, wired into the agent maps and with an empty stock. The tile may already
// carry a work; it is replaced.
function placeProducer(state, id, civ = 0) {
let coords = null;
for (const candidate of state._territoryByCiv.get(civ) || []) {
if (state.cityAt(candidate)) continue;
if (!state._isLand(candidate)) continue;
if (state.tileImprovements.has(key(candidate.x, candidate.y))) continue;
coords = candidate;
break;
}
const k = key(coords.x, coords.y);
state.tileImprovements.set(k, id);
state.tileImprovementOwner.set(k, civ);
state.tileImprovementHp.set(k, 1000);
state._ensureResourceStock(k);
const agent = state._ensureBuildingAgent(k);
return { coords, k, agent };
}
// Gives the nation's cities enough stores to cover an upgrade's steel bill.
function fillMaterialStores(state, civ, amount = 1e7) {
for (const city of state.cities) {
if (city.civ !== civ) continue;
state.getCityResourceStock(city).steel = amount;
state.getCityResourceStock(city).hightech = amount;
}
}
export class ProductionBuildingUpgradeMathTest extends TestCase {
test_output_compounds_exponentially_with_the_level() {
const proto = { outputPerDay: 1000 };
const alpha = RESOURCE_BUILDING_UPGRADE.alpha;
this.assertApprox(resourceBuildingOutputAt(proto, 0), 1000);
this.assertApprox(resourceBuildingOutputAt(proto, 1), 1000 * alpha);
this.assertApprox(resourceBuildingOutputAt(proto, 2), 1000 * alpha * alpha);
}
test_input_grows_linearly_with_the_level() {
const proto = { energyPerDay: 500 };
this.assertApprox(resourceBuildingInputAt(proto, 0, "energyPerDay"), 500);
this.assertApprox(resourceBuildingInputAt(proto, 2, "energyPerDay"), 1500);
this.assertApprox(
resourceBuildingInputAt(proto, 1, "energyPerDay"),
500 * (1 + RESOURCE_BUILDING_UPGRADE.inputPerLevel)
);
}
test_each_upgrade_cost_more_than_the_last() {
const proto = { buildCost: 800_000_000 };
const first = resourceBuildingUpgradeMoneyCost(proto, 0);
const second = resourceBuildingUpgradeMoneyCost(proto, 1);
this.assertApprox(first, 800_000_000);
this.assertGreater(second, first);
this.assertApprox(second, 800_000_000 * RESOURCE_BUILDING_UPGRADE.costAlpha);
}
}
export class ProductionBuildingAgentTest extends TestCase {
test_a_production_building_keeps_what_it_sells() {
const state = smallState();
const { coords, k, agent } = placeProducer(state, "steel_mill");
const code = state.currencyOf(0).code;
const city = cityOf(state, 0);
const regionBefore = state.getRegionCashValue(city);
state._paySupplier({ kind: "building", coords, civ: 0 }, code, 12_345);
this.assertApprox(agent.cash, 12_345, 1e-6, "the sale reached the building");
this.assertApprox(state.getRegionCashValue(city), regionBefore, 1e-6, "not the region");
this.assertEqual(state.getPrivateBuildingCash(0), agent.cash);
}
test_plants_trade_their_power_like_any_producer() {
const state = smallState();
const plant = tileImprovementById("coal_power_plant");
const mill = tileImprovementById("steel_mill");
this.assertTrue(state._isProductionAgent(plant), "a plant sells power");
this.assertTrue(state._isProductionAgent(mill), "a mill sells steel");
}
test_growth_needs_the_recent_half_of_the_window_to_beat_the_older_half() {
const state = smallState();
const { agent } = placeProducer(state, "steel_mill");
agent.sales = [10, 10, 10];
this.assertFalse(state._buildingDemandGrowing(agent), "flat demand is not growth");
agent.sales = [20, 20, 20, 30, 30, 30];
this.assertTrue(state._buildingDemandGrowing(agent), "rising demand is growth");
agent.sales = [30, 30, 30, 20, 20, 20];
this.assertFalse(state._buildingDemandGrowing(agent), "falling demand is not growth");
}
test_a_building_starts_an_upgrade_and_cannot_start_a_second() {
const state = smallState();
const { coords, k, agent } = placeProducer(state, "steel_mill");
agent.cash = 1e12;
agent.sales = [10, 12, 15, 18, 22, 27];
fillMaterialStores(state, 0);
state._tickBuildingDecisions();
this.assertNotNull(agent.upgrading, "the upgrade started");
const job = agent.upgrading;
this.assertEqual(agent.level, 0, "the level waits for the work");
// A second read may not replace the work already under way.
agent.sales = [100, 200, 300, 400, 500, 600];
state._tickBuildingDecisions();
this.assertEqual(agent.upgrading, job, "only one upgrade at a time");
}
test_spending_on_an_upgrade_consumes_cash_and_materials() {
const state = smallState();
const { agent, k } = placeProducer(state, "steel_mill");
agent.cash = 1e12;
agent.sales = [10, 12, 15, 18, 22, 27];
fillMaterialStores(state, 0);
const before = agent.cash;
state._tickBuildingDecisions();
this.assertLess(agent.cash, before, "the agent paid for its materials");
this.assertGreater(state.getCityResourceStock(cityOf(state, 0)).steel, 0, "stores left over");
this.assertNotNull(tileImprovementById(state.tileImprovements.get(k)));
}
test_completing_an_upgrade_raises_the_level_and_its_output() {
const state = smallState();
const { k, agent } = placeProducer(state, "steel_mill");
const proto = tileImprovementById("steel_mill");
agent.cash = 1e12;
agent.sales = [10, 12, 15, 18, 22, 27];
fillMaterialStores(state, 0);
const baseOutput = state.resourceBuildingOutputAt(k, proto);
// Keep the day-boundary decision from firing while the work is completed.
state.totalHours = 1;
state._tickBuildingDecisions();
this.assertNotNull(agent.upgrading);
agent.upgrading.totalHours = 0;
state._tickIndustry();
this.assertEqual(agent.level, 1, "the level arrived");
this.assertNull(agent.upgrading, "the work is done");
this.assertGreater(
state.resourceBuildingOutputAt(k, proto), baseOutput,
"the upgraded building makes more"
);
}
test_an_agent_without_enough_cash_waits() {
const state = smallState();
const { agent } = placeProducer(state, "steel_mill");
agent.cash = 0;
agent.sales = [10, 12, 15, 18, 22, 27];
fillMaterialStores(state, 0);
state._tickBuildingDecisions();
this.assertNull(agent.upgrading, "no cash, no upgrade");
}
test_the_snapshot_ships_the_production_agents_apart_from_the_regions() {
const state = smallState();
const { coords, agent } = placeProducer(state, "steel_mill");
agent.cash = 4_000_000;
const stats = state.snapshot(0).viewerStats;
this.assertApprox(stats.privateBuildingCash, state.getPrivateBuildingCash(0));
const entry = stats.productionBuildings.find(
(item) => item.coords[0] === coords.x && item.coords[1] === coords.y
);
this.assertNotNull(entry, "the building is listed on its own row");
this.assertEqual(entry.level, 0);
this.assertApprox(entry.cash, 4_000_000, 1e-6);
}
}
export class PowerMarketTest extends TestCase {
test_a_grid_pays_its_plants_for_the_power_it_delivers() {
const state = smallState();
const plant = placeProducer(state, "coal_power_plant");
const converter = placeProducer(state, "steel_mill");
const city = cityOf(state, 0);
const plantBefore = plant.agent.cash;
const converterBefore = converter.agent.cash;
const regionBefore = state.getRegionCashValue(city);
const price = state.getResourcePrice("energy");
const ledger = new Map([[0, {
civ: 0,
supply: 1000,
cityEnergy: 100,
converterEnergy: 200,
foodEnergy: 0,
billableCityEnergy: 100,
cityShortage: 0,
industrialScale: 1,
producers: [{ k: plant.k, output: 1000 }],
cities: [{ city, amount: 100 }],
converters: [{ k: converter.k, amount: 200 }],
foodConsumers: [],
imports: 0,
importCost: 0,
}]]);
state._settleGridPower(ledger);
this.assertApprox(plant.agent.cash - plantBefore, 300 * price, 1e-6, "the plant is paid");
this.assertApprox(converter.agent.cash - converterBefore, -200 * price, 1e-6, "the converter pays");
this.assertApprox(state.getRegionCashValue(city) - regionBefore, -100 * price, 1e-6,
"the region pays");
this.assertApprox(plant.agent.salesToday, 300, 1e-6, "the sale feeds the demand trend");
}
test_imported_power_is_paid_to_the_exporting_grid_s_plants() {
const state = smallState();
const plant = placeProducer(state, "coal_power_plant");
const component = { civ: 0, producers: [{ k: plant.k, output: 100 }] };
const before = plant.agent.cash;
state._payGridProducers(component, 5_000, 40);
this.assertApprox(plant.agent.cash - before, 5_000, 1e-6);
this.assertApprox(plant.agent.salesToday, 40, 1e-6);
}
test_an_overstocked_converter_throttles_to_what_it_sells() {
const state = smallState();
const { k, agent } = placeProducer(state, "steel_mill");
const proto = tileImprovementById("steel_mill");
this.assertEqual(
state._converterProductionScale(k, proto, agent), 1,
"a building with no history runs flat out"
);
agent.sales = [4_000, 4_000, 4_000, 4_000];
state._ensureResourceStock(k).steel = 1e9;
const scale = state._converterProductionScale(k, proto, agent);
this.assertLess(scale, 1, "a full store eases off");
this.assertApprox(scale, 4_000 / proto.outputPerDay, 1e-3);
}
test_a_converter_sells_out_and_then_grows() {
const state = smallState();
const { k, agent } = placeProducer(state, "steel_mill");
const proto = tileImprovementById("steel_mill");
agent.sales = new Array(6).fill(proto.outputPerDay);
this.assertTrue(
state._buildingDemandGrowing(agent, proto.outputPerDay),
"selling out is a reason to grow"
);
}
}