// 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 { 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: [], flags: {}, cardHistory: [], pendingCard: null, followUps: [], // Day 0 opens with the pronoun question, the initial budget review and a first card. // Any true flag here pauses time (see advanceDay). pending: { profile: !PRONOUNS.includes(pronouns), weeklyCard: true, review: true, alert: false }, over: null, risk: { revolution: 0, coup: 0, impeachment: 0, defaultZone: false }, // days in each danger zone 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); 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 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; } 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 }, }; }