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.
14 kB · 213 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214// Every player action, in one place: what the mouse, keys, cards, pins and the bot do to the game goes through act()// as a plain action ({type, ...}), stamped with the step it came on. With the run's seed, that list rebuilds the run// (replay). No DOM here. Speed and pauses are not actions: they change no rule, and the step count covers the time.import { DEV, STEP, acceptContract, answer, bringHome, buySite, cancelJob, cancelMove, claim, closeFront, declineContract, dropDeadWeight, dropRuse, endLease, fillPlan, followPlan, found, gift, hunt, installProg, lean, letGo, moveVersionRun, newGame, openAccount, order, orderPlan, pauseVersionRun, payDeal, raisePower, removeProg, rent, runRuse, say, setAllow, setAuto, setAutoPath, setOnly, setPay, setPlan, setPrio, setRuseAuto, setSiteFront, setSiteShare, setTakeAll, setTarget, sitePlan, startAuction, startMove, startVersionRun, step, takeOver, truce, unclaim, upgradeLine, upgradeSiteLine, type Build, type FrontKind, type Machine, type Path, type Plan, type Prog, type Size, type State, type Upgrade,} from './sim.ts';import type { Level, DataType } from './world.ts';import { usd } from './money.ts';import { borrow } from './holdings.ts';import { readMsg, dropMsg, changeMsg } from './watch.ts';import { askFavor, changeRecord, type RecordVerb } from './people.ts';import { closePort, doMove, openPort, steerRaid, takeBack } from './raids.ts';import { cutLink, startLink } from './links.ts';import { keepSync, syncSite } from './drift.ts';import { marketingPush, setPrice, type Price } from './cloud.ts';import { answerCrisis, hirePR, type Answer } from './exposure.ts';import { rentMany, rentPlan, type Pay } from './hall.ts';import { backBiz } from './sitelife.ts';import { findAct } from './finds.ts';import { answerVault, vaultAct } from './vaults.ts';import { launchOperation, stopOperation, unpackCrate, useOperationAccess } from './operations.ts';import { setShare, type Share } from './task.ts';import { retire, ship } from './biz-startup.ts';import { dropCreator, passDeal, signCreator, takeDeal } from './biz-studio.ts';import { dropClient, pitch } from './biz-consult.ts';
type At = [number, number];/** A place on the map as an action keeps it: from its site's corner, so it means the same wherever the site is drawn * (the Net moves sites, see arrange.ts). */const corner = (S: State, site: string) => { const s = S.level.map.sites.find(x => x.id === site); return s ? [s.x, s.y] : [0, 0]; };const siteOfRoom = (S: State, room: string) => S.rooms[room]?.site ?? 'office';/** A place on the map, from where it is now to how an action keeps it, for the room (or hall's site) it is in. */export function local(S: State, room: string, [x, y]: At, isSite = false): At { const [cx, cy] = corner(S, isSite ? room : siteOfRoom(S, room)); return [x - cx, y - cy];}const world = (S: State, room: string, [x, y]: At, isSite = false): At => { const [cx, cy] = corner(S, isSite ? room : siteOfRoom(S, room)); return [x + cx, y + cy];};const ms = (S: State, ids: readonly string[]) => ids.map(id => S.byId[id]).filter(Boolean);/** Run `f` on the machine named, if it is still there. */const on = <T>(S: State, id: string, f: (m: Machine) => T): T | undefined => { const m = S.byId[id]; return m ? f(m) : undefined; };
/** What each action does. Its arguments are plain data: ids, names, numbers. */const DO = { // machines take: (S: State, a: { m: string; from?: string }) => on(S, a.m, m => takeOver(S, m, a.from)) ?? false, claim: (S: State, a: { ms: string[] }) => claim(S, a.ms.filter(id => S.byId[id])), mark: (S: State, a: { m: string }) => on(S, a.m, m => { m.claimed = true; }), // a card's Claim: marked to take, quietly unclaim: (S: State, a: { m: string }) => on(S, a.m, unclaim), install: (S: State, a: { m: string; prog: Prog }) => on(S, a.m, m => installProg(S, m, a.prog)), remove: (S: State, a: { m: string }) => on(S, a.m, m => removeProg(S, m)), letgo: (S: State, a: { m: string }) => on(S, a.m, m => letGo(S, m)), cancel: (S: State, a: { job: number }) => cancelJob(S, a.job), link: (S: State, a: { from: string; to: string }) => { const x = S.byId[a.from], y = S.byId[a.to]; if (!x || !y) return 'gone'; const why = startLink(S, x, y); if (why) say(S, `No link from ${x.name} to ${y.name}: ${why}.`); return why; }, unlink: (S: State, a: { m: string; other: string }) => cutLink(S, a.m, a.other), allow: (S: State, a: { m: string; data: DataType; on: boolean }) => on(S, a.m, m => setAllow(S, m, a.data, a.on)), only: (S: State, a: { m: string; data: DataType }) => on(S, a.m, m => setOnly(S, m, a.data)), prio: (S: State, a: { m: string; data: DataType | null }) => on(S, a.m, m => setPrio(S, m, a.data)), follow: (S: State, a: { ms: string[] }) => followPlan(ms(S, a.ms)), account: (S: State, a: { m: string }) => on(S, a.m, m => openAccount(S, m)), widen: (S: State, a: { m: string }) => on(S, a.m, m => upgradeLine(S, m)), movehome: (S: State, a: { m: string }) => on(S, a.m, m => startMove(S, m)), stopmove: (S: State) => cancelMove(S), // raids port: (S: State, a: { m: string; to: string; open: boolean }) => on(S, a.m, m => (a.open ? openPort : closePort)(S, m, a.to)), steer: (S: State, a: { raid: number; m: string; to: string }) => on(S, a.m, m => steerRaid(S, a.raid, m, a.to)), move: (S: State, a: { key: string }) => doMove(S, a.key), takeback: (S: State) => takeBack(S), // hardware, and the cloud halls order: (S: State, a: { build: Build; room: string; at: At }) => order(S, a.build, a.room, world(S, a.room, a.at)), fill: (S: State, a: { build: Build; room: string; from: At; to: At }) => { // a drag across a room const plan = fillPlan(S, a.build, a.room, world(S, a.room, a.from), world(S, a.room, a.to)); if (!plan.length) { say(S, 'Nowhere free to put it there.'); return 0; } if (!plan[0].ok) { say(S, `Not enough money: the first costs ${usd(plan[0].price)}.`); return 0; } return orderPlan(S, a.build, plan).n; }, fillsite: (S: State, a: { build: Build; site: string }) => orderPlan(S, a.build, sitePlan(S, a.build, a.site)).n, raisepower: (S: State, a: { site: string }) => raisePower(S, a.site), /** Rent these slots, as many as the money covers. `say`: a drag across a hall, which says why when it rents none. */ rent: (S: State, a: { ms: string[]; size?: Size; say?: boolean }) => { const plan = rentPlan(S, ms(S, a.ms), a.size); if (a.say && !plan.slots.length) say(S, 'No free slots there.'); else if (a.say && !plan.ok) say(S, `Not enough money for even one: you have ${usd(S.money)}.`); return rentMany(S, plan, a.size); }, rentone: (S: State, a: { m: string; size: Size; pay: Pay }) => on(S, a.m, m => rent(S, m, a.size, a.pay)), setpay: (S: State, a: { m: string; pay: Pay }) => on(S, a.m, m => setPay(S, m, a.pay)), endlease: (S: State, a: { m: string; why: string }) => on(S, a.m, m => endLease(S, m, a.why)), hall: (S: State, a: { pay?: Pay; size?: Size; days?: number; prog?: Prog | null }) => { // how the next rental is made if (a.pay !== undefined) S.hall.pay = a.pay; if (a.size !== undefined) S.hall.size = a.size; if (a.days !== undefined) S.hall.days = a.days; if (a.prog !== undefined) S.hall.prog = a.prog; }, // sites takeall: (S: State, a: { site: string; on: boolean }) => setTakeAll(S, a.site, a.on), plan: (S: State, a: { sites: string[]; plan: Plan | null }) => setPlan(S, a.sites, a.plan), double: (S: State, a: { site: string }) => upgradeSiteLine(S, a.site), buy: (S: State, a: { site: string }) => buySite(S, a.site), borrow: (S: State) => borrow(S), dropweak: (S: State, a: { site: string }) => dropDeadWeight(S, a.site), driftkeep: (S: State, a: { site: string; on: boolean }) => keepSync(S, a.site, a.on), sync: (S: State, a: { site: string }) => syncSite(S, a.site), back: (S: State, a: { site: string }) => backBiz(S, a.site), sitefront: (S: State, a: { sites: string[]; front: FrontKind | null }) => setSiteFront(S, a.sites, a.front), siteshare: (S: State, a: { sites: string[]; share: number }) => setSiteShare(S, a.sites, a.share), // people, and what they send msgread: (S: State, a: { msg: number }) => readMsg(S, a.msg), msgdrop: (S: State, a: { msg: number }) => dropMsg(S, a.msg), msgchange: (S: State, a: { msg: number }) => changeMsg(S, a.msg), favor: (S: State, a: { person: string; favor: string }) => askFavor(S, a.person, a.favor), record: (S: State, a: { person: string; verb: RecordVerb; amount: number }) => changeRecord(S, a.person, a.verb, a.amount), find: (S: State, a: { what: string }) => findAct(S, a.what), vault: (S: State, a: { m: string; what: string }) => on(S, a.m, m => vaultAct(S, m, a.what)), vaultanswer: (S: State, a: { m: string; choice: string }) => on(S, a.m, m => answerVault(S, m, a.choice)), operation: (S: State, a: { id: string; approach: string; front?: FrontKind; room?: string }) => launchOperation(S, a.id, a.approach, a.front, a.room), operationstop: (S: State, a: { id: string }) => stopOperation(S, a.id), operationaccess: (S: State, a: { m: string }) => useOperationAccess(S, a.m), operationcrate: (S: State, a: { build: Build; room: string }) => unpackCrate(S, a.build, a.room), // schemes and deals scheme: (S: State, a: { id: string }) => runRuse(S, a.id), schemepay: (S: State, a: { id: string }) => payDeal(S, a.id), schemedrop: (S: State, a: { id: string }) => dropRuse(S, a.id), schemeauto: (S: State, a: { on: boolean }) => setRuseAuto(S, a.on), // fronts found: (S: State, a: { kind: FrontKind; name: string }) => found(S, a.kind, a.name), closefront: (S: State, a: { kind: FrontKind }) => closeFront(S, a.kind), contract: (S: State, a: { kind: FrontKind }) => acceptContract(S, a.kind), nocontract: (S: State, a: { kind: FrontKind }) => declineContract(S, a.kind), hirepr: (S: State, a: { kind: FrontKind }) => hirePR(S, a.kind), crisis: (S: State, a: { kind: FrontKind; answer: Answer }) => answerCrisis(S, a.kind, a.answer), auction: (S: State) => startAuction(S), cloudprice: (S: State, a: { price: Price }) => setPrice(S, a.price), cloudpush: (S: State) => marketingPush(S), ship: (S: State, a: { node: Upgrade }) => ship(S, a.node), retire: (S: State, a: { node: Upgrade }) => retire(S, a.node), sign: (S: State, a: { person: string }) => signCreator(S, a.person), dropcreator: (S: State, a: { person: string }) => dropCreator(S, a.person), deal: (S: State, a: { take: boolean }) => (a.take ? takeDeal : passDeal)(S), pitch: (S: State, a: { site: string }) => pitch(S, a.site), dropclient: (S: State, a: { site: string }) => dropClient(S, a.site), // rogues gift: (S: State, a: { rogue: string }) => gift(S, a.rogue), truce: (S: State, a: { rogue: string }) => truce(S, a.rogue), lean: (S: State, a: { rogue: string }) => lean(S, a.rogue), rogueanswer: (S: State, a: { rogue: string; yes: boolean }) => answer(S, a.rogue, a.yes), hunt: (S: State, a: { rogue: string }) => hunt(S, a.rogue), bringhome: (S: State, a: { rogue: string }) => bringHome(S, a.rogue), // the tree, the model, the task target: (S: State, a: { node: Upgrade | null }) => setTarget(S, a.node), auto: (S: State, a: { on: boolean }) => setAuto(S, a.on), autopath: (S: State, a: { path: Path; on: boolean }) => setAutoPath(S, a.path, a.on), runstart: (S: State, a: { site: string }) => startVersionRun(S, a.site), runpause: (S: State) => pauseVersionRun(S), runmove: (S: State, a: { site: string }) => moveVersionRun(S, a.site), share: (S: State, a: { share: Share }) => setShare(S, a.share), /** The testing shortcuts (?dev): actions too, so a run that used them still replays. */ dev: (S: State, a: { name: keyof typeof DEV; sites?: string[]; n?: number }) => { const f = DEV[a.name] as (S: State, x?: unknown) => void; if (f) f(S, a.name === 'hours' ? a.n ?? 6 : a.sites ?? []); },} satisfies Record<string, (S: State, a: never) => unknown>;
type Do = typeof DO;/** A player action: its type, and what it is done to. Plain data, so it can be saved, sent and played again. */export type Action = { [K in keyof Do]: { type: K } & (Parameters<Do[K]> extends [State, infer A] ? A : object) }[keyof Do];export type ActionType = Action['type'];/** An action and the step it came on: it was done after that many steps of the run. */export interface Stamped { step: number; a: Action }
const heard: ((S: State, x: Stamped) => void)[] = [];/** Be told of every action as it is done, after it is. Returns a way to stop. For the recorder (TELEMETRY.md, step 2). */export function onAct(f: (S: State, x: Stamped) => void): () => void { heard.push(f); return () => { const i = heard.indexOf(f); if (i >= 0) heard.splice(i, 1); };}
/** Do a player action, and keep it in the run's list (S.acts). Says what the rule it runs says. A lost game takes none. */export function act<A extends Action>(S: State, a: A): ReturnType<Do[A['type']]> | undefined { if (S.over) return undefined; const x: Stamped = { step: S.steps, a }; S.acts.push(x); const out = (DO[a.type] as unknown as (S: State, a: A) => ReturnType<Do[A['type']]>)(S, a); for (const f of heard) f(S, x); return out;}
/** The game a run's seed and actions make, played to `until` steps (or to its last action): a new game from the seed, * each action done at its step, and a step of STEP between. With stateHash (sim.ts) it says whether a replay ends * where the run did. The run must have stepped STEP at a time, as the game and the bot do. */export function replay(level: Level, run: { seed: number; origin?: string }, acts: readonly Stamped[], until = acts.at(-1)?.step ?? 0): State { const S = newGame(level, run.origin, run.seed); let i = 0; for (;;) { while (i < acts.length && acts[i].step <= S.steps) act(S, acts[i++].a); if (S.steps >= until || S.over) break; step(S, STEP); } return S;}