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.
18 kB · 297 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298// Cumulo's hall: hundreds of slots, who else rents there, what that does to the price, the three ways you pay,// and renting many at once.import { test } from 'node:test';import assert from 'node:assert/strict';import { BUILDING } from '../src/world.ts';import { HOUR, SIZES, billOf, canRent, clusters, endLease, hourOf, leaseRate, loadGame, localHour, newGame, openAccount, payBlock, rent, rentBlock, rentRate, reserveCost, saveGame, seedRandom, setPay, step, type State } from '../src/sim.ts';import { CUSTOMERS, call } from '../src/cloud.ts';import { NOTICE, PRICE_EMPTY, PRICE_FULL, RESERVE, RESERVE_HOURS, SPOT, blockOf, hallCount, hallFull, hallPrice, hallWanted, halls, nextBumped, priceDay, rentMany, rentPlan, rentProgs, runOf, slotsAt, tenantOf } from '../src/hall.ts';import { HALL_COLS, HALL_PAIRS } from '../src/world.ts';import { DAY_SECONDS } from '../src/money.ts';
const run = (S: State, secs: number) => { for (let i = 0; i < secs * 20; i++) step(S, 0.05); };/** A game where the office and the exchange's line are yours, you have an account at Cumulo, and money to spend. */function game() { seedRandom(1); const S = newGame(BUILDING); for (const m of S.machines) if ((S.rooms[m.room].site ?? 'office') === 'office') m.owned = true; S.byId.ixgw.owned = true; // the exchange's line: Cumulo is on it openAccount(S, S.byId.cloudgw); S.money = 5000; step(S, 0.05); return S;}/** Empty the hall of other companies, and keep new ones away for a long time. */const clear = (S: State) => { S.hall.tenants = {}; S.hall.wants = {}; S.hall.next.cloud = S.t + 1e6; };/** Put a company in every free slot but these: the first company, or these ones in turn. The others may still come looking. */const fill = (S: State, ...keep: string[]) => fillWith(S, [0], ...keep);const fillWith = (S: State, who: number[], ...keep: string[]) => { let i = 0; for (const m of slotsAt(S, 'cloud')) if (!m.owned && !keep.includes(m.id)) S.hall.tenants[m.id] = { who: who[i++ % who.length], until: S.t + 1e6 };};const SLOTS = HALL_COLS * HALL_PAIRS * 2;/** A company comes looking now, on a day when they want the whole hall. */const arrive = (S: State) => { S.cloud.drift = 1; S.cloud.driftAim = 1; S.cloud.driftT = S.t + 1e6; S.hall.next.cloud = S.t; step(S, 0.05); };
test('the hall: hundreds of slots in rows of bays, and an account lets you rent any free one', () => { seedRandom(1); const T = newGame(BUILDING); assert.equal(slotsAt(T, 'cloud').length, SLOTS); assert.ok(SLOTS >= 200); assert.ok(!canRent(T, T.byId.c9), 'no account, no slot'); const S = game(); const bays = S.machines.filter(m => /^cb\d+$/.test(m.id)); assert.equal(bays.length, SLOTS / 8, 'a bay switch for every eight slots'); for (const id of ['cloudgw', ...bays.map(m => m.id)]) assert.ok(S.byId[id].owned, `${id} comes with the account`); for (const m of slotsAt(S, 'cloud')) assert.equal(canRent(S, m), !tenantOf(S, m), `${m.id}: free slots can be rented, a company's slot cannot`); assert.ok(S.adj.c1.includes('c2') && S.adj.c19.includes('c20') && !S.adj.c20.includes('c21'), 'a slot links to the next in its row, not round the end'); for (const m of slotsAt(S, 'cloud')) assert.equal(S.adj[m.id].filter(id => bays.some(b => b.id === id)).length, 1, `${m.id} hangs off one bay switch`); // every slot reaches the gateway in a few hops const far: Record<string, number> = { cloudgw: 0 }, q = ['cloudgw']; for (const id of q) for (const o of S.adj[id]) if (far[o] === undefined && (S.rooms[S.byId[o].room].site === 'cloud')) { far[o] = far[id] + 1; q.push(o); } assert.ok(slotsAt(S, 'cloud').every(m => far[m.id] !== undefined && far[m.id] <= 12), `the farthest slot is ${Math.max(...slotsAt(S, 'cloud').map(m => far[m.id]))} hops in`);});
test('other companies: some are there from the start, more by day than at night', () => { const S = game(); assert.equal(hallCount(S, 'cloud').theirs, hallWanted(S, 'cloud'), 'the hall starts as full as the hour wants'); assert.ok(hallCount(S, 'cloud').theirs >= SLOTS / 4); assert.ok(hallWanted(S, 'cloud', 14) > hallWanted(S, 'cloud', 2) + SLOTS / 4, 'they want far more slots in the afternoon than in the small hours'); for (const site of Object.keys(halls(S))) { const stays: Record<number, { n: number; until: Set<number> }> = {}; for (const m of slotsAt(S, site)) { const t = tenantOf(S, m); if (t) { const x = stays[t.who] ??= { n: 0, until: new Set() }; x.n++; x.until.add(t.until); } } for (const [who, x] of Object.entries(stays)) { assert.equal(x.until.size, 1, `${site}: a company has one stay in a hall, its slots ending together`); assert.ok(x.n <= blockOf(+who), `${site}: ${CUSTOMERS[+who][0]} has ${x.n} slots, no more than it wants`); } assert.ok(Object.keys(stays).length > 3 && Object.values(stays).some(x => x.n > 1), `${site}: several companies, some with many slots`); } // a company's slots sit side by side where they can const runs = slotsAt(S, 'cloud').filter((m, i, all) => i && tenantOf(S, m) && tenantOf(S, all[i - 1])?.who === tenantOf(S, m)!.who).length; assert.ok(runs > hallCount(S, 'cloud').theirs / 2, `${runs} of ${hallCount(S, 'cloud').theirs} slots sit beside another of the same company's`); let noon = 0, night = 0; let most = 0; for (let h = 0; h < 72; h++) { run(S, HOUR); const n = hallCount(S, 'cloud').theirs, at = hourOf(S); most = Math.max(most, n); if (at >= 12 && at < 17) noon += n; if (at >= 1 && at < 6) night += n; } assert.ok(noon > night * 1.5, `the hall fills by day and empties at night: ${noon} against ${night}`); assert.ok(most >= SLOTS / 2, `on a busy afternoon the companies fill much of it: ${most} of ${SLOTS}`);});
test('the going price follows how full the hall is, and your own machines fill it too', () => { const S = game(); clear(S); assert.equal(hallPrice(S, 'cloud'), PRICE_EMPTY); assert.equal(rentRate(S, 'medium', 'demand', 'cloud'), SIZES.medium.rate * PRICE_EMPTY); rent(S, S.byId.c1, 'small', 'demand'); assert.ok(hallPrice(S, 'cloud') > PRICE_EMPTY, 'a slot you take counts'); fill(S); assert.equal(hallFull(S, 'cloud'), 1); assert.equal(hallPrice(S, 'cloud'), PRICE_FULL); assert.equal(billOf(S, S.byId.c1), SIZES.small.rate * PRICE_FULL, 'on demand pays the going price as it moves'); assert.equal(rentRate(S, 'medium', 'spot', 'cloud'), SIZES.medium.rate * PRICE_FULL * SPOT, 'spot is a share of it'); assert.equal(rentRate(S, 'medium', 'reserved', 'cloud'), leaseRate(S, 'medium') * RESERVE, 'and reserved does not move'); const day = priceDay(S, 'cloud'); assert.equal(day.length, 24); assert.ok(day[14] >= day[2], 'the price picture is dearer in the afternoon');});
test('on demand is billed by the second; reserved is paid now and goes on demand when its day is up', () => { const S = game(); clear(S); rent(S, S.byId.c1, 'medium', 'demand'); const was = S.money, rate = billOf(S, S.byId.c1); run(S, 2); assert.ok(Math.abs(was - S.money - rate * 2) < 0.05, `on demand: ${was - S.money} for two seconds at ${rate}`); const cost = reserveCost(S, 'medium'), had = S.money; rent(S, S.byId.c2, 'medium', 'reserved'); assert.equal(S.byId.c2.lease?.pay, 'reserved'); assert.ok(Math.abs(had - S.money - cost) < 1e-6, 'reserved is paid at once'); assert.equal(billOf(S, S.byId.c2), 0, 'and then costs nothing by the second'); assert.ok(cost < SIZES.medium.rate * RESERVE_HOURS * HOUR, 'a reserved day is cheaper than a day at the list price'); assert.match(payBlock(S, S.byId.c2, 'spot')!, /reserved/, 'a reserved machine stays reserved until its day is up'); run(S, RESERVE_HOURS * HOUR + 1); assert.equal(S.byId.c2.lease?.pay, 'demand', 'when the day is up it stays, on demand'); assert.ok(S.byId.c2.owned && billOf(S, S.byId.c2) > 0);});
test('spot: when the hall is full, a company that wants a slot takes your spot machine, with notice', () => { const S = game(); clear(S); rent(S, S.byId.c1, 'medium', 'spot'); rent(S, S.byId.c2, 'small', 'spot'); rent(S, S.byId.c3, 'small', 'demand'); assert.equal(nextBumped(S, 'cloud')?.id, 'c2', 'the smallest spot machine goes first'); S.byId.c2.prog = 'cache'; S.byId.c2.held = ['lab', 'lab']; // free slots left: a company takes those first, and only then your spot machines const room = blockOf(1); fillWith(S, [0, ...CUSTOMERS.map((_, i) => i).slice(2)], ...slotsAt(S, 'cloud').slice(10, 10 + room).map(m => m.id)); // every company but one has its slots: Thesis Panic comes, and finds just enough room arrive(S); assert.ok(slotsAt(S, 'cloud').slice(10, 10 + room).every(m => tenantOf(S, m)?.who === 1), 'it takes the free slots'); assert.equal(S.byId.c2.lease?.until, null, 'and your spot machines are safe'); // now the hall is full: the next company wants slots, and has to take spot machines for (const m of slotsAt(S, 'cloud')) if (tenantOf(S, m)?.who === 2) delete S.hall.tenants[m.id]; // Crumb Analytics is done elsewhere, and comes looking for (const m of slotsAt(S, 'cloud')) if (!m.owned && !tenantOf(S, m)) S.hall.tenants[m.id] = { who: 0, until: S.t + 1e6 }; arrive(S); const who = S.hall.wants.c2; assert.equal(who, 2); assert.ok(S.byId.c2.lease!.until !== null, 'the next one gives your smallest spot machine notice'); assert.equal(S.hall.wants.c1 !== undefined, blockOf(2) > 1, 'and the next smallest, if it wants more than one slot'); assert.ok(S.log.some(l => l.text.includes(CUSTOMERS[who][0]) && /goe?s? in \d hours/.test(l.text)), 'and the log names it'); assert.equal(S.byId.c3.lease?.until, null, 'on demand is never taken'); S.hall.next.cloud = S.t + 1e6; run(S, NOTICE * HOUR + 0.5); assert.ok(!S.byId.c2.owned && !S.byId.c2.lease && !S.byId.c2.held.length, 'after the notice the machine and its data are gone'); assert.equal(tenantOf(S, S.byId.c2)?.who, who, 'and the company has the slot'); if (blockOf(2) === 1) assert.ok(S.byId.c1.owned, 'the bigger spot machine is still yours');});
test('spot: paying on demand keeps the machine, and it cannot go back to spot while the hall is full', () => { const S = game(); clear(S); rent(S, S.byId.c1, 'medium', 'spot'); fill(S); arrive(S); assert.ok(S.hall.wants.c1 !== undefined); setPay(S, S.byId.c1, 'demand'); S.hall.next.cloud = S.t + 1e6; run(S, NOTICE * HOUR + 0.5); assert.ok(S.byId.c1.owned && S.byId.c1.lease?.pay === 'demand' && S.byId.c1.lease.until === null, 'it is still yours'); assert.equal(S.hall.wants.c1, undefined, 'the company went elsewhere'); assert.match(payBlock(S, S.byId.c1, 'spot')!, /full/); delete S.hall.tenants.c12; assert.equal(payBlock(S, S.byId.c1, 'spot'), null, 'with a slot free it can'); assert.match(rentBlock(S, S.byId.c12, 'large', 'reserved') ?? 'ok', /ok|you have/);});
test('one market: a full hall sends companies to your Neo Cloud, and a company you sign leaves the hall', () => { const S = game(); S.fronts.push({ kind: 'neocloud', name: 'Fogbank', exposure: 0 }); S.sites.office = { front: 'neocloud', share: 1 }; run(S, 0.5); clear(S); S.cloud.customers = []; S.cloud.reputation = 100; rent(S, S.byId.c1, 'small', 'demand'); // you hold a slot the hall's other companies would have had fillWith(S, CUSTOMERS.map((_, i) => i).slice(12)); // the big companies have the hall, so a small one comes looking: one your office can serve arrive(S); assert.equal(S.cloud.customers.length, 1, 'no room at Cumulo: it calls you, and signs'); assert.ok(S.log.some(l => /had no room/.test(l.text))); // a company with a slot in the hall signs with you const slot = 'c5', who = S.hall.tenants[slot].who, theirs = slotsAt(S, 'cloud').filter(m => tenantOf(S, m)?.who === who); call(S, who, '', { need: 2, hours: 2, pay: 1 }); assert.ok(S.cloud.customers.some(c => c.who === who)); assert.ok(theirs.length > 1 && theirs.every(m => !tenantOf(S, m)), 'all its slots at Cumulo are free again');});
test('machines rented side by side train as one cluster; a company between them splits it', () => { const S = game(); clear(S); for (const id of ['c1', 'c2', 'c4']) { rent(S, S.byId[id], 'medium', 'demand'); S.byId[id].prog = 'trainer'; } const cl = clusters(S); assert.equal(cl.c1.length, 2, 'c1 and c2 are linked'); assert.equal(cl.c4.length, 1, 'c4 is alone: c3 is not yours'); rent(S, S.byId.c3, 'small', 'demand'); S.byId.c3.prog = 'trainer'; assert.equal(clusters(S).c1.length, 4);});
test('the hall survives a save, and ending a rental frees the slot', () => { const S = game(); clear(S); fill(S, 'c1', 'c2'); rent(S, S.byId.c1, 'small', 'spot'); const L = loadGame(BUILDING, saveGame(S))!; assert.deepEqual(L.hall.tenants, S.hall.tenants); assert.equal(L.byId.c1.lease?.pay, 'spot'); endLease(L, L.byId.c1, 'x'); assert.ok(canRent(L, L.byId.c1)); assert.equal(hallCount(L, 'cloud').free, 2);});
test('a hall in each region: its gateway joins that region\'s line, and it keeps that region\'s hours', () => { const S = game(); assert.deepEqual(Object.keys(halls(S)).sort(), ['cloud', 'cloudas', 'cloude', 'cloudeu']); const region = (id: string) => S.level.map.sites.find(s => s.id === id)!.region; assert.deepEqual(['cloud', 'cloude', 'cloudeu', 'cloudas'].map(region), ['West Coast', 'East Coast', 'Europe', 'East Asia']); for (const id of ['cloude', 'cloudeu', 'cloudas']) assert.equal(slotsAt(S, id).length, SLOTS); assert.ok(S.adj.cegw.includes('e1gw') && S.adj.cugw.includes('eu1gw')); // no account until you hold the line it hangs off openAccount(S, S.byId.cugw); assert.ok(!S.byId.cugw.owned, 'Europe is out of reach'); S.byId.eu1gw.owned = true; openAccount(S, S.byId.cugw); assert.ok(S.byId.cugw.owned && S.byId.cub2.owned && !S.byId.cegw.owned, 'an account at one hall is not an account at the others'); clear(S); rent(S, S.byId.cu5, 'medium', 'demand'); assert.equal(S.byId.cu5.name, 'Medium 5'); // the same market, on each hall's own clock: when the office's afternoon fills the West hall, Europe's is emptying S.t += (14 - hourOf(S)) * HOUR; assert.equal(Math.round(localHour(S, 'cloudeu')), 23); assert.equal(hallWanted(S, 'cloud'), hallWanted(S, 'cloud', 14)); assert.equal(hallWanted(S, 'cloudeu'), hallWanted(S, 'cloud', 23)); assert.ok(hallWanted(S, 'cloudeu') < hallWanted(S, 'cloud') - 2, 'so there is always a hall where it is night'); assert.ok(rentRate(S, 'medium', 'demand', 'cloudeu') > rentRate(S, 'medium', 'demand', 'cloud'), 'each hall has its own price: the one you just rented in is fuller');});
test('a company can have a slot in each hall, and one you sign leaves them all', () => { const S = game(); run(S, 1); clear(S); S.hall.tenants.c5 = { who: 3, until: S.t + 1e6 }; S.hall.tenants.ce2 = { who: 3, until: S.t + 1e6 }; S.fronts.push({ kind: 'neocloud', name: 'Fogbank', exposure: 0 }); S.sites.office = { front: 'neocloud', share: 1 }; run(S, 0.5); call(S, 3, '', { need: 2, hours: 2, pay: 1 }); assert.ok(!tenantOf(S, S.byId.c5) && !tenantOf(S, S.byId.ce2));});
test('renting many at once: the slots a drag covers, what they cost, and the money they stop at', () => { const S = game(); clear(S); const slots = slotsAt(S, 'cloud').slice(0, 30); S.hall.tenants.c3 = { who: 0, until: S.t + 1e6 }; // another company's slot in the middle is skipped const plan = rentPlan(S, slots, 'medium', 'demand', 1); assert.equal(plan.slots.length, 29); assert.equal(plan.ok, 29); const n = hallCount(S, 'cloud'), after = PRICE_EMPTY + (PRICE_FULL - PRICE_EMPTY) * (n.all - n.free + 29) / n.all; assert.ok(Math.abs(plan.perSec - 29 * leaseRate(S, 'medium') * after) < 1e-9, 'each one in fills the hall: the price is the one after'); assert.ok(plan.perSec * DAY_SECONDS > 0 && plan.now === 0); const res = rentPlan(S, slots, 'large', 'reserved', 3); assert.equal(res.now, reserveCost(S, 'large') * 3 * res.ok, 'reserved is paid now, for its days'); assert.equal(res.perSec, 0); // money: ten seconds of the bill each, all together, or a reserved machine's days S.money = reserveCost(S, 'medium') * 2 * 4 + 1; assert.equal(rentPlan(S, slots, 'medium', 'reserved', 2).ok, 4, 'four reserved for two days, and no more'); const big = rentPlan(S, slotsAt(S, 'cloud'), 'large', 'demand', 1); assert.ok(big.ok > 0 && big.ok < big.slots.length, `on demand stops at what you can pay: ${big.ok} of ${big.slots.length}`); S.money = 0; assert.equal(rentPlan(S, slots, 'small', 'spot', 1).ok, 0);});
test('rented together, they come up running the program picked, and say so once', () => { const S = game(); clear(S); S.money = 1e6; const lines = S.log.length; const got = rentMany(S, rentPlan(S, slotsAt(S, 'cloud').slice(0, 10), 'small', 'reserved', 3), 'small', 'reserved', 3, 'trainer'); assert.equal(got, 10); const ms = slotsAt(S, 'cloud').slice(0, 10); assert.ok(ms.every(m => m.owned && m.prog === 'trainer' && m.hand && m.lease?.pay === 'reserved'), 'trainers, set by hand'); assert.ok(ms.every(m => Math.abs(m.lease!.until! - S.t - 3 * RESERVE_HOURS * HOUR) < 1e-6), 'reserved for three days'); const said = S.log.slice(0, S.log.length - lines).filter(l => /^Reserved/.test(l.text)); assert.equal(said.length, 1, 'one line in the log, not one a machine'); assert.match(said[0].text, /10 smalls running trainers/); assert.equal(clusters(S).c1.length, 10, 'side by side, they train as one cluster'); // a program no rented machine runs is left off, and so are guards before any rogue assert.ok(!rentProgs(S).includes('tap') && !rentProgs(S).includes('firewall')); rentMany(S, rentPlan(S, slotsAt(S, 'cloud').slice(10, 12)), 'medium', 'demand', 1, 'tap'); assert.ok(S.byId.c11.owned && S.byId.c11.prog === null);});
test('Rent N takes the longest run of free slots side by side', () => { const S = game(); clear(S); fill(S, ...slotsAt(S, 'cloud').slice(40, 48).map(m => m.id), 'c7', 'c9'); const run = runOf(S, 'cloud', 5); assert.deepEqual(run.map(m => m.id), ['c41', 'c42', 'c43', 'c44', 'c45']); assert.deepEqual(runOf(S, 'cloud', 10).map(m => m.id), ['c41', 'c42', 'c43', 'c44', 'c45', 'c46', 'c47', 'c48', 'c7', 'c9'], 'then the next free ones along'); assert.equal(runOf(S, 'cloude', 3).length, 0, 'no account there: nothing you can rent');});