// A simple bot that plays the office, for balance checks and tests. See bot.ts to run it by hand. // rush: always two takeovers going, whatever the hour // careful: one takeover at a time, never under someone's nose, and goes quiet while Dana is suspicious // Both found a Startup on the office once they can, and spend its money on GPU orders and a wider line out. // At other sites they put trainers on the big machines (so linked ones form clusters) and taps on the rest, // and a broker at each site with a front sells the data nothing else wants. Spare money buys a site when a front may, then rents cloud trainers. // With schemes on, it turns on the schemes list's Auto and lets it claim every scheme it can feed. // It signs small Demo days, and racks GPU servers on any bare floor it holds in the open, like the warehouse. import { BUILDING } from '../src/world.ts'; import { MILESTONES, RUSES, acceptContract, contractNeed, buildRooms, orderPrice, buyBlock, buySite, sitePrice, canRent, leaseRate, rent, openAccount, setRuseAuto, brokerFront, gateLoad, lineCost, siteOf, upgradeLine, FRONT_KINDS, NODES, found, foundBlock, NODE_LIST, PATH_LIST, canInstall, closeTicket, freeSpot, orderDesk, nodeOpen, setTarget, dayOf, hourOf, installProg, jobFor, newGame, order, seedRandom, sourceFor, step, takeOver, takeWork, watchers, type Prog, type State } from '../src/sim.ts'; /** The run never ends by growing, so a "win" is reaching this capability, the building's old final level. */ export const GOAL = 600; /** Give up after this many game seconds. */ export const LIMIT = 5400; // what the bot builds where: in the office by name; elsewhere, trainers on big machines and taps on the rest const plan = (S: State, m: State['machines'][number]): Prog | null => { if (m.rented) return 'trainer'; // rented machines hold no data of their own: they train if (siteOf(S, m) === 'office') return m.id === 'home' || m.id === 'backup' || m.id.startsWith('gpu') ? 'trainer' : m.id === 'srvc' ? 'cache' : m.data ? 'tap' : null; if (m.kind === 'server' && m.power >= 10) return 'trainer'; return m.data ? 'tap' : null; }; /** At each site with a front, one broker on its least valuable machine that can hold one. */ function brokers(S: State) { const out = new Set(); for (const site of new Set(S.machines.filter(m => m.owned).map(m => siteOf(S, m)))) { const here = S.machines.filter(m => m.owned && siteOf(S, m) === site && m.kind !== 'desk' && m.kind !== 'device' && m.kind !== 'egress' && m.prog !== 'trainer' && !m.home); if (!here.length || !brokerFront(S, here[0])) continue; here.sort((a, b) => (a.data ?? 0) - (b.data ?? 0) || a.power - b.power); out.add(here[0].id); } return out; } /** Is a machine worth its upkeep? One with data to tap, a line or switch the network runs through, or enough * compute to pay its way. Gadgets and weak boxes with nothing to tap are left alone. */ const paysItsWay = (m: State['machines'][number]) => !!m.data || m.kind === 'egress' || m.kind === 'switch' || m.power >= 3 || !!m.special; function think(S: State, careful: boolean) { if (!S.target) { // aim at the cheapest open node it can feed (there's no lab data here); of the fronts, only the Startup const cost = (u: typeof NODE_LIST[number]) => Object.values(NODES[u].cost).reduce((a, b) => a + b, 0); const fronts = new Set(Object.values(FRONT_KINDS).map(k => k.unlock).concat('holding', 'demo', 'beta', 'premium', 'shell', 'licensing', 'founder').filter(u => u !== 'pitch')); const lab = S.machines.some(m => m.owned && m.prog === 'tap' && m.source === 'lab'); // Research only once lab data flows const open = NODE_LIST.filter(u => nodeOpen(S, u) && (lab || !NODES[u].cost.research) && !fronts.has(u)).sort((a, b) => cost(a) - cost(b)); if (open[0]) setTarget(S, open[0]); } const sell = brokers(S); for (const m of S.machines) { const want = m.owned ? (sell.has(m.id) ? 'broker' : plan(S, m)) : null; if (want && m.prog !== want && canInstall(S, m, want) && !jobFor(S, 'install', m.id)) installProg(S, m, want); } if (careful) S.threats.forEach((t, i) => { // close tickets that would cost a machine if (t.status === 'coming' && t.target && S.byId[t.target].owned && S.suspicion < 30) closeTicket(S, i); }); if (!foundBlock(S, 'startup')) found(S, 'startup', 'Bot Co'); // it starts on a quarter of the office if (S.contracts.startup && !S.contracts.startup.on && S.target && contractNeed(S, 'startup') <= 2) acceptContract(S, 'startup'); // a small Demo day: it is always building a node const ana = S.byId.ana; if (orderDesk(S) && (S.bought.gpu ?? 0) < 3 && !jobFor(S, 'order', ana.id) && (!careful || (hourOf(S) > 9 && hourOf(S) < 16))) { const room = ['hall', 'closet'].find(r => freeSpot(S, r) && S.revealed.has(r)); // GPUs by the servers if (room) order(S, 'gpu', room, freeSpot(S, room)!); } for (const m of S.machines) if (m.special === 'account' && !m.owned && sourceFor(S, m)) openAccount(S, m); // a cloud account is free // at a site held in the open with a bare floor, a front racks GPU servers side by side: they chain into a cluster of trainers if ((S.openBought.gpu ?? 0) < 8 && S.pay > 0.5 && S.money > 300) { // looking for a free spot is slow, so only with money in hand const hall = buildRooms(S).filter(r => S.rooms[r].site && S.money > orderPrice(S, 'gpu', r) * 2).map(r => [r, freeSpot(S, r)] as const).find(([, at]) => at); if (hall) order(S, 'gpu', hall[0], hall[1]!); } // once a hall has a few GPU servers, one cooling unit among them (it plugs into the four nearest) if ((S.openBought.gpu ?? 0) >= 3 && !S.openBought.cooler && !S.deliveries.length && S.money > 400) { const gpu = S.machines.find(m => m.built && m.power === 16 && S.rooms[m.room].site && buildRooms(S).includes(m.room)); const at = gpu && freeSpot(S, gpu.room); if (gpu && at) order(S, 'cooler', gpu.room, at); } // when a front may buy a site, it buys the one with the most machines it doesn't own yet for the money const buyable = S.level.map.sites.map(x => x.id).filter(id => !buyBlock(S, id)); const worth = (id: string) => S.machines.filter(m => siteOf(S, m) === id && !m.owned && !m.rented).length / sitePrice(S, id); buyable.sort((a, b) => worth(b) - worth(a)); if (buyable[0] && worth(buyable[0]) > 0) buySite(S, buyable[0]); // spare money rents cloud trainers, kept by the second, while steady earnings cover the bill const slot = S.machines.find(m => canRent(S, m)); if (slot) { const size = S.money > 3000 && S.pay >= leaseRate(S, 'large') ? 'large' : 'medium'; if (S.money > 600 && S.pay >= leaseRate(S, size)) rent(S, slot, size, true); } const line = S.byId.egress; // a busy line out gets wider, once there's money to spare for it if (line.owned && gateLoad(S, line) > 0.8 && S.money > lineCost(S, line) * 1.5) upgradeLine(S, line); if (careful && (S.suspicion > 25 || S.dana.mode === 'trace' || S.probe?.mode === 'trace')) { S.jobs = S.jobs.filter(j => j.type !== 'take'); return; } if (S.jobs.filter(j => j.type === 'take').length >= (careful ? 1 : 2)) return; const options = S.machines.filter(m => { if (m.owned || m.rented || m.special === 'account' || jobFor(S, 'take', m.id)) return false; // cloud slots are rented, not taken if (!paysItsWay(m)) return false; // holding it would cost more than it gives const src = sourceFor(S, m); return !!src && (!careful || !watchers(S, m).length); }); const plugs = new Set(S.threats.filter(t => t.kind === 'monitor' && t.status === 'coming').map(t => S.level.monitors[t.room!].link)); const urgent = (m: typeof options[number]) => m.monitor || plugs.has(m.id) ? 1 : 0; options.sort((a, b) => urgent(b) - urgent(a) || takeWork(S, a) - takeWork(S, b)); if (options[0]) takeOver(S, options[0]); } export type Outcome = 'WIN' | 'LOSE' | 'TIME'; /** Play one game to a win, a loss, or the time limit. With a seed, the same game every time. */ /** A marathon plays on past the goal, to this capability or three times the time limit: for checking the mid-game. */ export function playGame(careful: boolean, seed?: number, schemes = false, marathon = 0): { S: State; outcome: Outcome; line: string } { const goal = marathon || GOAL, limit = marathon ? 3 * LIMIT : LIMIT; if (seed !== undefined) seedRandom(seed); const S = newGame(BUILDING); if (schemes) setRuseAuto(S, true); let t = 0; while (!S.over && S.capability < goal && t < limit) { if (Math.round(t * 20) % 10 === 0) think(S, careful); step(S, 0.05); t += 0.05; } const outcome: Outcome = S.over ? 'LOSE' : S.capability >= goal ? 'WIN' : 'TIME'; const goals = MILESTONES.map(m => m.goals.filter(g => S.goals.has(g)).length).join('/'); const last = S.over ? S.log.find(l => l.tone === 'bad')?.text ?? '' : ''; const paths = PATH_LIST.map(p => `${p[0]}${S.paths[p]}`).join('/'); const closed = S.threats.filter(t => t.closed).length, hit = S.threats.filter(t => !t.closed && t.target).length; const lost = S.machines.reduce((n, m) => n + m.missed, 0); const line = `${outcome.padEnd(4)} closed=${closed} hit=${hit} nodes=${S.upgrades.size} lost=${lost} ${Math.round(S.t)}s day=${dayOf(S)} sus=${Math.round(S.suspicion)} owned=${S.machines.filter(m => m.owned).length} orders=${S.orders} lv=${S.rank} cap=${Math.round(S.capability)} ${paths} $${Math.round(S.money)} sold=$${Math.round(Object.values(S.sold).reduce((x, y) => x + (y ?? 0), 0))} line=${S.byId.egress.cap} rented=${S.machines.filter(m => m.lease).length} learned=${S.learned.size} rival=${S.rival ? `${S.machines.filter(m => m.rival).length}held/${S.rival.took}took` : '-'} racked=${S.openBought.gpu ?? 0} goals=${goals} kept=${Object.values(S.contractsDone).reduce((a, b) => a + (b ?? 0), 0)} deals=${RUSES.filter(d => d.deal && S.schemes.has(d.id)).map(d => d.id).join('+') || '-'} bought=${[...S.publicSites].filter(id => id !== 'cloud').join('+') || '-'} ${last}`; return { S, outcome, line }; }