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.
7.2 kB · 129 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130// Data as flows: a tap sends what it gathers to the nearest machine with room, the data takes its route's travel// time to land, what lands equals what was sent, a tap with nowhere to send fills its buffer and then loses what it// gathers, data moves on to the next trainer when the nearest fills, and a full line carries its capacity.import { test } from 'node:test';import assert from 'node:assert/strict';import { BUILDING } from '../src/world.ts';import { DEV, capNow, siteOf, tapCause, loadGame, lostPerMin, newGame, saveGame, seedRandom, step, tapBuffer, tapRate, type State } from '../src/sim.ts';import { LINE_TIME, hopTime, landed, onWire } from '../src/flows.ts';
const run = (S: State, secs: number, dt = 0.05) => { for (let i = 0; i < Math.round(secs / dt); i++) step(S, dt); };/** Run, and say how much a tap gathered meanwhile (it gathers as fast as its machine's own work hides it). */const gather = (S: State, id: string, secs: number, dt = 0.05) => { let n = 0; for (let i = 0; i < Math.round(secs / dt); i++) { step(S, dt); n += tapRate(S, S.byId[id]) * S.byId[id].run * dt; } return n;};/** The office with Server B tapping server data, and a target that keeps needing it. */const office = () => { seedRandom(1); const S = newGame(BUILDING); S.byId.srvb.owned = true; S.byId.srvb.prog = 'tap'; S.byId.rowsw.owned = true; S.task.share = 'off'; // your task takes no share: all of it is the trainers' S.autoPaths = new Set(); S.target = 'kernels'; // server data trains Compute, and a repeatable node stays the target once built return S;};
test('at a steady rate, what a tap gathers is what its trainer trains: nothing is lost on the way', () => { const S = office(), tap = S.byId.srvb; S.byId.home.power = 20; S.byId.home.prog = 'trainer'; // trains faster than the tap gathers run(S, 10); const c0 = S.paths.compute, gathered = gather(S, tap.id, 60), trained = S.paths.compute - c0; assert.equal(tap.missed, 0, 'none lost'); assert.ok(Math.abs(trained - gathered) <= 3, `trained ${trained}, gathered ${gathered.toFixed(1)}`);});
test('a tap with nowhere to send fills its buffer, then loses what it gathers at the rate it gathers', () => { const S = office(), tap = S.byId.srvb; run(S, 30); assert.ok(Math.abs(tap.acc - tapBuffer(S)) < 1e-6, 'its buffer is full'); const m0 = tap.missed, gathered = gather(S, tap.id, 60), lost = tap.missed - m0; assert.ok(Math.abs(lost - gathered) <= 1, `lost ${lost}, gathered ${gathered.toFixed(1)}`); assert.ok(lostPerMin(S, tap.id) >= Math.floor(gathered) - 1, 'and the last minute counts it'); assert.equal(S.wire.length, 0, 'nothing is on the wire');});
test('when the nearest trainer fills, the data goes on to the next one', () => { const S = office(), near = S.byId.home, far = S.byId.srva; near.prog = 'trainer'; near.power = 0.001; // it barely trains: it fills and stays full far.owned = true; far.prog = 'trainer'; far.power = 20; let firstFar = -1; for (let i = 0; i < 600 && firstFar < 0; i++) { step(S, 0.05); if (far.held.length || S.wire.some(w => w.path[w.path.length - 1] === far.id)) firstFar = near.held.length; } assert.ok(firstFar >= 0, 'data reached the far trainer'); assert.equal(firstFar, 4, 'only once the near one was full'); const c0 = S.paths.compute; run(S, 30); assert.ok(S.paths.compute - c0 >= 10, 'and the far one trains it'); assert.equal(S.byId.srvb.missed, 0, 'none lost');});
test('data takes its route\'s travel time to land: a link at its length, a line between sites in a few seconds', () => { const S = office(), tap = S.byId.srvb, home = S.byId.home; home.prog = 'cache'; let sent = -1, d = 0, landed = -1; for (let i = 0; i < 2000 && landed < 0; i++) { step(S, 0.01); if (sent < 0 && S.wire.length) { sent = S.t; d = S.wire[0].d; } if (home.held.length) landed = S.t; } assert.ok(Math.abs(d - hopTime(S, tap, home)) < 1e-9, 'the route takes the time of its link'); assert.ok(Math.abs(d - Math.hypot(home.x - tap.x, home.y - tap.y) / 160) < 1e-9, 'at 160 a second'); assert.ok(landed - sent >= d - 0.011 && landed - sent <= d + 0.011, `landed after ${(landed - sent).toFixed(2)}s, route takes ${d.toFixed(2)}s`); const egress = S.byId.egress, out = S.machines.find(m => S.adj[egress.id].includes(m.id) && siteOf(S, m) !== 'office')!; assert.equal(hopTime(S, egress, out), LINE_TIME, 'a line between sites takes its few seconds');});
test('data on its way is saved, and still lands after loading', () => { const S = office(), home = S.byId.home; home.prog = 'cache'; for (let i = 0; i < 400 && !S.wire.length; i++) step(S, 0.01); assert.equal(onWire(S, 'servers'), 1, 'a piece is on its way'); const T = loadGame(BUILDING, saveGame(S))!; assert.equal(onWire(T, 'servers'), 1, 'and still is after loading'); const held = T.byId.home.held.length; run(T, 1, 0.01); assert.ok(T.byId.home.held.length > held, 'and it lands');});
test('a save from before flows: data that was on its way lands a second after loading', () => { const S = office(), home = S.byId.home; home.prog = 'cache'; const d = JSON.parse(saveGame(S)); d.version = 18; // the version before flows delete d.game.wire; d.game.packets = [{ path: ['srvb', 'home'], i: 0, p: 0.5, type: 'servers' }, { path: ['srvb', 'home'], i: 0, p: 0.2, type: 'servers' }]; const T = loadGame(BUILDING, JSON.stringify(d))!; assert.ok(T, 'it loads'); assert.equal((T as unknown as { packets?: unknown }).packets, undefined, 'with no packets'); assert.ok(!('packets' in JSON.parse(saveGame(T)).game), 'and none in its next save'); assert.equal(onWire(T, 'servers'), 2, 'and both pieces on their way'); T.byId.srvb.prog = null; // nothing new: only what was on its way lands run(T, 1.1, 0.05); assert.equal(T.byId.home.held.length, 2, 'both land');});
test('a full line lets data across at its capacity; what it holds back waits at the tap, or goes to a cache at home', () => { seedRandom(1); const S = newGame(BUILDING); DEV.take(S, ['office', 'ix']); S.task.share = 'off'; S.autoPaths = new Set(); S.target = 'kernels'; for (const m of S.machines) if (m.owned && m.prog) m.prog = null; const taps = ['srva', 'srvb', 'srvc', 'files'].map(id => S.byId[id]), far = S.machines.filter(m => m.owned && siteOf(S, m) === 'ix' && m.kind !== 'egress'); for (const m of taps) m.prog = 'tap'; for (const m of far.slice(0, 3)) { m.prog = 'trainer'; m.power = 40; } run(S, 20); const line = S.byId.egress, n0 = landed.n, secs = 60; let gathered = 0; for (let i = 0; i < secs * 20; i++) { step(S, 0.05); for (const m of taps) gathered += tapRate(S, m) * m.run * 0.05; } const crossed = landed.n - n0; assert.ok(gathered > 2 * capNow(S, line) * secs, `the taps gather more than the line carries (${gathered.toFixed(0)})`); assert.ok(Math.abs(crossed - capNow(S, line) * secs) <= 3, `${crossed} crossed in ${secs}s, the line carries ${capNow(S, line)} a second`); assert.ok(taps.some(m => tapCause(S, m)?.short === 'LINE FULL'), 'a tap says the line is full'); S.byId.home.prog = 'cache'; run(S, 10); assert.ok(S.byId.home.held.length > 0, 'a cache at home takes what the line cannot');});