77 lines
4.4 KiB
JavaScript
77 lines
4.4 KiB
JavaScript
// Overview of the card pool, for reviewing content and balance.
|
|
// node tools/card-stats.mjs whole pool
|
|
// node tools/card-stats.mjs economy one category in detail (adds the longest texts)
|
|
|
|
import fs from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { optionImpact } from "../web/js/engine/index.js";
|
|
|
|
const dir = fileURLToPath(new URL("../web/data/cards/", import.meta.url));
|
|
const files = JSON.parse(fs.readFileSync(dir + "index.json", "utf8")).files;
|
|
const only = process.argv[2];
|
|
|
|
const pool = files.flatMap((f) => JSON.parse(fs.readFileSync(dir + f, "utf8")));
|
|
const cards = only ? pool.filter((c) => c.category === only) : pool;
|
|
if (!cards.length) {
|
|
console.error("no cards for", only);
|
|
process.exit(1);
|
|
}
|
|
|
|
const count = (xs, key) => xs.reduce((m, x) => ((m[key(x)] = (m[key(x)] ?? 0) + 1), m), {});
|
|
const mean = (xs) => (xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0);
|
|
const fmt = (n) => n.toFixed(2);
|
|
const table = (o) => Object.entries(o).map(([k, v]) => `${k} ${v}`).join(", ");
|
|
|
|
console.log(`cards: ${cards.length}${only ? ` in ${only}` : ` in ${files.length} categories`}`);
|
|
if (!only) console.log("per category:", table(count(cards, (c) => c.category)));
|
|
console.log("doc types:", table(count(cards, (c) => c.doc)));
|
|
console.log("senders:", table(count(cards, (c) => c.sender)));
|
|
console.log("follow-up only:", cards.filter((c) => c.followUpOnly).length, "| repeatable:", cards.filter((c) => c.once === false).length);
|
|
|
|
const bucket = (c) => {
|
|
const d = c.conditions?.dayMin ?? 0;
|
|
if (d < 90) return "start";
|
|
if (d < 365) return "y1";
|
|
if (d < 730) return "y2";
|
|
if (d < 1095) return "y3";
|
|
return "y4+";
|
|
};
|
|
console.log("available from:", table(count(cards, bucket)));
|
|
console.log("with conditions:", cards.filter((c) => c.conditions).length, "| with stat conditions:",
|
|
cards.filter((c) => c.conditions?.min || c.conditions?.max).length);
|
|
|
|
const set = new Set();
|
|
const used = new Set();
|
|
let links = 0;
|
|
for (const c of cards) {
|
|
for (const side of ["yes", "no"]) {
|
|
for (const f of c[side].setFlags ?? []) set.add(f);
|
|
if (c[side].followUp) links++;
|
|
}
|
|
for (const f of [...(c.conditions?.flags ?? []), ...(c.conditions?.notFlags ?? [])]) used.add(f);
|
|
}
|
|
console.log(`flags set: ${set.size} | flags used: ${used.size} | follow-up links: ${links}`);
|
|
|
|
// Balance: what an average card does to the country, for each answer
|
|
const stat = (c, side, pick) => (c[side].effects ?? []).filter(pick).reduce((s, e) => s + (e.delta ?? e.oneOff ?? e.extraSpending ?? 0), 0);
|
|
const row = (label, f) => console.log(label.padEnd(34), `yes ${fmt(mean(cards.map((c) => f(c, "yes"))))}`.padEnd(12), `no ${fmt(mean(cards.map((c) => f(c, "no"))))}`.padEnd(12),
|
|
`coin flip ${fmt(mean(cards.map((c) => (f(c, "yes") + f(c, "no")) / 2)))}`);
|
|
console.log("\naverage per card (instant, spread and bias deltas added up):");
|
|
row("impact (size of the effects)", (c, s) => optionImpact(c[s]));
|
|
for (const id of ["opinion", "stability", "economy", "order", "civilRights", "unrest"]) row(`${id}`, (c, s) => stat(c, s, (e) => e.stat === id));
|
|
row("one-off cost, % of GDP", (c, s) => stat(c, s, (e) => e.oneOff !== undefined));
|
|
row("permanent extra spending, % GDP", (c, s) => stat(c, s, (e) => e.extraSpending !== undefined));
|
|
row("tax rate points (income+corp+vat)", (c, s) => stat(c, s, (e) => ["income", "corporate", "vat"].includes(e.revenue)));
|
|
row("other revenue, % of GDP", (c, s) => stat(c, s, (e) => e.revenue === "other"));
|
|
const effects = cards.flatMap((c) => ["yes", "no"].flatMap((s) => c[s].effects ?? []));
|
|
console.log("\neffects per option:", fmt(effects.length / (cards.length * 2)),
|
|
"| with bias:", effects.filter((e) => e.bias).length, "| spread over time:", effects.filter((e) => e.days && !e.bias).length,
|
|
"| instant:", effects.filter((e) => (e.stat || e.sector) && !e.days && !e.bias).length);
|
|
|
|
const long = cards.map((c) => ({ id: c.id, en: c.body.en.length, fr: c.body.fr.length })).filter((x) => x.en > 360 || x.fr > 400);
|
|
if (long.length) console.log("\nlong bodies (en > 360 or fr > 400 chars):", long.map((x) => `${x.id} (${x.en}/${x.fr})`).join(", "));
|
|
const dupTitles = Object.entries(count(cards, (c) => c.title.en.toLowerCase())).filter(([, n]) => n > 1);
|
|
if (dupTitles.length) console.log("duplicate English titles:", dupTitles.map(([t]) => t).join("; "));
|
|
const labelDup = cards.filter((c) => c.yes.label.en === c.no.label.en);
|
|
if (labelDup.length) console.log("same label on both options:", labelDup.map((c) => c.id).join(", "));
|