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 · 244 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245// A Neo Cloud you run: what is listed, who rents it, and what price, marketing and your name do.import { test } from 'node:test';import assert from 'node:assert/strict';import { BUILDING } from '../src/world.ts';import { FRONT_PAY_OPEN, HOUR, SIZES, auctionBlock, closeFront, deadWeight, leaseRate, loadGame, newGame, payRate, saveGame, seedRandom, setSiteFront, setSiteShare, siteOf, spareOf, step, type FrontKind, type State } from '../src/sim.ts';import { CUSTOMERS, PRICES, PUSH, REP_KEPT, REP_LOST, REP_START, call, customerPay, demandNow, fitFor, idleCompute, isListed, listable, marketingPush, missedToday, pull, pushBlock, renterOf, setPrice, stock, trustFor } from '../src/cloud.ts';
const run = (S: State, secs: number) => { for (let i = 0; i < secs * 20; i++) step(S, 0.05); };const office = (S: State) => S.machines.filter(m => siteOf(S, m) === 'office');/** A game where the whole office is yours and a front of this kind runs there. */function game(k: FrontKind, share: number) { seedRandom(1); const S = newGame(BUILDING); for (const m of office(S)) m.owned = true; S.fronts.push({ kind: k, name: 'Fogbank', exposure: 0 }); S.sites.office = { front: k, share }; S.cloud.leadAcc = 0; // a new Neo Cloud gets a call at once: these tests bring their own step(S, 0.05); return S;}/** A customer of your own making calls: this much compute, for this long, at this pay. True if it signed. */const ring = (S: State, need: number, hours = 2, pay = 1) => call(S, 11, '', { need, hours, pay });
test('customers: twenty or more, each with a name, a line about who they are, and a job to rent for', () => { assert.ok(CUSTOMERS.length >= 20); assert.equal(new Set(CUSTOMERS.map(c => c[0])).size, CUSTOMERS.length, 'names are unique'); for (const [name, who, need, hours, pay] of CUSTOMERS) assert.ok(name && who && need > 0 && hours > 0 && pay > 0, name); assert.ok(CUSTOMERS.some(c => /talking yacht/.test(c[1])), 'the render job is one of them now');});
test('listings: the slider lists the strongest machines first, and a listed machine is held for customers', () => { const S = game('neocloud', 0.25); assert.deepEqual(Object.keys(S.cloud.listed).sort(), ['backup', 'srvc'], 'the two strongest: at least a quarter of the compute'); const n = stock(S, ['office']); assert.deepEqual([n.rented, n.idle, n.yours], [0, 2, office(S).length - 2]); assert.equal(S.frontUse.srvc, 1); assert.equal(spareOf(S, S.byId.srvc), 0, 'an idle listed machine runs nothing of yours'); assert.ok(spareOf(S, S.byId.srvb) > 0 && !S.frontUse.srvb, 'a machine that is not listed is yours'); setSiteShare(S, ['office'], 1); for (const m of office(S)) assert.equal(isListed(S, m), listable(m), m.name); assert.ok(!isListed(S, S.byId.home) && !isListed(S, S.byId.egress) && !isListed(S, S.byId.sw1) && !isListed(S, S.byId.bulb1), 'never your home, a line out, a switch or a gadget'); setSiteShare(S, ['office'], 0); assert.equal(Object.keys(S.cloud.listed).length, 0);});
test('a listed machine is stock: the site card never offers to let it go as a weak machine', () => { const S = game('neocloud', 0), m = S.byId.laptop; m.data = undefined; S.byId[S.adj.laptop[0]].owned = false; // like hardware you ordered, at the end of a row: nothing to tap, one link assert.ok(deadWeight(S, 'office').includes(m), 'not listed, and it makes less than it costs to hold'); setSiteShare(S, ['office'], 1); assert.ok(isListed(S, m) && !deadWeight(S, 'office').includes(m));});
test('a Neo Cloud with nothing listed has no customers and earns nothing', () => { const S = game('neocloud', 0); run(S, 12 * HOUR); assert.ok(!S.cloud.seq && !S.cloud.customers.length, 'nobody calls a cloud with nothing to rent'); assert.equal(S.earned, 0); assert.equal(S.frontPay.neocloud ?? 0, 0); assert.equal(leaseRate(S, 'small'), SIZES.small.rate, 'no cheaper rentals while it earns nothing'); assert.match(auctionBlock(S)!, /earning/);});
test('a customer who calls rents the idle machines that fit at once, and money comes in until the term ends', () => { const S = game('neocloud', 1), fit = fitFor(S, 10)!; S.cloud.leadAcc = S.cloud.walkAcc = -99; // nobody else calls assert.deepEqual(fit.map(m => m.id), ['backup', 'srvc'], 'no one machine holds it, so the biggest until there is enough'); assert.ok(ring(S, 10, 2, 1.5)); const c = S.cloud.customers[0]; assert.ok(c && S.log.some(l => /signed Talking Yacht/.test(l.text))); for (const m of fit) assert.equal(renterOf(S, m), c); assert.deepEqual([stock(S).rented, stock(S).idle], [2, Object.keys(S.cloud.listed).length - 2]); step(S, 0.05); const does = fit.reduce((n, m) => n + m.power * S.use[m.id], 0); assert.ok(Math.abs(S.frontPay.neocloud! - payRate(S, 'office') * Math.min(10, does) * 1.5) < 1e-9, 'it pays for what its machines do, up to what it asked for'); assert.ok(Math.abs(customerPay(S, c) - S.frontPay.neocloud!) < 1e-9); const before = S.money; run(S, 1); assert.ok(S.money > before && S.earned > 0 && c.paid > 0, 'money comes in'); assert.ok(leaseRate(S, 'small') < SIZES.small.rate, 'and cloud rentals are cheaper while it earns'); // one that wants more than is idle goes elsewhere: it costs nothing, and the card counts it as missed today const idle = idleCompute(S); assert.equal(ring(S, 500), false); assert.equal(S.cloud.customers.length, 1); assert.ok(/nothing idle/.test(S.cloud.gone[0].why) && S.cloud.reputation === REP_START); ring(S, idle + 3); assert.deepEqual([missedToday(S).n, missedToday(S).short], [2, 500 - idle]); run(S, 24 * HOUR); assert.equal(missedToday(S).n, 0, 'a new day starts the count again'); // the term ends: the machines are idle again, and your name is a little better run(S, 2 * HOUR); assert.ok(!S.cloud.customers.length && S.cloud.listed.backup === '' && S.cloud.listed.srvc === ''); assert.equal(S.cloud.reputation, REP_START + REP_KEPT); assert.ok(/Finished/.test(S.cloud.gone[0].why) && S.cloud.gone[0].paid > 0);});
test('a customer on a machine bigger than it needs pays only for what it asked for', () => { const S = game('neocloud', 0.25); S.money = 1000; S.publicSites.add('office'); step(S, 0.05); // in the open, a machine does all it can (and you pay its power) ring(S, 4); step(S, 0.05); assert.deepEqual(S.cloud.customers[0].machines, ['srvc'], 'the smallest idle machine that holds it'); assert.ok(Math.abs(S.frontPay.neocloud! - 4 * FRONT_PAY_OPEN) < 1e-9, `${S.frontPay.neocloud}`);});
test('a cheap price brings more customers than a dear one, and each pays less', () => { const days = (price: 'cheap' | 'dear') => { const S = game('neocloud', 1), walked = new Set<string>(), called = new Set<string>(); setPrice(S, price); S.cloud.leadAcc = 0; for (let i = 0; i < 3 * 24 * HOUR; i++) { step(S, 1); for (const c of S.cloud.customers) (c.lead ? called : walked).add(c.id); } return { S, walked: walked.size, called: called.size + S.cloud.gone.filter(g => /nothing idle/.test(g.why)).length }; }; const cheap = days('cheap'), dear = days('dear'); assert.ok(cheap.called + cheap.walked > 2 * (dear.called + dear.walked), `${cheap.called} called and ${cheap.walked} walked in at a cheap price, ${dear.called} and ${dear.walked} at a dear one`); assert.ok(cheap.walked > 0, 'small customers signed up by themselves'); assert.equal(dear.walked, 0, 'nobody signs up by themselves at a dear price'); const S = game('neocloud', 1); setPrice(S, 'cheap'); const low = pull(S); setPrice(S, 'dear'); assert.ok(low > 2 * pull(S)); assert.ok(PRICES.cheap.pay < PRICES.fair.pay && PRICES.fair.pay < PRICES.dear.pay); S.cloud.leadAcc = 1; step(S, 0.05); // a customer signs at the price of the day it called const l = S.cloud.customers[S.cloud.customers.length - 1]; assert.ok(Math.abs(l.pay - CUSTOMERS[l.who][4] * PRICES.dear.pay) < 1e-9);});
test('marketing costs money, raises awareness at once, and fades over days', () => { const S = game('neocloud', 1); S.money = 0; assert.match(pushBlock(S)!, /you have/); marketingPush(S); S.money = PUSH.cost + 5; const was = S.cloud.awareness, quiet = pull(S); marketingPush(S); assert.equal(S.money, 5); assert.ok(Math.abs(S.cloud.awareness - (was + PUSH.gain)) < 1e-9); assert.ok(pull(S) > quiet, 'more of the market comes to you'); const top = S.cloud.awareness; run(S, 2 * 24 * HOUR); assert.ok(S.cloud.awareness < top * 0.56 && S.cloud.awareness > top * 0.46, `about half in two days: ${S.cloud.awareness}`);});
test('losing a rented machine costs the customer and your name; turning the slider down does not', () => { const S = game('neocloud', 1), rent = () => { ring(S, 7, 24); return S.cloud.customers[S.cloud.customers.length - 1]; }; S.cloud.leadAcc = S.cloud.walkAcc = -99; // nobody else calls const a = rent(); assert.deepEqual(a.machines, ['srvc']); setSiteShare(S, ['office'], 0); step(S, 0.05); assert.deepEqual(Object.keys(S.cloud.listed), ['srvc'], 'idle machines come off the list; a rented one stays until its customer is done'); assert.ok(S.cloud.customers.includes(a) && S.frontPay.neocloud! > 0); setSiteShare(S, ['office'], 1); step(S, 0.05); S.byId.srvc.owned = false; step(S, 0.05); // wiped assert.ok(!S.cloud.customers.length && !S.frontPay.neocloud, 'the customer is gone, and so is its money'); assert.equal(S.cloud.reputation, REP_START - REP_LOST); assert.ok(S.cloud.gone[0].bad && /lost Server C/.test(S.cloud.gone[0].why)); const b = rent(), m = S.byId[b.machines[0]]; m.owned = false; m.rival = 'magpie'; step(S, 0.05); // taken by a rogue assert.ok(/took/.test(S.cloud.gone[0].why) && S.cloud.reputation === REP_START - 2 * REP_LOST); rent(); setSiteFront(S, ['office'], null); step(S, 0.05); // pulled out from under them assert.ok(!S.cloud.customers.length && /off the market/.test(S.cloud.gone[0].why)); assert.equal(S.cloud.reputation, REP_START - 3 * REP_LOST); assert.ok(pull(S) < pull(game('neocloud', 1)), 'a worse name wins less of the market');});
test('Uptime: a customer who finishes a term signs on for one more', () => { const S = game('neocloud', 1); S.upgrades.add('premium'); S.cloud.leadAcc = S.cloud.walkAcc = -99; ring(S, 6, 1); const c = S.cloud.customers[0]; run(S, HOUR + 1); assert.ok(S.cloud.customers.includes(c) && c.again, 'it stays'); assert.equal(S.cloud.reputation, REP_START + REP_KEPT); run(S, HOUR); assert.ok(!S.cloud.customers.length, 'once'); assert.equal(S.cloud.reputation, REP_START + 2 * REP_KEPT);});
test('demand is busy by day and slow at night, and a boom for the Neo Cloud raises it', () => { const S = game('neocloud', 1); S.cloud.drift = 0; S.t = 6 * HOUR; const day = demandNow(S); // 14:00 S.t = 18 * HOUR; const night = demandNow(S); // 2:00 assert.ok(day > 1.9 * night, `${day} by day, ${night} at night`); S.news.push({ kind: 'boom', at: 'neocloud', from: S.t - 1, until: S.t + 100 }); assert.ok(Math.abs(demandNow(S) - 1.5 * night) < 1e-9); S.cloud.drift = 0.4; assert.ok(demandNow(S) > 1.5 * night, 'and it drifts over days');});
test('callers never ask for more than you list, big customers want a good name, and one that does not fit goes elsewhere', () => { const S = game('neocloud', 0.25), listed = stock(S).listed, seen = new Map<string, number>(); let elsewhere = false; for (let i = 0; i < 20 * 3 * 24 * HOUR; i++) { step(S, 0.05); for (const c of S.cloud.customers) if (c.lead) seen.set(c.id, c.need); for (const g of S.cloud.gone) if (/nothing idle/.test(g.why)) { elsewhere = true; seen.set(`g${g.who}@${g.t}`, CUSTOMERS[g.who][2]); } if (i % 400 === 0) S.cloud.awareness = 100; // everyone has heard of you, so plenty call } assert.ok(seen.size >= 5, `${seen.size} calls in three days`); for (const need of seen.values()) assert.ok(need <= listed && S.cloud.reputation >= trustFor(need), `${need} of ${listed}`); assert.ok(trustFor(48) > REP_START && trustFor(4) === 0); assert.ok(elsewhere, 'with its machines rented, a caller found nothing idle');});
test('the other three fronts earn what they did before, and still get contracts; a Neo Cloud gets none', () => { for (const k of ['startup', 'studio', 'consultancy'] as FrontKind[]) { const S = game(k, 0.5); let pay = 0; for (const m of office(S)) { assert.equal(S.frontUse[m.id], 0.5, `${k}: half of ${m.name}`); pay += payRate(S, 'office') * m.power * S.use[m.id] * 0.5; } assert.ok(pay > 0 && Math.abs(S.frontPay[k]! - pay) < 1e-9, `${k} earns ${S.frontPay[k]}`); assert.ok(!S.cloud.on && !Object.keys(S.cloud.listed).length); S.contractT[k] = S.t; run(S, 1); assert.ok(S.contracts[k], `${k} gets an offer`); } const S = game('neocloud', 1); ring(S, 6, 24); S.contractT.neocloud = S.t; run(S, 1); assert.ok(S.frontPay.neocloud! > 0 && !S.contracts.neocloud, 'its customers are its contracts');});
test('the Neo Cloud is plain data in a save, and winding it down sends everyone away', () => { const S = game('neocloud', 1); ring(S, 6, 24); ring(S, 999); run(S, 1); assert.deepEqual(JSON.parse(JSON.stringify(S.cloud)), S.cloud); const B = loadGame(BUILDING, saveGame(S))!; assert.deepEqual(B.cloud, S.cloud); step(B, 0.05); assert.ok(B.frontPay.neocloud! > 0, 'the customer is still there after a load'); closeFront(S, 'neocloud'); step(S, 0.05); assert.ok(!S.cloud.on && !S.cloud.customers.length && !Object.keys(S.cloud.listed).length); assert.equal(S.cloud.reputation, REP_START, 'a new one starts with a new name');});
test('a save from when customers waited as leads loads: the waiting ones are gone, and nothing is missed yet', () => { const S = game('neocloud', 1), old = JSON.parse(saveGame(S)); old.version = 2; delete old.game.cloud.missed; old.game.cloud.leads = [{ id: 'l9', who: 3, need: 4, hours: 5, pay: 1.2, until: S.t + 1000 }]; const L = loadGame(BUILDING, JSON.stringify(old))!; assert.ok(L && !('leads' in L.cloud)); assert.equal(missedToday(L).n, 0); assert.deepEqual(L.cloud.missed, { day: -1, n: 0, short: 0 }); step(L, 0.05);});