176 lines
6.5 KiB
JavaScript
176 lines
6.5 KiB
JavaScript
// Game creation and the daily tick. Pure: data (balance, scenario) is passed in.
|
|
|
|
import { STATE_VERSION, IND_IDS, VAR_IDS, qualityFromRatio } from "./state.js";
|
|
import { computeFiscal, computeDevs, advanceEconomy } from "./economy.js";
|
|
import { applyShifts, ageBias } from "./modifiers.js";
|
|
import { stepSectors } from "./sectors.js";
|
|
import { stepWorld } from "./world.js";
|
|
import { stepIndicators, stepVars } from "./indicators.js";
|
|
import { checkGameOver } from "./gameover.js";
|
|
|
|
export const PRONOUNS = ["he", "she", "they"];
|
|
|
|
// pronouns: omit it and the game opens with the "who are you" question (pending.profile).
|
|
export function createGame({ scenario, balance, seed = Date.now(), countryName, lang = "en", pronouns }) {
|
|
const se = scenario.economy;
|
|
const gdp = se.gdp;
|
|
|
|
const sectors = {};
|
|
for (const [id, s] of Object.entries(scenario.sectors)) {
|
|
sectors[id] = { need: s.need, alloc: s.alloc, quality: qualityFromRatio(s.alloc / s.need, balance) };
|
|
}
|
|
|
|
const state = {
|
|
version: STATE_VERSION,
|
|
seed: seed >>> 0,
|
|
rngState: seed >>> 0,
|
|
day: 0,
|
|
speed: 1, // 0 = paused, otherwise one of balance.time.speeds
|
|
startDate: scenario.startDate,
|
|
lang,
|
|
pronouns: PRONOUNS.includes(pronouns) ? pronouns : "they",
|
|
countryName: countryName ?? scenario.name[lang] ?? scenario.name.en,
|
|
scenarioId: scenario.id,
|
|
economy: {
|
|
gdp,
|
|
debt: (gdp * se.debtRatio) / 100,
|
|
growth: se.growth,
|
|
unemployment: se.unemployment,
|
|
inflation: se.inflation,
|
|
taxes: { ...se.taxes },
|
|
otherRevenue: se.otherRevenue,
|
|
extraSpending: 0,
|
|
},
|
|
sectors,
|
|
indicators: { ...scenario.indicators },
|
|
modifiers: [],
|
|
world: { cycle: 0 }, // global business cycle, see world.js
|
|
flags: {},
|
|
cardHistory: [],
|
|
inbox: [], // proposals waiting in the tray, oldest first: { id, day, category }
|
|
followUps: [],
|
|
// Day 0 opens with the pronoun question, the initial budget review and a first card.
|
|
// Any true flag here pauses time (see advanceDay). weeklyCard is momentary: the caller answers it
|
|
// at once with ensureCard(); inboxFull stays until the tray is below its limit.
|
|
pending: { profile: !PRONOUNS.includes(pronouns), weeklyCard: true, review: true, alert: false, inboxFull: false },
|
|
over: null,
|
|
// days in each danger zone; emergency = an unscheduled budget session is open (see advanceDay)
|
|
risk: { revolution: 0, coup: 0, impeachment: 0, defaultZone: false, emergency: false, lastEmergency: -999 },
|
|
alerts: [], // urgent telegrams waiting to be read, oldest first
|
|
history: { t: [], series: {} },
|
|
log: [],
|
|
ref: null,
|
|
};
|
|
|
|
state.ref = {
|
|
taxes: { ...se.taxes },
|
|
ind: { ...scenario.indicators },
|
|
vars: { growth: se.growth, unemployment: se.unemployment, inflation: se.inflation },
|
|
sectors: Object.fromEntries(Object.entries(sectors).map(([id, s]) => [id, s.quality])),
|
|
ratio: Object.fromEntries(Object.entries(sectors).map(([id, s]) => [id, s.alloc / s.need])),
|
|
dev: null,
|
|
};
|
|
state.ref.dev = computeDevs(state, balance, computeFiscal(state, balance));
|
|
|
|
recordHistory(state, balance);
|
|
return state;
|
|
}
|
|
|
|
function recordHistory(state, bal) {
|
|
const f = computeFiscal(state, bal);
|
|
const h = state.history;
|
|
h.t.push(state.day);
|
|
const sample = { debtRatio: f.debtRatio, deficit: f.deficit };
|
|
for (const id of IND_IDS) sample[id] = state.indicators[id];
|
|
for (const id of VAR_IDS) sample[id] = state.economy[id];
|
|
for (const [k, v] of Object.entries(sample)) (h.series[k] ??= []).push(round(v));
|
|
}
|
|
|
|
const round = (v) => Math.round(v * 100) / 100;
|
|
|
|
export function setSpeed(state, bal, speed) {
|
|
if (speed !== 0 && !bal.time.speeds.includes(speed)) return false;
|
|
state.speed = speed;
|
|
return true;
|
|
}
|
|
|
|
// Clears a pause condition once the player has dealt with it.
|
|
export function clearPending(state, kind) {
|
|
state.pending[kind] = false;
|
|
}
|
|
|
|
export function setPronouns(state, pronouns) {
|
|
if (!PRONOUNS.includes(pronouns)) return false;
|
|
state.pronouns = pronouns;
|
|
state.pending.profile = false;
|
|
return true;
|
|
}
|
|
|
|
// True while the game is waiting on the player (profile, budget review, weekly card, alert).
|
|
export const isWaiting = (state) => Object.values(state.pending).some(Boolean);
|
|
|
|
// Advances one in-game day unless the game is over or waiting on the player.
|
|
export function advanceDay(state, bal) {
|
|
if (state.over) return { advanced: false, blocked: "over", events: null };
|
|
if (isWaiting(state)) return { advanced: false, blocked: "pending", events: null };
|
|
|
|
applyShifts(state, bal);
|
|
stepWorld(state, bal);
|
|
const fiscal = computeFiscal(state, bal);
|
|
const devs = computeDevs(state, bal, fiscal);
|
|
|
|
advanceEconomy(state, fiscal);
|
|
stepVars(state, bal, devs);
|
|
stepIndicators(state, bal, devs);
|
|
stepSectors(state, bal, fiscal);
|
|
ageBias(state);
|
|
|
|
state.day += 1;
|
|
|
|
const events = { weeklyCard: false, review: false, termEnd: false, gameOver: null };
|
|
const alertsBefore = state.alerts.length;
|
|
const fallen = checkGameOver(state, bal);
|
|
if (fallen) {
|
|
state.over = { type: fallen, day: state.day };
|
|
events.gameOver = fallen;
|
|
recordHistory(state, bal);
|
|
} else if (state.day >= bal.time.termDays) {
|
|
state.over = { type: "term", day: state.day };
|
|
events.termEnd = true;
|
|
recordHistory(state, bal);
|
|
} else {
|
|
if (state.day % bal.time.historyEvery === 0) recordHistory(state, bal);
|
|
if (state.day % bal.time.daysPerWeek === 0) state.pending.weeklyCard = events.weeklyCard = true;
|
|
if (state.day % bal.time.daysPerQuarter === 0) {
|
|
state.pending.review = events.review = true;
|
|
state.risk.emergency = false;
|
|
} else if (state.alerts.length > alertsBefore && state.day - state.risk.lastEmergency >= bal.gameOver.emergencyCooldown) {
|
|
// A new danger warning: the cabinet meets at once, so the player can react before the next quarter.
|
|
state.pending.review = events.review = true;
|
|
state.risk.emergency = true;
|
|
state.risk.lastEmergency = state.day;
|
|
}
|
|
}
|
|
return { advanced: true, blocked: null, events };
|
|
}
|
|
|
|
// Read-only snapshot for the UI and the sim.
|
|
export function summarize(state, bal) {
|
|
const f = computeFiscal(state, bal);
|
|
return {
|
|
day: state.day,
|
|
gdp: state.economy.gdp,
|
|
debt: state.economy.debt,
|
|
debtRatio: f.debtRatio,
|
|
interestRate: f.rate,
|
|
revenue: f.revenue,
|
|
spending: f.spending,
|
|
interest: f.interest,
|
|
deficit: f.deficit,
|
|
growth: state.economy.growth,
|
|
unemployment: state.economy.unemployment,
|
|
inflation: state.economy.inflation,
|
|
indicators: { ...state.indicators },
|
|
};
|
|
}
|