From 8cbf5c46985a6740e13066a80f0ee459d6200dbc Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 28 Sep 2026 23:15:40 -0700 Subject: [PATCH] Auto research picks the node you can finish soonest with the data you have It estimates each open node's time from the data already stored plus what your taps gather a second, skips nodes nothing feeds, and picks the quickest. With nothing fed yet, it falls back to the fewest points left. --- DESIGN.md | 5 +++-- src/sim.ts | 25 ++++++++++++++++++++++--- src/ui.ts | 2 +- tests/sim.test.ts | 7 ++++++- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 1bb43f48..7ba5df93 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -81,8 +81,9 @@ rewiring taps, caches, and cache switches to feed it. Data the target doesn't need waits in caches or is lost at the tap. Each path has useful standalone nodes, with stronger combinations later. Compute can add slots to Tap buffers; Research can later compress data so those slots hold more. The first target, -Bigger buffers, is set at the start. The tree's **Auto** button picks the open node with the fewest -points left, and picks again each time one is built. Each path ends in one big node that +Bigger buffers, is set at the start. The tree's **Auto** button picks the open node you can finish +soonest with the data you have (what's stored plus what your taps gather), +skipping nodes nothing feeds, and picks again each time one is built. Each path ends in one big node that changes how you play: Swarm (every job runs twice as fast), Ghost (trails fade four times as fast), Familiar face (people stop noticing their own machines), and Recursive self-improvement (each node built speeds up training). Research diff --git a/src/sim.ts b/src/sim.ts index d612e5df..d188f39b 100644 --- a/src/sim.ts +++ b/src/sim.ts @@ -299,7 +299,7 @@ export interface State { hunting: boolean; upgrades: Set; // tree nodes you've built target: Upgrade | null; // the node you're working on. None: trainers stop and data waits. - auto: boolean; // auto-research: with no target, pick the cheapest open node + auto: boolean; // auto-research: with no target, pick the open node you can finish soonest modelVersion: number; // the first large training run makes v2 versionRun: VersionRun | null; progress: Partial>>>; // kept if you switch away @@ -1502,9 +1502,28 @@ export function setTarget(S: State, u: Upgrade | null) { /** Points a node still needs, in all paths. */ export const costLeft = (S: State, u: Upgrade) => PATH_LIST.reduce((n, p) => n + Math.max(0, (NODES[u].cost[p] ?? 0) - (S.progress[u]?.[p] ?? 0)), 0); -/** Auto-research: target the open node with the fewest points left. */ +/** Seconds to finish a node with the data you have: what's stored now, plus what your taps gather. + * Infinite when some path it needs has no data and no tap feeding it. */ +export function timeLeft(S: State, u: Upgrade) { + let worst = 0; + for (const p of PATH_LIST) { + const left = (NODES[u].cost[p] ?? 0) - (S.progress[u]?.[p] ?? 0); + if (left <= 0) continue; + const kind = PATHS[p].from; + const stored = S.machines.reduce((n, m) => n + (m.owned ? m.held.filter(t => t === kind).length : 0), 0); + const rate = S.machines.reduce((r, m) => r + (m.owned && m.prog === 'tap' && dataType(m) === kind ? tapRate(S, m) : 0), 0); + const need = left - stored; + if (need <= 0) continue; + if (rate <= 0) return Infinity; + worst = Math.max(worst, need / rate); // paths fill side by side: the slowest one decides + } + return worst; +} +/** Auto-research: the open node you can finish soonest with the data you have. If none can be fed, the one with the fewest points left. */ export function autoTarget(S: State) { - const best = NODE_LIST.filter(u => nodeOpen(S, u)).sort((a, b) => costLeft(S, a) - costLeft(S, b))[0]; + const open = NODE_LIST.filter(u => nodeOpen(S, u)); + const fed = open.filter(u => timeLeft(S, u) < Infinity).sort((a, b) => timeLeft(S, a) - timeLeft(S, b) || costLeft(S, a) - costLeft(S, b)); + const best = fed[0] ?? open.sort((a, b) => costLeft(S, a) - costLeft(S, b))[0]; if (best) setTarget(S, best); } export function setAuto(S: State, on: boolean) { diff --git a/src/ui.ts b/src/ui.ts index 0c7b7fd2..0171ee2c 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -765,7 +765,7 @@ export function renderTree(S: State) { return `
${PATHS[p].name} ${S.paths[p]} pointsfrom ${TYPE_NAME[t]} data · ${d.held} held · ${d.rate.toFixed(1)}/s${d.lost ? ` · ${d.lost} lost` : ''}
`; }; - const acts = `
+ const acts = `
`; if (phone.matches) { // one path's nodes in a list, first to last; a node that needs two paths shows under both diff --git a/tests/sim.test.ts b/tests/sim.test.ts index c156b5db..52a4b9ea 100644 --- a/tests/sim.test.ts +++ b/tests/sim.test.ts @@ -438,7 +438,7 @@ test('taking a whole site claims it all, and takes a couple at a time', () => { assert.ok(n > 0 && n <= CLAIM_AT_ONCE, `${n} takeovers at once`); }); -test('auto-research targets the open node with the fewest points left, and again after each build', () => { +test('auto-research targets the node it can finish soonest (the cheapest if nothing feeds one), and again after each build', () => { const S = fresh(); setTarget(S, null); setAuto(S, true); @@ -450,6 +450,11 @@ test('auto-research targets the open node with the fewest points left, and again setAuto(S, false); setTarget(S, null); step(S, 0.05); assert.equal(S.target, null, 'off means off'); + // with a tap feeding one path, it picks what that data can finish, over a cheaper node nothing feeds + const T = fresh(); + T.byId.srvb.owned = true; T.byId.srvb.prog = 'tap'; // server data: Compute only + setTarget(T, null); setAuto(T, true); + assert.ok(T.target && Object.keys(NODES[T.target].cost).every(p => p === 'compute'), `picked ${T.target}, which server data can finish`); }); test('letting go of a machine on the trail breaks a trace', () => { -- 2.51.2