feat: Making the game harder to 'beat'
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
// 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user