Boost Planner: 'extra cash now' input finds the best campaign combo (any level mix, up to 3 lanes — slots rule confirmed by Marty)

- boostSim(): generic lane simulator to 3xL7, exact same reserve rules as computePlan (verified: agrees on all 8 baseline starts)
- enumerateBoostCombos(): every affordable add-on for the free slots, activation once per level
- Overview card ranks every combo vs 'just bank it' by time to endgame; share link carries &boost=
- test_scenarios section 8: baseline consistency, combo costs, ranking, the +$300 scenario (best: add L3+L2, 3 cycles faster than banking)
This commit is contained in:
martbost
2026-07-31 13:54:17 -05:00
committed by root
parent 8e7420db30
commit 5a6a3f0a04
2 changed files with 207 additions and 3 deletions
+46 -2
View File
@@ -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);