94 lines
3.6 KiB
JavaScript
94 lines
3.6 KiB
JavaScript
// End-of-term review: a score from the whole term (not just the last day), a letter
|
|
// grade, the historian's title and the decisions that mattered most.
|
|
// Pure: works for a finished term and for a game that ended early.
|
|
|
|
import { clamp } from "./state.js";
|
|
import { indexCards } from "./cards.js";
|
|
|
|
// Names usable in the `min` / `max` conditions of endgame.json titles.
|
|
export const METRIC_IDS = [
|
|
"order", "stability", "civilRights", "opinion", "economy", "hardPower", "softPower", "unrest", "armyLoyalty",
|
|
"growth", "unemployment", "inflation", "debt", "score", "years",
|
|
];
|
|
|
|
const mean = (xs) => (xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0);
|
|
|
|
// Averages over the whole term, plus the final debt ratio.
|
|
function termAverages(state) {
|
|
const s = state.history.series;
|
|
const m = {};
|
|
for (const id of ["order", "stability", "civilRights", "opinion", "economy", "hardPower", "softPower", "unrest", "armyLoyalty", "growth", "unemployment", "inflation"]) {
|
|
m[id] = mean(s[id] ?? []);
|
|
}
|
|
m.debt = s.debtRatio?.at(-1) ?? 0;
|
|
m.years = state.day / 365;
|
|
return m;
|
|
}
|
|
|
|
function computeScore(m, bal) {
|
|
const sc = bal.score;
|
|
let base = 0;
|
|
for (const [id, w] of Object.entries(sc.weights)) base += w * (id === "calm" ? 100 - m.unrest : m[id]);
|
|
const bonus = (cfg, value) => clamp((cfg.ref - value) * cfg.per, cfg.min, cfg.max);
|
|
const total = base
|
|
+ bonus(sc.debt, m.debt)
|
|
+ clamp((m.growth - sc.growth.ref) * sc.growth.per, sc.growth.min, sc.growth.max)
|
|
+ bonus(sc.unemployment, m.unemployment);
|
|
return clamp(total, 0, 100);
|
|
}
|
|
|
|
const gradeOf = (score, bal) => bal.score.grades.find(([min]) => score >= min)?.[1] ?? "F";
|
|
|
|
// How strongly one effect moves the country, in rough "points". Used to rank decisions.
|
|
export function effectImpact(e) {
|
|
const d = Math.abs(e.delta ?? 0);
|
|
if (e.stat !== undefined || e.sector !== undefined) {
|
|
if (e.bias) return d * clamp((e.days ?? 365) / 90, 1, 8);
|
|
return d;
|
|
}
|
|
if (e.revenue !== undefined) return d * (e.revenue === "other" ? 8 : 2);
|
|
if (e.oneOff !== undefined) return Math.abs(e.oneOff) * 5;
|
|
if (e.extraSpending !== undefined) return Math.abs(e.extraSpending) * 8;
|
|
return 0;
|
|
}
|
|
|
|
export const optionImpact = (option) => (option.effects ?? []).reduce((sum, e) => sum + effectImpact(e), 0);
|
|
|
|
function topDecisions(state, cards, count) {
|
|
const byId = indexCards(cards);
|
|
return state.cardHistory
|
|
.map((h) => {
|
|
const card = byId.get(h.id);
|
|
return card ? { id: h.id, day: h.day, choice: h.choice, impact: optionImpact(card[h.choice]) } : null;
|
|
})
|
|
.filter(Boolean)
|
|
.sort((a, b) => b.impact - a.impact || a.day - b.day)
|
|
.slice(0, count);
|
|
}
|
|
|
|
function meets(entry, m) {
|
|
const c = entry.conditions ?? {};
|
|
for (const [k, v] of Object.entries(c.min ?? {})) if (!(m[k] >= v)) return false;
|
|
for (const [k, v] of Object.entries(c.max ?? {})) if (!(m[k] <= v)) return false;
|
|
return true;
|
|
}
|
|
|
|
// endgame: the parsed data/endgame.json (its `titles` list is in priority order).
|
|
export function buildReview(state, bal, cards, endgame) {
|
|
const metrics = termAverages(state);
|
|
let score = computeScore(metrics, bal);
|
|
if (state.over && state.over.type !== "term") score = Math.min(score, bal.score.overCap);
|
|
metrics.score = score;
|
|
|
|
const earlyEnd = state.over && state.over.type !== "term" ? state.over.type : null;
|
|
const title = endgame.titles.find((t) => (t.over ?? null) === earlyEnd && (earlyEnd || meets(t, metrics)));
|
|
return {
|
|
over: state.over,
|
|
metrics,
|
|
score,
|
|
grade: earlyEnd ? "F" : gradeOf(score, bal),
|
|
titleId: title?.id ?? null,
|
|
topDecisions: topDecisions(state, cards, bal.score.topDecisions),
|
|
};
|
|
}
|