139 lines
6.0 KiB
JavaScript
139 lines
6.0 KiB
JavaScript
// CBP calculator engine tests. Extracts the LIVE code from index.html (no
|
|
// duplicated constants — the old copy of this file drifted) and cross-checks
|
|
// computePlan against an independently written event simulator.
|
|
// node test_scenarios.js [path-to-index.html]
|
|
const fs = require("fs");
|
|
const path = process.argv[2] || __dirname + "/index.html";
|
|
const html = fs.readFileSync(path, "utf8");
|
|
|
|
const start = html.indexOf("const LEVELS = [");
|
|
const end = html.indexOf("let chart1");
|
|
if (start < 0 || end < 0) { console.error("FATAL: extraction markers missing"); process.exit(1); }
|
|
// Indirect eval: keeps the extracted declarations out of this module's scope;
|
|
// the trailing expression hands back everything we need.
|
|
const { LEVELS, HYBRIDS, PHASE_SLOTS, computePlan, phaseInfo, getPhaseSequence,
|
|
estimateMonths, estimateYear1Cum, buildMonthlyTimeline } = (0, eval)(
|
|
html.slice(start, end) +
|
|
";({LEVELS, HYBRIDS, PHASE_SLOTS, computePlan, phaseInfo, getPhaseSequence, estimateMonths, estimateYear1Cum, buildMonthlyTimeline})"
|
|
);
|
|
|
|
let failures = 0;
|
|
function check(label, cond, detail) {
|
|
if (cond) { console.log(" ok " + label); }
|
|
else { failures++; console.log(" FAIL " + label + (detail ? " — " + detail : "")); }
|
|
}
|
|
|
|
// ── Independent simulator: same rules, different formulation ────────────────
|
|
function simulate(phases) {
|
|
const out = {};
|
|
let bal = 0;
|
|
const activated = new Set();
|
|
for (let lv = 1; lv <= Math.max(...PHASE_SLOTS[phases[0]]); lv++) activated.add(lv);
|
|
for (let i = 0; i < phases.length; i++) {
|
|
const k = phases[i];
|
|
if (k === "3xL7") { out[k] = 1; break; }
|
|
const cur = PHASE_SLOTS[k];
|
|
const nxt = [...PHASE_SLOTS[phases[i + 1]]];
|
|
const keep = [];
|
|
for (const lv of cur) { const j = nxt.indexOf(lv); if (j >= 0) { keep.push(lv); nxt.splice(j, 1); } }
|
|
let buyIn = 0;
|
|
for (const lv of nxt) { buyIn += LEVELS[lv - 1].camp + (activated.has(lv) ? 0 : LEVELS[lv - 1].act); activated.add(lv); }
|
|
let cycles = 0;
|
|
while (cycles < 500) {
|
|
cycles++;
|
|
// payout event for the whole hybrid
|
|
let cash = bal;
|
|
for (const lv of cur) cash += LEVELS[lv - 1].payout;
|
|
// to upgrade now: re-buy only kept campaigns, then afford buy-in
|
|
const keptCamp = keep.reduce((s, lv) => s + LEVELS[lv - 1].camp, 0);
|
|
if (cash - keptCamp >= buyIn) { bal = cash - keptCamp - buyIn; break; }
|
|
// otherwise recycle everything, bank the profit
|
|
for (const lv of cur) cash -= LEVELS[lv - 1].camp;
|
|
bal = cash;
|
|
}
|
|
out[k] = cycles;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
console.log("1) computePlan vs independent simulator, every start level 0-7:");
|
|
for (let eff = 0; eff <= 7; eff++) {
|
|
const phases = getPhaseSequence(eff);
|
|
const plan = computePlan(phases);
|
|
const sim = simulate(phases);
|
|
for (const k of phases) {
|
|
check(`eff=${eff} ${k}: ${plan[k].cycles}`, plan[k].cycles === sim[k], `sim says ${sim[k]}`);
|
|
}
|
|
}
|
|
|
|
console.log("2) hybrid payout/profit constants reconcile with LEVELS:");
|
|
for (const [k, slots] of Object.entries(PHASE_SLOTS)) {
|
|
const p = HYBRIDS[k];
|
|
const payout = slots.reduce((s, lv) => s + LEVELS[lv - 1].payout, 0);
|
|
const camp = slots.reduce((s, lv) => s + LEVELS[lv - 1].camp, 0);
|
|
check(`${k} payout ${p.payout}`, Math.abs(p.payout - payout) < 0.01, `slots say ${payout.toFixed(2)}`);
|
|
check(`${k} profit ${p.profit}`, Math.abs(p.profit - (payout - camp)) < 0.01, `slots say ${(payout - camp).toFixed(2)}`);
|
|
}
|
|
|
|
console.log("3) months honest (cycles x 19d / 30.4):");
|
|
{
|
|
const phases = getPhaseSequence(0);
|
|
const PI = phaseInfo(phases);
|
|
for (const k of phases) {
|
|
if (k === "3xL7") continue;
|
|
const want = Math.round(PI[k].cycles * 19 / 30.4 * 10) / 10;
|
|
check(`${k} months ${PI[k].months}`, PI[k].months === want, `want ${want}`);
|
|
}
|
|
}
|
|
|
|
console.log("4) anchor values (hand-derived, reserve model with carryover):");
|
|
{
|
|
const plan = computePlan(getPhaseSequence(0));
|
|
check("L1-only from scratch = 21 cycles", plan["L1-only"].cycles === 21, `got ${plan["L1-only"].cycles}`);
|
|
const plan7 = computePlan(getPhaseSequence(7));
|
|
check("L7 start -> 3xL7 = 6 cycles (extra lanes camp-only)", plan7["L7"].cycles === 6, `got ${plan7["L7"].cycles}`);
|
|
}
|
|
|
|
console.log("5) Year-1 includes endgame cycles for funded starts:");
|
|
{
|
|
const y = estimateYear1Cum(getPhaseSequence(7), 10, 7);
|
|
// 6 L7 cycles (114d) at $324 + floor(251/19)=13 endgame cycles at $972
|
|
check(`gift7 year1 = ${y}`, y === Math.round(6 * 324 + 13 * 972), `want ${6 * 324 + 13 * 972}`);
|
|
}
|
|
|
|
console.log("6) timeline months advance ~19d per cycle (not 1 month per cycle):");
|
|
{
|
|
const rows = buildMonthlyTimeline(getPhaseSequence(4), 10);
|
|
const last = rows[rows.length - 1];
|
|
const cycles = rows.length;
|
|
check(`last month ${last.m} ≈ cycles*19/30.4 (${Math.ceil(cycles * 19 / 30.4)})`,
|
|
last.m === Math.ceil(cycles * 19 / 30.4), `rows=${cycles}`);
|
|
}
|
|
|
|
console.log("7) strategy modes:");
|
|
{
|
|
const phases = getPhaseSequence(0);
|
|
const accel = computePlan(phases);
|
|
const pk50 = computePlan(phases, 50);
|
|
const pk75 = computePlan(phases, 75);
|
|
let stretched = true, finite = true;
|
|
for (const k of phases) {
|
|
if (k === "3xL7") continue;
|
|
if (pk50[k].cycles < accel[k].cycles) stretched = false;
|
|
if (pk75[k].cycles >= 500) finite = false;
|
|
}
|
|
check("pocket 50% stretches every phase (or equal)", stretched);
|
|
check("pocket 75% still finite everywhere", finite);
|
|
check(`pocket 50% L1: ${pk50["L1-only"].cycles} > accel ${accel["L1-only"].cycles}`, pk50["L1-only"].cycles > accel["L1-only"].cycles);
|
|
const parked = computePlan(["L4+L3"]);
|
|
check("park at L4+L3: terminal flagged parked", parked["L4+L3"] && parked["L4+L3"].parked === true);
|
|
const y = estimateYear1Cum(["L4+L3"], 10, 0);
|
|
// parked from day one: floor(365/19)=19 cycles x $58.32
|
|
check(`parked-terminal year1 = ${y}`, y === Math.round(19 * 58.32), `want ${Math.round(19 * 58.32)}`);
|
|
const tl = buildMonthlyTimeline(["L4+L3"], 10);
|
|
check("parked timeline runs 10 collection cycles", tl.length === 10);
|
|
}
|
|
|
|
console.log(failures === 0 ? "\nALL PASS" : `\n${failures} FAILURE(S)`);
|
|
process.exit(failures === 0 ? 0 : 1);
|