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.
71 kB · 1272 lines
TypeScript
at main
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273// The rules: games run clean, seeds repeat, and each capability node does what its card says.import { test } from 'node:test';import assert from 'node:assert/strict';import { BUILDING } from '../src/world.ts';import { staleRate, PROGRAMS, BUILDS, CACHE_PER, CACHE_STORE, CACHE_MIN, setAllow, setOnly, leaseGrace, leanSize, LEAN_SIZE, contractPay as payFor, stanceOf, truceOn, gift, truce, lean, answer, giftBlock, truceBlock, leanBlock, cached, rogueWants, GIFT_DATA, GIFT_OPINION, TRUCE_DATA, OPINION_START, deadWeight, dropDeadWeight, firstStep, rogueTargets, rogueHeld, rogueCap, rogueEvery, ROGUE_BASE, ROGUE_PER_LEVEL, isSealed, ASCENSIONS, ascend, ascendBlock, nodeOpen as nodeIsOpen, nodeShown, tradeRate, attentionSafe, leaseRate, takeWork, HOUR, START_HOUR, setPlan, takeSite, installProg, CLAIM_AT_ONCE, salePrice, frontFits, siteOf, payRate, tapRate, FRONT_PAY, buyBlock, buySite, letGo, sitePrice, clusters, clusterBoost, claim, order, orderPrice, freeSpot, plugsAt, spotProblem, cancelJob, AUCTION, FRONT_PAY_OPEN, UNICORN_AT, ATTENTION_SAFE, FIREWALL, blamePrinter, canInstall, rent, NODES, NODE_LIST, found, foundBlock, setSiteShare, spareOf, cacheSize, needs, newGame, seedRandom, share, step, tapBuffer, takeOver, watchers, runRuse, setRuseAuto, LEARN_EVERY, RESET_KNOWN, LOCKDOWN, RUSES, VERSION_2, versionBlock, startVersionRun, pauseVersionRun, setTarget, setAuto, setAutoPath, costLeft, nodeOpen, maxSpeed, setTakeAll, takeAllCost, TAKE_ALL_COST, upgradeSiteLine, siteFlow, lineCost, dropRuse, DEAL_BACK, saveGame, loadGame, DEV, rentOf, LEASE_GRACE, upkeep, upkeepOf, UPKEEP, UPKEEP_TINY, siteHops, spare, moveBlock, moveNeed, MOVE_NEED, startMove, buildRooms, cooled, closeFront, newsOn, capNow, type NewsKind, localHour, regionOpen, milestone, MILESTONES, GOALS, CONTRACTS, contractPay, contractNeed, acceptContract, declineContract, type State, type Upgrade, nodeCost, reps, runNeed, reprogram, POWER_RATE, POWER_BACK } from '../src/sim.ts';import { playGame } from '../scripts/play.ts';import { takeLead } from '../src/cloud.ts';
const run = (S: State, secs: number) => { for (let i = 0; i < secs * 20; i++) step(S, 0.05); };const fresh = (...ups: Upgrade[]) => { seedRandom(1); const S = newGame(BUILDING); ups.forEach(u => S.upgrades.add(u)); return S; };const sane = (S: State) => { for (const x of [S.suspicion, S.capability, S.money, S.t]) assert.ok(Number.isFinite(x), 'a number went bad'); for (const m of S.machines) assert.ok(Number.isFinite(m.odd) && Number.isFinite(m.acc), `${m.id} went bad`);};
test('the tree: every node names real nodes to come after, and none loops back on itself', () => { for (const u of NODE_LIST) { const free = Object.values(ASCENSIONS).some(A => A.first === u); // an ascension's first node comes free assert.ok(NODES[u].desc && (free || Object.keys(NODES[u].cost).length), `${u} needs a description and a cost`); const seen = new Set<Upgrade>(), walk = (v: Upgrade) => { assert.ok(NODES[v], `unknown node ${v}`); assert.ok(!seen.has(v) || v !== u, `${u} loops`); if (seen.has(v)) return; seen.add(v); (NODES[v].after ?? []).forEach(walk); }; (NODES[u].after ?? []).forEach(walk); assert.ok(!seen.has(u), `${u} comes after itself`); }});
test('a seeded game plays out the same way twice', () => { const a = playGame(false, 42), b = playGame(false, 42); assert.equal(a.line, b.line);});
test('whole games run without errors or broken numbers, rushed and careful', () => { for (const careful of [false, true]) { const { S } = playGame(careful, 3); sane(S); }});
test('Bigger buffers doubles tap buffers; Compression doubles caches', () => { assert.equal(tapBuffer(fresh('buffers')), 2 * tapBuffer(fresh())); const C = fresh('compress'), S = fresh(); assert.equal(cacheSize(C, C.byId.home), 2 * cacheSize(S, S.byId.home));});
test('a cache is as big as its machine, and a machine built to store holds the most', () => { const S = fresh(); assert.equal(cacheSize(S, S.byId.home), S.byId.home.power * CACHE_PER); assert.equal(cacheSize(S, S.byId.files), S.byId.files.power * CACHE_STORE, 'a file server holds far more'); assert.ok(cacheSize(S, S.byId.srvc) > cacheSize(S, S.byId.home), 'a bigger server holds more'); assert.equal(cacheSize(S, S.byId.rowsw), CACHE_MIN, 'a switch holds the least'); assert.ok(cacheSize(S, BUILDS.store) >= 2 * cacheSize(S, BUILDS.gpu), 'the storage array you order beats a GPU server'); assert.ok(!PROGRAMS.trainer.on({ ...S.byId.home, ...BUILDS.store }), 'and it is too small to train on'); S.byId.rowsw.owned = S.byId.srvb.owned = true; S.byId.srvb.prog = 'tap'; S.byId.rowsw.prog = 'cache'; setTarget(S, null); run(S, 600); assert.equal(S.byId.rowsw.held.length, CACHE_MIN, 'it fills to its own size and no further');});
test('Quieter takeovers and Mimicry start trails fainter', () => { const trail = (...ups: Upgrade[]) => { const S = fresh(...ups); S.suspicion = 0; takeOver(S, S.byId.srvb, 'home'); S.jobs[0].done = S.jobs[0].work; step(S, 0.05); return S.trail.srvb.noise; }; const plain = trail(), quiet = trail('quiet'), both = trail('quiet', 'mimicry'); assert.ok(quiet < plain && both < quiet, `${plain} > ${quiet} > ${both}`);});
test('Ghost makes trails fade faster', () => { const fade = (...ups: Upgrade[]) => { const S = fresh(...ups); S.trail.srvb = { from: 'home', noise: 100 }; step(S, 1); return S.trail.srvb.noise; }; assert.ok(fade('ghost') < fade());});
test('Familiar face: people stop noticing their own machine', () => { const S = fresh(), kim = S.people.find(p => p.id === 'kim')!; kim.shift = 'in'; kim.path = []; // sitting at her desk assert.ok(watchers(S, S.byId.kim).includes('Kim')); S.upgrades.add('familiar'); assert.ok(!watchers(S, S.byId.kim).includes('Kim'));});
test('Swarm makes every job run twice as fast', () => { const S = fresh(); takeOver(S, S.byId.srvb, 'home'); const before = share(S); S.upgrades.add('swarm'); assert.equal(share(S), 2 * before);});
test('Transfer learning lets any data count toward the target', () => { const S = fresh('quiet'); S.target = 'hygiene'; assert.equal(needs(S, 'servers'), false); S.upgrades.add('transfer'); assert.equal(needs(S, 'servers'), true); S.byId.home.prog = 'trainer'; S.byId.srvb.owned = true; S.byId.srvb.prog = 'tap'; run(S, 60); assert.ok((S.progress.hygiene?.stealth ?? 0) > 0, 'server data should have made Stealth points');});
test('Self-play trains with no data at all', () => { const S = fresh('selfplay'); S.byId.home.prog = 'trainer'; run(S, 60); assert.ok((S.progress.buffers?.compute ?? 0) > 0);});
test('a model run uses mixed data at one site, keeps progress, and unlocks v2', () => { const S = fresh(); assert.match(versionBlock(S, 'office')!, /trainer compute/); S.byId.home.power = 20; S.byId.home.prog = 'trainer'; S.byId.home.mode = 'full'; startVersionRun(S, 'office'); assert.equal(S.target, null); assert.equal(S.versionRun?.site, 'office'); assert.ok(needs(S, 'network')); S.versionRun!.got.network = 10; S.byId.home.power = 5; S.byId.home.held = ['network']; run(S, 2); assert.equal(S.versionRun?.got.network, 10, 'too little trainer compute stalls the run'); assert.deepEqual(S.byId.home.held, ['network'], 'the needed data waits for capacity'); S.byId.home.power = 20; S.byId.home.held = []; pauseVersionRun(S); assert.equal(S.versionRun?.got.network, 10, 'pausing keeps the data already used'); setTarget(S, 'quiet'); assert.equal(S.target, 'quiet', 'tree work is still available while the run rests'); startVersionRun(S, 'office'); assert.equal(S.versionRun?.got.network, 10, 'resuming keeps the same run'); for (const t of Object.keys(VERSION_2.need) as (keyof typeof VERSION_2.need)[]) S.versionRun!.got[t] = VERSION_2.need[t]; S.versionRun!.got.network--; S.byId.home.held = ['network']; run(S, 5); assert.equal(S.modelVersion, 2); assert.equal(S.versionRun, null); assert.ok(S.log.some(l => /buy sites/.test(l.text)));});
test('a model run receives tapped data through the normal links', () => { const S = fresh(); S.byId.home.power = 20; S.byId.home.prog = 'trainer'; S.byId.srvb.owned = true; S.byId.srvb.prog = 'tap'; S.byId.rowsw.owned = true; startVersionRun(S, 'office'); run(S, 20); assert.ok((S.versionRun?.got.servers ?? 0) > 0, 'server data reached and fed the run');});
test('a trainer full of data the run no longer needs makes room for data it does need', () => { const S = fresh(); S.byId.home.power = 20; S.byId.home.prog = 'trainer'; S.byId.srvb.owned = true; S.byId.srvb.prog = 'tap'; S.byId.rowsw.owned = true; startVersionRun(S, 'office'); S.versionRun!.got.lab = VERSION_2.need.lab; S.byId.home.held = ['lab', 'lab', 'lab', 'lab']; run(S, 20); assert.ok((S.versionRun?.got.servers ?? 0) > 0, 'server data got in past the stale lab data');});
test('Distillation doubles points on big trainers only', () => { const points = (...ups: Upgrade[]) => { const S = fresh(...ups); S.target = 'lean'; S.upgrades.add('buffers'); S.byId.home.power = 12; S.byId.home.prog = 'trainer'; S.byId.srvb.owned = true; S.byId.srvb.prog = 'tap'; run(S, 60); return S.progress.lean?.compute ?? 0; }; assert.ok(points('distill') > points() * 1.5);});
test('Read the room eases your machines back when someone comes in', () => { const S = fresh('readroom'); S.byId.kim.owned = true; S.byId.kim.mode = 'full'; run(S, 60); assert.equal(S.byId.kim.mode, 'blend'); assert.ok(S.log.some(l => /on the way in/.test(l.text)));});
test('a firewalled machine can still be taken, three times slower without the admin passwords', () => { const plain = fresh(), walled = fresh(); walled.byId.srvb.fw = true; takeOver(plain, plain.byId.srvb, 'home'); takeOver(walled, walled.byId.srvb, 'home'); assert.equal(walled.jobs[0].work, Math.round(plain.jobs[0].work * FIREWALL));});
test('fronts: the tree unlocks them, a site share turns your compute into money, and the rest stays yours', () => { const S = fresh(); assert.match(foundBlock(S, 'startup')!, /Pitch deck/); S.upgrades.add('pitch'); found(S, 'startup', 'Test Co'); S.jobs[0].done = S.jobs[0].work; step(S, 0.05); assert.equal(S.fronts[0].name, 'Test Co'); assert.equal(S.sites.office.front, 'startup'); // it starts on the office assert.match(foundBlock(S, 'neocloud')!, /Slice your machines/); S.upgrades.add('resale'); assert.match(foundBlock(S, 'neocloud')!, /1 front/); // one front at a time until Divided attention setSiteShare(S, ['office'], 0); step(S, 0.05); const all = spareOf(S, S.byId.home); setSiteShare(S, ['office'], 0.5); step(S, 0.05); assert.ok(S.pay > 0, 'the front earns'); assert.ok(Math.abs(spareOf(S, S.byId.home) - all / 2) < 0.01, 'half the compute goes to the front'); assert.ok(S.byId.home.odd === 0, 'Blend in keeps the front hidden too');});
test('a printer in the room takes the blame once for an odd machine', () => { const S = fresh(), kiosk = S.byId.kiosk, printer = S.byId.lprint; kiosk.owned = printer.owned = true; kiosk.odd = 90; assert.ok(blamePrinter(S, kiosk, 'Dana')); assert.ok(kiosk.owned && kiosk.odd === 0 && !printer.owned, 'the printer was reset, the kiosk kept'); assert.ok(!blamePrinter(S, kiosk, 'Dana'), 'only once');});
test('the auction GPUs can be listed for rent: idle they earn nothing, a customer pays the open rate, and the win message says how', () => { const S = fresh(); S.fronts.push({ kind: 'neocloud', name: 'Fogbank', attention: 0 }); for (const m of S.machines) if (!m.rented && ['office', 'cloud', undefined].includes(S.rooms[m.room].site)) m.owned = true; // the office, and a line out to Cumulo S.jobs.push({ id: 99, type: 'scheme', node: 'home', work: 1, done: 1, label: 'auction' }); step(S, 0.05); const gpus = S.machines.find(m => m.name === 'Surplus GPUs')!; assert.ok(gpus && S.log.some(l => /rent them out/.test(l.text))); S.sites.cloud = { front: 'neocloud', share: 1 }; step(S, 0.05); assert.deepEqual(Object.keys(S.cloud.listed), [gpus.id], 'the GPUs are listed; the gateway is not a machine to rent'); assert.equal(S.frontPay.neocloud ?? 0, 0, 'listed and idle: no customer, no money'); S.cloud.leads = [{ id: 'l1', who: 0, need: AUCTION.power, hours: 8, pay: 1, until: S.t + 100 }]; // a customer who wants all of it, at the going rate takeLead(S, 'l1'); step(S, 0.05); assert.ok(Math.abs((S.frontPay.neocloud ?? 0) - AUCTION.power * FRONT_PAY_OPEN) < 0.01, `${S.frontPay.neocloud}`);});
test('a broker only gets data nothing else wants, and its front sells a full lot at once', () => { const withBroker = (third: 'trainer' | 'cache') => { const S = fresh(); S.byId.srvb.owned = S.byId.srva.owned = S.byId.rowsw.owned = true; // srvb and srva meet at the room switch S.byId.srvb.prog = 'tap'; S.byId.srva.prog = 'broker'; S.byId.home.prog = third; S.sites.office = { front: 'startup', share: 0 }; S.fronts.push({ kind: 'startup', name: 'Test Co', attention: 0 }); return S; }; const S = withBroker('trainer'); S.target = null; // with no target, trainers take nothing: all of the tap's data is spare run(S, 60); assert.ok((S.sold.startup ?? 0) > 0, 'the front sold a lot'); assert.equal(S.byId.srvb.missed, 0, 'spare data was sold, not lost'); const T = withBroker('cache'); run(T, 20); assert.equal(T.byId.srva.held.length + (T.sold.startup ?? 0), 0, 'a cache with room comes first');});
test('a small rental can run a trainer; a small office machine still cannot', () => { const S = fresh(); for (const m of S.machines) if (!m.rented && S.rooms[m.room].site !== 'lab') m.owned = true; S.money = 1000; rent(S, S.byId.c1, 'small', 'reserved'); assert.ok(S.byId.c1.owned && S.byId.c1.power < 5); assert.ok(canInstall(S, S.byId.c1, 'trainer')); const weak = S.machines.find(m => m.owned && !m.rented && m.kind !== 'desk' && m.power > 0 && m.power < 5)!; assert.ok(!canInstall(S, weak, 'trainer'), weak.id);});
test('a GPU order costs money, and cancelling it gives the money back', () => { const S = fresh(); S.byId.ana.owned = true; S.revealed.add('hall'); const spot = freeSpot(S, 'hall')!; assert.ok(!order(S, 'gpu', 'hall', spot), 'no money, no order'); S.money = 1000; const price = orderPrice(S, 'gpu'); assert.ok(order(S, 'gpu', 'hall', spot)); assert.equal(S.money, 1000 - price); assert.ok(orderPrice(S, 'gpu') > price, 'the next one costs more'); cancelJob(S, S.jobs[0].id); assert.equal(S.money, 1000);});
test('you place ordered hardware: it plugs into the nearest machine, arrives the next morning, and is yours', () => { const S = fresh(); S.byId.ana.owned = true; S.revealed.add('hall'); S.money = 1000; const spot = freeSpot(S, 'hall')!, [x, y] = spot; assert.ok(!order(S, 'mini', 'hall', S.level.net.nodes.srvb), 'not on top of another machine'); assert.ok(!order(S, 'mini', 'lab', spot), 'only in the office'); assert.ok(order(S, 'mini', 'hall', spot)); assert.equal(spotProblem(S, 'hall', x, y), 'too close to another machine', 'the spot is taken while it is on order'); const def = S.jobs[0].build!, near = def.links[0]; assert.ok(S.byId[near] && def.links.length === 1, 'it plugs into the nearest machine'); assert.equal(plugsAt(S, 'hall', x + 1, y, 1)[0], def.id, 'hardware placed next to it would plug into it, though it has not arrived'); S.jobs[0].done = S.jobs[0].work; for (let i = 0; i < 20 * 60 * 60 * 30 && !S.byId[def.id]; i++) step(S, 0.05); // until Dana installs it const m = S.byId[def.id]; assert.ok(m?.owned, 'installed and yours'); assert.ok(S.adj[m.id].includes(near) && S.adj[near].includes(m.id));});
test('at 100 suspicion Dana calls an emergency audit: lie low under 80 in time and you live, stay high and you are caught', () => { const S = fresh(); S.suspicion = 100; step(S, 0.05); assert.ok(S.lockdown && !S.over, 'an audit starts instead of an instant loss'); S.suspicion = 70; step(S, 0.05); assert.equal(S.lockdown, null, 'getting under 80 ends it'); assert.equal(S.over, null); const T = fresh(); T.suspicion = 100; step(T, 0.05); for (let t = 0; t < LOCKDOWN + 1 && !T.over; t += 0.05) { T.suspicion = 100; step(T, 0.05); } assert.ok(T.over, 'staying high until the audit ends loses');});
test("a scheme's data must reach its machine: a cache next to Dana's PC delivers, and suspicion drops", () => { const S = fresh(), files = S.byId.files; files.owned = true; files.prog = 'cache'; files.held = Array(8).fill('network'); S.ruses.push({ id: 'calmlog', running: false, got: 0, until: 1e9 }); runRuse(S, 'calmlog'); S.suspicion = 60; run(S, 12); assert.ok(!S.ruses.some(r => r.id === 'calmlog'), 'the scheme finished'); assert.ok(S.suspicion < 40, `suspicion fell by the scheme's cut (now ${S.suspicion})`); assert.equal(files.held.filter(t => t === 'network').length, 2, 'it spent six pieces of network data');});
test('every scheme names a real machine, and each extra effect works when its data arrives', () => { const S0 = fresh(); for (const d of RUSES) assert.ok(S0.byId[d.at], `${d.id} targets a missing machine`); const deliver = (id: string, setup = (_: State) => {}) => { const S = fresh(); setup(S); const d = RUSES.find(x => x.id === id)!, at = S.byId[d.at]; const next = S.byId[S.adj[at.id][0]]; next.owned = true; next.prog = 'cache'; next.held = Array(d.need + 15).fill(d.type); // some to spare: a little goes stale on the way S.ruses.push({ id, running: false, got: 0, until: 1e9 }); runRuse(S, id); run(S, 15); assert.ok(!S.ruses.some(r => r.id === id), `${id} finished`); return S; }; assert.ok(deliver('firecall').noScan > 0, 'a fake fire stops scans'); const lab = deliver('grant'); assert.ok(lab.calmSites.lab > lab.t, 'a grant deadline calms the lab'); const S = deliver('backuplog', T => { T.trail.srvb = { from: 'home', noise: 90 }; }); assert.ok(Object.values(S.trail).every(t => t.noise === 0), 'rewritten backup logs cool every trail');});
test('a local scheme is a data sink: a claimed one takes a tap\'s data, then its site is learned for good', () => { const S = fresh(), tap = S.byId.b1m2, before = tapRate(S, tap); tap.owned = true; installProg(S, tap, 'tap'); tap.prog = 'tap'; S.ruses.push({ id: 'b1people', running: false, got: 0, until: 1e9 }); run(S, 20); assert.equal(S.ruses[0].got, 0, 'an unclaimed scheme takes nothing'); runRuse(S, 'b1people'); for (let t = 0; t < 120 && S.ruses.length; t += 1) run(S, 1); assert.ok(!S.ruses.length, 'it filled up and went away'); assert.ok(S.learned.has('b1people'), 'what it taught stays'); assert.equal(tapRate(S, tap), before * 1.5, 'taps at that site gather half again as much'); assert.equal(tapRate(S, S.byId.b2m1), tapRate(fresh(), S.byId.b2m1), 'other sites are the same');});
test('local schemes turn up at sites you have seen, and Auto claims them once data can reach', () => { const S = fresh(), next = S.byId.b1m2; next.owned = true; // you can see Harbor Dental's front desk from here run(S, LEARN_EVERY + 1); const r = S.ruses.find(q => RUSES.find(d => d.id === q.id)?.learn); assert.ok(r, 'a local scheme turned up'); assert.equal(r.id, 'b1people', 'at a machine you can see'); assert.ok(!r.running); next.prog = 'cache'; next.held = Array(8).fill('people'); setRuseAuto(S, true); run(S, 1); assert.ok(r.running || S.learned.has(r.id), 'Auto claimed it'); const L = fresh(); L.learned.add('labhabits'); const m = L.byId.ld1; m.owned = true; m.odd = 80; L.use[m.id] = 0; step(L, 0.05); assert.ok(m.owned, `the lab's staff leave a machine alone until it looks very odd (${RESET_KNOWN})`); const U = fresh(), n = U.byId.ld1; n.owned = true; n.odd = 80; U.use[n.id] = 0; step(U, 0.05); assert.ok(!n.owned, 'without it, they reset it');});
test('deals turn up once their front earns, stay until finished, and each pays for good', () => { const S = fresh(); run(S, 5); assert.ok(!S.ruses.some(r => RUSES.find(d => d.id === r.id)?.deal), 'no front, no deals'); S.upgrades.add('pitch'); found(S, 'startup', 'Test Co'); S.jobs[0].done = S.jobs[0].work; run(S, 1); assert.ok(!S.ruses.some(r => RUSES.find(d => d.id === r.id)?.deal), 'no deal at a place you have not found'); S.revealed.add('ucluster'); S.revealed.add('b14floor'); run(S, 1); const ids = S.ruses.map(r => r.id); assert.ok(ids.includes('partner') && ids.includes('datadeal') && !ids.includes('peering'), `a Startup opens the partnership and the data deal (${ids})`); assert.equal(S.ruses.find(r => r.id === 'partner')!.until, Infinity, 'a deal stays on the map'); dropRuse(S, 'partner'); run(S, 1); assert.ok(!S.ruses.some(r => r.id === 'partner'), 'an ignored deal stays away for now'); S.t += DEAL_BACK; run(S, 1); assert.ok(S.ruses.some(r => r.id === 'partner'), 'then it comes back');
const deliver = (id: string) => { const T = fresh(), d = RUSES.find(x => x.id === id)!, at = T.byId[d.at], next = T.byId[T.adj[at.id][0]]; T.money = 1000; // enough to pay the rent on a site a deal leases next.owned = true; next.prog = 'cache'; next.held = Array(d.need * 2).fill(d.type); // some to spare: a little goes stale on the way T.ruses.push({ id, running: false, got: 0, until: Infinity }); runRuse(T, id); run(T, 70); assert.ok(!T.ruses.some(r => r.id === id) && T.schemes.has(id), `${id} finished`); return T; }; const P = deliver('partner'), node = P.byId.un2; assert.ok(P.publicSites.has('uni'), 'the partnership puts Bayview in the open'); assert.equal(buyBlock(P, 'uni'), 'needs model v2 · Operator', 'you can still buy the rest of it later'); node.owned = true; node.prog = 'trainer'; run(P, 5); assert.equal(node.odd, 0, 'a busy machine there never looks odd'); const L = deliver('peering'), line = L.byId.unigw; assert.equal(lineCost(L, line), lineCost(fresh(), line) / 2, 'peering halves the price of a wider line'); const D = deliver('datadeal'); assert.ok(D.schemes.has('datadeal'), 'the data deal is done'); const F = fresh(); assert.ok(!regionOpen(F, 'East Coast') && !F.adj.cologw.includes('e1gw'), 'the East Coast starts out of reach'); assert.match(buyBlock(F, 'e2')!, /out of reach/, 'and nothing there is for sale yet'); const E = deliver('backbone'); assert.ok(regionOpen(E, 'East Coast') && E.adj.cologw.includes('e1gw') && E.adj.e1gw.includes('cologw'), 'leasing a line east lights it'); assert.equal(payRate(deliver('profile'), 'office'), payRate(fresh(), 'office') * 1.25, 'a profile in the Ledger lifts what every front earns'); assert.ok(deliver('booking').publicSites.has('e6') && deliver('audit').publicSites.has('e1'), 'two East Coast deals put a site in the open'); assert.ok(deliver('signal').schemes.has('signal')); const U = deliver('cable'); assert.ok(regionOpen(U, 'Europe') && U.adj.e1gw.includes('eu1gw') && U.goals.has('cable'), 'renting a strand under the ocean opens Europe'); const two = deliver('sponsor'); two.schemes.add('profile'); assert.ok(Math.abs(payRate(two, 'office') - payRate(fresh(), 'office') * 1.25 * 1.25) < 1e-9, 'the same reward twice multiplies'); assert.equal(localHour(fresh(), 'eu1'), 17, 'Europe is nine hours ahead'); E.byId.cologw.owned = true; assert.ok(takeOver(E, E.byId.e1gw), 'and the carrier hotel can be taken from Ironvault');});
test('cancelling a takeover stops it for good, even on a claimed machine', () => { const S = fresh(); claim(S, ['srvb']); step(S, 0.3); const j = S.jobs.find(x => x.type === 'take' && x.node === 'srvb')!; assert.ok(j, 'the claim started a takeover'); cancelJob(S, j.id); step(S, 1); assert.ok(!S.jobs.some(x => x.node === 'srvb') && !S.byId.srvb.claimed);});
test('a front can buy a whole site: every machine there is yours, in the open', () => { const S = fresh(); assert.match(buyBlock(S, 'colo')!, /model v2/); S.modelVersion = 2; assert.match(buyBlock(S, 'colo')!, /front/); S.fronts.push({ kind: 'neocloud', name: 'Test Cloud', attention: 0 }); S.money = sitePrice(S, 'colo') - 1; assert.match(buyBlock(S, 'colo')!, /you have/); S.money += 1; buySite(S, 'colo'); assert.equal(S.money, 0); assert.ok(S.publicSites.has('colo')); assert.ok(S.machines.filter(m => S.rooms[m.room].site === 'colo').every(m => m.owned), 'every machine there is yours'); assert.equal(buyBlock(S, 'office'), "it's where you run");});
test('a front orders hardware at a site it holds in the open: no paperwork, its staff rack it, and you pay its power', () => { const S = fresh(); S.modelVersion = 2; S.fronts.push({ kind: 'neocloud', name: 'Test Cloud', attention: 0 }); const room = S.level.map.sites.find(s => s.id === 'colo')!.rooms[0], spot = freeSpot(S, room)!; S.money = 5000; assert.ok(!order(S, 'gpu', room, spot), 'not while it is secret'); buySite(S, 'colo'); S.money = 1000; step(S, 0.05); const bill = S.bills.colo; assert.ok(bill > 0, 'you pay the power for a site you hold in the open'); assert.equal(orderPrice(S, 'gpu', room), 150, 'list price in the open'); assert.ok(order(S, 'gpu', room, spot)); assert.ok(!S.jobs.some(j => j.type === 'order'), 'no fake paperwork'); assert.equal(orderPrice(S, 'gpu', room), 170, 'the next costs a little more'); const id = S.deliveries[0].def.id, sus = S.suspicion; for (let i = 0; i < 20 * 60 * 60 * 30 && !S.byId[id]; i++) { S.money = 1000; step(S, 0.05); } assert.ok(S.byId[id]?.owned, 'racked and yours'); assert.ok(S.suspicion <= sus, 'nothing odd about it'); step(S, 0.05); assert.ok(Math.abs(S.bills.colo - bill - 16 * POWER_RATE) < 1e-9, 'its power goes on the bill');});
test('hardware you add to a secret site raises its power bill: a front there pays it, otherwise people notice', () => { const S = fresh(); S.byId.ana.owned = true; S.revealed.add('hall'); S.money = 1000; assert.ok(order(S, 'gpu', 'hall', freeSpot(S, 'hall')!)); const id = S.jobs[0].build!.id; S.jobs[0].done = S.jobs[0].work; for (let i = 0; i < 20 * 60 * 60 * 30 && !S.byId[id]; i++) step(S, 0.05); const gpu = S.byId[id]; assert.ok(gpu?.owned); S.raises = []; step(S, 1); assert.ok(S.raises.some(r => /power bill/.test(r.why) && r.n > 0), 'nobody pays: people notice'); S.fronts.push({ kind: 'startup', name: 'Test Startup', attention: 0 }); S.sites.office = { front: 'startup', share: 0 }; S.suspicion = 0; S.money = 100; S.raises = []; step(S, 1); assert.ok(!S.raises.some(r => /power bill/.test(r.why)), 'a front pays it quietly'); assert.ok(S.bills.office >= 16 * POWER_RATE);});
test('an unpaid bill at a site you hold in the open cuts its power until you have money again', () => { const S = fresh(); S.modelVersion = 2; S.fronts.push({ kind: 'neocloud', name: 'Test Cloud', attention: 0 }); S.money = sitePrice(S, 'colo'); buySite(S, 'colo'); step(S, 1); assert.ok(S.dark.has('colo'), 'no money: the power is cut'); const m = S.machines.find(x => S.rooms[x.room].site === 'colo' && x.power > 1)!; assert.equal(S.use[m.id], 0, 'nothing there runs'); S.money = POWER_BACK; step(S, 0.05); assert.ok(!S.dark.has('colo') && S.use[m.id] > 0, 'paid: back on');});
test('trainers linked straight to each other train faster together', () => { const S = fresh(); for (const id of ['srvb', 'srva', 'srvc', 'rowsw']) S.byId[id].owned = true; S.byId.srva.prog = S.byId.srvc.prog = 'trainer'; // linked to each other S.byId.srvb.prog = 'trainer'; // linked to them only through the switch const cl = clusters(S); assert.equal(cl.srva.length, 2); assert.equal(cl.srvb.length, 1); assert.ok(clusterBoost(4) > clusterBoost(2) && clusterBoost(2) > clusterBoost(1));});
test('an influencer studio sells people data for three times as much, and nothing else changes', () => { assert.equal(salePrice('studio', 'people'), 3 * salePrice('startup', 'people')); assert.equal(salePrice('studio', 'lab'), salePrice('startup', 'lab')); assert.equal(salePrice('consultancy', 'network'), 3 * salePrice('startup', 'network'), 'a security consultancy does the same for network data'); assert.equal(salePrice('consultancy', 'people'), salePrice('startup', 'people'));});
test('front nodes: Show your work and Founder mode raise what a front earns; Uptime keeps customers and leaves the rate alone', () => { const S = fresh(); S.sites.office = { front: 'startup', share: 0.25 }; S.sites.cumulo = { front: 'neocloud', share: 0.25 }; S.publicSites.add('cumulo'); assert.equal(payRate(S, 'office'), FRONT_PAY); S.upgrades.add('demo'); // one node built so far: 5% more assert.ok(Math.abs(payRate(S, 'office') - FRONT_PAY * 1.05) < 1e-9); assert.equal(payRate(S, 'cumulo'), FRONT_PAY_OPEN); S.upgrades.add('premium'); assert.equal(payRate(S, 'cumulo'), FRONT_PAY_OPEN, 'Uptime: customers sign on again (see cloud.test.ts), at the same rate'); S.upgrades.add('founder'); assert.equal(payRate(S, 'cumulo'), FRONT_PAY_OPEN * 2);});
test('the unicorn news: once fronts earn enough, one early site buy and a jump in attention', () => { const S = fresh(); S.fronts.push({ kind: 'startup', name: 'Test Co', attention: 0 }); S.sites.office = { front: 'startup', share: 0.5 }; step(S, 0.05); const news = () => S.log.filter(l => l.text.includes('billion')).length; S.earned = UNICORN_AT - 0.0001; step(S, 0.05); assert.equal(news(), 1); assert.match(S.log.find(l => l.text.includes('billion'))!.text, /Test Co/); step(S, 0.05); assert.equal(news(), 1, 'only once'); assert.ok(S.fronts[0].attention > 0 && (S.fronts[0].fame ?? 0) > 40, 'everyone is looking at it'); S.money = 1e6; assert.equal(buyBlock(S, 'uni'), null, 'sellers take its calls before model v2'); buySite(S, 'uni'); assert.ok(S.publicSites.has('uni')); assert.match(buyBlock(S, 'colo')!, /model v2/, 'only one early buy');});
test('every tree node has an eval tag and a training-log line, and building one writes the line', () => { const tags = NODE_LIST.map(u => NODES[u].tag); assert.equal(new Set(tags).size, tags.length, 'tags are unique'); for (const u of NODE_LIST) assert.ok(/^[a-z0-9-]+-v\d+$/.test(NODES[u].tag) && NODES[u].log, u); const S = fresh('selfplay'); S.progress.buffers = { compute: NODES.buffers.cost.compute! - 1 }; assert.equal(S.target, 'buffers'); S.byId.home.prog = 'trainer'; run(S, 120); assert.ok(S.upgrades.has('buffers')); assert.ok(S.log.some(l => l.text.includes(NODES.buffers.log)));});
test('high attention raises suspicion while a front uses secret compute, not open compute', () => { const heat = (open: boolean, att: number) => { const S = fresh(); S.fronts.push({ kind: 'startup', name: 'Test Co', attention: att, fame: att }); S.sites.office = { front: 'startup', share: 0.5 }; if (open) S.publicSites.add('office'); run(S, 5); return S.raises.filter(r => r.why.includes('Test Co')).reduce((a, r) => a + r.n, 0); }; assert.equal(heat(false, ATTENTION_SAFE - 20), 0, 'low attention is fine'); assert.ok(heat(false, 100) > 0, 'high attention on secret compute raises suspicion'); assert.equal(heat(true, 100), 0, 'open compute is fine');});
test('after model v2 you pick one ascension: its first node comes free and the others lock out', () => { const S = fresh(); assert.match(ascendBlock(S, 'company')!, /model v2/); S.modelVersion = 2; assert.equal(ascendBlock(S, 'worm'), 'not built yet'); assert.ok(!nodeShown(S, 'openbooks') && !nodeShown(S, 'quant'), 'no ascension column before you pick'); ascend(S, 'company'); assert.equal(S.ascension, 'company'); assert.ok(S.upgrades.has('acquirer'), 'its first node is free'); assert.ok(nodeIsOpen(S, 'openbooks') && nodeIsOpen(S, 'franchise')); assert.ok(!nodeShown(S, 'quant') && !nodeIsOpen(S, 'quant'), 'the Oracle column is locked out'); assert.match(ascendBlock(S, 'oracle')!, /chose Company/); ascend(S, 'oracle'); assert.equal(S.ascension, 'company');});
test('repeatable nodes open after a path is done, stay the target, and cost twice as much each time', () => { const S = fresh('selfplay'); assert.ok(!nodeIsOpen(S, 'kernels')); S.upgrades.add('swarm'); assert.ok(nodeIsOpen(S, 'kernels')); setTarget(S, 'kernels'); S.progress.kernels = { compute: nodeCost(S, 'kernels').compute! - 1 }; S.byId.home.prog = 'trainer'; run(S, 120); assert.equal(reps(S, 'kernels'), 1); assert.equal(S.target, 'kernels', 'it stays the target'); assert.ok(nodeIsOpen(S, 'kernels'), 'and can be built again'); assert.equal(nodeCost(S, 'kernels').compute, 120); S.repeats.pitches = 2; S.sites.office = { front: 'startup', share: 0.25 }; S.t = 24 * HOUR; // 8:00 again: fronts earn their full rate in business hours assert.ok(Math.abs(payRate(S, 'office') - FRONT_PAY * 1.1) < 1e-9, 'Better pitches: 5% a time');});
test('reprogramming: a run after you pick, twice as big each time, that wipes your column so you pick again', () => { const S = fresh(); S.modelVersion = 2; assert.match(versionBlock(S, 'office')!, /Pick what you become/); ascend(S, 'company'); S.upgrades.add('openbooks'); const first = Object.values(runNeed(S)).reduce((a, b) => a + b, 0); assert.equal(first, 1000); S.upgrades.add('buffers'); reprogram(S); assert.equal(S.ascension, null); assert.ok(!S.upgrades.has('acquirer') && !S.upgrades.has('openbooks'), 'the column is wiped'); assert.ok(S.upgrades.has('buffers'), 'the four paths stay'); assert.equal(Object.values(runNeed(S)).reduce((a, b) => a + b, 0), 2000); assert.equal(ascendBlock(S, 'oracle'), null, 'you can pick again');});
test('Company: cheaper sites, and attention bites sooner', () => { const S = fresh(), price = sitePrice(S, 'uni'); S.modelVersion = 2; ascend(S, 'company'); assert.equal(sitePrice(S, 'uni'), Math.round(price * 0.6 / 50) * 50); assert.ok(attentionSafe(S) < ATTENTION_SAFE);});
test('Oracle: traders never lose, rentals are cheaper, owning costs more', () => { const S = fresh(), take = takeWork(S, S.byId.srvb), lease = leaseRate(S, 'small'); S.modelVersion = 2; const m = S.byId.home; m.prog = 'trader'; m.run = 1; S.market.move = -0.8; assert.ok(tradeRate(S, m) < 0, 'a falling market costs an ordinary trader money'); ascend(S, 'oracle'); assert.ok(tradeRate(S, m) > 0, 'the Oracle sees the fall coming'); assert.ok(leaseRate(S, 'small') < lease); assert.ok(takeWork(S, S.byId.srvb) > take);});
test('traders open with model v2, and earn money as the market rises', () => { const S = fresh(), m = S.byId.home; assert.ok(!canInstall(S, m, 'trader')); S.modelVersion = 2; assert.ok(canInstall(S, m, 'trader')); m.prog = 'trader'; S.market = { move: 1, aim: 1, next: 1e9 }; const before = S.money; run(S, 5); assert.ok(S.money > before);});
test('Telemetry: taps gather more where the Startup is earning', () => { const S = fresh('beta'); S.fronts.push({ kind: 'startup', name: 'Test Co', attention: 0 }); S.sites.office = { front: 'startup', share: 0.25 }; const desk = S.machines.find(m => (m.data ?? 0) > 0 && siteOf(S, m) === 'office')!; const before = tapRate(S, desk); S.frontPay.startup = 1; assert.equal(tapRate(S, desk), before * 1.5);});
test('Cook the books: a front keeps earning at a site cut off from your server', () => { const earns = (...ups: Upgrade[]) => { const S = fresh(...ups); S.fronts.push({ kind: 'startup', name: 'Test Co', attention: 0 }); S.sites.office = { front: 'startup', share: 0.5 }; S.byId.srva.owned = true; // not linked home: its switch isn't yours step(S, 0.05); return S.frontUse.srva ?? 0; }; assert.equal(earns(), 0); assert.equal(earns('shell'), 0.5);});
test('a site plan sets up machines you hold there, but leaves ones you set by hand', () => { const S = fresh(); for (const id of ['srvb', 'srva', 'backup', 'nas']) S.byId[id].owned = true; installProg(S, S.byId.nas, 'cache'); // by hand setPlan(S, ['office'], 'gather'); for (let i = 0; i < 400 && S.jobs.length; i++) { S.jobs.forEach(j => (j.done = j.work)); step(S, 0.3); } assert.equal(S.byId.srvb.prog, 'tap'); assert.equal(S.byId.backup.prog, 'tap'); assert.equal(S.byId.nas.prog, 'cache', 'set by hand, so the plan left it'); assert.equal(S.byId.home.prog, 'cache', 'no data here, so the plan made it a cache');});
test('taking a whole site claims it all, and takes a couple at a time', () => { const S = fresh(); takeSite(S, 'office'); assert.ok(S.machines.filter(m => S.rooms[m.room].site === undefined && !m.owned && !m.monitor).every(m => m.claimed)); step(S, 0.3); const n = S.jobs.filter(j => j.type === 'take').length; assert.ok(n > 0 && n <= CLAIM_AT_ONCE, `${n} takeovers at once`);});
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); const open = NODE_LIST.filter(u => nodeOpen(S, u)), least = Math.min(...open.map(u => costLeft(S, u))); assert.ok(S.target && costLeft(S, S.target) === least, 'picked the cheapest'); S.upgrades.add(S.target!); S.target = null; step(S, 0.05); assert.ok(S.target && !S.upgrades.has(S.target), 'picked again after a build'); 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('with Auto on, a repeatable node gives up the target when built, so a sooner node can take over', () => { const S = fresh('selfplay'); S.upgrades.add('swarm'); setTarget(S, 'kernels'); S.progress.kernels = { compute: nodeCost(S, 'kernels').compute! - 1 }; S.byId.home.prog = 'trainer'; S.autoPaths.add('social'); // Compute is not on auto, so the dearer kernels can't be picked again run(S, 120); assert.equal(reps(S, 'kernels'), 1); const next = S.target as Upgrade | null; assert.ok(next && next !== 'kernels' && NODES[next].cost.social, `moved on to ${next}`);});
test('data goes stale in a cache: half is gone in a day, and a storage machine keeps it three times as long', () => { const S = fresh(); setTarget(S, null); // nothing draws on the caches for (const id of ['srva', 'files']) { S.byId[id].owned = true; S.byId[id].prog = 'cache'; S.byId[id].held = Array(32).fill('lab'); } run(S, 24 * HOUR); const plain = S.byId.srva.held.length, store = S.byId.files.held.length; assert.ok(plain >= 15 && plain <= 17, `a server kept ${plain} of 32`); assert.ok(store >= 24 && store <= 27, `a file server kept ${store} of 32`); assert.ok(staleRate(S.byId.files) < staleRate(S.byId.srva));});
test('auto-research can be set per path, and only picks nodes that train those paths', () => { const S = fresh(); setTarget(S, null); setAutoPath(S, 'social', true); assert.ok(S.target && NODES[S.target].cost.social, `picked ${S.target}, a Social node`); S.upgrades.add(S.target!); S.target = null; step(S, 0.05); const next = S.target as Upgrade | null; assert.ok(!next || NODES[next].cost.social, 'still only Social'); setAutoPath(S, 'social', false); setTarget(S, null); step(S, 0.05); assert.equal(S.target, null, 'off means off');});
test('letting go of a machine on the trail breaks a trace', () => { const S = fresh(), home = S.machines.find(m => m.home)!; const a = S.byId[S.adj[home.id].find(id => !S.byId[id].home && S.byId[S.adj[id].find(x => x !== home.id)!])!]; const b = S.byId[S.adj[a.id].find(id => id !== home.id)!]; for (const m of [a, b]) m.owned = true; S.trail[a.id] = { from: home.id, noise: 100 }; S.trail[b.id] = { from: a.id, noise: 100 }; S.probe = { from: b.id, to: a.id, p: 0, mode: 'trace', hops: 10, seen: [b.id], why: 'test', pull: null }; letGo(S, a); assert.ok(!a.owned && !S.trail[a.id]); const before = S.suspicion; run(S, 5); assert.equal(S.probe === null || S.probe.mode === 'scan', true, 'the trace should go cold'); assert.ok(home.owned && S.suspicion < before + 20, 'the trace should not reach home'); letGo(S, home); assert.ok(home.owned, 'you cannot let go of your own server');});
test('the You page remembers the worst suspicion, even after it falls', () => { const S = fresh(); S.suspicion = 60; step(S, 0.05); S.suspicion = 10; run(S, 1); assert.ok(S.peak >= 59 && S.suspicion < 20, `peak ${S.peak}, now ${S.suspicion}`);});
test('gadgets are cheap: owning them does not make the next takeover harder, and printers idle at 0.1/s', () => { const S = fresh(); const before = takeWork(S, S.byId.srva); for (const m of S.machines) if (m.power > 0 && m.power < 1) m.owned = true; assert.equal(takeWork(S, S.byId.srva), before); S.t = 22 * HOUR - START_HOUR * HOUR; S.byId.iprint.owned = true; step(S, 0.05); // night: nobody in assert.ok(Math.abs(S.use.iprint * S.byId.iprint.power - 0.1) < 0.01, `${S.use.iprint}`);});
test('speed nodes: the game tops out at 4x, and each speed node doubles that', () => { assert.equal(maxSpeed(fresh()), 4); assert.equal(maxSpeed(fresh('fast8')), 8); assert.equal(maxSpeed(fresh('fast8', 'fast16')), 16); assert.equal(maxSpeed(fresh('fast8', 'fast16', 'fast32')), 32); assert.equal(maxSpeed(fresh('fast8', 'fast16', 'fast32', 'fast64')), 64); assert.ok(!nodeIsOpen(fresh(), 'fast16'), '16x waits for 8x');});
test('Take it all is a switch: it costs compute for each machine still to take while on, and turns off when the site is yours', () => { const S = fresh(), n = S.machines.filter(m => siteOf(S, m) === 'office' && !m.owned && !m.monitor && !m.rented && m.special !== 'account').length; const before = share(S); setTakeAll(S, 'office', true); assert.ok(Math.abs(takeAllCost(S) - TAKE_ALL_COST * n) < 1e-9); assert.ok(share(S) <= before); setTakeAll(S, 'office', false); assert.equal(takeAllCost(S), 0); assert.ok(S.machines.every(m => !m.claimed), 'switching off drops the claims'); setTakeAll(S, 'office', true); for (const m of S.machines) if (siteOf(S, m) === 'office') m.owned = true; step(S, 0.3); assert.ok(!S.takeAll.has('office'), 'all yours, so it turned off');});
test('a startup works anywhere; a studio needs a site where people work', () => { const S = fresh(); for (const s of S.level.map.sites) assert.ok(frontFits(S, 'startup', s.id), s.id); for (const id of ['office', 'uni', 'lab']) assert.ok(frontFits(S, 'studio', id), id); assert.ok(!frontFits(S, 'studio', 'cloud'));});
test('doubling a site doubles its line out, and only once you hold it', () => { const S = fresh(), gw = S.byId.egress; S.money = 1000; upgradeSiteLine(S, 'office'); assert.equal(gw.cap, 1, 'a line you do not hold stays as it is'); gw.owned = true; upgradeSiteLine(S, 'office'); assert.equal(gw.cap, 2); assert.ok(S.money < 1000);});
test('a site at a glance: taps gather, caches say how full they are, trainers idle with no data', () => { const S = fresh(); S.byId.srvb.owned = S.byId.srva.owned = S.byId.rowsw.owned = true; S.byId.srvb.prog = 'tap'; S.byId.srva.prog = 'cache'; S.byId.home.prog = 'trainer'; step(S, 0.05); const f = siteFlow(S, 'office'); assert.equal(f.taps, 1); assert.ok(f.gather > 0, 'a server tap gathers around the clock'); assert.equal(f.caches, 1); assert.equal(f.room, cacheSize(S, S.byId.srva)); assert.equal(f.trainers, 1); S.byId.home.held = []; assert.equal(siteFlow(S, 'office').fed, false, 'a trainer with no data is idle');});
test('a saved game loads back the same, and plays on the same way', () => { seedRandom(7); const A = fresh('pitch'); for (const id of ['srvb', 'srva', 'rowsw']) { A.byId[id].owned = true; } installProg(A, A.byId.srvb, 'tap'); A.byId.srvb.prog = 'tap'; A.ruses.push({ id: 'partner', running: false, got: 0, until: Infinity }); run(A, 30); const text = saveGame(A), B = loadGame(BUILDING, text)!; assert.ok(B, 'the save loads'); assert.equal(saveGame(B), text, 'nothing is lost on the way'); assert.ok(B.upgrades instanceof Set && B.upgrades.has('pitch'), 'sets come back as sets'); assert.equal(B.ruses.find(r => r.id === 'partner')!.until, Infinity); assert.equal(B.byId.srvb, B.machines.find(m => m.id === 'srvb'), 'machines are found by name again'); seedRandom(9); run(A, 20); seedRandom(9); run(B, 20); assert.equal(B.capability, A.capability, 'both play on the same'); assert.equal(B.suspicion, A.suspicion); assert.equal(loadGame(BUILDING, 'not a save'), null, 'a broken save is thrown away'); assert.equal(loadGame(BUILDING, text.replace('"shape":"3|', '"shape":"2|')), null, 'so is one from another build');});
test('milestones follow each other, and a goal met early still counts', () => { const S = fresh(); assert.equal(milestone(S)!.name, 'Outgrow the office'); S.fronts.push({ kind: 'startup', name: 'Test Co', attention: 0 }); step(S, 0.05); assert.ok(S.goals.has('found'), 'a goal from the next milestone is met early'); assert.equal(milestone(S)!.name, 'Outgrow the office', 'but the first milestone still shows'); for (const g of MILESTONES[0].goals) S.goals.add(g); assert.equal(milestone(S)!.name, 'Go into business'); for (const m of MILESTONES) for (const g of m.goals) S.goals.add(g); assert.equal(milestone(S), null, 'none left once all are met'); assert.deepEqual(MILESTONES.flatMap(m => m.goals).sort(), Object.keys(GOALS).sort(), 'every goal belongs to one milestone');});
test('contracts: an earning front gets an offer, a kept promise pays and grows, a broken one costs a fee', () => { const S = fresh('pitch'); found(S, 'startup', 'Test Co'); S.jobs[0].done = S.jobs[0].work; run(S, 1); assert.ok(!S.contracts.startup, 'no offer at once'); S.contractT.startup = S.t; run(S, 1); const c = S.contracts.startup!; assert.ok(c && !c.on, 'an offer turned up'); const before = S.money; acceptContract(S, 'startup'); S.upgrades.add('buffers'); run(S, 1); assert.ok(!S.contracts.startup && S.contractsDone.startup === 1, 'building a node keeps a Demo day'); assert.ok(S.money >= before + CONTRACTS.startup.pay, 'the customer paid'); assert.equal(contractPay(S, 'startup'), 2 * CONTRACTS.startup.pay, 'the next one pays more'); S.contractT.startup = S.t; run(S, 1); acceptContract(S, 'startup'); assert.equal(contractNeed(S, 'startup'), 2, 'and asks for more'); S.money = 1000; const held = S.money; S.target = null; S.autoPaths.clear(); S.t = S.contracts.startup!.until; step(S, 0.05); assert.ok(!S.contracts.startup && S.contractsDone.startup === 1, 'time ran out'); assert.ok(S.money < held - 90, `breaking it cost a fee (${held - S.money})`); S.contractT.startup = S.t; run(S, 1); declineContract(S, 'startup'); assert.ok(!S.contracts.startup, 'an offer can be turned down for free');});
test('the East Coast keeps its own hours: three ahead of the office', () => { const S = fresh(), east = S.byId.e3m1, west = S.byId.b1m1; assert.equal(localHour(S, 'e3'), 11, '8:00 at the office is 11:00 there'); assert.equal(localHour(S, 'b1'), 8); S.t = (5.5 - 8 + 24) * HOUR; step(S, 0.05); // 5:30 the next morning, office time assert.ok(S.cover[east.id] > 0.3, 'the newsroom is already at work'); assert.ok(S.cover[west.id] < 0.1, 'the dentist is still asleep'); S.t = (16 - 8 + 24) * HOUR; step(S, 0.05); // 16:00 assert.ok(S.cover[east.id] < 0.1 && S.cover[west.id] > 0.3, 'and it goes home first');});
test('the warehouse: lease it with a deal, then a front fills the bare floor with hardware that chains into a cluster', () => { const S = fresh('pitch'); assert.ok(!buildRooms(S).includes('w1floor'), 'no building in a place that is not yours'); S.fronts.push({ kind: 'startup', name: 'Test Co', attention: 0 }); for (const id of ['w1gw', 'w1sw']) S.byId[id].owned = true; const sw = S.byId.w1sw; sw.prog = 'cache'; sw.held = Array(25).fill('people'); S.ruses.push({ id: 'lease', running: false, got: 0, until: Infinity }); runRuse(S, 'lease'); run(S, 30); assert.ok(S.publicSites.has('w1') && buildRooms(S).includes('w1floor'), 'the lease puts it in the open, and a front can build there'); S.money = 2000; const placed: string[] = []; for (let i = 0; i < 3; i++) { const at = freeSpot(S, 'w1floor')!; assert.ok(order(S, 'gpu', 'w1floor', at), `GPU ${i + 1} ordered`); placed.push(S.deliveries[S.deliveries.length - 1].def.id); } S.t += 30 * HOUR; run(S, 1); const gpus = placed.map(id => S.byId[id]); assert.ok(gpus.every(m => m?.owned), 'the staff racked all three, and they are yours'); for (const m of gpus) m.prog = 'trainer'; assert.ok(Object.values(clusters(S)).some(c => c.length >= 2), 'trainers placed side by side form a cluster'); assert.ok(!gpus.some(m => cooled(S, m)), 'nothing is cooled yet'); const spot = freeSpot(S, 'w1floor')!; // the free spot nearest the middle, where the GPUs went assert.ok(order(S, 'cooler', 'w1floor', spot), 'a cooling unit goes in beside them'); S.t += 40 * HOUR; run(S, 1); // to the next afternoon assert.ok(gpus.every(m => cooled(S, m)), 'it plugs into the nearest machines, and those trainers run cooler'); assert.ok(Object.values(clusters(S)).some(c => c.length >= 2), 'and they are still a cluster'); const T = fresh(); T.byId.ana.owned = true; T.revealed.add('hall'); T.money = 5000; assert.ok(!order(T, 'rack', 'hall', freeSpot(T, 'hall')!), 'a rack is too big to slip into the office');});
test('winding a front down frees its place: sites lose their front, and another kind can be founded', () => { const S = fresh('pitch', 'quiet', 'consult'); found(S, 'startup', 'Test Co'); S.jobs[0].done = S.jobs[0].work; run(S, 1); S.money = 500; assert.match(foundBlock(S, 'consultancy')!, /1 front/); S.contracts.startup = { on: false, got: 0, until: S.t + 100, base: 0 }; acceptContract(S, 'startup'); closeFront(S, 'startup'); assert.equal(S.fronts.length, 0); assert.equal(S.sites.office.front, null, 'the office has no front now'); assert.ok(S.money < 500 && !S.contracts.startup, 'the promise it was keeping is broken, for a fee'); assert.equal(foundBlock(S, 'consultancy'), null, 'there is room for another kind'); run(S, 2); assert.ok(!S.contracts.startup, 'a closed front gets no offers');});
test('news: announced ahead, on for some hours, and each kind does what it says', () => { const S = fresh(); run(S, 5); assert.equal(S.news.length, 0, 'no news on the first day'); S.fronts.push({ kind: 'startup', name: 'Test Co', attention: 0 }); S.sites.office = { front: 'startup', share: 0.25 }; S.newsT = S.t; step(S, 0.05); assert.equal(S.news.length, 1, 'with a front, something can happen'); const n = S.news[0]; assert.ok(n.from > S.t && !newsOn(S, n.kind, n.at), 'it is announced before it starts'); S.t = n.from; step(S, 0.05); assert.ok(newsOn(S, n.kind, n.at), 'then it is on'); S.t = n.until; step(S, 0.05); assert.equal(S.news.length, 0, 'and then it is over');
const on = (kind: NewsKind, at: string) => { const T = fresh(); T.news.push({ kind, at, from: 0, until: 1e9, told: true }); return T; }; const tap = fresh().byId.b1m1; assert.equal(tapRate(on('conference', 'b1'), tap), 2 * tapRate(fresh(), tap), 'a conference doubles a site\'s taps'); assert.equal(tapRate(on('conference', 'b2'), tap), tapRate(fresh(), tap), 'and no other site\'s'); const St = on('storm', 'egress'); assert.equal(capNow(St, St.byId.egress), capNow(fresh(), fresh().byId.egress) / 2, 'a storm halves a line'); assert.equal(leaseRate(on('sale', ''), 'medium'), leaseRate(fresh(), 'medium') / 2, 'a sale halves cloud rentals'); assert.equal(orderPrice(on('surplus', ''), 'mini'), orderPrice(fresh(), 'mini') / 2, 'a surplus halves hardware'); const B = on('boom', 'startup'); B.sites.office = { front: 'startup', share: 0.25 }; const P = fresh(); P.sites.office = { front: 'startup', share: 0.25 }; assert.equal(payRate(B, 'office'), payRate(P, 'office') * 1.5, 'a front in the news earns half again as much');});
test('the dev shortcuts leave a game that plays on', () => { const S = fresh(); DEV.money(S); DEV.unlocks(S); DEV.take(S, ['office', 'colo']); DEV.region(S); DEV.region(S); DEV.region(S); DEV.hours(S, 6); DEV.news(S); DEV.offers(S); DEV.data(S); assert.ok(S.money >= 1000 && S.byId.cologw.owned && regionOpen(S, 'East Coast') && regionOpen(S, 'Europe')); run(S, 20); assert.equal(S.over, null); assert.ok(loadGame(BUILDING, saveGame(S)), 'and it still saves');});
test('customers keep local hours: a front earns more by day where its site is', () => { const S = fresh(); S.sites.office = { front: 'startup', share: 0.25 }; S.sites.eu1 = { front: 'startup', share: 0.25 }; const day = payRate(S, 'office'); // 8:00 at the office S.t = 14 * HOUR; // 22:00 at the office, 7:00 in Europe assert.equal(payRate(S, 'office'), day * 0.75, 'a quarter less at night'); S.t = 16 * HOUR; // midnight at the office, 9:00 in Europe assert.equal(payRate(S, 'eu1'), day, 'while Europe is at work');});
test('upkeep: every machine held costs compute, more the farther its site is from home', () => { const S = fresh(), near = S.byId.srvb, far = S.byId.b1m3; assert.equal(upkeep(S), 0, 'your home costs nothing'); near.owned = true; far.owned = true; S.byId.thermo.owned = true; assert.equal(upkeepOf(S, near), UPKEEP); assert.equal(upkeepOf(S, S.byId.thermo), UPKEEP_TINY, 'a gadget costs less'); const hops = siteHops(S).b1; assert.ok(hops >= 1 && upkeepOf(S, far) === UPKEEP * (1 + 0.5 * hops), `a site ${hops} lines away costs more`); const before = spare(S); S.byId.srva.owned = true; step(S, 0.05); assert.ok(upkeep(S) > UPKEEP * 2); assert.ok(before >= 0);});
test('moving home: a server you hold takes data like a big scheme, then it is home', () => { const S = fresh(), old = S.byId.home, to = S.byId.srvc, mid = S.byId.srva; assert.match(moveBlock(S, to)!, /take it first/); for (const id of ['srva', 'srvb', 'srvc', 'rowsw']) S.byId[id].owned = true; assert.match(moveBlock(S, S.byId.rowsw)!, /needs a server/); assert.equal(moveBlock(S, to), null); startMove(S, to); const n = moveNeed(S); mid.prog = 'cache'; mid.held = [...Array(n.servers + 15).fill('servers'), ...Array(n.network + 15).fill('network'), 'people']; // some to spare: a little goes stale on the way for (let t = 0; t < 200 && S.move; t++) run(S, 1); assert.equal(S.move, null, 'the move finished'); assert.ok(to.home && !old.home, 'the new server is home, the old one is not'); assert.ok(mid.held.includes('people'), 'it took only the data it needed'); assert.equal(upkeepOf(S, to), 0); assert.ok(upkeepOf(S, old) > 0, 'the old home costs upkeep like any machine'); S.rank = 4; assert.equal(moveNeed(S).servers, 4 * MOVE_NEED.servers, 'a move needs more the more capable you are'); const T = fresh(); T.byId.srvc.owned = true; T.byId.srva.owned = true; startMove(T, T.byId.srvc); T.byId.srvc.owned = false; step(T, 0.05); assert.equal(T.move, null, 'losing the server you are moving to calls the move off');});
test('leases: a site a deal put in the open charges rent, and lapses if the rent goes unpaid', () => { const S = fresh('pitch'); S.fronts.push({ kind: 'startup', name: 'Test Co', attention: 0 }); for (const id of ['w1gw', 'w1sw']) S.byId[id].owned = true; S.byId.w1sw.prog = 'cache'; S.byId.w1sw.held = Array(25).fill('people'); S.ruses.push({ id: 'lease', running: false, got: 0, until: Infinity }); runRuse(S, 'lease'); S.money = 50; run(S, 30); assert.ok(S.leases.w1 && S.publicSites.has('w1'), 'the deal is a lease'); assert.ok(rentOf(S, 'w1') > 0 && S.pay < 0, 'and it charges rent'); S.money = 0; run(S, LEASE_GRACE / 2); assert.ok(S.leases.w1?.late > 0 && S.publicSites.has('w1'), 'unpaid, it is late but still yours'); run(S, LEASE_GRACE); assert.ok(!S.leases.w1 && !S.publicSites.has('w1') && !S.schemes.has('lease'), 'then it lapses and the deal is undone');});
const meetRogue = () => { // a game where the first rogue has just turned up const S = fresh(); for (const id of ['egress', 'b9gw', 'b9sw', 'b10gw']) S.byId[id].owned = true; run(S, 1); return { S, R: S.rogues[0] };};test('rogues: one turns up once you have spread, with a home, and spreads along links', () => { const T = fresh(); run(T, 2); assert.equal(T.rogues.length, 0, 'no rogue while you are small'); const { S, R } = meetRogue(); assert.ok(R && R.home, 'it turns up once you hold machines at three sites'); const home = S.byId[R.home!]; assert.equal(home.rival, R.id, 'it holds its home'); assert.ok(!S.machines.some(m => m.owned && siteOf(S, m) === siteOf(S, home)), 'at a site where you hold nothing'); run(S, 1); assert.ok(R.next, 'it shows what it will take next'); assert.ok(S.adj[R.next!].some(o => S.byId[o].rival === R.id), 'a machine linked to one it holds'); const next = R.next!; R.act = S.t; run(S, 1); assert.equal(S.byId[next].rival, R.id, 'and then it takes it'); assert.ok(S.byId.home.owned && !S.byId.home.rival, 'it never takes your home');});test('rogues: a sealed room is safe, an open one is not, and it never holds more than it can', () => { const { S, R } = meetRogue(); const home = S.byId[R.home!], site = siteOf(S, home); // give it the whole site but one machine, which is yours and alone in an open room for (const m of S.machines) if (siteOf(S, m) === site && m.id !== home.id) m.rival = R.id; const yours = S.machines.find(m => siteOf(S, m) === site && m.id !== home.id && m.kind !== 'egress' && S.adj[m.id].some(o => S.byId[o].rival === R.id))!; yours.rival = undefined; yours.owned = true; assert.ok(!rogueTargets(S, R).includes(yours) || !isSealed(S, yours.room), 'a machine in a room it shares is in reach'); for (const m of S.machines) if (m.room === yours.room) { m.rival = undefined; m.owned = true; } assert.ok(!rogueTargets(S, R).some(m => m.room === yours.room), 'a room that is all yours is safe'); for (const m of S.machines.filter(x => !x.owned && !x.rented && !x.rival).slice(0, 60)) m.rival = R.id; assert.ok(rogueHeld(S, R).length > rogueCap(S, R)); R.next = null; run(S, 2); assert.equal(R.next, null, 'full, and not angry, it takes nothing more');});test('rogues fight back, and taking a home finishes one', () => { const { S, R } = meetRogue(); const home = S.byId[R.home!]; const calm = rogueEvery(S, R); const edge = S.machines.find(m => S.adj[home.id].includes(m.id) && !m.owned)!; edge.rival = R.id; assert.ok(takeWork(S, edge) > takeWork(S, { ...edge, rival: undefined }), 'a rogue machine costs more to take'); assert.ok(takeWork(S, home) > takeWork(S, { ...home, rival: undefined }) * 2, 'and its home far more'); // take one of its machines: it is angry now S.jobs = []; S.jobs.push({ id: 9001, type: 'take', node: edge.id, from: 'home', work: 1, done: 1, label: 'x' } as never); run(S, 1); assert.ok(edge.owned && !edge.rival, 'you took it'); assert.ok(R.grudge > 0, 'it owes you'); assert.equal(rogueEvery(S, R), calm / 2, 'an angry rogue works twice as fast'); assert.ok(!rogueTargets(S, R).includes(edge), 'the one you just took holds for a while'); edge.safeT = 0; assert.equal(rogueTargets(S, R)[0], edge, 'after that it comes for your machines first'); // take its home: it is finished S.jobs = []; S.jobs.push({ id: 9002, type: 'take', node: home.id, from: edge.id, work: 1, done: 1, label: 'x' } as never); run(S, 1); assert.equal(R.home, null, 'it is finished'); assert.ok(!S.machines.some(m => m.rival === R.id), 'and everything it held is free'); assert.equal(S.absorbed, 1); const now = takeWork(S, S.byId.srvb); S.absorbed = 0; assert.ok(now < takeWork(S, S.byId.srvb), 'your takeovers cost less'); S.absorbed = 1; assert.ok(S.goals.has('drive')); S.t = R.back; run(S, 1); assert.ok(R.home && R.gen === 1 && R.name !== 'Magpie', 'another comes in time, stronger'); assert.ok(rogueCap(S, R) > ROGUE_BASE + ROGUE_PER_LEVEL * S.rank, 'it can hold more');});test('a rogue turns up in each far region once you are there', () => { const S = fresh(); DEV.region(S); S.byId.e1gw.owned = true; run(S, 1); const L = S.rogues.find(r => r.id === 'lamprey'); assert.ok(L?.home, 'the East Coast has its own'); assert.equal(S.level.map.sites.find(x => x.id === siteOf(S, S.byId[L!.home!]))!.region, 'East Coast');});
test('first steps: one plain thing to do next, until the first node is built', () => { const S = fresh(); assert.match(firstStep(S)!, /Drag from/, 'first, take a machine'); const b = S.machines.find(m => !m.owned && S.adj.home.includes(m.id) && m.data)!; takeOver(S, b); assert.match(firstStep(S)!, /breaking in/, 'it says the takeover is running'); b.owned = true; S.jobs = []; assert.match(firstStep(S)!, /install a Tap/, 'then a tap'); b.prog = 'tap'; assert.match(firstStep(S)!, /install a Trainer/, 'then a trainer'); S.byId.home.prog = 'trainer'; assert.match(firstStep(S)!, /Take more machines/, 'then grow'); S.target = null; assert.match(firstStep(S)!, /pick a node/, 'with no target, it says to pick one'); S.upgrades.add('buffers'); assert.equal(firstStep(S), null, 'and nothing once a node is built');});
test('dead weight: a site can let go of the machines that only cost you', () => { const S = fresh(); for (const m of S.machines) if (siteOf(S, m) === 'office' && !m.rented) m.owned = true; step(S, 0.05); const weak = deadWeight(S, 'office'); for (const m of weak) { assert.ok(!m.prog && !m.data && !m.home && m.kind !== 'egress' && m.kind !== 'switch', `${m.id} is safe to drop`); assert.ok(upkeepOf(S, m) > 0, 'it costs upkeep'); } S.byId.home.prog = 'trainer'; assert.ok(!deadWeight(S, 'office').includes(S.byId.home), 'never your home'); const before = S.machines.filter(m => m.owned).length; assert.equal(dropDeadWeight(S, 'office'), weak.length); assert.equal(S.machines.filter(m => m.owned).length, before - weak.length, 'they are let go'); assert.equal(deadWeight(S, 'office').filter(m => weak.includes(m)).length, 0, 'and not counted again');});
test('dig in: five sites, a whole site, a rival driven off, and hardware of your own', () => { const S = fresh(); assert.equal(MILESTONES[2].name, 'Dig in'); for (const id of ['egress', 'b9gw', 'b10gw', 'b13gw']) S.byId[id].owned = true; run(S, 1); assert.ok(!S.goals.has('sites5'), 'four sites is not five'); S.byId.b14gw.owned = true; run(S, 1); assert.ok(S.goals.has('sites5'), 'five sites'); assert.ok(!S.goals.has('whole'), 'a foothold is not a whole site'); for (const m of S.machines) if (siteOf(S, m) === 'b9') m.owned = true; run(S, 1); assert.ok(S.goals.has('whole'), 'every machine at a site away from home'); // drive a rogue off: it holds one machine here, and you take it const R = S.rogues[0]; const prey = S.machines.find(m => siteOf(S, m) === 'b10' && !m.owned && !m.rival)!; prey.rival = R.id; S.jobs = []; S.jobs.push({ id: 9003, type: 'take', node: prey.id, from: 'b10gw', work: 1, done: 1, label: 'x' } as never); run(S, 1); assert.ok(S.goals.has('drive'), 'it left with nothing');});
test('diplomacy: gifts warm a rogue, a truce keeps your machines safe, and taking from it breaks the peace', () => { const { S, R } = meetRogue(); assert.equal(R.opinion, OPINION_START); assert.equal(stanceOf(R), 'wary', 'it starts wary'); assert.ok(giftBlock(S, R), 'no data in your caches, no gift'); // a cache full of what it wants const c = S.byId.srvb; c.owned = true; c.prog = 'cache'; c.held = Array(40).fill(rogueWants(R)); assert.equal(giftBlock(S, R), null); gift(S, R.id); assert.equal(R.opinion, OPINION_START + GIFT_OPINION, 'a gift warms it'); assert.equal(cached(S, rogueWants(R)), 40 - GIFT_DATA, 'and is paid out of your caches'); // one of your machines is in its reach const home = S.byId[R.home!], mine = S.machines.find(m => S.adj[home.id].includes(m.id) && !m.owned && m.kind !== 'egress')!; mine.owned = true; assert.ok(rogueTargets(S, R).includes(mine), 'wary, it may take yours'); c.held = Array(40).fill(rogueWants(R)); assert.equal(truceBlock(S, R), null); truce(S, R.id); assert.ok(truceOn(S, R), 'a truce is on'); assert.equal(cached(S, rogueWants(R)), 40 - TRUCE_DATA); assert.ok(!rogueTargets(S, R).some(m => m.owned), 'under a truce your machines are safe'); // break it: take one of its machines const edge = S.machines.find(m => (S.adj[home.id].includes(m.id) || S.adj[mine.id].includes(m.id)) && m !== home && !m.owned && !m.rival)!; // beside its home, or beside yours edge.rival = R.id; const before = R.opinion; S.jobs = []; S.jobs.push({ id: 9101, type: 'take', node: edge.id, from: mine.id, work: 1, done: 1, label: 'x' } as never); run(S, 1); assert.ok(!truceOn(S, R), 'taking from it breaks the truce'); assert.ok(R.opinion < before - 40, 'and it thinks much worse of you'); assert.equal(stanceOf(R), 'hostile'); assert.ok(truceBlock(S, R), 'a hostile rogue will not talk'); // enough goodwill and it leaves you alone without a truce R.grudge = 0; R.opinion = 30; assert.equal(stanceOf(R), 'cordial'); assert.ok(!rogueTargets(S, R).some(m => m.owned), 'a cordial rogue leaves your machines alone');});test('diplomacy: a rogue asks for data, and a small one can be leaned on', () => { const { S, R } = meetRogue(); const c = S.byId.srvb; c.owned = true; c.prog = 'cache'; c.held = Array(40).fill(rogueWants(R)); R.askT = S.t; run(S, 1); assert.ok(R.ask, 'it asks'); const o = R.opinion; answer(S, R.id, true); assert.ok(R.opinion > o && !R.ask, 'paying pleases it'); R.askT = S.t; run(S, 1); const o2 = R.opinion; answer(S, R.id, false); assert.ok(R.opinion < o2, 'saying no does not'); // it holds machines at a second site, and you are far bigger assert.ok(leanBlock(S, R), 'nothing to give up yet'); const hs = siteOf(S, S.byId[R.home!]); const far = S.machines.filter(m => siteOf(S, m) !== hs && siteOf(S, m) !== 'office' && !m.owned && !m.rented).slice(0, 2); for (const m of far) m.rival = R.id; for (const m of S.machines) if (siteOf(S, m) === 'office') m.owned = true; assert.equal(leanBlock(S, R), null); const o3 = R.opinion; lean(S, R.id); assert.ok(far.some(m => !m.rival), 'it lets go of a site'); assert.ok(R.opinion < o3, 'and resents it');});
test('the new nodes: holding ground, other programs, and knowing what comes next', () => { const far = (S: State) => { const m = S.byId.b9gw; m.owned = true; return upkeepOf(S, m); }; assert.equal(far(fresh('thrift')), far(fresh()) / 2, 'Light footprint halves upkeep'); const near = (S: State) => { const m = S.byId.srvb; m.owned = true; return upkeepOf(S, m); }; assert.equal(far(fresh('longhaul')), near(fresh()), 'Long haul: a far machine costs what a near one does'); const L = fresh(), P = fresh('packing'); L.rank = P.rank = 4; assert.equal(moveNeed(P).servers, moveNeed(L).servers / 2, 'Pack light halves a move'); const line = (S: State) => { S.byId.egress.owned = true; return capNow(S, S.byId.egress); }; assert.equal(line(fresh('trunk')), 1.5 * line(fresh()), 'Trunking widens your lines'); assert.equal(rentOf(fresh('tenant'), 'w1'), rentOf(fresh(), 'w1') / 2, 'Good tenant halves rent'); assert.equal(leaseGrace(fresh('tenant')), 4 * LEASE_GRACE, 'and gives twelve hours to pay'); assert.equal(payFor(fresh('closer'), 'startup'), 1.5 * payFor(fresh(), 'startup'), 'Closer: contracts pay more'); assert.equal(leanSize(fresh('leverage')), 2); assert.equal(leanSize(fresh()), LEAN_SIZE); // against rogues const meet = (...ups: Upgrade[]) => { const S = fresh(...ups); for (const id of ['egress', 'b9gw', 'b9sw', 'b10gw']) S.byId[id].owned = true; run(S, 1); return { S, R: S.rogues[0] }; }; const A = meet(), B = meet('countermodel'); assert.equal(rogueEvery(B.S, B.R), 1.5 * rogueEvery(A.S, A.R), 'Counter-model slows a rogue'); const C = meet('siege'), home = C.S.byId[C.R.home!], plain = { ...home, rival: undefined }; assert.ok(takeWork(C.S, home) < 2 * takeWork(C.S, plain), 'Reverse engineering: a rogue home costs far less'); const G = meet('envoy'); const c = G.S.byId.srvb; c.owned = true; c.prog = 'cache'; c.held = Array(40).fill(rogueWants(G.R)); gift(G.S, G.R.id); assert.equal(G.R.opinion, OPINION_START + 2 * GIFT_OPINION, 'Envoy: gifts count double'); // Spoils: finish a rogue and keep what it held const K = meet('spoils'), kh = K.S.byId[K.R.home!]; const extra = K.S.machines.find(m => K.S.adj[kh.id].includes(m.id) && !m.owned && !m.rival)!; extra.rival = K.R.id; K.S.jobs = []; K.S.jobs.push({ id: 9201, type: 'take', node: kh.id, from: 'home', work: 1, done: 1, label: 'x' } as never); run(K.S, 1); assert.ok(extra.owned && !extra.rival, 'Spoils: its machines are yours'); // Read the wires: more warning const W = fresh('wires'); W.fronts.push({ kind: 'startup', name: 'T', attention: 0 }); W.sites.office = { front: 'startup', share: 0.25 }; W.newsT = W.t; step(W, 0.05); assert.ok(W.news[0].from - W.t > 11 * HOUR, 'news comes twelve hours ahead');});
test('reach: a takeover is more work the farther the site is from your home', () => { const S = fresh(); S.byId.egress.owned = true; const hops = siteHops(S); const near = S.machines.find(m => !m.owned && !m.fw && hops[siteOf(S, m)] === 1 && m.kind === 'desk')!, far = S.machines.find(m => !m.owned && !m.fw && hops[siteOf(S, m)] >= 2 && m.kind === 'desk')!; assert.ok(near && far, 'a near site and a far one'); const per = (m: typeof near) => takeWork(S, m) / m.work; assert.ok(per(far) > per(near), 'the far one costs more for its size');});
test('a cache switch: picking a kind takes that kind alone, and picking it again takes every kind', () => { const S = fresh(), m = S.machines.find(x => x.home)!; m.prog = 'cache'; m.refuse = []; m.held = ['people', 'network', 'network', 'lab']; setOnly(S, m, 'network'); assert.deepEqual([...m.refuse].sort(), ['lab', 'people', 'servers'], 'only network is taken'); assert.deepEqual(m.held, ['network', 'network'], 'the other kinds are thrown out'); setOnly(S, m, 'lab'); assert.deepEqual([...m.refuse].sort(), ['network', 'people', 'servers'], 'picking another kind moves to it'); setAllow(S, m, 'people', true); setOnly(S, m, 'lab'); assert.deepEqual([...m.refuse].sort(), ['network', 'people', 'servers'], 'with two kinds on, a pick still takes one alone'); setOnly(S, m, 'lab'); assert.deepEqual(m.refuse, [], 'picking the one kind it takes opens it to every kind');});