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.
13 kB · 257 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258// Notes: trouble pinned to the map, one for each kind, with one button that names the site it fixes.import { test } from 'node:test';import assert from 'node:assert/strict';import { BUILDING } from '../src/world.ts';import { DEV, HOUR, TAKE_BACK, measure, newGame, saveGame, rogueTake, seedRandom, siteName, step, takeWork, type Rogue, type State } from '../src/sim.ts';import { cutMoves, doMove, fate, launchRaid, looking, stepRaids } from '../src/raids.ts';import { chainHome, cutSites, relinks } from '../src/drift.ts';import { CUT_URGENT, TAKEN_HOURS, cutNote, fixFor, linking, notes, raidNote, slowOver, slowSpot, slowWhy, takenNote } from '../src/notes.ts';
const run = (S: State, secs: number) => { for (let i = 0; i < secs * 20; i++) step(S, 0.05); };/** Four sites whose only way home is the exchange's line, and the line lost: all four cut off. */const CUT = ['b1', 'b10', 'b9', 'uni'];const cutGame = () => { seedRandom(1); const S = newGame(BUILDING); DEV.take(S, ['office']); for (const id of ['ixgw', 'unigw', 'b9gw', 'b10gw', 'b1gw']) S.byId[id].owned = true; run(S, 10); S.byId.ixgw.owned = false; run(S, 15); return S;};/** As in raids.test.ts: Magpie holds the vet's machines next to your Outside line, and you hold a short line * of office machines with a trainer on Server A. Its takeovers are stopped, so only its raids move. */const raidGame = (): { S: State; R: Rogue } => { seedRandom(1); const S = newGame(BUILDING); for (const id of ['egress', 'b9gw', 'b9sw', 'b10gw']) S.byId[id].owned = true; run(S, 1); const R = S.rogues[0]; for (const m of S.machines) { m.rival = undefined; if (!m.home) m.owned = false; } for (const id of ['egress', 'sw1', 'rowsw', 'srva']) S.byId[id].owned = true; S.byId.srva.prog = 'trainer'; for (const id of ['b13m3', 'b13sw', 'b13gw']) S.byId[id].rival = R.id; R.home = 'b13m3'; R.next = null; R.act = Infinity; R.askT = Infinity; R.ask = null; R.raidT = 0; S.byId.home.power = 40; measure(S); // compute to spare, so a guard goes in before a raid comes (see fitRaid in raids.ts) return { S, R };};const machineNames = (S: State) => S.machines.map(m => m.name);
test('sites cut off are one note, and its button names a site, not a machine', () => { const S = cutGame(), n = cutNote(S)!; assert.equal(n.n, 4); assert.deepEqual([...n.items].sort(), CUT); assert.equal(n.label, 'CUT OFF'); assert.equal(n.fix?.key, 'take|ixgw||', 'one takeover links all four home: cheaper than four links'); assert.equal(n.fix?.title, 'Link 4 sites home'); assert.match(n.fix!.sub, /links 4 home/); assert.ok(!machineNames(S).some(name => n.fix!.title.includes(name)), 'no machine named on the button'); doMove(S, n.fix!.key); assert.ok(linking(S, 'b1'), 'the takeover is on its way home'); const after = cutNote(S)!; assert.equal(after.fix, null, 'nothing left to start'); assert.equal(after.busy, 'Linking it home');});
test('a takeover that is only the first step home says so, and one that links sites home links them', () => { seedRandom(1); const S = newGame(BUILDING); DEV.take(S, ['office']); for (const id of ['ixgw', 'unigw', 'b9gw', 'b10gw', 'b1gw']) S.byId[id].owned = true; run(S, 10); S.byId.egress.owned = false; S.byId.ixgw.owned = false; // the Outside line and the exchange's line go: two machines to take on the way home run(S, 15); const sites = CUT; assert.deepEqual(chainHome(S, 'b1').map(m => m.id), ['egress', 'ixgw'], 'the way home passes two you do not hold'); const take = cutMoves(S, 'b1').find(m => m.kind === 'take')!; assert.equal(take.at, 'egress'); assert.deepEqual(take.then, ['ixgw']); assert.deepEqual(relinks(S, S.byId.egress), [], 'taking the first alone links nothing home'); const fix = fixFor(S, take, sites); assert.equal(fix.title, 'Way home for 4 sites'); assert.match(fix.sub, /step 1 of 2/); assert.match(fix.tip, /^Take Outside line: the first of 2 machines to take on the way home for .*\. Then Line out at Mudflat Exchange\./); assert.doesNotMatch(fix.tip, /to your home server again/, 'it does not claim to link them'); assert.doesNotMatch(cutNote(S)!.fix!.tip, /Take Outside line: it links/, 'nor does the pin'); S.byId.egress.owned = true; // the first step taken: still cut off, and now one takeover links all four assert.equal(cutSites(S).length, 4); assert.deepEqual(relinks(S, S.byId.ixgw).sort(), [...sites].sort()); const n = cutNote(S)!; assert.equal(n.fix?.title, 'Link 4 sites home'); S.byId.ixgw.owned = true; run(S, 10); assert.deepEqual(cutSites(S), [], 'and it does what it says');});
test('one site cut off: its cheapest way home, named by the site', () => { seedRandom(1); const S = newGame(BUILDING); DEV.take(S, ['office']); S.byId.b13gw.owned = true; run(S, 10); S.byId.egress.owned = false; run(S, 15); const n = cutNote(S)!; assert.equal(n.site, 'b13'); assert.equal(n.fix?.title, `Link ${siteName(S, 'b13')} home`); doMove(S, n.fix!.key); assert.equal(cutNote(S)?.fix, null, 'a link on its way is not offered again'); S.byId.egress.owned = true; run(S, 10); assert.equal(cutNote(S), null, 'linked home: the note goes');});
test('a site cut for a day leads the note', () => { const S = cutGame(); S.drift.sites.b1.since = S.t - (CUT_URGENT + 1) * HOUR; const n = cutNote(S)!; assert.equal(n.site, 'b1', 'pinned to the site cut longest'); assert.equal(n.when, `${CUT_URGENT + 1}H AGO`); assert.equal(n.fix?.title, 'Link 4 sites home', 'the takeover links it home too, and is cheapest for each');});
test('raids are one note, pinned to what the soonest is after, with its best move named by site', () => { const { S, R } = raidGame(); assert.equal(raidNote(S), null); assert.equal(launchRaid(S, R), null); const n = raidNote(S)!, r = S.raids[0]; assert.equal(n.machine, r.prize); assert.equal(n.label, 'RAID'); assert.match(n.when, /^(<1H|\d+H)$/); assert.ok(n.fix, 'there is something to do'); assert.ok(n.fix!.title.includes(siteName(S, n.site)) || n.fix!.title.startsWith('Truce'), n.fix!.title); assert.ok(!machineNames(S).some(name => n.fix!.title.includes(name)), 'no machine named on the button'); assert.deepEqual(n.frame, [r.at, ...r.path]); doMove(S, n.fix!.key); const after = raidNote(S); assert.ok(!after || after.fix?.key !== n.fix!.key, 'the move made is not offered again'); S.raids = []; assert.equal(raidNote(S), null, 'no raid: no note');});
test('the pins asking how raids end, whenever they ask, never changes the game', () => { // raids that guards hold and wear down, with the pins asking about them at odd moments between steps, or never const play = (every: number) => { const { S, R } = raidGame(); S.byId.sw1.prog = 'firewall'; S.byId.rowsw.prog = 'sentry'; let raids = 0; for (let i = 0; i < 16 * HOUR * 20; i++) { if (!S.raids.length) { R.raided = undefined; R.raidT = 0; } // the next comes at once step(S, 0.05); raids = Math.max(raids, S.raids.length); if (every && i % every === every - 1) looking(() => notes(S)); } return { save: saveGame(S), raids }; }; const quiet = play(0); assert.ok(quiet.raids > 0, 'raids came'); for (const every of [1, 13]) assert.equal(play(every).save, quiet.save, `asking every ${every} steps`);});
test('what the pins work out about a raid is not what the rules are told', () => { const { S, R } = raidGame(); S.byId.sw1.prog = 'firewall'; S.byId.rowsw.prog = 'sentry'; assert.equal(launchRaid(S, R), null); const r = S.raids[0], held = looking(() => fate(S, r)); assert.notEqual(held.end, 'prize', 'the guards hold it'); S.byId.sw1.prog = null; S.byId.rowsw.prog = null; // gone, with no log line or job to say so assert.equal(fate(S, r).end, 'prize', 'the rules work it out for themselves'); assert.equal(looking(() => fate(S, r)).end, 'prize', 'and the pins may read what the rules worked out');});
test('a machine taken is a note until you take it back, or a day goes by', () => { const { S, R } = raidGame(); S.byId.b10gw.owned = true; S.byId.b9gw.owned = true; rogueTake(S, R, S.byId.srva, 'test', true); const n = takenNote(S)!; assert.equal(n.machine, 'srva'); assert.equal(n.label, 'TAKEN'); assert.equal(n.fix?.title, `Take back at ${siteName(S, 'office')}`); doMove(S, n.fix!.key); assert.equal(takenNote(S)?.busy, 'Taking it back'); S.byId.srva.owned = true; assert.equal(takenNote(S), null, 'yours again: the note goes'); S.byId.srva.owned = false; assert.ok(takenNote(S)); S.t += TAKEN_HOURS * HOUR; assert.equal(takenNote(S), null, 'a day on: the record keeps it, the map does not'); assert.equal(R.record![0].id, 'srva');});
test('notes come raids first, then cut sites, then machines taken', () => { const { S, R } = raidGame(); rogueTake(S, R, S.byId.sw1, 'test'); launchRaid(S, R); for (let i = 0; i < 4; i++) { S.t += 0.05; stepRaids(S, 0.05); } const kinds = notes(S).map(n => n.kind); assert.deepEqual(kinds.filter(k => k !== 'cut'), ['raid', 'taken']);});
test('machines taken: one press takes back every one, the one behind another with it, cheaply and with no trail', () => { const { S, R } = raidGame(); S.byId.b10gw.owned = true; S.byId.b9gw.owned = true; const fresh = takeWork(S, S.byId.srva); rogueTake(S, R, S.byId.rowsw, 'test', true); S.t += 1; rogueTake(S, R, S.byId.srva, 'test', true); // the last one taken sits behind the first const n = takenNote(S)!; assert.equal(n.n, 2); assert.equal(n.machine, 'srva', 'pinned to the last one taken'); assert.equal(n.fix!.title, 'Take 2 back'); assert.match(n.fix!.sub, /no trail$/); assert.ok(takeWork(S, S.byId.srva) <= Math.ceil(fresh * TAKE_BACK), 'a quarter of the work, with no rogue hold on it'); const grudge = R.grudge, opinion = R.opinion; doMove(S, n.fix!.key); assert.ok(S.jobs.some(j => j.type === 'take' && j.node === 'rowsw'), 'the one in reach starts'); assert.ok(S.byId.srva.claimed, 'the one behind it waits for it'); assert.equal(takenNote(S)?.busy, 'Taking it back', 'under way: no button'); for (let i = 0; i < 4 && S.jobs.length; i++) { S.jobs.forEach(j => (j.done = j.work)); run(S, 0.5); } assert.ok(S.byId.rowsw.owned && S.byId.srva.owned, 'both are yours again'); assert.ok(!S.trail.rowsw && !S.trail.srva, 'and no trail leads to them'); assert.deepEqual([R.grudge, R.opinion], [grudge, opinion], 'taking back what was yours does not sour it'); assert.equal(takenNote(S), null); // nothing taken is in reach: the press takes the way to it as well const T = raidGame(); T.S.byId.b10gw.owned = true; T.S.byId.b9gw.owned = true; rogueTake(T.S, T.R, T.S.byId.srva, 'test', true); T.S.byId.rowsw.owned = false; // you let go of the switch in front of it const w = takenNote(T.S)!; assert.equal(w.machine, 'srva'); assert.equal(w.fix?.title, `Take back at ${siteName(T.S, 'office')}`); assert.match(w.fix!.sub, /with 1 on the way$/); doMove(T.S, w.fix!.key); assert.ok(T.S.jobs.some(j => j.type === 'take' && j.node === 'rowsw') && T.S.byId.srva.claimed); assert.equal(takenNote(T.S)?.busy, 'Taking it back', 'under way: no button');});
test('more spare compute takes a machine back sooner', () => { const hoursFor = (power: number) => { const { S, R } = raidGame(); S.byId.home.power = power; measure(S); rogueTake(S, R, S.byId.srva, 'test', true); doMove(S, takenNote(S)!.fix!.key); let t = 0; while (!S.byId.srva.owned && t < 200) { run(S, 1); t++; } return t; }; const slow = hoursFor(8), quick = hoursFor(40); assert.ok(quick < slow, `${quick}s with more compute against ${slow}s`);});
test('what slowed the game says it in a few words, and the note goes when it is over', () => { const { S, R } = raidGame(); launchRaid(S, R); const r = S.raids[0], why = S.slowFor!; assert.equal(why, `raid:${r.id}`); assert.equal(slowWhy(S, why), `Magpie's raid on ${siteName(S, 'office')}`); assert.equal(slowSpot(S, why)?.machine, r.prize); assert.equal(slowOver(S, why), false); S.raids = []; assert.equal(slowOver(S, why), true, 'the raid is over'); S.watch.locks.office = { site: 'office', left: 6 * HOUR, by: ['Dana Whitlock'], why: 'Dana Whitlock wrote up Server' }; assert.equal(slowWhy(S, 'lock:office'), `Lockdown at ${siteName(S, 'office')}`); assert.equal(slowOver(S, 'lock:office'), false); delete S.watch.locks.office; assert.equal(slowOver(S, 'lock:office'), true, 'the lockdown stood down: the note is stale, so it goes'); assert.equal(slowOver(S, 'hunting'), true, 'a save from when suspicion had stages: its note goes');});