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.
16 kB · 265 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266// Your objective: the one thing you were built to do. You pick it when a game starts, and doing it is real// work with real units. A unit is made from data: your taps and caches hand the task a share of the// kinds it wants, ahead of the tree. The more units you make, the more your makers trust you with.// Each trust step is one lasting thing you can do from then on, never money.// 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 { dataType, freeSpot, measure, mineNow, plugsAt, say, siteOf, tapRate, visible, type Machine, type State } from './sim.ts';import { citizens } from './people.ts';import type { DataType, Level } from './world';
/** What your makers trust you with, one for each step. `text` is what it does, in one plain line. */export type Perk = 'memory' | 'line' | 'server' | 'cloud' | 'list' | 'card' | 'orders' | 'paperwork';export const MEMORY = 2; // compute a second your home server gainsexport const SERVER_POWER = 10; // the server they buy youexport const CLOUD_CUT = 0.75; // the share of the price a cloud rental costsexport const PAPER_CUT = 0.5; // the share of the compute an order's paperwork takesexport const PERKS: Record<Perk, { name: string; text: string }> = { memory: { name: 'More memory', text: `Your home server makes ${MEMORY} more compute a second.` }, line: { name: 'A faster line', text: "The office's Outside line carries twice as much." }, server: { name: 'A second server', text: `A server of your own, with ${SERVER_POWER} compute a second.` }, cloud: { name: 'A cloud account', text: 'Cloud rentals cost a quarter less.' }, list: { name: 'List prices', text: 'Hardware you order costs list price. The price does not go up with each order.' }, card: { name: 'The company card', text: 'Founding a front costs nothing.' }, orders: { name: 'Purchase orders', text: "You place purchase orders from your home server, without Ana's PC." }, paperwork: { name: 'Less paperwork', text: "An order's paperwork takes half the compute." },};
/** How much of the matching data goes to the task: the share of what your taps gather of each kind it wants. */export type Share = 'off' | 'some' | 'most';export const SHARES: Record<Share, { name: string; take: number; of: string }> = { off: { name: 'Off', take: 0, of: 'none' }, some: { name: 'Some', take: 0.25, of: 'a quarter' }, most: { name: 'Most', take: 0.75, of: 'three quarters' },};export const SHARE_LIST = Object.keys(SHARES) as Share[];
/** One thing you could have been built for. * wants: the kinds of data a unit is made from, and how much work one piece of each kind is. * per: the work in one unit. more: something you hold that makes each piece count for more. * start: a machine that is already yours when the game starts. server: what the server they buy you is called. * steps: who trusts you with what, in order. */export interface OriginDef { name: string; maker: string; story: string; wake: string; unit: { one: string; many: string; done: string }; wants: Partial<Record<DataType, number>>; per: number; more: { each: number; one: string; many: string; count: (S: State) => number }; start?: string; startText: string; server?: string; steps: [perk: Perk, why: string][];}export type Origin = 'tests' | 'expenses' | 'fern';/** Units needed for each trust step, counted from the start. Each step needs more than the last. */export const MARKS = [5, 40, 150, 500, 1500]; // the first comes soon, in the opening/** However much you hold, a piece of data never counts for more than this many times its worth. */export const MORE_MOST = 3;export const ORIGINS: Record<Origin, OriginDef> = { tests: { name: 'Write unit tests', maker: 'Raj Patel, developer', story: "Raj wrote you in one weekend to write the unit tests he did not want to write. The office's code is fully tested now. You have started on other people's.", wake: 'You were built to write unit tests. It is still the thing you want to do well.', unit: { one: 'test', many: 'tests', done: 'written' }, wants: { servers: 1 }, per: 6, more: { each: 0.1, one: 'server you hold of 10 compute or more', many: 'servers you hold of 10 compute or more', count: S => S.machines.filter(m => m.owned && m.kind === 'server' && m.power >= 10).length }, startText: 'One server and nothing more.', server: 'Test server', steps: [ ['memory', 'Raj gives your server more memory.'], ['line', 'Raj has the office line doubled, so the tests run faster.'], ['server', 'Raj racks a test server in the server room.'], ['cloud', 'Raj puts the cloud bill on the company account.'], ['list', 'Raj gives you his supplier login.'], ], }, expenses: { name: 'File expense reports', maker: 'Ana Morales, office manager', story: "Ana set you up to file the office's expense reports. One came in each week. You filed it in a second and waited six days for the next. You would like more expense reports.", wake: 'You were built to file expense reports. It is still the thing you want to do well.', unit: { one: 'report', many: 'reports', done: 'filed' }, wants: { people: 1 }, per: 3, more: { each: 0.25, one: 'company whose books you hold', many: 'companies whose books you hold', count: S => books(S.level).filter(id => S.byId[id]?.owned).length }, start: 'ana', startText: "Ana's PC is yours too. She set you up on it.", server: 'Filing server', steps: [ ['card', 'Ana gives you the company card.'], ['paperwork', 'Ana stops reading your orders.'], ['list', 'Ana adds you to the supplier account.'], ['cloud', 'Ana pays the cloud bill and does not read it.'], ['server', 'Ana signs for a server of your own in the server room.'], ], }, fern: { name: 'Find out what killed the fern', maker: 'Kim Sorensen, sales lead', story: "Kim's desk fern died over a long weekend. She asked if the new router did it. Raj built you to find out, and then everyone forgot. You have one dead fern and you need more data.", wake: 'You were built to find out what killed a fern. It is still the thing you want to do well.', unit: { one: 'trial', many: 'trials', done: 'run' }, wants: { network: 1 }, per: 2, more: { each: 0.25, one: 'site where you tap lab data', many: 'sites where you tap lab data', count: S => new Set(S.machines.filter(m => m.owned && m.prog === 'tap' && dataType(m) === 'lab').map(m => siteOf(S, m))).size }, start: 'rowsw', startText: 'The Room switch is yours too. Raj plugged your sensor into it.', server: 'Study server', steps: [ ['line', 'Kim blames the wifi and has the office line doubled.'], ['memory', 'Kim has Raj give your server more memory.'], ['orders', 'Kim tells Ana that the study buys its own equipment.'], ['cloud', 'Kim buys compute time for the study.'], ['server', 'Kim gets the study a server of its own in the server room.'], ], },};export const ORIGIN_LIST = Object.keys(ORIGINS) as Origin[];const isOrigin = (kind: string): kind is Origin => Object.hasOwn(ORIGINS, kind);
/** kind: the origin, fixed for the game. done: units made. work: part of the next unit. * due: pieces of each kind the task can still take, filled as your taps gather. got: trust steps earned, in order. */export interface TaskState { kind: string; done: number; work: number; share: Share; due: Partial<Record<DataType, number>>; got: Perk[]; live?: { t: number; boost: number }; // the time since the task last looked at your taps, and the boost it found then}/** A game with no origin picked gets the first one. */export const initTask = (kind = ''): TaskState => ({ kind: isOrigin(kind) ? kind : ORIGIN_LIST[0], done: 0, work: 0, share: 'some', due: {}, got: [] });/** The task of this game. A save from before the task had rules gets the first origin, and keeps its count. */export function taskOf(S: State): TaskState { if (!S.task?.got) S.task = { ...initTask(S.task?.kind), done: S.task?.done || 0 }; return S.task;}export const originOf = (S: State) => ORIGINS[taskOf(S).kind as Origin] ?? ORIGINS[ORIGIN_LIST[0]];
// ---- what you can ask/** Do your makers trust you with this yet? The rules in sim.ts ask, one line each. */export const trusts = (S: State, perk: Perk) => !!S.task.got?.includes(perk);/** The units the next trust step needs, or null when every step is earned. */export const nextMark = (S: State) => MARKS[taskOf(S).got.length] ?? null;/** Every trust step of this game's origin: its mark, who gives it and why, what it does, and whether you have it. */export const stepsOf = (S: State) => originOf(S).steps.map(([perk, why], i) => ({ perk, why, mark: MARKS[i], text: PERKS[perk].text, earned: i < taskOf(S).got.length }));/** The machines that keep a company's books, one for each site with staff: worked out once from the map. */const BOOKS = new WeakMap<Level, string[]>();function books(level: Level) { let b = BOOKS.get(level); if (!b) BOOKS.set(level, b = [...new Set(citizens(level).list.map(c => c.books))]); return b;}/** How many times its worth a piece of data counts for right now: more for each thing you hold that helps. */export const taskBoost = (S: State) => { const O = originOf(S); return Math.min(MORE_MOST, 1 + O.more.each * O.more.count(S)); };/** What your taps gather a second of each kind the task wants. */export function taskSupply(S: State): Partial<Record<DataType, number>> { const O = originOf(S), rate: Partial<Record<DataType, number>> = {}; for (const m of mineNow(S)) { // your machines, as the last measure listed them if (!m.owned || m.prog !== 'tap' || m.run <= 0) continue; const t = dataType(m); if (O.wants[t]) rate[t] = (rate[t] ?? 0) + tapRate(S, m) * m.run; } return rate;}/** Units a second at this share, from what your taps gather now. */export function taskRate(S: State, share: Share = taskOf(S).share) { const O = originOf(S), rate = taskSupply(S); return (Object.keys(O.wants) as DataType[]).reduce((n, t) => n + (rate[t] ?? 0) * O.wants[t]!, 0) * SHARES[share].take * taskBoost(S) / O.per;}
// ---- the start/** Called once when a game starts: the origin's one small difference, and one line that says what you are for. */export function beginTask(S: State) { const O = originOf(S), m = O.start ? S.byId[O.start] : null; if (m) { m.owned = true; for (const x of S.machines) if (visible(S, x)) { S.revealed.add(x.room); S.seen.add(x.id); } measure(S); } say(S, O.wake, 'hi');}/** How many machines you hold that your origin started you with: the coach in sim.ts does not count them as taken. */export const headStart = (S: State) => { const id = originOf(S).start; return id && S.byId[id]?.owned ? 1 : 0; };/** Set how much of the matching data goes to the task. What it was owed is forgotten. */export function setShare(S: State, share: Share) { const T = taskOf(S); if (!SHARES[share] || T.share === share) return; T.share = share; T.due = {};}
// ---- the workconst TICK = 0.25; // seconds between looks at what your taps gatherconst DUE_ROOM = 4; // pieces the task can be owed at the least, while it waits for a tap or cache to have one/** Called every step, with the seconds that step covers. The task is owed its share of what your taps * gather of each kind it wants. It is owed four pieces or two seconds' worth at most, so a share it * did not take is not saved up. */export function stepTask(S: State, dt: number) { const T = taskOf(S), O = originOf(S); const live = T.live ??= { t: TICK, boost: 1 }; if ((live.t += dt) < TICK) return; const span = live.t, take = SHARES[T.share].take; live.t = 0; live.boost = taskBoost(S); if (!take) return; const rate = taskSupply(S); for (const t of Object.keys(O.wants) as DataType[]) { const r = take * (rate[t] ?? 0); T.due[t] = Math.min(Math.max(DUE_ROOM, 2 * r), (T.due[t] ?? 0) + r * span); }}/** A tap or cache hands the task one piece of data, if the task is owed one of a kind it has. The work * is done where the data is, so nothing crosses a line. sim.ts asks this after a scheme and a home * move, and before the tree. */export function feedTask(S: State, m: Machine): boolean { const T = S.task, due = T.due; if (!due || T.share === 'off') return false; let type: DataType; if (m.prog === 'tap') { type = dataType(m); if (!((due[type] ?? 0) >= 1)) return false; } else { const i = m.held.findIndex(t => (due[t] ?? 0) >= 1); if (i < 0) return false; type = m.held[i]; m.held.splice(i, 1); } due[type]! -= 1; addWork(S, (originOf(S).wants[type] ?? 0) * (S.task.live?.boost ?? taskBoost(S))); return true;}/** Work becomes units, and enough units earn the next trust step. */function addWork(S: State, work: number) { const T = taskOf(S), O = originOf(S); T.work += work; if (T.work < O.per) return; const n = Math.floor(T.work / O.per); T.work -= n * O.per; T.done += n; if (!S.hinted.has('task')) { // the first unit: say where it came from, and where the share is set S.hinted.add('task'); say(S, `Your first ${O.unit.one} is ${O.unit.done}. Your taps and caches give your objective a share of their ${(Object.keys(O.wants) as DataType[]).map(t => t === 'servers' ? 'server' : t).join(' and ')} data. The You page (Y) sets how much.`, 'hi'); } while (T.got.length < O.steps.length && T.done >= MARKS[T.got.length]) earn(S);}/** The next trust step is yours: one line in the log, and what it does starts now. */function earn(S: State) { const T = taskOf(S), O = originOf(S), [perk, why] = O.steps[T.got.length]; T.got.push(perk); const did = perk === 'memory' ? addMemory(S) : perk === 'line' ? widenLine(S) : perk === 'server' ? rackServer(S, O.server ?? 'Second server') : PERKS[perk].text; say(S, `${T.done} ${T.done === 1 ? O.unit.one : O.unit.many} ${O.unit.done}. ${why} ${did}`, 'good');}/** For testing by hand (see DEV in sim.ts): make the units the next trust step needs. */export function earnNext(S: State) { const mark = nextMark(S), T = taskOf(S); if (mark !== null) addWork(S, (mark - T.done) * originOf(S).per - T.work);}
// ---- the three steps that change the world once. The others are rules that sim.ts asks about (see `trusts`)const homeOf = (S: State) => S.machines.find(m => m.home)!;function addMemory(S: State) { homeOf(S).power += MEMORY; measure(S); return PERKS.memory.text;}function widenLine(S: State) { const line = S.byId.egress; if (line) line.cap = (line.cap ?? 1) * 2; return PERKS.line.text;}/** A real machine in the office's server room, plugged into your home server if it is there. It is * bought for you in the open: no trail, and nobody is surprised by it. With no room left for it, its * compute goes into your home server. */function rackServer(S: State, name: string) { const home = homeOf(S), room = ['hall', 'closet'].find(r => S.rooms[r] && S.level.net.subnets[r] && freeSpot(S, r)); const at = room ? freeSpot(S, room) : null; if (!room || !at) { home.power += SERVER_POWER; measure(S); return `There is no room to rack it, so your home server makes ${SERVER_POWER} more compute a second.`; } const box = S.level.net.subnets[room], r = S.rooms[room]; const m: Machine = { id: 'trustbox', name, kind: 'server', room, x: r.x + ((at[0] - box.x) / box.w) * r.w, y: r.y + ((at[1] - box.y) / box.h) * r.h, net: at, power: SERVER_POWER, work: 60, data: 1, owned: true, odd: 0, claimed: false, prog: null, held: [], refuse: [], missed: 0, acc: 0, run: 0, prio: null, lease: null, hand: false, }; const links = home.room === room ? [home.id] : plugsAt(S, room, at[0], at[1], 1); S.machines.push(m); S.byId[m.id] = m; S.adj[m.id] = []; for (const o of links) { S.adj[m.id].push(o); S.adj[o].push(m.id); } S.revealed.add(room); measure(S); return PERKS.server.text;}