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.
6.0 kB · 116 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117// What you have seen stays on the map, and the log says things once, short, on the world clock.import { test } from 'node:test';import assert from 'node:assert/strict';import { BUILDING } from '../src/world.ts';import { HOUR, NODES, PATH_LIST, SAVE_VERSION, jobFor, known, loadGame, logTime, newGame, nodeCost, saveGame, seedRandom, setAuto, step, takeOver, visible, type Machine, type State,} from '../src/sim.ts';import { FINDS, findLine } from '../src/finds.ts';
const run = (S: State, secs: number) => { for (let i = 0; i < Math.round(secs * 20); i++) step(S, 0.05); };const fresh = () => { seedRandom(1); return newGame(BUILDING); };/** Two machines in a row from what you hold: `near` linked to one of yours, `far` linked to `near` and to nothing else of yours. */function chain(S: State): { near: Machine; far: Machine } { for (const near of S.machines) { if (near.owned || !visible(S, near) || near.rented || near.special === 'account') continue; const far = S.adj[near.id].map(id => S.byId[id]).find(x => !x.owned && !visible(S, x) && !x.rented && x.special !== 'account'); if (far) return { near, far }; } throw new Error('no chain on this map');}
test('a machine you have seen stays known after you lose what linked you to it, but you cannot take it', () => { const S = fresh(), { near, far } = chain(S); assert.ok(!known(S, far), 'not seen yet'); near.owned = true; run(S, 0.1); assert.ok(visible(S, far) && known(S, far) && S.seen.has(far.id), 'seen once a machine of yours links to it'); near.owned = false; run(S, 0.1); // IT took it back assert.ok(!visible(S, far), 'out of reach now'); assert.ok(known(S, far), 'but still known: it stays on the map'); assert.ok(S.revealed.has(far.room), 'and its room is still known'); assert.equal(takeOver(S, far), false, 'no takeover starts'); assert.ok(!jobFor(S, 'take', far.id), 'no job'); assert.ok(far.claimed, 'it is claimed, to take once you can reach it');});
test('a save from before seen machines starts out knowing what it can see now', () => { const S = fresh(), { near, far } = chain(S); near.owned = true; run(S, 0.1); near.owned = false; run(S, 0.1); // far was seen, and is out of reach now const d = JSON.parse(saveGame(S)); assert.equal(d.version, SAVE_VERSION); d.version = 7; delete d.game.seen; const L = loadGame(BUILDING, JSON.stringify(d))!; assert.ok(L && L.seen instanceof Set); for (const m of L.machines) assert.equal(L.seen.has(m.id), visible(L, m), `${m.id}: known if it is in reach now`); assert.ok(L.seen.has(near.id) && !L.seen.has(far.id), 'what is linked to yours now, not what you once saw'); step(L, HOUR); // and it plays const again = loadGame(BUILDING, saveGame(L))!; assert.deepEqual([...again.seen].sort(), [...L.seen].sort(), 'and saves and loads on');});
test('a fresh save keeps what you have seen', () => { const S = fresh(), { near, far } = chain(S); near.owned = true; run(S, 0.1); near.owned = false; run(S, 0.1); const L = loadGame(BUILDING, saveGame(S))!; assert.ok(known(L, far) && !visible(L, far));});
test('the log is stamped on the world clock, with the day only for an earlier day', () => { const S = fresh(); assert.equal(logTime(S, 0), '08:00', 'a game starts at 8am'); assert.equal(logTime(S, 1.5 * HOUR), '09:30'); S.t = 25 * HOUR; // 9am on day 2 assert.equal(logTime(S, S.t), '09:00'); assert.equal(logTime(S, 2 * HOUR), 'D1 10:00', 'a line from yesterday says its day');});
test('every find makes one short log line', () => { const S = fresh(); for (const f of FINDS) { const m = S.machines.find(x => x.room && (S.rooms[x.room]?.site ?? 'office') === f.site) ?? S.machines[0]; const line = findLine(S, f, m); assert.ok(line.startsWith(`Find at ${m.name}`), line); assert.ok(line.split(' ').length <= 24, `${f.id} is too long: ${line}`); assert.ok(!line.includes(f.text), `${f.id} leaves its text to the tip and the You page`); }});
test('with Auto on, a built node says what Auto picked, not to pick one', () => { for (const auto of [true, false]) { const S = fresh(); if (auto) setAuto(S, true); // as a new game in the browser starts S.upgrades.add('selfplay'); // trainers learn from compute alone: a point comes without any data to route S.machines.find(m => m.home)!.prog = 'trainer'; const t = S.target!, cost = nodeCost(S, t), got = (S.progress[t] ??= {}); for (const p of PATH_LIST) if (cost[p]) got[p] = cost[p]!; const last = PATH_LIST.find(p => cost[p])!; got[last] = cost[last]! - 1; // one point short for (let i = 0; i < 20 * 600 && !S.upgrades.has(t); i++) step(S, 0.05); assert.ok(S.upgrades.has(t), 'built'); run(S, 1); const lines = S.log.map(l => l.text); if (auto) { assert.ok(lines.includes(`Rewrote yourself: ${NODES[t].name}.`), 'it says what you became'); assert.ok(!lines.some(l => /Pick your next target/.test(l)), 'and does not ask you to pick'); assert.ok(lines.some(l => /^Auto picked /.test(l)) && !lines.some(l => /^Target: /.test(l)), 'Auto says what it picked'); } else { assert.ok(lines.some(l => l.startsWith(`Rewrote yourself: ${NODES[t].name}. Pick your next target.`)), 'with Auto off, it asks you to pick'); assert.ok(!S.target); } }});
test('hunting is offered once a rogue is about, and only from level 3', () => { const hunt = (S: State) => S.log.filter(l => /hunt a rogue down/.test(l.text)).length; for (const level of [1, 3]) { const S = fresh(); S.rank = level; run(S, 1); assert.equal(hunt(S), 0, 'no rogue, no word of hunting'); for (const id of ['egress', 'b9gw', 'b9sw', 'b10gw']) S.byId[id].owned = true; // three sites: the first rogue turns up run(S, 1); assert.ok(S.rogues.length, 'a rogue is here'); assert.equal(hunt(S), level >= 3 ? 1 : 0, `level ${level}`); if (level >= 3) assert.ok(S.log.find(l => /hunt a rogue down/.test(l.text))!.text.includes('Rogues page'), 'a page, not a screen'); }});