// 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, boostSim, enumerateBoostCombos, boostPlan } = (0, eval)( html.slice(start, end) + ";({LEVELS, HYBRIDS, PHASE_SLOTS, computePlan, phaseInfo, getPhaseSequence, estimateMonths, estimateYear1Cum, buildMonthlyTimeline, boostSim, enumerateBoostCombos, boostPlan})" ); 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("8) Boost Planner (extra-funds combos, Marty 2026-07-31):"); { // 8a. boostSim with $0 extra must agree with computePlan for every start. for (let eff = 0; eff <= 7; eff++) { const phases = getPhaseSequence(eff); const plan = computePlan(phases); const planTotal = phases.reduce((s, k) => s + plan[k].cycles, 0); const sim = boostSim([...PHASE_SLOTS[phases[0]]], 0); check(`eff=${eff}: boostSim baseline ${sim.cycles} == computePlan total ${planTotal}`, sim.cycles === planTotal, `sim path ${sim.path.join(" → ")}`); } // 8b. combo enumeration: costs + slot/activation rules from an L1-only start. const combos = enumerateBoostCombos([1], 300); const byKey = Object.fromEntries(combos.map(c => [c.lanes.join("+"), c.cost])); check("add L3 costs 165 (camp 150 + act 15)", byKey["3"] === 165, `got ${byKey["3"]}`); check("add L3+L1 costs 178 (L1 already activated: camp only)", byKey["3+1"] === 178, `got ${byKey["3+1"]}`); check("add L1+L1 costs 26 (two camp-only lanes)", byKey["1+1"] === 26, `got ${byKey["1+1"]}`); check("L4 (330) not affordable at $300", !("4" in byKey)); check("no combo exceeds 2 added lanes (3 slots total)", combos.every(c => c.lanes.length <= 2)); // 8c. Marty's scenario: extra cash buys a real speedup over banking it. const p300 = boostPlan("L1-only", 300); const best = p300.options[0]; check(`+$300 from L1: best combo [add L${best.lanes.slice(1).join("+L")}] beats banking ` + `(${best.sim.cycles} < ${p300.baseline.cycles} cycles)`, best.sim.cycles < p300.baseline.cycles); // 8d. options are ranked (cycles ascending, cost tiebreak). const sorted = p300.options.every((o, i, a) => i === 0 || a[i - 1].sim.cycles < o.sim.cycles || (a[i - 1].sim.cycles === o.sim.cycles && a[i - 1].cost <= o.cost)); check("options ranked by cycles then cost", sorted); // 8e. community claim probe: report best 1-added-lane vs 2-added-lanes at +$1000. const p1k = boostPlan("L1-only", 1000); const best1 = p1k.options.filter(o => o.lanes.length === 2)[0]; const best2 = p1k.options.filter(o => o.lanes.length === 3)[0]; if (best1 && best2) { console.log(` info: +$1000 from L1 — best 2-lane total [${best1.lanes.join("+")}] ${best1.sim.cycles}cy` + ` vs best 3-lane total [${best2.lanes.join("+")}] ${best2.sim.cycles}cy`); } console.log(` info: +$300 from L1 top 3: ` + p300.options.slice(0, 3) .map(o => `[add ${o.lanes.slice(1).map(l => "L" + l).join("+") || "?"}] $${o.cost} → ${o.sim.cycles}cy/${o.sim.months}mo`) .join(" · ") + ` · bank: ${p300.baseline.cycles}cy/${p300.baseline.months}mo`); } console.log(failures === 0 ? "\nALL PASS" : `\n${failures} FAILURE(S)`); process.exit(failures === 0 ? 0 : 1);