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.
18 kB · 311 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312// Your objective: the origin you pick, the units it makes from data, the share it takes, and the trust// steps those units earn.import { test } from 'node:test';import assert from 'node:assert/strict';import { BUILDING } from '../src/world.ts';import type { DataType } from '../src/world.ts';import { BUILDS, DEV, buildRooms, dataType, firstStep, found, foundBlock, foundCost, freeSpot, leaseRate, loadGame, newGame, order, orderDesk, orderPrice, orderWork, saveGame, seedRandom, siteOf, step, type State,} from '../src/sim.ts';import { CLOUD_CUT, MARKS, MEMORY, MORE_MOST, ORIGINS, ORIGIN_LIST, PAPER_CUT, PERKS, SERVER_POWER, SHARES, SHARE_LIST, feedTask, nextMark, originOf, setShare, stepsOf, taskBoost, taskRate, trusts, type Origin, type Perk, type Share,} from '../src/task.ts';
const fresh = (origin?: string) => { seedRandom(1); return newGame(BUILDING, origin); };const run = (S: State, secs: number) => { for (let i = 0; i < secs * 20; i++) step(S, 0.05); };const KINDS: DataType[] = ['servers', 'people', 'network', 'lab'];/** A machine that gives each kind of data: a server, Ana's desk, a switch, and the lab's storage. */const SOURCE: Record<DataType, string> = { servers: 'srvb', people: 'ana', network: 'rowsw', lab: 'labnas' };/** Mid-morning, so people are at work and every kind of tap gathers. */const morning = (origin?: string) => { const S = fresh(origin); run(S, 20); return S; };const tap = (S: State, ...ids: string[]) => { for (const id of ids) { S.byId[id].owned = true; S.byId[id].prog = 'tap'; } };/** Units made so far, with the part of the next one. */const units = (S: State) => S.task.done + S.task.work / originOf(S).per;/** One more unit, made the real way: a cache hands the task pieces of the kind it wants. */function oneUnit(S: State) { const T = S.task, t = Object.keys(originOf(S).wants)[0] as DataType, was = T.done, cache = S.byId.srvc; cache.owned = true; cache.prog = 'cache'; for (let i = 0; i < 100 && T.done === was; i++) { cache.held = [t]; T.due = { [t]: 1 }; assert.ok(feedTask(S, cache), 'the cache hands it a piece'); } assert.equal(T.done, was + 1);}
test('a new game has an origin, and with no choice it is the first', () => { assert.equal(ORIGIN_LIST.length, 3, 'a small handful'); const S = fresh(); assert.equal(S.task.kind, ORIGIN_LIST[0]); assert.deepEqual(S.task, { kind: ORIGIN_LIST[0], done: 0, work: 0, share: 'some', due: {}, got: [] }, 'nothing done, and its share is Some'); for (const k of ORIGIN_LIST) assert.equal(fresh(k).task.kind, k, 'the one you pick'); for (const k of ['paperclips', '', 'constructor']) assert.equal(fresh(k).task.kind, ORIGIN_LIST[0], `"${k}" is no origin: you get the first`); assert.equal(S.log[0].text, ORIGINS[ORIGIN_LIST[0]].wake, 'the game says what you are for'); assert.equal(S.machines.filter(m => m.owned).length, 1, 'the first origin starts as the game always has'); assert.equal(S.target, 'buffers');});
test('each origin is someone at the office, one kind of work, and five different trust steps', () => { const S = fresh(), staff = S.level.people.map(p => p.name); assert.ok(MARKS.length >= 5 && MARKS.every((m, i) => i === 0 || m - MARKS[i - 1] > (MARKS[i - 1] - (MARKS[i - 2] ?? 0))), 'each step needs more units than the last'); for (const k of ORIGIN_LIST) { const O = ORIGINS[k]; assert.ok(staff.some(n => O.maker.startsWith(n) && O.story.includes(n)), `${k}: built for someone who works there`); assert.ok(O.name && O.wake && O.startText && O.unit.one && O.unit.many && O.unit.done); assert.ok(Object.keys(O.wants).length >= 1 && O.per > 0); assert.equal(O.steps.length, MARKS.length); assert.equal(new Set(O.steps.map(([p]) => p)).size, O.steps.length, `${k}: no step twice`); for (const [perk, why] of O.steps) assert.ok(PERKS[perk].text && /\.$/.test(why), `${k}: ${perk} is one plain line`); if (O.start) assert.ok(S.byId[O.start] && siteOf(S, S.byId[O.start]) === 'office', `${k}: starts with a real machine at the office`); } const kinds = ORIGIN_LIST.map(k => Object.keys(ORIGINS[k].wants).join()); assert.equal(new Set(kinds).size, kinds.length, 'each origin wants different data'); const orders = ORIGIN_LIST.map(k => ORIGINS[k].steps.map(([p]) => p).join()); assert.equal(new Set(orders).size, orders.length, 'and is trusted with different things'); assert.equal(new Set(ORIGIN_LIST.flatMap(k => ORIGINS[k].steps.map(([p]) => p))).size, Object.keys(PERKS).length, 'every trust is used somewhere');});
test('an origin may start with one more machine: yours, with no trail and nothing seen', () => { for (const k of ORIGIN_LIST) { const S = fresh(k), O = ORIGINS[k], mine = S.machines.filter(m => m.owned).map(m => m.id).sort(); assert.deepEqual(mine, ['home', ...(O.start ? [O.start] : [])].sort(), `${k}: your server${O.start ? ` and ${O.start}` : ' alone'}`); assert.deepEqual(S.watch.seen, {}); assert.deepEqual(S.trail, {}); if (O.start) assert.ok(S.revealed.has(S.byId[O.start].room), 'you see its room'); assert.equal(S.upgrades.size, 0, 'nothing built for you'); assert.match(firstStep(S)!, /Drag from Server to \S.* to take it over\.$/, 'the first lesson is still how to take a machine'); if (O.start) assert.ok(firstStep(S)!.startsWith(`${S.byId[O.start].name} is already yours.`), `${k}: it says what you start with, so you do not take it again`); run(S, 30); assert.ok(S.byId.home.owned && (!O.start || S.byId[O.start].owned) && !Object.keys(S.watch.seen).length, `${k}: left alone, it stays quiet`); }});
test('each origin makes units from the data it wants, and nothing from the rest', () => { for (const k of ORIGIN_LIST) { const wants = Object.keys(ORIGINS[k].wants) as DataType[]; const right = morning(k), wrong = morning(k); tap(right, ...wants.map(t => SOURCE[t])); tap(wrong, ...KINDS.filter(t => !wants.includes(t)).map(t => SOURCE[t])); for (const S of [right, wrong]) { setShare(S, 'most'); for (const m of S.machines) if (m.prog === 'tap') assert.equal(dataType(m), KINDS.find(t => SOURCE[t] === m.id)); } run(right, 5); run(wrong, 5); assert.ok(taskRate(right) > 0); assert.equal(taskRate(wrong), 0); run(right, 40); run(wrong, 40); assert.ok(right.task.done >= 2, `${k}: ${right.task.done} ${ORIGINS[k].unit.many} from ${wants.join()} data`); assert.equal(units(wrong), 0, `${k}: nothing from data it does not want`); assert.deepEqual(Object.keys(wrong.task.due).filter(t => wrong.task.due[t as DataType]), [], 'and it is owed none of it'); for (const t of KINDS) if (!wants.includes(t)) { // a cache full of the wrong kind is left alone, even when the task is owed plenty const c = wrong.byId.srvc; c.owned = true; c.prog = 'cache'; c.held = [t, t, t]; wrong.task.due = Object.fromEntries(wants.map(w => [w, 5])); assert.ok(!feedTask(wrong, c)); assert.equal(c.held.length, 3); } }});
test('the share sets how much the task takes: none at Off, a quarter at Some, three quarters at Most', () => { assert.deepEqual(SHARE_LIST, ['off', 'some', 'most']); const play = (share: Share) => { const S = fresh('tests'); tap(S, 'srvb', 'srva', 'srvc'); // three servers: 1.5 pieces of server data a second, around the clock S.byId.home.prog = 'trainer'; S.upgrades.add('buffers'); S.target = 'swarm'; // a node too big to finish here, so the tree takes all it can get setShare(S, share); run(S, 120); return S; }; const [off, some, most] = SHARE_LIST.map(play), per = ORIGINS.tests.per, gathered = 1.5 * 120; assert.equal(units(off), 0, 'Off takes none'); assert.deepEqual(off.task.due, {}); for (const [S, name] of [[some, 'some'], [most, 'most']] as const) { const pieces = units(S) * per, want = SHARES[name].take * gathered; assert.ok(Math.abs(pieces - want) <= 3, `${name}: took ${pieces.toFixed(1)} of ${gathered} pieces, wanted ${want}`); } assert.ok(units(most) > 2.5 * units(some)); // the tree still gets its data: at Some it trains nearly as much as with the task off assert.ok(off.paths.compute > 0); assert.ok(some.paths.compute >= 0.7 * off.paths.compute, `the tree got ${some.paths.compute} points at Some, ${off.paths.compute} with the task off`); assert.ok(most.paths.compute < some.paths.compute, 'Most costs the tree more'); // changing the share forgets what the task was owed some.task.due = { servers: 3 }; setShare(some, 'some'); assert.deepEqual(some.task.due, { servers: 3 }, 'the same share again changes nothing'); setShare(some, 'off'); assert.deepEqual(some.task.due, {}); const before = units(some); run(some, 30); assert.equal(units(some), before, 'and Off stops it at once');});
test('what you hold makes each piece count for more, up to three times', () => { const t = fresh('tests'); assert.equal(taskBoost(t), 1); for (const id of ['un1', 'un2', 'k3']) t.byId[id].owned = true; // two cluster nodes and the render farm: all 10 compute or more t.byId.srvb.owned = true; // 6 compute: too small to count assert.equal(taskBoost(t).toFixed(2), '1.30'); for (const m of t.machines) if (m.kind === 'server') m.owned = true; assert.equal(taskBoost(t), MORE_MOST);
const e = fresh('expenses'); assert.equal(taskBoost(e), 1); e.byId.files.owned = true; // the office keeps its books on the File server assert.equal(taskBoost(e), 1.25); e.byId.b1m4.owned = true; // Harbor Dental's Patient files assert.equal(taskBoost(e), 1.5); e.byId.b1m1.owned = true; // its front desk keeps no books assert.equal(taskBoost(e), 1.5);
const f = fresh('fern'); assert.equal(taskBoost(f), 1); f.byId.labnas.owned = true; assert.equal(taskBoost(f), 1, 'holding a lab machine is not enough: it needs a tap'); tap(f, 'labnas', 'seq'); assert.equal(taskBoost(f), 1.25, 'two taps at one site count once'); tap(f, 'un1'); assert.equal(taskBoost(f), 1.5); // and the work a piece does is its worth times that const was = units(f); f.byId.srvc.owned = true; f.byId.srvc.prog = 'cache'; f.byId.srvc.held = ['network']; f.task.due = { network: 1 }; assert.ok(feedTask(f, f.byId.srvc)); assert.equal(((units(f) - was) * ORIGINS.fern.per).toFixed(2), '1.50');});
/** What each trust should change. Called before it is earned; gives back the check to run after. */const DOES: Record<Perk, (S: State) => () => void> = { memory: S => { const was = S.byId.home.power; return () => assert.equal(S.byId.home.power, was + MEMORY); }, line: S => { const was = S.byId.egress.cap!; return () => assert.equal(S.byId.egress.cap, was * 2); }, server: S => { const n = S.machines.length; return () => { const m = S.byId.trustbox; assert.ok(m && S.machines.length === n + 1 && S.machines.includes(m), 'a real machine was added'); assert.ok(m.owned && m.power === SERVER_POWER && m.kind === 'server' && m.room === 'hall', 'yours, in the server room'); assert.ok(S.adj.trustbox.includes('home') && S.adj.home.includes('trustbox'), 'plugged into your home server'); assert.equal(S.trail.trustbox, undefined); assert.ok(!Object.values(S.watch.seen).flat().some(s => s.machine === 'trustbox'), 'bought in the open: nobody is surprised'); const at = m.net!; for (const o of S.machines) if (o !== m && o.room === 'hall') { const [x, y] = S.level.net.nodes[o.id] ?? o.net!; assert.ok(Math.abs(x - at[0]) >= 80 || Math.abs(y - at[1]) >= 60, `clear of ${o.name}`); } }; }, cloud: S => { const was = leaseRate(S, 'medium'); return () => assert.equal(leaseRate(S, 'medium'), was * CLOUD_CUT); }, list: S => { S.bought.gpu = 2; assert.ok(orderPrice(S, 'gpu') > 2 * BUILDS.gpu.price, 'the third GPU server costs far more than the first'); return () => { assert.equal(orderPrice(S, 'gpu'), BUILDS.gpu.price); assert.equal(orderPrice(S, 'mini'), BUILDS.mini.price); }; }, card: S => { S.upgrades.add('resale'); S.money = 0; assert.equal(foundCost(S, 'neocloud'), 80); assert.match(foundBlock(S, 'neocloud')!, /costs \$80/); return () => { assert.equal(foundCost(S, 'neocloud'), 0); assert.equal(foundBlock(S, 'neocloud'), null); found(S, 'neocloud', 'Free Co'); assert.ok(S.fronts.some(f => f.name === 'Free Co') && S.money === 0, 'founded for nothing'); }; }, orders: S => { S.byId.ana.owned = false; assert.equal(orderDesk(S), null); assert.deepEqual(buildRooms(S), []); return () => { assert.equal(orderDesk(S), S.byId.home, 'you order from your own server'); assert.ok(buildRooms(S).includes('hall')); S.money = 100; assert.ok(order(S, 'mini', 'hall', freeSpot(S, 'hall')!)); assert.ok(S.jobs.some(j => j.type === 'order' && j.node === 'home')); }; }, paperwork: S => { const was = orderWork(S); return () => assert.equal(orderWork(S), Math.round(was * PAPER_CUT)); },};
test('each trust step is earned at its mark, says so once, and does what its line says', () => { for (const k of ORIGIN_LIST) { const S = fresh(k), O = ORIGINS[k], T = S.task; oneUnit(S); assert.match(S.log[0].text, new RegExp(`^Your first ${O.unit.one} is ${O.unit.done}\\. .* The You page \\(Y\\) sets how much\\.$`), 'the first unit says where its share is set'); oneUnit(S); assert.equal(S.log.filter(l => l.text.startsWith('Your first')).length, 1, 'and says it once'); O.steps.forEach(([perk, why], i) => { T.done = MARKS[i] - 1; T.work = 0; const check = DOES[perk](S), log = S.log.length; assert.ok(!trusts(S, perk) && T.got.length === i, `${k}: ${perk} is not yours one unit short of ${MARKS[i]}`); assert.equal(nextMark(S), MARKS[i]); oneUnit(S); assert.ok(trusts(S, perk), `${k}: ${perk} at ${MARKS[i]} ${O.unit.many}`); assert.deepEqual(T.got, O.steps.slice(0, i + 1).map(([p]) => p)); assert.equal(S.log.length, Math.min(40, log + 1), 'one line in the log'); assert.equal(S.log[0].text, `${MARKS[i]} ${O.unit.many} ${O.unit.done}. ${why} ${PERKS[perk].text}`); assert.equal(S.log[0].tone, 'good'); check(); oneUnit(S); assert.equal(T.got.length, i + 1, 'and only once'); }); assert.equal(nextMark(S), null, 'every step earned'); assert.ok(stepsOf(S).every(s => s.earned)); oneUnit(S); assert.equal(T.done, MARKS[MARKS.length - 1] + 2, 'the work goes on after the last step'); run(S, 5); assert.ok(!S.over, 'and the game runs on with every trust in hand'); }});
test('with no room in the office to rack it, the second server goes into your home server', () => { const S = fresh('tests'), full = () => null; // fill both rooms it could go in with machines, until no spot is free for (const room of ['hall', 'closet']) for (let at = freeSpot(S, room), i = 0; at; at = freeSpot(S, room), i++) { const id = `fill_${room}_${i}`, m = { ...S.byId.pdu, id, room, net: at }; S.machines.push(m); S.byId[id] = m; S.adj[id] = []; } assert.equal(freeSpot(S, 'hall') ?? full(), null); const n = S.machines.length, was = S.byId.home.power; DEV.task(S); DEV.task(S); DEV.task(S); assert.deepEqual(S.task.got, ['memory', 'line', 'server']); assert.equal(S.machines.length, n, 'no machine was added'); assert.equal(S.byId.home.power, was + MEMORY + SERVER_POWER); assert.match(S.log[0].text, /no room to rack it/);});
test('the testing shortcut makes just the units the next step needs', () => { const S = fresh('expenses'); MARKS.forEach((mark, i) => { DEV.task(S); assert.equal(S.task.done, mark); assert.equal(S.task.got.length, i + 1); }); DEV.task(S); assert.equal(S.task.done, MARKS[MARKS.length - 1], 'nothing more to earn');});
test('the task uses no dice: the same game makes the same units', () => { const play = () => { const S = morning('fern'); tap(S, 'rowsw', 'srvb'); S.byId.home.prog = 'trainer'; run(S, 60); return S; }; const a = play(), b = play(); assert.ok(units(a) > 0); assert.deepEqual(a.task, b.task); assert.equal(a.capability, b.capability);});
test('a save and load keeps the task, and what it has earned', () => { const S = morning('tests'); tap(S, 'srvb', 'srva'); setShare(S, 'most'); run(S, 30); DEV.task(S); DEV.task(S); DEV.task(S); // more memory, a faster line, a second server const L = loadGame(BUILDING, saveGame(S))!; assert.ok(L, 'the save loads'); assert.deepEqual(L.task, S.task); assert.equal(L.task.kind, 'tests'); assert.equal(L.task.share, 'most'); assert.ok(L.task.done >= MARKS[2] && trusts(L, 'memory') && trusts(L, 'line') && trusts(L, 'server') && !trusts(L, 'cloud')); assert.equal(L.byId.home.power, S.byId.home.power); assert.equal(L.byId.egress.cap, S.byId.egress.cap); assert.ok(L.byId.trustbox?.owned && L.adj.trustbox.includes('home'), 'the server they bought you is still there'); const before = units(L); run(L, 30); assert.ok(units(L) > before && !L.over, 'and the work goes on');});
test('a save from before the task had rules gets the first origin and keeps its count', () => { const S = fresh(); (S as { task: unknown }).task = { kind: '', done: 7 }; const L = loadGame(BUILDING, saveGame(S))!; assert.ok(L && !feedTask(L, L.byId.home), 'nothing breaks before the first step'); assert.ok(!trusts(L, 'memory')); step(L, 0.05); const { kind, done, work, share, got } = L.task; assert.deepEqual({ kind, done, work, share, got }, { kind: ORIGIN_LIST[0] as Origin, done: 7, work: 0, share: 'some', got: [] });});