150 lines
5.8 KiB
JavaScript
150 lines
5.8 KiB
JavaScript
// 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 -- <hours>` 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 };
|
|
}
|