feat: Cards writings and balancing (M8)
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
// Hill-climbing search for the best budget policy (M8): shows what the ceiling of the game is and
|
||||
// how a strong player would play, so the balance can be tuned against it.
|
||||
// node tools/balance-search.mjs [iterations] [seeds] [cardPolicy]
|
||||
// Prints the best parameters found and re-checks them on fresh seeds.
|
||||
|
||||
import * as E from "../web/js/engine/index.js";
|
||||
import { bal, playWith } from "./balance-check.mjs";
|
||||
|
||||
const GROUPS = {
|
||||
core: ["health", "social", "housing"],
|
||||
order: ["police", "justice", "administration"],
|
||||
army: ["army"],
|
||||
growth: ["education", "research", "infrastructure", "industry", "transport"],
|
||||
soft: ["culture", "foreignAffairs", "environment"],
|
||||
};
|
||||
const GKEYS = Object.keys(GROUPS);
|
||||
const TAXES = ["income", "corporate", "vat"];
|
||||
// every group multiplier and every tax shift has an early value (first two years) and a late one
|
||||
const START = { brake: 0 };
|
||||
const RANGE = { brake: [0, 1] };
|
||||
for (const g of GKEYS) for (const ph of ["E", "L"]) { START[g + ph] = 1; RANGE[g + ph] = g === "army" ? [0.3, 2.5] : [0.4, 2.6]; }
|
||||
for (const t of TAXES) for (const ph of ["E", "L"]) { START[t + ph] = 0; RANGE[t + ph] = [-10, 12]; }
|
||||
|
||||
export function policyFrom(p) {
|
||||
return (state) => {
|
||||
const ph = state.day < 730 ? "E" : "L";
|
||||
const alloc = {};
|
||||
for (const [g, ids] of Object.entries(GROUPS)) {
|
||||
for (const id of ids) {
|
||||
const s = state.sectors[id];
|
||||
alloc[id] = Math.min(s.need * bal.limits.allocMaxRatio, s.need * state.ref.ratio[id] * p[g + ph]);
|
||||
}
|
||||
}
|
||||
const taxes = {};
|
||||
for (const id of TAXES) taxes[id] = Math.max(0, state.ref.taxes[id] + p[id + ph]);
|
||||
// debt brake: above 90 % of GDP, trim every sector toward the baseline and add a point of VAT
|
||||
if (p.brake > 0) {
|
||||
const f = E.computeFiscal(state, bal);
|
||||
if (f.debtRatio > 90) {
|
||||
const k = 1 - 0.25 * p.brake;
|
||||
for (const id of Object.keys(alloc)) alloc[id] *= k;
|
||||
taxes.vat += 2 * p.brake;
|
||||
}
|
||||
}
|
||||
return { alloc, taxes };
|
||||
};
|
||||
}
|
||||
|
||||
function score(p, seeds, policy) {
|
||||
let total = 0;
|
||||
let survived = 0;
|
||||
for (const seed of seeds) {
|
||||
const r = playWith(policyFrom(p), policy, seed);
|
||||
total += r.end === "term" ? r.score : r.score * 0.5;
|
||||
if (r.end === "term") survived++;
|
||||
}
|
||||
return { mean: total / seeds.length, survival: survived / seeds.length };
|
||||
}
|
||||
|
||||
const iterations = Number(process.argv[2] ?? 120);
|
||||
const nSeeds = Number(process.argv[3] ?? 8);
|
||||
const policy = process.argv[4] ?? "intuitive";
|
||||
const seeds = Array.from({ length: nSeeds }, (_, i) => 100 + i);
|
||||
|
||||
let best = { ...START };
|
||||
let bestScore = score(best, seeds, policy);
|
||||
console.log("start", JSON.stringify(best), bestScore);
|
||||
let seedState = 12345;
|
||||
const rnd = () => (seedState = (seedState * 1664525 + 1013904223) % 4294967296) / 4294967296;
|
||||
|
||||
for (let it = 0; it < iterations; it++) {
|
||||
const cand = { ...best };
|
||||
const keys = Object.keys(RANGE);
|
||||
const nChange = 1 + Math.floor(rnd() * 3);
|
||||
for (let c = 0; c < nChange; c++) {
|
||||
const k = keys[Math.floor(rnd() * keys.length)];
|
||||
const [lo, hi] = RANGE[k];
|
||||
const step = (hi - lo) * 0.18 * (rnd() * 2 - 1);
|
||||
cand[k] = Math.min(hi, Math.max(lo, cand[k] + step));
|
||||
}
|
||||
const sc = score(cand, seeds, policy);
|
||||
if (sc.mean > bestScore.mean) {
|
||||
best = cand;
|
||||
bestScore = sc;
|
||||
console.log(`it ${it}`, JSON.stringify(Object.fromEntries(Object.entries(best).map(([k, v]) => [k, +v.toFixed(2)]))), JSON.stringify({ mean: +sc.mean.toFixed(1), survival: sc.survival }));
|
||||
}
|
||||
}
|
||||
|
||||
const fresh = Array.from({ length: 24 }, (_, i) => 500 + i);
|
||||
const check = score(best, fresh, policy);
|
||||
console.log("fresh seeds:", JSON.stringify({ mean: +check.mean.toFixed(1), survival: check.survival }));
|
||||
Reference in New Issue
Block a user