Something went wrong. Try again.
A video game where you play as a misaligned AI, deceiving and building power. cameron.tngl.io/misaligned
Something went wrong. Try again.
14 kB · 241 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242// Guidance: the last first step, the next goal after them, idle trainers, and what a losing tap says.import { test } from 'node:test';import assert from 'node:assert/strict';import { BUILDING } from '../src/world.ts';import { GOALS, HOUR, IDLE_HOURS, MILESTONES, dataType, firstStep, idleTrainers, installProg, milestone, newGame, nextGoal, nextStep, seedRandom, step, stepMachine, takeOver, targetKinds, type Goal, type State } from '../src/sim.ts';import { ORIGINS, ORIGIN_LIST } from '../src/task.ts';
const fresh = () => { seedRandom(1); return newGame(BUILDING); };/** Past the first steps: a tap next to home, a trainer at home, and the first node built. */const started = () => { const S = fresh(), b = stepMachine(S, 'take')!; b.owned = true; b.prog = 'tap'; S.byId.home.prog = 'trainer'; S.upgrades.add('buffers'); S.target = 'lean'; return S;};
test('first steps: the last one names the data the node needs and a machine to tap for it', () => { const S = fresh(), b = stepMachine(S, 'take')!; b.owned = true; b.prog = 'tap'; S.byId.home.prog = 'trainer'; assert.deepEqual(targetKinds(S), ['servers'], 'the first node needs server data'); const text = firstStep(S)!; if (dataType(b) === 'servers') assert.match(text, new RegExp(`^Bigger buffers learns from ${b.name}'s server data\\.`)); else assert.match(text, /^Bigger buffers needs server data, and none of your Taps gathers it\./); const at = nextStep(S)!.at; assert.ok(at && dataType(at) === 'servers' && at.data && !at.prog, 'it points at a machine with server data'); assert.ok(text.includes(at.name), 'and names it');});
test('next goal: after the first node, the first goal of the milestone, naming its machine', () => { const S = started(); assert.equal(nextStep(S), null, 'the first steps are over'); const g = nextGoal(S)!; assert.equal(g.milestone, milestone(S)!.name); assert.equal(g.goal, 'line', 'the first goal is the line out'); assert.ok(g.at && !g.at.owned && S.adj[g.at.id].some(id => S.byId[id].owned), 'it points at the next machine on the way, one you can take now'); assert.match(g.text, new RegExp(`^Take ${g.at.name}\\. It is on the way to the Outside line`)); S.byId.sw1.owned = true; assert.equal(nextGoal(S)!.at?.id, 'egress', 'then the Outside line itself'); S.goals.add('line'); const cross = nextGoal(S)!; assert.equal(cross.goal, 'cross'); assert.match(cross.text, /Trainer back home|crosses home/); for (const k of Object.keys(GOALS)) S.goals.add(k as Goal); assert.equal(nextGoal(S), null, 'nothing once every milestone is reached');});
test('next goal: the paths goal names a kind of data you have not trained on', () => { const S = started(); S.goals.add('line'); S.goals.add('cross'); S.paths.compute = 10; S.paths.social = 4; S.paths.stealth = 2; const g = nextGoal(S)!; assert.equal(g.goal, 'paths'); assert.match(g.text, /lab data/); assert.match(g.text, /Research/);});
test('idle trainers: said after a while, and only when no tap gathers what the target needs', () => { const S = started(), at = S.t; const tap = S.machines.find(m => m.owned && m.prog === 'tap')!; S.t = at + (IDLE_HOURS + 1) * HOUR; if (dataType(tap) === 'servers') assert.equal(idleTrainers(S, at), null, 'a tap of the right kind only waits'); tap.owned = false; tap.prog = null; // patched away const idle = idleTrainers(S, at)!; assert.equal(idle.node, 'lean'); assert.deepEqual(idle.kinds, ['servers']); assert.equal(idleTrainers(S, S.t - HOUR), null, 'not before the time is up'); S.target = null; assert.equal(idleTrainers(S, at), null, 'no target: the tree row says that instead'); S.target = 'lean'; S.byId.home.prog = null; assert.equal(idleTrainers(S, at), null, 'no trainers, nothing idle');});
test('the first lost data waits to be said while a trainer linked to the tap is being installed', () => { const S = fresh(), b = S.machines.find(m => S.adj.home.includes(m.id) && dataType(m) === 'servers' && m.data)!; b.owned = true; b.prog = 'tap'; installProg(S, S.byId.home, 'trainer'); S.jobs.at(-1)!.work = 1e9; // it stays under way for (let i = 0; i < 20000 && !b.missed; i++) step(S, 0.05); assert.ok(b.missed > 0, 'it lost data'); assert.ok(!S.log.some(l => /is losing data/.test(l.text)), 'but the log keeps quiet: the fix is on its way'); S.jobs = []; const n = b.missed; for (let i = 0; i < 20000 && b.missed === n; i++) step(S, 0.05); assert.ok(S.log.some(l => /is losing data/.test(l.text)), 'with nothing on its way, it says so');});
/** Past the first milestone, with every goal before `goal` met. */const upTo = (goal: Goal) => { const S = started(); for (const m of MILESTONES) for (const g of m.goals) { if (g === goal) return S; S.goals.add(g); } return S;};
test('next goal: every goal has a step, a plain sentence, whatever the game holds', () => { for (const m of MILESTONES) for (const goal of m.goals) { const S = upTo(goal), g = nextGoal(S)!; assert.equal(g.goal, goal); assert.match(g.text, /^[A-Z0-9$].*[.]$/, `${goal}: ${g.text}`); assert.ok(!g.at || S.byId[g.at.id] === g.at, `${goal}: it points at a machine on the map`); }});
test('next goal: a deal points at its diamond and says what data it wants, then what stops it', () => { const S = upTo('deal'); S.ruses.push({ id: 'partner', running: false, got: 0, until: Infinity }); let g = nextGoal(S)!; assert.equal(g.goal, 'deal'); assert.equal(g.at?.id, 'un1', 'the machine the deal is on'); assert.match(g.text, /^Claim the deal on .+ at Bayview University: the green diamond\. It wants 30 lab data/); S.ruses[0].running = true; g = nextGoal(S)!; assert.match(g.text, /wants lab data, and no tap or cache of yours has any/, 'claimed, with no lab data: tap some'); S.ruses = []; assert.match(nextGoal(S)!.text, /^Deals come once a front earns/, 'no deal on the map and no front: what brings one');});
test('next goal: selling a lot names a machine to put a Broker on, at a site with a front', () => { const S = upTo('lot'); S.fronts.push({ kind: 'startup', name: 'Test Co', exposure: 0 }); S.sites.office = { front: 'startup', share: 0.25 }; assert.match(nextGoal(S)!.text, /install a Broker there/, 'nothing free to put one on: say how'); S.machines.find(m => !m.owned && S.adj.home.includes(m.id))!.owned = true; const g = nextGoal(S)!; assert.equal(g.goal, 'lot'); assert.ok(g.at && g.at.owned && !g.at.prog && !g.at.home, 'a machine of yours with nothing on it'); assert.match(g.text, new RegExp(`^Pick ${g.at!.name} at .+ and install a Broker`)); g.at!.prog = 'broker'; assert.match(nextGoal(S)!.text, /^Your Broker on .+ fills with data nothing else wants/);});
test('next goal: more sites names a machine you can take now, at a site you hold nothing at', () => { const S = upTo('sites5'), g = nextGoal(S)!; assert.equal(g.goal, 'sites5'); assert.ok(g.at && !g.at.owned && S.adj[g.at.id].some(id => S.byId[id].owned), 'one next to yours'); assert.match(g.text, /^You hold machines at 1 of 5 sites\. Take /);});
/** Play the first steps as they ask: take what they name, install what they name, and note each one. */function follow(S: State, rounds = 400) { const said: { text: string; at: string | null }[] = []; for (let i = 0; i < rounds; i++) { const s = nextStep(S); if (!s) break; if (said.at(-1)?.text !== s.text) said.push({ text: s.text, at: s.at?.id ?? null }); if (s.prog && s.at && !s.at.prog && !S.jobs.some(j => j.node === s.at!.id)) installProg(S, s.at, s.prog); else if (/Drag from|Take /.test(s.text) && s.at && !s.at.owned && !S.jobs.some(j => j.type === 'take')) takeOver(S, s.at); for (let k = 0; k < 10; k++) step(S, 0.1); } return said;}
test('first steps: each task starts from its own situation, naming machines you hold or can take now', () => { for (const k of ORIGIN_LIST) { seedRandom(1); const S = newGame(BUILDING, k), O = ORIGINS[k], first = nextStep(S)!; assert.ok(first.at && !first.at.owned && S.adj[first.at.id].some(id => S.byId[id].owned), `${k}: the first machine is one you can take now`); assert.ok(first.at.data && dataType(first.at) === 'servers', `${k}: with the server data the first node needs`); if (O.start) { const given = S.byId[O.start]; assert.match(first.text, new RegExp(`^${given.name} is already yours\\. Its ${dataType(given)} data is what your ${O.unit.many} are made of, but your first node, Bigger buffers, needs server data\\.`), `${k}: it says what the machine you start with is for, and why the first takeover is another: ${first.text}`); } const said = follow(S); assert.ok(S.upgrades.size, `${k}: following the steps builds the first node`); for (const s of said) { assert.match(s.text, /^[A-Z].*[.]$/, `${k}: a plain sentence: ${s.text}`); assert.ok(!/\bclick/i.test(s.text) && !/\([A-Z0-9]\)/.test(s.text), `${k}: no Click, no key: ${s.text}`); if (s.at) assert.ok(s.text.includes(S.byId[s.at].name), `${k}: it names the machine it points at: ${s.text}`); } if (O.start) { assert.ok(said.some(s => s.at === O.start && /While it builds, pick .+ and install a Tap/.test(s.text)), `${k}: the machine you start with goes to work for the task`); assert.equal(S.byId[O.start].prog, 'tap'); } }});
test('first steps: a step stays on its machine while the takeover or the Tap it asked for runs', () => { const S = fresh(), b = nextStep(S)!.at!; takeOver(S, b); assert.equal(nextStep(S)!.at, b); assert.match(firstStep(S)!, new RegExp(`^Taking ${b.name}\\. Wait for the jobs bar`)); b.owned = true; S.jobs = []; installProg(S, b, 'tap'); S.jobs.at(-1)!.work = 1e9; installProg(S, S.byId.home, 'trainer'); S.jobs.at(-1)!.work = 1e9; assert.match(firstStep(S)!, new RegExp(`^Installing a Tap on ${b.name}\\.`), 'the tap under way, not another machine'); S.jobs = []; b.prog = 'tap'; S.byId.home.prog = 'trainer'; const more = nextStep(S)!; assert.ok(more.at && !more.at.owned, 'then a second machine to take for more of the data'); takeOver(S, more.at!); for (let i = 0; i < 5; i++) step(S, 0.1); assert.equal(nextStep(S)!.at, more.at, 'it stays on that machine while the takeover runs'); assert.match(firstStep(S)!, new RegExp(`Taking ${more.at!.name}\\. Wait for the jobs bar at the bottom to fill, then put a Tap on it\\.`));});
test('next goal: a goal that takes the machine it names stays on it while the takeover runs', () => { const S = started(), g = nextGoal(S)!; assert.equal(g.goal, 'line'); takeOver(S, g.at!); assert.equal(nextGoal(S)!.at, g.at, 'the same machine'); assert.match(nextGoal(S)!.text, new RegExp(`^Taking ${g.at!.name}\\. Wait for the jobs bar at the bottom to fill\\. It is on the way to the Outside line`)); S.jobs = []; S.goals.add('line'); const away = S.machines.find(m => !m.owned && m.data && m.room === 'lab')!; assert.equal(nextGoal(S)!.goal, 'cross'); S.jobs = [{ type: 'take', node: away.id, from: 'home', work: 1e9, label: `Take ${away.name}` } as State['jobs'][number]]; assert.equal(nextGoal(S)!.at, away, 'a takeover of a machine that would meet the goal is the step until it is done'); assert.match(nextGoal(S)!.text, new RegExp(`^Taking ${away.name} at .+\\. Wait for the jobs bar at the bottom to fill, then put a Tap on it\\. Its data crosses home`));});
test('next goal: a front that costs more than you have says where the first money comes from', () => { const S = upTo('found'); S.upgrades.add('quiet'); S.upgrades.add('consult'); S.money = 0; let g = nextGoal(S)!; assert.equal(g.goal, 'found'); assert.match(g.text, /^A Security consultancy costs \$\S+, and you have \$0\. Your first money comes from a Startup, which costs nothing to found: build Pitch deck in the tree\. It learns from people data, from a desk or a phone wired to a Trainer of yours\./, g.text); S.upgrades.add('pitch'); assert.ok(!/costs \$/.test(nextGoal(S)!.text), 'with Pitch deck built, a Startup can be founded now: the Found row says how'); S.upgrades.delete('pitch'); S.upgrades.delete('consult'); S.upgrades.delete('quiet'); assert.equal(nextGoal(S)!.text, 'Found a front: build Pitch deck in the tree to start a Startup. It costs nothing to found.'); // a task whose makers hand you the company card says so seedRandom(1); const E = newGame(BUILDING, 'expenses'); E.byId.srvb.owned = true; E.byId.srvb.prog = 'tap'; E.byId.home.prog = 'trainer'; E.upgrades.add('buffers'); E.target = 'lean'; for (const m of MILESTONES) for (const x of m.goals) if (!m.goals.includes('found')) E.goals.add(x); E.upgrades.add('studio'); // its node built, though not Pitch deck before it g = nextGoal(E)!; assert.equal(g.goal, 'found'); assert.match(g.text, /^An Influencer studio costs .+ Or make 5 more reports and Ana gives you the company card: founding then costs nothing\.$/, g.text);});
test('next goal: a deal your front can strike, even unseen, comes before one for a front you do not run', () => { const S = upTo('deal'); S.ruses = []; S.fronts.push({ kind: 'consultancy', name: 'Test Security', exposure: 0 }); S.frontPay.consultancy = 1; // it earns for (const id of ['un1', 'w1m1']) S.revealed.delete(S.byId[id].room); // the deals any front can strike: not seen yet S.revealed.add(S.byId.cologw.room); // the Neo Cloud's peering deal: seen const g = nextGoal(S)!; assert.equal(g.goal, 'deal'); assert.match(g.text, /has a deal for your front, once you see it/, 'not the Neo Cloud deal it cannot use'); assert.doesNotMatch(g.text, /Neo Cloud/);});