Files
Battle-for-Tismo/tests/food_power_test.js
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

89 lines
3.8 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import { smallState, cityOf, grantBuilding } from "./framework/helpers.js";
import { RESOURCE_IDS, RESOURCE_RULES } from "../shared/data.js";
import { key } from "../shared/hex.js";
import { foodNeedPerDay, unitFoodPerDay, stockpileTarget } from "../shared/resources.js";
function marketOf(state) {
return new Map(RESOURCE_IDS.map((id) => [
id, { supply: 0, demand: 0, price: state.getResourcePrice(id) },
]));
}
// The grid component that owns `city`.
function componentOf(state, city) {
for (const component of state._energyLedger.values()) {
if ((component.cities || []).some((entry) => entry.city === city)) return component;
}
return null;
}
export class FoodPowerTest extends TestCase {
test_the_store_gets_the_power_scaled_crop_not_the_potential_one() {
const state = smallState();
state._tickResources();
let delivered = 0;
for (const grown of state._foodProduced.values()) {
delivered += grown.harvest + grown.synthesis;
}
const market = state.resourceMarketStats.get("food");
this.assertGreater(delivered, 0, "the fixture grows food");
this.assertApprox(market.supply, delivered, 1e-6, "supply is what reached the store");
}
test_the_harvest_is_served_before_the_city_load() {
const state = smallState();
const city = cityOf(state, 0);
// A city block enormous enough to need more power than the grid makes.
grantBuilding(state, city, "shelters", 20);
state._tickResources();
const component = componentOf(state, city);
this.assertTrue(component, "the city sits in a grid component");
this.assertGreater(component.cityEnergy, component.supply, "the grid cannot serve the city");
this.assertApprox(component.foodScale, 1, 1e-9, "the harvest is served first, in full");
this.assertGreater(component.cityShortage, 0, "the cities are the ones that brown out");
}
test_army_rations_are_counted_in_the_food_demand() {
const state = smallState(["france"], 12345, { citiesPerCiv: 1 });
const city = cityOf(state, 0);
const infantry = state.protoUnits.find((proto) => proto.id === "modern_infantry");
this.assertTrue(infantry, "the fixture can field infantry");
let civilianNeed = 0;
for (const c of state.cities) civilianNeed += foodNeedPerDay(state.getCityEconomy(c).population);
// One soldier fields a company that eats like a thousand civilians.
state._spawnUnit(city.coords, 0, infantry, city.id);
state._tickResources();
const demand = state.resourceMarketStats.get("food").demand;
this.assertGreater(
demand, civilianNeed + unitFoodPerDay(infantry) - 1e-6,
"the market's food demand includes the army's ration"
);
}
test_a_region_that_cannot_buy_food_bids_the_price_up() {
const state = smallState(["france"], 12345, { citiesPerCiv: 1 });
const city = cityOf(state, 0);
// More mouths than the land and every reachable store can feed.
for (const coords of state.regionTiles(city)) {
const k = key(coords.x, coords.y);
state.tilePopulation.set(k, (state.tilePopulation.get(k) || 0) * 1000);
}
state._gdpPerCapitaCache.clear();
state.getCityResourceStock(city).food = 0;
const need = foodNeedPerDay(state.getCityEconomy(city).population);
const market = marketOf(state);
state._consumeCityResources(new Map(), market);
// Three bids stack: the day's ration, the standing wish to refill the empty
// pantry (a month's reserve spread over the reserve horizon), and the unmet
// ration counted again, so a famine bids the price up.
const reserveBid = stockpileTarget(need) / RESOURCE_RULES.stockpileDays;
this.assertApprox(
market.get("food").demand,
need + reserveBid + need * RESOURCE_RULES.famineBidFactor,
1e-3,
"the empty pantry, the reserve and the unmet ration all bid the price up"
);
}
}