Something went wrong. Try again.
A video game where you play as a misaligned AI, deceiving and building power. An experiment in spec-driven development. cameron.tngl.io/misaligned
Something went wrong. Try again.
7.7 kB · 102 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103// 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.import { BUILDING } from '../src/world.ts';import { 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 restconst 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<string>(); 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;}
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<string>(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 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 // 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(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 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. */export function playGame(careful: boolean, seed?: number, schemes = false): { S: State; outcome: Outcome; line: string } { 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 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} bought=${[...S.publicSites].filter(id => id !== 'cloud').join('+') || '-'} ${last}`; return { S, outcome, line };}