First commit, done on M6 from the PLAN.MD

This commit is contained in:
gauvainboiche
2026-10-25 17:34:44 +01:00
parent d5dc2a3901
commit 04193e2013
86 changed files with 8396 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
// 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 },
};
}