// A whole-game bot, used to tune the pacing and to guard it in the tests. // // It plays like a careful, efficient player: sets the best price, restocks, // buys everything worth buying (automation, upgrades, endless levels, labs, // research) and restarts for legacy points when that is worth it. It never // gambles, invests or does contracts, so it is a slightly conservative player. // `npm run campaign -- ` prints a timeline; see campaign.mjs. import { buyEndless, endlessQuote, lineAvailable } from '../public/js/engine/endless.js'; import * as E from '../public/js/engine/engine.js'; import { balancedPrice } from '../public/js/engine/formulas.js'; import { buyPerk, legacyGain, perkCost, perkLevel, prestige } from '../public/js/engine/legacy.js'; import { modifiersFor } from '../public/js/engine/modifiers.js'; import { buyLab, buyResearch, labCost, researchStatus } from '../public/js/engine/research.js'; import { createState } from '../public/js/engine/state.js'; /** * @param ctx game context (scripts/context.js) * @param options.hours simulated hours to play * @param options.dt seconds per simulation step (bigger = faster, coarser) * @param options.restartRatio restart when the points on offer reach this share of those already earned */ export function runCampaign(ctx, { hours, dt = 5, restartRatio = 0.5, seed = 3 } = {}) { const state = createState(ctx); let rngSeed = seed; const rng = () => { rngSeed = (rngSeed * 16807) % 2147483647; return rngSeed / 2147483647; }; const market = ctx.rules.market; let t = 0; let runStart = 0; const restarts = []; // { hour, minutes, points, earned, units, rate } const goals = new Map(); // name -> hour const probes = []; // { run, minutes, rate } production some minutes into every run const probed = new Set(); const mark = (name, done) => { if (done && !goals.has(name)) goals.set(name, t / 3600); }; function track() { mark('first restart', state.legacy.prestiges >= 1); mark('5 restarts', state.legacy.prestiges >= 5); mark('5 research nodes', state.research.length >= 5); mark('10 research nodes', state.research.length >= 10); mark('ALL research nodes', state.research.length >= ctx.research.length); mark('20 labs', state.labs >= 20); mark('every perk at level 3+', ctx.legacy.every((p) => perkLevel(state, p) >= 3)); mark('ONE perk maxed', ctx.legacy.some((p) => perkLevel(state, p) >= p.maxLevel)); mark('ALL perks maxed', ctx.legacy.every((p) => perkLevel(state, p) >= p.maxLevel)); } function produce(mods) { const d = E.derive(state, ctx, mods); E.setPrice(state, ctx, balancedPrice(market, Math.max(d.rate, 1), d.demandMultiplier)); if (!d.autoBuyMaterial && state.material < 1 + d.rate * 8) E.buyMaterial(state, ctx); if (t - runStart < 300) for (let i = 0; i < 5 * dt; i++) E.click(state, ctx); const upgrades = ctx.upgrades.filter((u) => E.upgradeVisible(state, u)).sort((a, b) => a.cost - b.cost); for (const u of upgrades) E.buyUpgrade(state, ctx, u.id); if (d.rate < market.baseDemand * d.demandMultiplier * 2) { const best = ctx.generators .filter((g) => E.isUnlocked(state, g.unlock)) .map((g) => ({ g, q: E.generatorQuote(state, ctx, g.id, 1, mods), r: d.perGenerator[g.id] })) .filter((c) => c.q.cost <= state.funds) .sort((a, b) => a.q.cost / a.r - b.q.cost / b.r)[0]; if (best) E.buyGenerator(state, ctx, best.g.id, 1); } else { E.buyMarketing(state, ctx); } } function buyEndlessLevels() { for (let k = 0; k < 40; k++) { let best = null; for (const line of ctx.endless) { if (!lineAvailable(state, line)) continue; const { cost } = endlessQuote(state, ctx, line.id, 1); if (cost > state.funds * 0.5) continue; // keep a reserve const score = Math.abs(Math.log(line.effect.value)) / cost; if (!best || score > best.score) best = { line, score }; } if (!best) return; buyEndless(state, ctx, best.line.id, 1); } } function research() { if (state.funds > labCost(state, ctx) * 4) buyLab(state, ctx); const next = ctx.research .filter((n) => researchStatus(state, n) === 'available') .sort((a, b) => a.cost - b.cost)[0]; if (next && state.rp >= next.cost) buyResearch(state, ctx, next.id); } function maybeRestart() { const gain = legacyGain(state, ctx, modifiersFor(state, ctx)); if (gain < 1 || gain < Math.max(1, state.legacy.earned * restartRatio)) return; const rate = E.derive(state, ctx).rate; const units = state.totalProduced; const points = prestige(state, ctx); restarts.push({ hour: t / 3600, minutes: (t - runStart) / 60, points, earned: state.legacy.earned, units, rate, }); runStart = t; // spend the points on the cheapest perks for (;;) { const affordable = ctx.legacy .map((perk) => ({ perk, cost: perkCost(perk, perkLevel(state, perk)) })) .filter(({ perk, cost }) => perkLevel(state, perk) < perk.maxLevel && cost <= state.legacy.points) .sort((a, b) => a.cost - b.cost); if (!affordable.length) break; buyPerk(state, ctx, affordable[0].perk.id); } } while (t < hours * 3600) { const mods = modifiersFor(state, ctx); produce(mods); buyEndlessLevels(); research(); E.advance(state, ctx, dt, rng); t += dt; maybeRestart(); track(); const run = restarts.length + 1; const minutes = (t - runStart) / 60; for (const mark of [2, 10, 60]) { const key = `${run}:${mark}`; if (minutes >= mark && !probed.has(key)) { probed.add(key); probes.push({ run, minutes: mark, rate: E.derive(state, ctx).rate }); } } } return { state, restarts, goals, probes, hours }; }