feat: Doing more polishing (M8b)

This commit is contained in:
gauvainboiche
2026-10-25 18:50:17 +01:00
parent c2103c32e7
commit c55e5a2484
34 changed files with 1605 additions and 106 deletions
+49
View File
@@ -0,0 +1,49 @@
// 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;