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" ); } }