95 lines
3.7 KiB
JavaScript
95 lines
3.7 KiB
JavaScript
// Headless balance runner. No DOM, no fetch: works in the browser (sim.html) and in Node.
|
|
|
|
import {
|
|
createGame, advanceDay, summarize, setBudget, confirmBudget, ensureCard, answerCard, nextFloat, dismissAlert,
|
|
} from "../js/engine/index.js";
|
|
|
|
// A strategy decides what to do at each quarterly review (review) and how to answer
|
|
// each weekly card (answer, default: a coin flip from the game's own RNG).
|
|
export const STRATEGIES = {
|
|
baseline: { label: "Baseline (change nothing)", review: () => ({}) },
|
|
yesMan: { label: "Yes to every card", review: () => ({}), answer: () => "yes" },
|
|
noMan: { label: "No to every card", review: () => ({}), answer: () => "no" },
|
|
spendthrift: {
|
|
label: "Spendthrift (all budgets x1.4)",
|
|
review: (state) => ({ alloc: scaled(state, 1.4) }),
|
|
},
|
|
austerity: {
|
|
label: "Austerity (all budgets x0.75)",
|
|
review: (state) => ({ alloc: scaled(state, 0.75) }),
|
|
},
|
|
policeState: {
|
|
label: "Police state (police x3, army x1.5)",
|
|
review: (state) => ({ alloc: { police: state.sectors.police.need * 3, army: state.sectors.army.need * 1.5 } }),
|
|
},
|
|
taxTheRich: {
|
|
label: "Tax hike (income +10, corporate +10)",
|
|
review: (state) => ({ taxes: { income: state.ref.taxes.income + 10, corporate: state.ref.taxes.corporate + 10 } }),
|
|
},
|
|
// Deliberately bad play: each should be able to end the term early.
|
|
ruin: {
|
|
label: "Ruin (all budgets x2, taxes cut to 15/12/10)",
|
|
review: (state) => ({ alloc: scaled(state, 2), taxes: { income: 15, corporate: 12, vat: 10 } }),
|
|
},
|
|
starve: {
|
|
label: "Starve (all budgets x0.25, taxes up)",
|
|
review: (state) => ({
|
|
alloc: scaled(state, 0.25),
|
|
taxes: { income: state.ref.taxes.income + 15, corporate: state.ref.taxes.corporate + 10, vat: state.ref.taxes.vat + 10 },
|
|
}),
|
|
},
|
|
tyrant: {
|
|
label: "Tyrant (police x3, army x2, justice and social x0.3)",
|
|
review: (state) => ({
|
|
alloc: {
|
|
...scaled(state, 0.7),
|
|
police: state.sectors.police.need * 3,
|
|
army: state.sectors.army.need * 2,
|
|
justice: state.sectors.justice.need * 0.3,
|
|
social: state.sectors.social.need * 0.3,
|
|
},
|
|
}),
|
|
},
|
|
neglectArmy: {
|
|
label: "Neglect the army (army x0.1, the rest x1.1)",
|
|
review: (state) => ({ alloc: { ...scaled(state, 1.1), army: state.sectors.army.need * 0.1 } }),
|
|
},
|
|
};
|
|
|
|
function scaled(state, k) {
|
|
const out = {};
|
|
for (const [id, s] of Object.entries(state.sectors)) out[id] = s.need * (state.ref.ratio[id] * k);
|
|
return out;
|
|
}
|
|
|
|
// Runs one game to the end of `years` (or the end of the term) and samples every `sampleEvery` days.
|
|
export function runSim({ scenario, balance, cards = [], strategy, years = 1, seed = 1, sampleEvery = 30 }) {
|
|
const state = createGame({ scenario, balance, seed, pronouns: "they" });
|
|
const maxDay = Math.min(balance.time.termDays, Math.round(years * 365));
|
|
const rows = [];
|
|
const strat = STRATEGIES[strategy];
|
|
|
|
const sample = () => rows.push(summarize(state, balance));
|
|
sample();
|
|
|
|
while (!state.over && state.day < maxDay) {
|
|
if (state.pending.review) {
|
|
const res = setBudget(state, balance, strat.review(state));
|
|
if (!res.ok) throw new Error(`strategy ${strategy} produced an invalid budget: ${res.errors.join(", ")}`);
|
|
confirmBudget(state);
|
|
}
|
|
while (state.pending.alert) dismissAlert(state);
|
|
if (state.pending.weeklyCard) {
|
|
const card = ensureCard(state, balance, cards);
|
|
if (card) {
|
|
const choice = strat.answer ? strat.answer(state, card) : nextFloat(state) < 0.5 ? "yes" : "no";
|
|
answerCard(state, balance, cards, choice);
|
|
}
|
|
}
|
|
advanceDay(state, balance);
|
|
if (state.day % sampleEvery === 0) sample();
|
|
}
|
|
sample();
|
|
return { rows, final: rows[rows.length - 1], over: state.over };
|
|
}
|