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