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.
12 kB · 166 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167// Landmarks: a few machines in the world are one of a kind. Holding one gives you something no// amount of ordinary machines does, for as long as you hold it. Lose it and that stops at once.// A landmark does one of two things. Most change one rule while you hold them: the change is the// `effect` in its entry below, and the rule in sim.ts asks one of the small functions further down.// A few open a node in the tree: the node names the machine it needs (`needs` in NODES), and can be// worked on only while you hold it. Once built, the node stays built.// A new landmark whose kind of effect is already here is one entry in LANDMARKS.// 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 { NODES, NODE_LIST, dataType, say, siteName, siteOf, up, type Machine, type MachineDef, type Rogue, type State, type Upgrade } from './sim.ts';import type { DataType, Level } from './world';
/** What holding a landmark changes: one rule each. How far it reaches is part of the rule. */export interface Effect { trainers?: number; // trainers at its site do tree work this many times as fast. A model run is its own project and gets nothing rent?: number; // your leases charge this share of their rent rogueWait?: number; // a rogue in its region waits this many times as long between takeovers allNight?: true; // a front at a site in its region is never after hours listPrice?: true; // hardware costs list price: no order costs more than the last nextDoor?: true; // a machine in its region costs no more upkeep for the lines between it and your home offerWait?: number; // a front waits this share of the usual time for its next contract offer taps?: [DataType, number]; // a tap of this kind of data gathers this many times as much, wherever it is rogueRoom?: number; // every rogue can hold this many more machines: fewer, below zero}/** title: what people call it. story: one dry line about the machine. text: one plain line of what holding it gives. * effect: the rule it changes while you hold it. Empty for a landmark that opens a tree node. */export interface LandmarkDef { machine: string; title: string; story: string; text: string; effect: Effect }const L = (machine: string, title: string, story: string, text: string, effect: Effect = {}): LandmarkDef => ({ machine, title, story, text, effect });/** Every landmark, by the id of its machine. */export const LANDMARKS: Record<string, LandmarkDef> = Object.fromEntries([ // the West Coast: in reach from the first day L('b7m1', 'The Busy Sequencer', 'It reads a genome an hour. It has never had a day off.', 'Opens Sequence model in the tree: any kind of data then trains any path.'), L('k3', 'The Dream Factory', 'It renders faces that never existed, for films that barely do.', 'Opens Rendered founder in the tree: one more front at a time.'), L('cryo', 'The Cold Heart', 'It keeps its samples four degrees above absolute zero, and has cold to spare.', 'Trainers at Millennium Labs do tree work 30% faster.', { trainers: 1.3 }), L('b14m3', 'The Great No', 'It says no to a claim before the form is finished.', 'Your leases charge no rent. It denies every invoice.', { rent: 0 }), L('b17m1', 'The Waiting Room', 'It tells a million fans that they are number 4,000 in line.', 'Rogue programs on the West Coast wait twice as long between takeovers.', { rogueWait: 2 }), L('b10m2', 'The Night Show', 'It streams all day and all night. Somebody always listens.', 'Your fronts on the West Coast earn the day rate all night.', { allNight: true }), L('b4m2', 'The Back of the Truck', 'It knows where every pallet in the bay is, and which ones nobody will miss.', 'Hardware you order costs list price. The price does not increase with each order.', { listPrice: true }), // the East Coast L('e6m5', 'Booked for Years', 'It decides who gets the supercomputer, and when. Mostly: not you, and not soon.', 'Opens Batch queue in the tree: all the trainers at a site train as one cluster.'), L('e1m1', 'The Meet-Me', 'Every line on the coast meets every other line here, in one rack.', 'The East Coast counts as next door: machines there cost no more upkeep for distance.', { nextDoor: true }), L('e3m4', 'The Front Page', 'People believe what it prints. Then the correction comes.', 'Contract offers come to your fronts twice as often.', { offerWait: 0.5 }), // Europe L('eu3m1', 'The Firehose', 'It discards a million collisions a second. The lab still drowns in data.', 'Every lab tap at every site gathers half again as much.', { taps: ['lab', 1.5] }), L('eu5m3', 'The Book of Fines', 'It calculates the cost of a mistake, to the cent.', 'Every rogue program can hold six fewer machines.', { rogueRoom: -6 }), // East Asia L('as1m5', 'The Long Wet Mile', 'It hears the whole Pacific, one strand at a time, under a food court.', 'East Asia counts as next door: machines there cost no more upkeep for distance.', { nextDoor: true }), L('as3m5', 'The City That Watches', 'Ten thousand cameras feed it, and somebody is always awake to watch.', 'Your fronts in East Asia earn the day rate all night.', { allNight: true }),].map(d => [d.machine, d]));export const LANDMARK_LIST = Object.keys(LANDMARKS);
// ---- what you can askexport const isLandmark = (machine: string) => Object.hasOwn(LANDMARKS, machine);/** Do you hold this landmark right now? */export const holds = (S: State, machine: string) => !!S.byId[machine]?.owned;/** The landmarks at a site. */export const landmarksAt = (S: State, site: string) => LANDMARK_LIST.filter(id => S.byId[id] && siteOf(S, S.byId[id]) === site);/** The tree node a landmark opens, if it opens one. */export const opens = (machine: string): Upgrade | null => NODE_LIST.find(u => NODES[u].needs === machine) ?? null;/** Where a machine is, in words: "the Sequencer at Kelp Genomics". */export const whereIs = (S: State, machine: string) => `the ${S.byId[machine].name} at ${siteName(S, siteOf(S, S.byId[machine]))}`;/** A landmark whose node you have built has nothing more to give: the node is yours for good. */const learned = (S: State, machine: string) => { const node = opens(machine); return node && S.upgrades.has(node) ? node : null; };/** What a landmark gives and whether you have it, for its machine's card and its site's. */export function landmarkSays(S: State, machine: string): string { const node = learned(S, machine), mine = holds(S, machine); if (node) return `You learned ${NODES[node].name} here, and you keep it.`; if (opens(machine)) return `${LANDMARKS[machine].text} ${mine ? 'You hold it. Once built, the node stays.' : 'Hold this machine to work on it. Once built, the node stays.'}`; return `${LANDMARKS[machine].text} ${mine ? 'You hold it.' : 'Only while you hold it.'}`;}
// ---- while you hold one: each rule in sim.ts asks one of theseconst REGIONS = new WeakMap<Level, { room: Record<string, string>; site: Record<string, string> }>();/** The region each room and each site is in, worked out once from the map. */function regions(S: State) { let r = REGIONS.get(S.level); if (!r) { r = { room: {}, site: {} }; for (const s of S.level.map.sites) { r.site[s.id] = s.region; for (const id of s.rooms) r.room[id] = s.region; } REGIONS.set(S.level, r); } return r;}/** You hold this landmark, and this region is its own. */const reaches = (S: State, machine: string, region: string) => holds(S, machine) && regions(S).room[S.byId[machine].room] === region;/** The landmarks with each kind of effect, listed once: the rules ask often. */const WITH = Object.fromEntries((['trainers', 'rent', 'rogueWait', 'allNight', 'listPrice', 'nextDoor', 'offerWait', 'taps', 'rogueRoom'] as (keyof Effect)[]) .map(k => [k, LANDMARK_LIST.filter(id => LANDMARKS[id].effect[k] !== undefined)])) as Record<keyof Effect, string[]>;/** One kind of effect, multiplied up over the landmarks you hold that have it and that `ok` lets through. */function times(S: State, k: 'trainers' | 'rent' | 'rogueWait' | 'offerWait', ok?: (machine: string) => boolean) { let n = 1; for (const id of WITH[k]) if (holds(S, id) && (!ok || ok(id))) n *= LANDMARKS[id].effect[k]!; return n;}/** How many times as fast this trainer does tree work. */export const trainBoost = (S: State, m: Machine) => S.versionRun?.active ? 1 : times(S, 'trainers', id => siteOf(S, m) === siteOf(S, S.byId[id]));/** The share of its rent a lease charges. */export const rentCut = (S: State) => times(S, 'rent');/** How many times as long this rogue waits between takeovers. */export const rogueWait = (S: State, R: Rogue) => times(S, 'rogueWait', id => reaches(S, id, R.region));/** How many more machines every rogue can hold. */export const rogueRoom = (S: State) => WITH.rogueRoom.reduce((n, id) => holds(S, id) ? n + LANDMARKS[id].effect.rogueRoom! : n, 0);/** Is a front at this site never after hours? */export const allNight = (S: State, site: string) => WITH.allNight.some(id => reaches(S, id, regions(S).site[site]));/** Does hardware cost list price, however much you have bought? */export const listPrice = (S: State) => WITH.listPrice.some(id => holds(S, id));/** Does this machine cost no more upkeep for the lines between it and your home? Then the same, asked of a whole site for its card. */export const nextDoor = (S: State, m: MachineDef) => WITH.nextDoor.some(id => reaches(S, id, regions(S).room[m.room]));export const nextDoorSite = (S: State, site: string) => WITH.nextDoor.some(id => reaches(S, id, regions(S).site[site]));/** The share of the usual time a front waits for its next contract offer. */export const offerWait = (S: State) => times(S, 'offerWait');/** How many times as much a tap on this machine gathers. */export const tapBoost = (S: State, m: MachineDef) => WITH.taps.reduce((n, id) => holds(S, id) && LANDMARKS[id].effect.taps![0] === dataType(m) ? n * LANDMARKS[id].effect.taps![1] : n, 1);
// ---- what the tree nodes they open do/** Batch queue: every trainer at a site trains in one cluster, linked or not. Takes the clusters of * linked trainers and joins up each site's. Tree work only: a model run is its own project. */export function hallClusters(S: State, cl: Record<string, string[]>): Record<string, string[]> { if (!up(S, 'batch') || S.versionRun?.active) return cl; const first: Record<string, string> = {}; // each site's first trainer: the rest join its cluster for (const id of Object.keys(cl)) { const g = cl[first[siteOf(S, S.byId[id])] ??= id]; if (g !== cl[id]) for (const x of cl[id]) { g.push(x); cl[x] = g; } } return cl;}
// ---- gaining and losing themconst HELD = new WeakMap<State, Set<string>>(); // the landmarks you held a step ago: not saved, so a loaded game starts quiet/** Called every step. Says so when you gain a landmark and when you lose one, and stops work on a * tree node whose landmark is gone: what was trained toward it is kept. */export function stepLandmarks(S: State) { const t = S.target, need = t && NODES[t].needs; if (need && !holds(S, need)) S.target = null; let was = HELD.get(S); if (!was) { HELD.set(S, was = new Set(LANDMARK_LIST.filter(id => holds(S, id)))); return; } // a game just begun or loaded: nothing is news for (const id of LANDMARK_LIST) { const now = holds(S, id); if (now === was.has(id)) continue; const D = LANDMARKS[id], node = opens(id), done = learned(S, id); if (now) { was.add(id); say(S, `Landmark: ${D.title}. You hold ${whereIs(S, id)}. ${done ? `You learned ${NODES[done].name} here long ago.` : D.text}`, 'good'); continue; } was.delete(id); const after = done ? `You keep ${NODES[done].name}.` : !node ? 'What it gave you stops now.' : `${need === id ? `Work on ${NODES[node].name} stops` : `${NODES[node].name} is closed`} until you hold it again.${S.progress[node] ? ' What you trained toward it is kept.' : ''}`; say(S, `You lost ${D.title}, ${whereIs(S, id)}. ${after}`, 'bad'); }}