๐ Cumulative Profit Over Time
@@ -1641,6 +1655,146 @@ function phaseInfo(phases, pocketPct = 0) {
return out;
}
+// โโ Boost Planner (Marty 2026-07-31) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+// "I have extra cash right now โ what's the best combination of campaigns to
+// buy?" Slots rule confirmed by Marty: any level mix, up to 3 simultaneous
+// campaigns, no sequential requirement. The planner enumerates every
+// affordable add-on combo for the free slots, runs each through a generic
+// lane simulator that follows the SAME reserve rules as computePlan (bank
+// profits, recycle carried campaigns, roll the full payout on the upgrade
+// cycle, activation once per level), and ranks by time to 3ร L7. The
+// baseline ("just bank it") gets the same cash as reserves, so the
+// comparison is honest: buying lanes has to BEAT banking to win.
+const LEVEL_TO_PHASE = { 1:'L1-only', 2:'L2+L1', 3:'L3+L1', 4:'L4+L3', 5:'L5+L4', 6:'L6', 7:'L7' };
+
+function laneStats(lanes) {
+ let payout = 0, profit = 0, camp = 0;
+ for (const lv of lanes) {
+ const L = LEVELS[lv - 1];
+ payout += L.payout; profit += L.netProfit; camp += L.camp;
+ }
+ return { payout, profit, camp };
+}
+
+// Simulate from an arbitrary lane set + starting reserve to 3รL7.
+// Mirrors computePlan's rules exactly; capped for safety.
+function boostSim(startLanes, bal0, pocketPct = 0) {
+ const keepFrac = 1 - pocketPct / 100;
+ let lanes = [...startLanes].sort((a, b) => b - a);
+ let bal = bal0;
+ let cycles = 0;
+ const activated = new Set(lanes);
+ const path = [lanes.join('+')];
+ let guard = 0;
+ while (!(lanes.length === 3 && lanes.every(l => l === 7)) && guard++ < 40) {
+ const maxLane = Math.max(...lanes);
+ const targetKey = maxLane >= 7 ? '3xL7' : LEVEL_TO_PHASE[maxLane + 1];
+ const targetSlots = targetKey === '3xL7' ? [7, 7, 7] : [...PHASE_SLOTS[targetKey]];
+ // Carried lanes: current campaigns that keep running in the target phase.
+ const remaining = [...targetSlots];
+ const carried = [];
+ for (const lv of lanes) {
+ const j = remaining.indexOf(lv);
+ if (j !== -1) { carried.push(lv); remaining.splice(j, 1); }
+ }
+ let cost = 0;
+ for (const lv of remaining) {
+ const L = LEVELS[lv - 1];
+ cost += L.camp + (activated.has(lv) ? 0 : L.act);
+ activated.add(lv);
+ }
+ const s = laneStats(lanes);
+ const carriedCamp = carried.reduce((sum, lv) => sum + LEVELS[lv - 1].camp, 0);
+ let spin = 0;
+ while (spin++ < 500) {
+ cycles++;
+ if (bal + s.payout - carriedCamp >= cost) { bal += s.payout - carriedCamp - cost; break; }
+ bal += s.profit * keepFrac;
+ }
+ lanes = targetSlots.sort((a, b) => b - a);
+ path.push(targetKey);
+ }
+ cycles += 1; // the first 3รL7 endgame cycle (matches computePlan's 3xL7 entry)
+ return { cycles, months: Math.round(cycles * 19 / 30.4 * 10) / 10, path };
+}
+
+// Every add-on combo (multiset of levels, up to the free slots) affordable
+// within `budget`. Activation charged once per newly-activated level.
+function enumerateBoostCombos(currentLanes, budget) {
+ const freeSlots = 3 - currentLanes.length;
+ const activated = new Set();
+ for (let lv = 1; lv <= Math.max(...currentLanes); lv++) activated.add(lv);
+ const combos = [];
+ const walk = (startLv, picked) => {
+ if (picked.length > 0) {
+ const seen = new Set(activated);
+ let cost = 0;
+ for (const lv of picked) {
+ const L = LEVELS[lv - 1];
+ cost += L.camp + (seen.has(lv) ? 0 : L.act);
+ seen.add(lv);
+ }
+ if (cost <= budget) combos.push({ lanes: [...picked], cost });
+ }
+ if (picked.length >= freeSlots) return;
+ for (let lv = startLv; lv >= 1; lv--) walk(lv, [...picked, lv]);
+ };
+ walk(7, []);
+ return combos;
+}
+
+// Rank every affordable move for `extra` dollars from the current phase.
+function boostPlan(phaseKey, extra, pocketPct = 0) {
+ const currentLanes = [...PHASE_SLOTS[phaseKey]];
+ const baseline = boostSim(currentLanes, extra, pocketPct);
+ const options = enumerateBoostCombos(currentLanes, extra).map(c => {
+ const lanes = [...currentLanes, ...c.lanes];
+ const sim = boostSim(lanes, extra - c.cost, pocketPct);
+ return { ...c, lanes, sim, profitPerCycle: laneStats(lanes).profit };
+ });
+ options.sort((a, b) => a.sim.cycles - b.sim.cycles || a.cost - b.cost);
+ return { baseline, currentLanes, options };
+}
+
+function renderBoost(phaseKey, mode, pocketPct) {
+ const wrap = document.getElementById('boostWrap');
+ if (!wrap) return;
+ const amount = parseInt(document.getElementById('boostFunds')?.value) || 0;
+ if (amount <= 0 || phaseKey === '3xL7') { wrap.style.display = 'none'; return; }
+ wrap.style.display = '';
+ const plan = boostPlan(phaseKey, amount, pocketPct);
+ const cur = plan.currentLanes.map(l => 'L' + l).join(' + ');
+ document.getElementById('boostSubtitle').textContent =
+ `$${amount.toLocaleString()} extra while running ${cur} โ every affordable combo, simulated to 3ร L7`;
+ const money = n => '$' + n.toLocaleString(undefined, { maximumFractionDigits: 0 });
+ const rows = [];
+ const base = plan.baseline;
+ const deltaTxt = sim => {
+ const d = base.cycles - sim.cycles;
+ if (d <= 0) return d === 0 ? 'same speed' : `${-d} cycle${d === -1 ? '' : 's'} slower`;
+ const mo = Math.round(d * 19 / 30.4 * 10) / 10;
+ return `${d} cycle${d === 1 ? '' : 's'} (~${mo} mo) faster`;
+ };
+ rows.push(`
| ๐ฆ Bank it (no new campaigns) | $0 | ` +
+ `${money(laneStats(plan.currentLanes).profit)} | ` +
+ `${base.cycles} cycles ยท ~${base.months} mo | โ |
`);
+ plan.options.slice(0, 5).forEach((o, i) => {
+ const label = 'Add ' + o.lanes.slice(plan.currentLanes.length).map(l => 'L' + l).join(' + ');
+ const best = i === 0 && o.sim.cycles < base.cycles;
+ rows.push(`
` +
+ `| ${best ? 'โญ ' : ''}${label} | ${money(o.cost)} | ` +
+ `${money(o.profitPerCycle)} | ` +
+ `${o.sim.cycles} cycles ยท ~${o.sim.months} mo | ${deltaTxt(o.sim)} |
`);
+ });
+ document.getElementById('boostBody').innerHTML = rows.join('');
+ document.getElementById('boostNote').textContent =
+ 'Rules per the program: up to 3 campaigns at once, any level mix โ no need to climb sequentially. ' +
+ 'Activation is charged once per level; unspent cash goes into reserves either way. ' +
+ (mode === 'park' ? 'Shown on the climb-to-endgame basis; if you park, the best combo is simply the highest profit/cycle you can afford.' :
+ mode === 'pocket' ? `Simulated with your ${pocketPct}% pocket setting.` :
+ 'Simulated in Accelerate mode, same reserve math as the rest of the calculator.');
+}
+
function getLevelCost(upTo) {
if (upTo <= 0) return 0;
return LEVELS.slice(0, upTo).reduce((s, l) => s + l.total, 0);
@@ -1756,6 +1910,7 @@ function updateAll() {
const selfCost = getLevelCost(selfLevel);
const effective = getEffectiveStart(giftLevel, selfLevel);
let phases = getPhaseSequence(effective);
+ renderBoost(phases[0], mode, pocketPct);
// Park & collect: keep the park-level select in sync with the sequence,
// then truncate the journey at the chosen level (terminal = collect there).
const parkSel = document.getElementById('parkPhase');
@@ -2121,7 +2276,7 @@ ${numPeople > 0 ? `
}
// โโโ Event Wiring โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-['numPeople','commissionPct','giftLevel','selfFundLevel','showReferral','alreadyJoined','strategyMode','pocketPct','parkPhase'].forEach(id => {
+['numPeople','commissionPct','giftLevel','selfFundLevel','showReferral','alreadyJoined','strategyMode','pocketPct','parkPhase','boostFunds'].forEach(id => {
const el = document.getElementById(id);
el.addEventListener('input', updateAll);
el.addEventListener('change', updateAll);
@@ -2160,6 +2315,8 @@ document.querySelectorAll('.tab-btn').forEach(btn => {
const park = p.get('park');
if (park) document.getElementById('parkPhase').dataset.want = park;
}
+ const boost = p.get('boost');
+ if (boost && /^\d{1,6}$/.test(boost)) document.getElementById('boostFunds').value = boost;
})();
updateAll();
@@ -2189,6 +2346,9 @@ function getShareUrl(refUser) {
if (modeSel.value === 'pocket') url += '&pk=' + document.getElementById('pocketPct').value;
if (modeSel.value === 'park') url += '&park=' + encodeURIComponent(document.getElementById('parkPhase').value);
}
+ const boostEl = document.getElementById('boostFunds');
+ const boostVal = boostEl ? parseInt(boostEl.value) || 0 : 0;
+ if (boostVal > 0) url += '&boost=' + boostVal;
return url;
}
diff --git a/test_scenarios.js b/test_scenarios.js
index 0ed938b..823fa59 100644
--- a/test_scenarios.js
+++ b/test_scenarios.js
@@ -12,9 +12,10 @@ if (start < 0 || end < 0) { console.error("FATAL: extraction markers missing");
// 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)(
+ estimateMonths, estimateYear1Cum, buildMonthlyTimeline,
+ boostSim, enumerateBoostCombos, boostPlan } = (0, eval)(
html.slice(start, end) +
- ";({LEVELS, HYBRIDS, PHASE_SLOTS, computePlan, phaseInfo, getPhaseSequence, estimateMonths, estimateYear1Cum, buildMonthlyTimeline})"
+ ";({LEVELS, HYBRIDS, PHASE_SLOTS, computePlan, phaseInfo, getPhaseSequence, estimateMonths, estimateYear1Cum, buildMonthlyTimeline, boostSim, enumerateBoostCombos, boostPlan})"
);
let failures = 0;
@@ -134,5 +135,48 @@ console.log("7) strategy modes:");
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);