// Start-state report for a scenario (M8b): is the start close to balance, and what does a year of // doing nothing do? Usage: node tools/scenario-check.mjs [scenarioId] import fs from "node:fs"; import * as E from "../web/js/engine/index.js"; const root = new URL("../web/data/", import.meta.url); const read = (p) => JSON.parse(fs.readFileSync(new URL(p, root), "utf8")); const id = process.argv[2] ?? "default"; const bal = read("balance.json"); const scn = read(`scenarios/${id}.json`); const state = E.createGame({ scenario: scn, balance: bal, seed: 1, pronouns: "they" }); const f = E.computeFiscal(state, bal); const warn = []; const line = (k, v) => console.log(k.padEnd(28), v); console.log(`Scenario ${id}: ${scn.name.en} (${scn.currency}), starts ${scn.startDate}`); line("GDP", `${scn.economy.gdp} bn ${scn.currency}`); line("Debt / GDP", `${f.debtRatio.toFixed(1)} % (interest rate ${f.rate.toFixed(2)} %)`); line("Revenue", `${f.revenue.toFixed(2)} % of GDP`); line("Sector spending", `${f.allocTotal.toFixed(2)} % of GDP`); line("Debt interest", `${f.interest.toFixed(2)} % of GDP`); line("Deficit (+) / surplus (-)", `${f.deficit.toFixed(2)} % of GDP`); if (Math.abs(f.deficit) > 2) warn.push(`the start deficit is ${f.deficit.toFixed(1)} % of GDP: the start should be close to balance (within 2 points)`); if (f.debtRatio > bal.gameOver.default.warnAbove - 25) warn.push("the start debt is close to the default warning"); for (const [k, s] of Object.entries(state.sectors)) { const ratio = s.alloc / s.need; if (ratio < 0.5 || ratio > 1.5) warn.push(`sector ${k} starts at ${(ratio * 100).toFixed(0)} % of its need`); } for (const [k, v] of Object.entries(state.indicators)) if (v < 5 || v > 95) warn.push(`indicator ${k} starts at ${v}`); for (const k of E.IND_IDS) if (!(k in state.indicators)) warn.push(`indicator ${k} is missing`); // A year of doing nothing: no card answered, the budget untouched, the same noise as the game. const x = structuredClone(state); const start = { ...x.indicators, debt: f.debtRatio, growth: x.economy.growth }; for (let d = 0; d < 365 && !x.over; d++) { for (const k of Object.keys(x.pending)) x.pending[k] = false; x.alerts.length = 0; E.advanceDay(x, bal); } const end = E.summarize(x, bal); console.log("\nOne year with no decisions:"); for (const k of E.IND_IDS) line(` ${k}`, `${start[k].toFixed(0)} -> ${x.indicators[k].toFixed(0)}`); line(" debt / GDP", `${start.debt.toFixed(0)} -> ${end.debtRatio.toFixed(0)}`); line(" growth", `${start.growth.toFixed(1)} -> ${end.growth.toFixed(1)}`); if (x.over) warn.push(`the game ends within a year of doing nothing (${x.over.type})`); if (end.indicators.opinion < start.opinion - 15) warn.push("opinion falls by more than 15 points in a year of inaction"); console.log(warn.length ? `\nWarnings:\n- ${warn.join("\n- ")}` : "\nNo warnings."); process.exitCode = warn.length ? 1 : 0;