First commit, done on M6 from the PLAN.MD
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
// 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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user