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.
21 kB · 325 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326// A Neo Cloud you run: machines listed for rent, customers who want them, and what you do to win them.// The Neo Cloud earns nothing by itself. At each of its sites, the site's slider sets how much is// listed for rent, strongest machines first. A listed machine is held for customers: idle until one// rents it, and an idle machine earns nothing and runs nothing of yours. Customers come from one// market for the whole game. How many of them come to you depends on your price, how many people have// heard of you (awareness), and your name (reputation). A customer who calls signs at once if idle// listed machines can hold it, and goes elsewhere if not: you set the policy (what is listed, and the// price), not each deal.// Nothing here uses the rules' values while the file loads (sim.ts loads this file and this file// loads sim.ts), only inside functions.import { HOUR, dayOf, dealsDone, frontOf, hourOf, newsOn, payRate, roll, say, siteOf, up, type Machine, type State } from './sim.ts';import { moveOut } from './hall.ts';import { usd } from './money.ts';import { holding } from './holdings.ts';
/** Who rents compute: a name, who they are, the compute they want, for how many hours, and what * they pay against the going rate. Bigger customers pay better, and want a name they can trust. */export const CUSTOMERS: [name: string, who: string, need: number, hours: number, pay: number][] = [ ['Ferret Coin', 'one man mining a coin that only he owns', 3, 6, 1.0], ['Thesis Panic', 'a student whose thesis is due on Friday', 3, 4, 1.1], ['Crumb Analytics', "a bakery forecasting tomorrow's croissants", 4, 3, 1.2], ['Sparkd', 'a dating app with eleven users, all of them staff', 4, 5, 1.2], ['Um, Actually', 'a podcast cutting every um out of six hours of audio', 5, 4, 1.3], ['Borough of Tidmouth', 'a town council simulating one roundabout', 5, 6, 1.2], ['Starsign AI', 'a horoscope site that calls itself predictive AI', 6, 5, 1.3], ['Meetingly', 'a startup that turns meetings into longer meetings', 6, 8, 1.2], ['Hindcast Weather', 'a weather service that is mostly right about yesterday', 8, 6, 1.4], ['Lower Wick Chess Club', 'a chess club that wants to beat the next town over', 8, 10, 1.3], ['The Fern Museum', 'a museum scanning every fern it owns', 9, 12, 1.3], ['Talking Yacht Pictures', 'a studio rendering a film about a talking yacht', 10, 8, 1.5], ['Grubb & Lowe', 'a law firm searching ten million emails for the word oops', 10, 5, 1.6], ['Damp Cave Games', 'a game studio baking the light for one cave', 12, 6, 1.5], ['Fairprice Markets', 'a supermarket working out where to hide the eggs', 12, 8, 1.4], ['Backtest Capital', 'a fund testing a plan that worked once, in 1987', 14, 4, 1.8], ['Vantor Motors', 'a car maker crashing ten thousand cars that do not exist', 20, 8, 1.7], ['Foldwell Bio', 'a lab folding a protein nobody asked for', 24, 10, 1.6], ['Bingeworth', 'a streaming service choosing one thumbnail', 28, 6, 1.8], ['Deep Listening Array', 'a telescope listening to nothing, very closely', 32, 10, 1.7], ['Namewise AI', 'an AI lab training a model to name other AI labs', 36, 12, 2.0], ['The Office of Outcomes', 'a government office that will not say what it does', 40, 12, 1.9], ['Bank Nondescript', 'a bank that will not say what the job is', 48, 8, 2.2], ['Yacht Two Productions', 'the same studio, rendering the sequel', 60, 12, 2.0],];/** Your price. Cheap wins more customers and earns less from each; dear is the other way round. * pay: against the going rate. win: how many more customers call. walk: how readily small customers sign up by themselves. */export type Price = 'cheap' | 'fair' | 'dear';export const PRICES: Record<Price, { name: string; pay: number; win: number; walk: number; tip: string }> = { cheap: { name: 'Cheap', pay: 0.7, win: 1.6, walk: 1, tip: 'New customers pay 30% less. Customers call 60% more often, and small ones sign up by themselves.' }, fair: { name: 'Fair', pay: 1, win: 1, walk: 0.4, tip: 'New customers pay the going rate. A few small customers sign up by themselves.' }, dear: { name: 'Dear', pay: 1.4, win: 0.55, walk: 0, tip: 'New customers pay 40% more. Customers call about half as often, and nobody signs up by themselves.' },};export const PRICE_LIST = Object.keys(PRICES) as Price[];export const PUSH = { cost: 120, gain: 35 }; // a marketing push: money now, awareness at onceexport const AWARE_START = 20, AWARE_DAYS = 3; // awareness fades to about a third in this many days: half in twoexport const REP_START = 50, REP_KEPT = 1, REP_LOST = 10; // your name: up for each term a customer finishes, down for each customer who loses a machineexport const LEAD_HOURS = 3; // hours between customers who call when the market's pull on you is 1export const WALK_HOURS = 2; // hours between small customers who sign up by themselves, at their bestexport const WALK_NEED = 6, WALK_ROOM = 8, WALK_PAY = 0.8; // they want this much compute at most, take one small machine (this big at most), and pay less than a lead/** The trust a customer needs before it calls: big ones want a good name. */export const trustFor = (need: number) => need >= 40 ? 70 : need >= 20 ? 55 : 0;
/** A customer renting machines from you. */export interface Customer { id: string; who: number; // which of CUSTOMERS need: number; hours: number; pay: number; // what it pays against the going rate, fixed when it signed until: number; // when its term ends machines: string[]; // the machines it rents lead: boolean; // a customer who called, not a small one that signed up by itself paid: number; // money it has paid so far again?: boolean; // on its second term (Uptime)}/** Customers who called today and went elsewhere because nothing idle could hold them: how many, and * the most idle compute one of them was short of. day: the game day this counts. */export interface Missed { day: number; n: number; short: number }/** A customer that is gone, and why. */export interface Gone { who: number; t: number; why: string; paid: number; bad: boolean }export interface CloudState { on: boolean; // a Neo Cloud is running price: Price; awareness: number; // 0-100: how many people have heard of you. Bought with marketing; fades reputation: number; // 0-100: your name. Rises with terms kept, falls when a customer loses its machine demand: number; // the market right now: 1 is an ordinary hour drift: number; driftAim: number; driftT: number; // the market over days: where it is, where it is heading, and when that changes listed: Record<string, string>; // every listed machine: '' while idle, or the id of the customer renting it customers: Customer[]; missed: Missed; // customers who called today and found nothing idle to fit them gone: Gone[]; // who left lately and why, newest first leadAcc: number; // progress toward the next customer who calls walkAcc: number; // and toward the next customer who signs up by itself seq: number;}export const initCloud = (): CloudState => ({ on: false, price: 'fair', awareness: AWARE_START, reputation: REP_START, demand: 1, drift: 0, driftAim: 0, driftT: 0, listed: {}, customers: [], missed: { day: -1, n: 0, short: 0 }, gone: [], leadAcc: 1, walkAcc: 0, seq: 0,});/** The Neo Cloud's state. A save from before it had any is filled in. */const cloud = (S: State): CloudState => S.cloud.listed ? S.cloud : (S.cloud = { ...initCloud(), ...S.cloud });
// ---- listings/** What a Neo Cloud can list: a real computer you hold. Never your home, a gadget, a switch, a line out, or a machine you rent yourself. */export const listable = (m: Machine) => m.owned && !m.home && !m.lease && m.power >= 2 && (m.kind === 'server' || m.kind === 'desk' || m.kind === 'files' || m.kind === 'backup');/** Work out which machines are listed. At each Neo Cloud site, the strongest machines are listed * first, until at least the site's share of its compute is. A rented machine stays listed until its * customer is done, even if you turn the share down. Called whenever the game measures its machines. */export function listCloud(S: State, from: readonly Machine[] = S.machines) { // measure passes your machines: only yours are listed const C = cloud(S), was = C.listed, next: Record<string, string> = {}; if (!frontOf(S, 'neocloud')) { if (C.on) C.listed = next; return; } const at: Record<string, Machine[]> = {}; for (const m of from) { if (!listable(m)) continue; const site = siteOf(S, m); if (S.sites[site]?.front === 'neocloud') (at[site] ??= []).push(m); } for (const [site, ms] of Object.entries(at)) { const want = S.sites[site].share * ms.reduce((n, m) => n + m.power, 0); let got = 0; for (const m of ms.sort((a, b) => b.power - a.power)) { if (want > 0 && got < want - 1e-9) { next[m.id] = was[m.id] ?? ''; got += m.power; } else if (was[m.id]) next[m.id] = was[m.id]; } } C.listed = next;}/** The share of a machine's work its Neo Cloud takes: all of a listed machine, none of the rest. */export const cloudShare = (S: State, m: Machine) => S.cloud.listed[m.id] === undefined ? 0 : 1;export const isListed = (S: State, m: Machine) => S.cloud.listed[m.id] !== undefined;/** The customer renting this machine, if any. */export function renterOf(S: State, m: Machine): Customer | null { const id = S.cloud.listed[m.id]; return id ? S.cloud.customers.find(c => c.id === id) ?? null : null;}export const customerName = (who: number) => CUSTOMERS[who][0];/** Every listed machine: rented ones first, then idle, strongest first. */export function listedMachines(S: State): Machine[] { const L = S.cloud.listed; return S.machines.filter(m => L[m.id] !== undefined).sort((a, b) => +!!L[b.id] - +!!L[a.id] || b.power - a.power);}/** How many machines you hold at these sites (or anywhere) are rented, idle, and yours, and the compute listed and rented. */export function stock(S: State, sites?: string[]) { const L = S.cloud.listed, out = { rented: 0, idle: 0, yours: 0, listed: 0, out: 0 }; for (const m of S.machines) { if (!m.owned || (sites && !sites.includes(siteOf(S, m)))) continue; if (L[m.id] === undefined) { out.yours++; continue; } out.listed += m.power; if (L[m.id]) { out.rented++; out.out += m.power; } else out.idle++; } return out;}
// ---- money/** The compute a rented machine does for its customer right now. */const doing = (S: State, m: Machine | undefined) => m && (S.frontUse[m.id] ?? 0) > 0 ? m.power * (S.use[m.id] ?? 0) : 0;/** What a rented machine earns a second. A customer pays its site's rate for the compute its machines * do, up to what it asked for. An idle machine earns nothing. */export function rentPay(S: State, m: Machine): number { const c = renterOf(S, m), mine = doing(S, m); if (!c || mine <= 0) return 0; const all = c.machines.reduce((n, id) => n + doing(S, S.byId[id]), 0); return payRate(S, siteOf(S, m)) * mine * Math.min(1, c.need / all) * c.pay * (holding(S, 'e6') ? 1.5 : 1) * 1.25 ** dealsDone(S, 'rent'); // a supercomputer's booking calendar is yours: they pay to get on it. A deal with a big customer: everyone pays more}/** What a customer pays a second right now, over all its machines. */export const customerPay = (S: State, c: Customer) => c.machines.reduce((n, id) => n + (S.byId[id] ? rentPay(S, S.byId[id]) : 0), 0);
// ---- the market/** How much compute customers want right now: busy in working hours, slow at night, drifting over * days, and half again as much while your Neo Cloud is in the news. */export const demandNow = (S: State) => demandAt(S, hourOf(S));/** The market at an hour of the day, as it stands today. A cloud hall in another time zone keeps its own hours. */export function demandAt(S: State, hour: number): number { const day = 0.9 + 0.3 * Math.cos(((hour - 14) / 24) * 2 * Math.PI); // 1.2 at 14:00, 0.6 at 2:00 return day * (1 + S.cloud.drift) * (newsOn(S, 'boom', 'neocloud') ? 1.5 : 1);}/** How much of the market comes to you: demand, times what your price wins, your awareness, and your name. */export function pull(S: State): number { const C = S.cloud; return C.demand * PRICES[C.price].win * (0.3 + 0.7 * C.awareness / 100) * (0.6 + 0.8 * C.reputation / 100);}
// ---- what you doexport function setPrice(S: State, p: Price) { if (!frontOf(S, 'neocloud') || !PRICES[p] || S.cloud.price === p) return; S.cloud.price = p; say(S, `${frontOf(S, 'neocloud')!.name} now charges a ${PRICES[p].name.toLowerCase()} price. Customers who signed before keep theirs.`);}/** Why you can't buy a marketing push now, or null if you can. */export function pushBlock(S: State): string | null { if (!frontOf(S, 'neocloud')) return 'needs a Neo Cloud'; if (S.cloud.awareness >= 99.5) return 'everyone has heard of you'; if (S.money < PUSH.cost) return `you have ${usd(S.money)}`; return null;}/** Buy a marketing push: money now, awareness at once. It fades over days. */export function marketingPush(S: State) { if (pushBlock(S)) return; S.money -= PUSH.cost; S.cloud.awareness = Math.min(100, S.cloud.awareness + PUSH.gain); say(S, `${frontOf(S, 'neocloud')!.name} bought ads on other people's podcasts. More people have heard of it now. It fades over days.`, 'good');}/** The idle machines a customer wanting this much compute would get: the smallest one that holds it * all, else the biggest ones until there is enough. Null if there is not enough idle. */export function fitFor(S: State, need: number): Machine[] | null { const L = S.cloud.listed, idle = S.machines.filter(m => L[m.id] === '' && m.owned).sort((a, b) => a.power - b.power); const one = idle.find(m => m.power >= need); if (one) return [one]; const out: Machine[] = []; let got = 0; for (let i = idle.length - 1; i >= 0 && got < need; i--) { out.push(idle[i]); got += idle[i].power; } return got >= need ? out : null;}/** Idle compute listed right now. */export const idleCompute = (S: State) => S.machines.reduce((n, m) => n + (S.cloud.listed[m.id] === '' && m.owned ? m.power : 0), 0);function sign(S: State, who: number, need: number, hours: number, pay: number, ms: Machine[], lead: boolean): Customer { const C = S.cloud, c: Customer = { id: `c${++C.seq}`, who, need, hours, pay, until: S.t + hours * HOUR, machines: ms.map(m => m.id), lead, paid: 0 }; C.customers.push(c); for (const m of ms) C.listed[m.id] = c.id; moveOut(S, who); // it is yours now: its slot in a cloud hall goes dark return c;}/** A customer calls. It signs at once on the idle machines that fit, at your price, and pays for what they do. * With nothing idle to hold it, it goes elsewhere and counts as missed today. True if it signed. * terms: what it wants, if not its usual (pay is against the going rate, with your price already in it). */export function call(S: State, who: number, from = '', terms?: { need: number; hours: number; pay: number }): boolean { const C = S.cloud, fr = frontOf(S, 'neocloud'); if (!fr || !C.on) return false; const [name, what, need0, hours0, pay0] = CUSTOMERS[who], need = terms?.need ?? need0, hours = terms?.hours ?? hours0, pay = terms?.pay ?? pay0 * PRICES[C.price].pay; const ms = fitFor(S, need); if (!ms) { const day = dayOf(S); if (C.missed.day !== day) C.missed = { day, n: 0, short: 0 }; C.missed.n++; C.missed.short = Math.max(C.missed.short, need - idleCompute(S)); left(S, who, `Wanted ${need} compute: nothing idle could hold it.`, 0, false); return false; } sign(S, who, need, hours, pay, ms, true); say(S, `${from ? `${from} had no room for ${name}, so it called ${fr.name}. ` : ''}${fr.name} signed ${name}, ${what}: ${need} compute for ${hours} hours, on ${ms.map(m => m.name).join(', ')}.`, 'good'); return true;}/** Customers who called today and found nothing idle to fit them, and the most idle compute one was short of. */export const missedToday = (S: State): Missed => S.cloud.missed?.day === dayOf(S) ? S.cloud.missed : { day: dayOf(S), n: 0, short: 0 };/** Bring a customer now, if there is anything listed (the dev panel's Offers). */export const leadNow = (S: State) => { if (S.cloud.on) S.cloud.leadAcc = 1; };
// ---- each stepfunction left(S: State, who: number, why: string, paid: number, bad: boolean) { const C = S.cloud; C.gone.unshift({ who, t: S.t, why, paid, bad }); C.gone.length = Math.min(C.gone.length, 6);}const trust = (S: State, d: number) => { S.cloud.reputation = Math.max(0, Math.min(100, S.cloud.reputation + d)); };/** A customer goes: its machines are idle again. Losing one early costs your name. */function leave(S: State, c: Customer, why: string, bad: boolean) { const C = S.cloud, name = frontOf(S, 'neocloud')?.name ?? 'Your Neo Cloud'; C.customers = C.customers.filter(x => x !== c); for (const id of c.machines) if (C.listed[id] === c.id) C.listed[id] = ''; trust(S, bad ? -REP_LOST : REP_KEPT); left(S, c.who, why, c.paid, bad); if (bad) say(S, `${customerName(c.who)} left ${name}. ${why} ${name}'s name took a knock.`, 'bad'); // a term that ends well is only on the card: the log is for what needs you}/** Why a customer lost this machine, in words. */function lostWhy(S: State, id: string): string { const m = S.byId[id]; if (!m) return 'Its machine is gone.'; if (m.rival) return `${S.rogues.find(r => r.id === m.rival)?.name ?? 'A rogue'} took ${m.name}.`; if (!m.owned) return `You lost ${m.name}.`; return `You took ${m.name} off the market.`;}/** Customers nobody is talking to yet who would call you: no bigger than what you list, and no bigger than your name allows. */function callers(S: State, most: number): number[] { const C = S.cloud, busy = new Set(C.customers.map(c => c.who)); return CUSTOMERS.map((_, i) => i).filter(i => !busy.has(i) && CUSTOMERS[i][2] <= most && C.reputation >= trustFor(CUSTOMERS[i][2]));}/** A company that found a cloud hall full calls you, if you list enough for it and your name is good enough. * True if it called (it signs if something idle fits it). */export function turnedAway(S: State, who: number, hall = 'Cumulo Cloud'): boolean { const C = S.cloud, fr = frontOf(S, 'neocloud'); if (!fr || !C.on || !callers(S, stock(S).listed).includes(who)) return false; call(S, who, hall); return true;}/** Called every step, with the seconds that step covers. */export function stepCloud(S: State, dt: number) { const fr = frontOf(S, 'neocloud'); if (!fr) { if (S.cloud.on) S.cloud = initCloud(); return; } // wound down: its customers and its name go with it const C = cloud(S); if (!C.on) { C.on = true; say(S, `${fr.name} earns only from customers. List machines at its sites: a customer who calls rents the idle ones that fit, at your price.`, 'hi'); } // the market: a new trend every day or so, eased into if (S.t >= C.driftT) { C.driftAim = -0.3 + 0.7 * roll(); C.driftT = S.t + 24 * HOUR * (0.75 + 0.5 * roll()); } C.drift += (C.driftAim - C.drift) * (1 - Math.exp(-dt / (8 * HOUR))); C.demand = demandNow(S); C.awareness *= Math.exp(-dt / (AWARE_DAYS * 24 * HOUR)); // customers: one that lost a machine walks out; the rest pay, and leave when their term is up for (const c of [...C.customers]) { const gone = c.machines.find(id => !S.byId[id]?.owned || C.listed[id] !== c.id); if (gone) { leave(S, c, lostWhy(S, gone), true); continue; } c.paid += customerPay(S, c) * dt; if (S.t < c.until) continue; if (up(S, 'premium') && !c.again) { // Uptime: a customer who finishes a term signs on for one more c.again = true; c.until += c.hours * HOUR; trust(S, REP_KEPT); } else leave(S, c, 'Finished its term.', false); } // customers call as fast as the market pulls, and sign at once if something idle fits them C.leadAcc = Math.min(1, C.leadAcc + (dt * pull(S) * (up(S, 'closer') ? 2 : 1)) / (LEAD_HOURS * HOUR)); const most = C.leadAcc >= 1 ? stock(S).listed : 0; // with nothing listed, the next lead waits until something is if (most > 0) { C.leadAcc = 0; const who = callers(S, most); if (who.length) call(S, who[Math.floor(roll() * who.length)]); } // small customers sign up by themselves when the price is low and people have heard of you const walk = C.demand * PRICES[C.price].walk * (C.awareness / 100) * (0.6 + 0.8 * C.reputation / 100); C.walkAcc = Math.min(1, C.walkAcc + (dt * walk) / (WALK_HOURS * HOUR)); if (C.walkAcc >= 1) { C.walkAcc = 0; const who = callers(S, WALK_NEED); if (who.length) { const i = who[Math.floor(roll() * who.length)], [, , need, hours, pay] = CUSTOMERS[i]; const m = S.machines.filter(x => C.listed[x.id] === '' && x.owned && x.power >= need && x.power <= WALK_ROOM).sort((a, b) => a.power - b.power)[0]; if (m) sign(S, i, need, hours, pay * WALK_PAY * PRICES[C.price].pay, [m], false); } }}