Files
Battle-for-Tismo/tests/food_synthesis_test.js
T
adrien 4600b16399 Stockpiled a month of every material and cached the trade walks
Food, steel, luxury and high-tech now share one flat month-long reserve. A region keeps bidding for the shortfall while its store is thin, so a war that empties a pantry refills it from the grid instead of starving; the harvest is served before the city load, unmet food bids the price up, the army's rations count as demand, and a converter that cannot cover its power bill idles instead of making a loss.

The delivery graph walks sea lanes any distance, so its walks are cached across days behind a road/railway/work/territory stamp and only dropped by a wartime unit move. Units are indexed per tile, market access and node populations are memoised for the day, and the server can settle a fresh world before serving (--warmup, 150 days by default).

Roadmap records the remaining follow-ups -- the politics/migration hot path and the luxury/high-tech price plateau -- and that freight's heavy energy draw is intended.
2026-09-23 15:59:28 +02:00

151 lines
6.1 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,
stockpileTarget,
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);
// The generated region may open self-sufficient; strip its farmland so the
// fixture is genuinely short, which is the point of the test.
for (const coords of state.regionTiles(city)) {
if (coords.x === city.coords.x && coords.y === city.coords.y) continue;
state.tilePopulation.set(key(coords.x, coords.y), 0);
}
const need = foodNeedPerDay(state.getCityEconomy(city).population);
const shortfall = need - regionFood(state, city);
this.assertGreater(shortfall, 0, "the fixture region opens short of food");
// Hold the reserve full so the test isolates the day's ration: with the
// pantry a month deep, only the meal the land cannot grow is forced.
state.getCityResourceStock(city).food = stockpileTarget(need);
// 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_grows_extra_to_refill_an_empty_reserve() {
const state = smallState();
const city = cityOf(state, 0);
// A self-sufficient region with an empty pantry still wants its month back.
this.assertGreater(regionFood(state, city), 0, "the region has farmland");
state.getCityResourceStock(city).food = 0;
// The reserve is dearer to force than the land, but cheap energy against a
// dear market makes the extra worth growing.
state.setResourcePrice("energy", 0.00001);
state.setResourcePrice("food", 1e9);
state._tickResources();
const extra = state._foodSynthesis.get(city.id) || 0;
this.assertGreater(extra, 0, "an empty reserve drives synthesis above the harvest");
this.assertGreater(
state.getCityResourceStock(city).food, regionFood(state, city),
"the store rises toward the reserve instead of hugging the day's harvest"
);
}
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"
);
}
}