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.
122 lines
4.6 KiB
JavaScript
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"
|
|
);
|
|
}
|
|
}
|