Added the resource economy, its works and their icons
- Five resources (energy, steel, food, luxury, high-tech) with per-city stores, a world market, delivery and material upkeep. - Resource-producing tile works built outside cities; every nation opens with the works its economy needs. - Real game-icons.net art replacing the dummy icons for the new resources and works, with the credits updated.
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
// Headless resource-economy simulation, used to tune the costs in
|
||||
// shared/data/resources.js. It boots a game, optionally raises a fixed set of
|
||||
// resource buildings for every nation, runs a number of in-game days and prints
|
||||
// how the economy settles: what each nation spends on power and goods, whether
|
||||
// its people are fed, and how each producer's build cost pays back at market
|
||||
// prices.
|
||||
//
|
||||
// node scripts/simulate_resources.js
|
||||
// node scripts/simulate_resources.js --days 720 --civs 2 --scenario infra
|
||||
// node scripts/simulate_resources.js --map full --days 365 --civs 6
|
||||
|
||||
import { GameState } from "../shared/game_state.js";
|
||||
import { CIVILISATIONS, MAP_CONFIG } from "../shared/data.js";
|
||||
import { RESOURCE_BUILDINGS, RESOURCE_RULES, RESOURCE_MARKET_BASE } from "../shared/data/resources.js";
|
||||
import {
|
||||
cityEnergyPerDay,
|
||||
foodNeedPerDay,
|
||||
steelNeedPerDay,
|
||||
luxuryNeedPerDay,
|
||||
} from "../shared/resources.js";
|
||||
import { key } from "../shared/hex.js";
|
||||
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const DAYS = Number(args.days || 360);
|
||||
const CIVS = Number(args.civs || 2);
|
||||
const SCENARIO = args.scenario || "baseline";
|
||||
const MAP = args.map || "medium";
|
||||
const seed = Number(args.seed || 12345);
|
||||
|
||||
const state = new GameState();
|
||||
state.mapConfig = mapConfig(MAP);
|
||||
const civs = CIVILISATIONS.slice(0, Math.max(1, Math.min(CIVS, CIVILISATIONS.length)));
|
||||
state.configure(civs, seed);
|
||||
|
||||
if (SCENARIO === "infra") seedInfrastructure(state);
|
||||
if (SCENARIO === "developed") seedBuildings(state);
|
||||
|
||||
const startBudget = civs.map((_, civ) => state.getBudget(civ));
|
||||
const spendTotals = civs.map(() => ({ energy: 0, goods: 0, transport: 0, construction: 0, upkeep: 0, combat: 0 }));
|
||||
|
||||
for (let day = 0; day < DAYS; day++) {
|
||||
for (let hour = 0; hour < 24; hour++) state.advanceHour();
|
||||
for (let civ = 0; civ < civs.length; civ++) {
|
||||
const record = state.resourceSpend.get(civ) || {};
|
||||
for (const kind of Object.keys(spendTotals[civ])) {
|
||||
spendTotals[civ][kind] += record[kind] || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function parseArgs(list) {
|
||||
const out = {};
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i];
|
||||
if (!item.startsWith("--")) continue;
|
||||
const name = item.slice(2);
|
||||
const next = list[i + 1];
|
||||
if (next === undefined || next.startsWith("--")) out[name] = true;
|
||||
else {
|
||||
out[name] = next;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function mapConfig(kind) {
|
||||
if (kind === "full") return MAP_CONFIG;
|
||||
return {
|
||||
...MAP_CONFIG,
|
||||
mapSize: { x: 48, y: 40 },
|
||||
minCapitalLandmass: 8,
|
||||
minCapitalDistance: 4,
|
||||
citiesPerCiv: 4,
|
||||
citySpacing: 3,
|
||||
minContinents: 2,
|
||||
smallIslandCount: 2,
|
||||
};
|
||||
}
|
||||
|
||||
// A representative national grid: one coal plant per city, one steel mill, one
|
||||
// chip foundry and a gold mine, all dropped on free land so the steady state
|
||||
// can be read without waiting for construction.
|
||||
function seedInfrastructure(state) {
|
||||
for (let civ = 0; civ < state.civilisations.length; civ++) {
|
||||
const plan = [
|
||||
"coal_power_plant",
|
||||
"coal_power_plant",
|
||||
"coal_power_plant",
|
||||
"coal_power_plant",
|
||||
"coal_power_plant",
|
||||
"steel_mill",
|
||||
"chip_foundry",
|
||||
"gold_mine",
|
||||
];
|
||||
const tiles = (state._territoryByCiv.get(civ) || []).filter(
|
||||
(coords) => state._isLand(coords) && !state.cityAt(coords)
|
||||
);
|
||||
let used = 0;
|
||||
for (const id of plan) {
|
||||
if (used >= tiles.length) break;
|
||||
const coords = tiles[used++];
|
||||
const k = key(coords.x, coords.y);
|
||||
state.tileImprovements.set(k, id);
|
||||
state.tileImprovementOwner.set(k, civ);
|
||||
state.tileImprovementHp.set(k, 1000);
|
||||
state._ensureResourceStock(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A mature nation: every city carries a believable basket of buildings, so the
|
||||
// material upkeep (and repair bill) can be read.
|
||||
function seedBuildings(state) {
|
||||
const basket = {
|
||||
market: 3, bank: 2, barracks: 2, shelters: 1, aa_building: 1,
|
||||
research_lab: 2, university: 1, museum: 1, cinema_studio: 1, airport: 1,
|
||||
};
|
||||
for (const city of state.cities) {
|
||||
for (const [id, level] of Object.entries(basket)) {
|
||||
const index = state.protoBuildings.findIndex((b) => b.id === id);
|
||||
if (index < 0) continue;
|
||||
if (state.protoBuildings[index].coastal && !state.isCoastalCity(city)) continue;
|
||||
city.buildings[index] = level;
|
||||
}
|
||||
}
|
||||
state._gdpPerCapitaCache.clear();
|
||||
state._touchModifiers();
|
||||
}
|
||||
|
||||
function eur(value) {
|
||||
const abs = Math.abs(value);
|
||||
if (abs >= 1e9) return `${(value / 1e9).toFixed(2)}B`;
|
||||
if (abs >= 1e6) return `${(value / 1e6).toFixed(2)}M`;
|
||||
if (abs >= 1e3) return `${(value / 1e3).toFixed(1)}k`;
|
||||
return value.toFixed(0);
|
||||
}
|
||||
|
||||
function num(value) {
|
||||
const abs = Math.abs(value);
|
||||
if (abs >= 1e9) return `${(value / 1e9).toFixed(2)}e9`;
|
||||
if (abs >= 1e6) return `${(value / 1e6).toFixed(2)}e6`;
|
||||
if (abs >= 1e3) return `${(value / 1e3).toFixed(1)}e3`;
|
||||
return value.toFixed(1);
|
||||
}
|
||||
|
||||
function pad(text, width, right = false) {
|
||||
const value = String(text);
|
||||
return right ? value.padStart(width) : value.padEnd(width);
|
||||
}
|
||||
|
||||
console.log(`\n=== Resource production costs (base market prices, ${DAYS} days) ===`);
|
||||
console.log(
|
||||
pad("building", 26) + pad("out/day", 12, true) + pad("energy/day", 13, true) +
|
||||
pad("energy €/d", 11, true) + pad("upkeep €/d", 11, true) + pad("gross €/d", 11, true) +
|
||||
pad("net €/d", 11, true) + pad("payback d", 10, true)
|
||||
);
|
||||
for (const building of RESOURCE_BUILDINGS) {
|
||||
const energyPrice = RESOURCE_MARKET_BASE.energy;
|
||||
const outputValue = building.resource === "energy"
|
||||
? building.outputPerDay * RESOURCE_MARKET_BASE.energy
|
||||
: building.outputPerDay * (RESOURCE_MARKET_BASE[building.resource] || 0);
|
||||
const energyCost = (building.energyPerDay || 0) * energyPrice;
|
||||
const upkeep = building.maintenance * 24;
|
||||
const net = outputValue - energyCost - upkeep;
|
||||
const payback = net > 0 ? building.buildCost / net : Infinity;
|
||||
console.log(
|
||||
pad(building.id, 26) + pad(num(building.outputPerDay), 12, true) +
|
||||
pad(num(building.energyPerDay || 0), 13, true) + pad(eur(energyCost), 11, true) +
|
||||
pad(eur(upkeep), 11, true) + pad(eur(outputValue), 11, true) +
|
||||
pad(eur(net), 11, true) + pad(Number.isFinite(payback) ? payback.toFixed(0) : "never", 10, true)
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`\n=== Settled economy after ${DAYS} days (scenario: ${SCENARIO}, map: ${MAP}) ===`);
|
||||
for (let civ = 0; civ < civs.length; civ++) {
|
||||
const population = state.getPlayerPopulation(civ);
|
||||
const gdp = state.getPlayerGdp(civ);
|
||||
const income = state.getCivHourlyIncome(civ) * 24;
|
||||
const moneyUpkeep = state.getPlayerUpkeep(civ) * 24;
|
||||
const upkeepDemand = state.getCivUpkeepResources(civ);
|
||||
const spend = spendTotals[civ];
|
||||
let energyNeed = 0;
|
||||
let energySupply = 0;
|
||||
for (const city of state.cities) {
|
||||
if (city.civ !== civ) continue;
|
||||
energyNeed += cityEnergyPerDay(state.getCityEconomy(city).gdp);
|
||||
}
|
||||
for (const [k, id] of state.tileImprovements) {
|
||||
if (state.tileImprovementOwner.get(k) !== civ) continue;
|
||||
const proto = RESOURCE_BUILDINGS.find((b) => b.id === id);
|
||||
if (proto && proto.resource === "energy") energySupply += proto.outputPerDay;
|
||||
}
|
||||
const stock = state.getCivResourceSummary(civ);
|
||||
console.log(
|
||||
`\n-- ${civs[civ].name} --`
|
||||
);
|
||||
console.log(
|
||||
`pop ${num(population)} gdp ${eur(gdp)} gdp/cap ${eur(population ? gdp / population : 0)}` +
|
||||
` budget ${eur(state.getBudget(civ))} (start ${eur(startBudget[civ])})`
|
||||
);
|
||||
console.log(
|
||||
`income/d ${eur(income)} campaign upkeep/d ${eur(moneyUpkeep)} net/d ${eur(income - moneyUpkeep)}`
|
||||
);
|
||||
console.log(
|
||||
`material upkeep/d: steel ${num(upkeepDemand.steel)} t energy ${num(upkeepDemand.energy)} kWh` +
|
||||
` high-tech ${num(upkeepDemand.hightech)} u`
|
||||
);
|
||||
console.log(
|
||||
`resource spend over run: energy ${eur(spend.energy)} goods ${eur(spend.goods)}` +
|
||||
` transport ${eur(spend.transport)} construction ${eur(spend.construction)}` +
|
||||
` upkeep ${eur(spend.upkeep)} combat ${eur(spend.combat)}`
|
||||
);
|
||||
console.log(
|
||||
`energy need/d ${num(energyNeed)} kWh own supply/d ${num(energySupply)} kWh` +
|
||||
` bought-ish/d ${num((spend.energy / Math.max(DAYS, 1)) / Math.max(state.getResourcePrice("energy"), 1e-9))} kWh`
|
||||
);
|
||||
for (const id of ["food", "steel", "luxury", "hightech"]) {
|
||||
console.log(
|
||||
` ${pad(id, 9)} stock ${num(stock.stock[id])} need/d ${num(stock.need[id] || 0)}` +
|
||||
` produced/d ${num(stock.production[id] || 0)}`
|
||||
);
|
||||
}
|
||||
let shortage = 0;
|
||||
for (const city of state.cities) {
|
||||
if (city.civ !== civ) continue;
|
||||
const info = state.getCityResourceInfo(city);
|
||||
for (const value of Object.values(info.shortage)) shortage += value;
|
||||
}
|
||||
console.log(` summed city shortage fraction: ${shortage.toFixed(3)}`);
|
||||
}
|
||||
|
||||
console.log("\n=== Market prices at the end ===");
|
||||
for (const id of Object.keys(RESOURCE_MARKET_BASE)) {
|
||||
const stats = state.resourceMarketStats.get(id) || {};
|
||||
console.log(
|
||||
` ${pad(id, 9)} price ${state.getResourcePrice(id).toFixed(4)}` +
|
||||
` (base ${RESOURCE_MARKET_BASE[id]}) supply ${num(stats.supply || 0)} demand ${num(stats.demand || 0)}`
|
||||
);
|
||||
}
|
||||
console.log(`\nRules: food ${RESOURCE_RULES.foodPerPersonPerDay} t/pp/d, steel ${RESOURCE_RULES.steelPerPersonPerDay} t/pp/d, luxury ${RESOURCE_RULES.luxuryPerPersonPerDay} ct/pp/d`);
|
||||
console.log(`energy intensity ${RESOURCE_RULES.gdpEnergyIntensity} kWh/€/yr, steel ${RESOURCE_RULES.energyPerSteelTonne / 1e6} GWh/t, food ${RESOURCE_RULES.energyPerFoodTonne / 1e6} GWh/t\n`);
|
||||
Reference in New Issue
Block a user