// 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, } 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`);