Files
Battle-for-Tismo/tests/food_synthesis_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

122 lines
4.6 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import { smallState, cityOf } from "./framework/helpers.js";
import { key } from "../shared/hex.js";
import { RESOURCE_RULES } from "../shared/data.js";
import {
foodNeedPerDay,
tileFoodOutput,
foodSynthesisEnergy,
foodSynthesisMarginalEnergy,
synthesisedFoodFor,
} from "../shared/resources.js";
const Q = RESOURCE_RULES.foodSynthesisQuadratic;
// The land's daily harvest in one region, exactly as the daily tick sums it.
function regionFood(state, city) {
let total = 0;
for (const coords of state.regionTiles(city)) {
const k = key(coords.x, coords.y);
if ((state.tilePopulation.get(k) || 0) <= 0) continue;
total += tileFoodOutput(state.tiles[k]) * state._regionFoodMultiplier(city);
}
return total;
}
export class FoodSynthesisMathTest extends TestCase {
test_energy_for_extra_food_is_quadratic() {
this.assertApprox(foodSynthesisEnergy(0), 0);
this.assertApprox(foodSynthesisEnergy(10), Q * 100);
this.assertApprox(foodSynthesisEnergy(20), 4 * foodSynthesisEnergy(10));
}
test_the_marginal_tonne_costs_more_than_the_last() {
this.assertApprox(foodSynthesisMarginalEnergy(0), 0);
this.assertApprox(foodSynthesisMarginalEnergy(10), 2 * Q * 10);
this.assertGreater(
foodSynthesisMarginalEnergy(20), foodSynthesisMarginalEnergy(10),
"diminishing returns: the next tonne costs more"
);
}
test_a_region_grows_until_energy_meets_the_market_price() {
const foodPrice = 5100;
const energyPrice = 0.051;
const breakEven = foodPrice / (2 * Q * energyPrice);
this.assertApprox(synthesisedFoodFor(foodPrice, energyPrice, 1e6), breakEven);
// A shortfall smaller than the break-even caps the extra food.
this.assertApprox(synthesisedFoodFor(foodPrice, energyPrice, 10), 10);
// Cheap energy grows past a larger shortfall; dear energy grows almost none.
this.assertGreater(synthesisedFoodFor(foodPrice, 0.0001, 1e6), breakEven);
this.assertApprox(
synthesisedFoodFor(foodPrice, 1000, 1e6), foodPrice / (2 * Q * 1000)
);
this.assertApprox(synthesisedFoodFor(foodPrice, 1e9, 1e6), foodPrice / (2 * Q * 1e9));
}
test_no_synthesis_without_a_price_or_a_shortfall() {
this.assertApprox(synthesisedFoodFor(5100, 0, 1000), 0);
this.assertApprox(synthesisedFoodFor(0, 0.05, 1000), 0);
this.assertApprox(synthesisedFoodFor(5100, 0.05, 0), 0);
this.assertApprox(synthesisedFoodFor(5100, 0.05, -10), 0);
}
test_the_region_grows_whatever_the_market_cannot_supply() {
const foodPrice = 5100;
const energyPrice = 0.051;
const shortfall = 10_000;
const breakEven = foodPrice / (2 * Q * energyPrice);
// No reachable supplier: grow the whole shortfall.
this.assertApprox(synthesisedFoodFor(foodPrice, energyPrice, shortfall, 0), shortfall);
// A market that covers only part: grow the rest beyond the break-even.
this.assertApprox(
synthesisedFoodFor(foodPrice, energyPrice, shortfall, 4000),
shortfall - 4000
);
// A market that covers the affordable portion: grow only the break-even.
this.assertApprox(
synthesisedFoodFor(foodPrice, energyPrice, shortfall, 9000),
breakEven
);
this.assertGreater(breakEven, 0);
}
}
export class FoodSynthesisModelTest extends TestCase {
test_a_region_short_of_food_forces_extra_from_energy() {
const state = smallState();
const city = cityOf(state, 0);
const shortfall = foodNeedPerDay(state.getCityEconomy(city).population) -
regionFood(state, city);
this.assertGreater(shortfall, 0, "the fixture region opens short of food");
// Cheap energy against a dear market: force as much as the shortfall allows.
state.setResourcePrice("energy", 0.00001);
state.setResourcePrice("food", 1e9);
state._tickResources();
const extra = state._foodSynthesis.get(city.id) || 0;
this.assertApprox(extra, shortfall, 1, "the whole shortfall is grown");
}
test_a_region_buys_instead_when_energy_is_dearer_than_food() {
const state = smallState();
const city = cityOf(state, 0);
state.setResourcePrice("energy", 1e9);
state.setResourcePrice("food", 1);
state._tickResources();
this.assertApprox(state._foodSynthesis.get(city.id) || 0, 0);
}
test_the_extra_food_lands_in_the_region_s_store() {
const state = smallState();
const city = cityOf(state, 0);
state.setResourcePrice("energy", 0.00001);
state.setResourcePrice("food", 1e9);
state._tickResources();
const produced = state.getCivResourceSummary(0).production.food;
this.assertGreater(
produced, regionFood(state, city),
"the nation's food production includes the forced extra"
);
}
}