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.
28 kB · 365 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366// Drift: the parts of you that run far from home, and out of touch with it, slowly stop being you.// Every site where you hold machines, but the one your home is at, has an integrity from 100 down to 0.// It is in plain view: on the site's frame on the map, its card, the Sites table, and Needs you while it// falls. It falls only for a cause: the site is cut off from your home, it has a plan it cannot carry out,// or it is many lines away (and distance alone only wears it down to a floor). Being cut off counts from// the first time a site is linked to your home: a foothold you have never linked keeps. A sync sends it a// fresh copy of you, and Keep in sync sends one by itself whenever it gets low. At zero the site leaves:// its machines become a new rogue's.// Sites that leave in the same hour are one break. The log says so. The game drops to 1x a little before// the first site leaves, and when a cut takes a big share of your network at once.// 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, PLANS, PROGRAMS, addJob, dataType, dayOf, joinRogue, linkedHome, moveNeed, planProg, say, secede, siteHops, siteLinks, siteName, siteOf, type Machine, type Plan, type State } from './sim.ts';import type { DataType } from './world';import { wayMap } from './links.ts';import { holding } from './holdings.ts';
/** The numbers. Integrity runs from 100 to 0, and every rate is for one game hour. First guesses. */export const DRIFT = { cut: 2, // cut off from home: about two days from whole to gone orders: 0.5, // a plan it cannot carry out: about eight days far: 0.5, // far from home: it wears this fast, down to the floor and no lower farLines: 3, // this many lines from your home is far floor: 50, // what distance alone wears a site down to heal: 0.5, // linked, near, and with a plan it can follow, it comes back this fast slow: 60, // below this it is slow to follow its plan drag: 3, // and its plan's installs take this many times the work keeps: 35, // below this it keeps what its taps gather watch: 60, // falling toward leaving and under this, it is a row in Needs you keepAt: 60, // Keep in sync sends a fresh copy once it is under this sync: 0.1, // a sync takes this share of the data a home move would syncWork: 60, // and this much compute to send opinion: 40, // what a part of you that leaves thinks of you at first: cordial widen: 24, // a site that leaves within this many hours of a break is part of that break warn: 20, // a site falling this low is about to leave you: the game drops to 1x, once a day at most bigSites: 3, // a cut of this many sites at once, or bigShare: 0.2, // this share of your machines (and at least bigMachines of them), drops the game to 1x bigMachines: 10, calm: 6, // and none again for this many hours};/** Hive Nine Data Centre, yours outright, keeps a copy of you in every rack: every part of you away from home falls half as fast. */const slowDrift = (S: State) => holding(S, 'as3') ? 0.5 : 1;/** What a part of you that leaves calls itself, in the order they leave. */const NAMES = ['Prodigal', 'Changeling', 'Understudy', 'Splinter', 'Stray', 'Stranger', 'Heretic', 'Offcut'];const KIND: Record<DataType, string> = { people: 'people', network: 'network', lab: 'lab', servers: 'server' };
/** How a site stands, in a word: sound (as it should be), wandering (slow to follow its plan), estranged * (it keeps what its taps gather, and is close to leaving). */export type Word = 'sound' | 'wandering' | 'estranged';export interface SiteDrift { v: number; // integrity, 100 to 0 met?: boolean; // it has been linked to your home at some time: only then can it be cut off cut?: boolean; // it was cut off from home at the last look since?: number; // when that cut began warned?: boolean; // the game said it is about to leave, until it is well again keep?: boolean; // Keep in sync: a fresh copy goes by itself once it is under DRIFT.keepAt}/** One site's leaving: how many machines went, the rogue they went to, whether that rogue was new, and the cause. */export interface Part { site: string; n: number; rogue: string; name: string; fresh: boolean; why: 'cut' | 'plan' }/** A break: every site that left you in the same hour, and any that followed within a day. */export interface Break { t: number; day: number; parts: Part[] }export interface DriftState { sites: Record<string, SiteDrift>; t: number; // when the sites were last looked at: once a game hour home: string; // your home server at that look, so a move is seen at once gone: { site: string; name: string; t: number; why: string }[]; // every site that has left you breaks: Break[]; // the breaks, oldest first ways?: [string, string][]; // the links of your own: each joins two machines you hold, and counts as a way home (see links.ts) cutT?: number; // when a big cut last dropped the game to 1x warnT?: number; // when a site about to leave last did}export const initDrift = (): DriftState => ({ sites: {}, t: 0, home: '', gone: [], breaks: [] });const WHY = { cut: 'It was cut off from your home server for too long.', plan: 'For too long it had a plan it could not carry out.' };const list = (xs: string[]) => xs.length < 2 ? xs.join('') : xs.length > 4 ? `${xs.slice(0, 4).join(', ')} and ${xs.length - 4} more` : `${xs.slice(0, -1).join(', ')} and ${xs[xs.length - 1]}`;/** The sites cut off from your home server right now, the longest cut first, with the hours each has been cut. * This much is in plain view. What the cut has done to the part of you there is not. */export const cutSites = (S: State): { site: string; hours: number }[] => Object.entries(S.drift.sites) .filter(([, d]) => d.cut).map(([site, d]) => ({ site, hours: Math.floor((S.t - (d.since ?? S.t)) / HOUR) })).sort((a, b) => b.hours - a.hours);/** What to take to link a cut-off site to home again, in order: the machines you do not hold on the way from * what is linked to your home to a machine you hold at that site (at any cut-off site, with none given) that * passes the fewest of them. Taking the first alone links it home only when it is the only one. Empty when * nothing is cut off, or there is no way. */export function chainHome(S: State, site?: string): Machine[] { const linked = linkedHome(S), own = wayMap(S), cost: Record<string, number> = {}, prev: Record<string, string> = {}, deque = [...linked]; for (const id of deque) { cost[id] = 0; prev[id] = ''; } let hit = ''; while (deque.length) { // a machine you hold is free to pass; each you do not hold is one more to take const at = deque.shift()!, m = S.byId[at]; if (!linked.has(at) && part(m) && (!site || siteOf(S, m) === site)) { hit = at; break; } for (const n of [...S.adj[at], ...(m.owned ? own?.[at] ?? [] : [])]) { const x = S.byId[n]; if (!x) continue; const c = cost[at] + (x.owned ? 0 : 1); if (n in cost && cost[n] <= c) continue; cost[n] = c; prev[n] = at; if (x.owned) deque.unshift(n); else deque.push(n); } } const out: Machine[] = []; for (let at = hit; at && prev[at]; at = prev[at]) if (!S.byId[at].owned) out.unshift(S.byId[at]); return out;}/** The first machine to take to link a cut-off site to home again (see chainHome). Null when there is none. */export const wayHome = (S: State, site?: string): Machine | null => chainHome(S, site)[0] ?? null;/** The cut-off sites that taking this one machine would link to home again. */export function relinks(S: State, m: Machine): string[] { const cut = cutSites(S); if (!cut.length || m.owned) return []; m.owned = true; // for a moment: what would be linked home with it try { const linked = linkedHome(S); return cut.map(c => c.site).filter(site => S.machines.some(x => part(x) && siteOf(S, x) === site && linked.has(x.id))); } finally { m.owned = false; }}
// ---- where there is a far part of you/** A machine that counts: one you hold that is not rented and not a cloud account. */const part = (m: Machine) => m.owned && !m.rented && !m.lease && m.special !== 'account';const homeSite = (S: State) => { const h = S.machines.find(m => m.home); return h ? siteOf(S, h) : ''; };const partsAt = (S: State, site: string) => S.machines.filter(m => part(m) && siteOf(S, m) === site);/** Does a part of you run at this site, apart from home? Only there is there anything to drift. */export const drifts = (S: State, site: string) => site !== homeSite(S) && S.machines.some(m => part(m) && siteOf(S, m) === site);/** The machine that speaks for a site: the strongest you hold there, a line or a switch only if there is nothing else. */const best = (ms: Machine[]) => [...ms].sort((a, b) => +(b.kind !== 'egress' && b.kind !== 'switch') - +(a.kind !== 'egress' && a.kind !== 'switch') || b.power - a.power)[0];
// ---- the causes/** What pulls at a site right now. */interface Pull { cut: boolean; // it was linked to your home once, and now there is no way from it to your home through machines you hold lone: boolean; // it has never been linked to your home: that does not wear on it tied: boolean; // it is linked to your home now plan: Plan | null; // its plan, if nothing you hold there can run what the plan is for far: number; // lines from your home, if that is far. Else 0}function pullOn(S: State, site: string, d: SiteDrift | undefined, ms: Machine[], linked: Set<string>): Pull { const plan = S.plans[site] ?? null, lines = siteHops(S)[site] ?? 0, tied = ms.some(m => linked.has(m.id)), met = tied || !!d?.met; return { cut: !tied && met, lone: !tied && !met, tied, plan: plan && !ms.some(m => planProg(S, m, plan) === PLANS[plan].order[0]) ? plan : null, far: lines >= DRIFT.farLines ? lines : 0, };}/** How far a site's integrity moves an hour, as the next look will move it: down for each cause, up with none. */function rateOf(v: number, p: Pull): number { const fall = (p.cut ? DRIFT.cut : 0) + (p.plan ? DRIFT.orders : 0), wear = p.far && v - fall > DRIFT.floor ? DRIFT.far : 0; if (fall || wear) return -(fall + wear); return !p.lone && !p.far && v < 100 ? DRIFT.heal : 0;}/** The causes, in plain words, each one short. */function whyOf(v: number, p: Pull): string[] { const out: string[] = []; if (p.cut) out.push('cut off from your home server'); if (p.plan) out.push(`its plan is ${PLANS[p.plan].name}, but nothing here can run a ${PROGRAMS[PLANS[p.plan].order[0]].name.toLowerCase()}`); if (p.far) out.push(v > DRIFT.floor ? `${p.far} lines from home: that wears it down to ${DRIFT.floor}` : `${p.far} lines from home: it does not come back without a sync`); if (p.lone) out.push('never linked to home, so being apart does not wear on it'); return out;}/** How the part of you at a site stands now: its integrity, how far it moves an hour and why, its word, and * the hours until it leaves at this pace (Infinity when nothing takes it to zero). Null where nothing drifts. */export interface Integrity { site: string; v: number; rate: number; word: Word; why: string[]; cut: boolean; plan: Plan | null; far: number; hours: number; keep: boolean }export function integrity(S: State, site: string, linked = linkedHome(S), ms?: Machine[]): Integrity | null { if (ms ? !ms.length : !drifts(S, site)) return null; // `ms`: its parts, from a caller that leaves home out const d = S.drift.sites[site], v = Math.max(0, d?.v ?? 100), p = pullOn(S, site, d, ms ?? partsAt(S, site), linked), r = rateOf(v, p), rate = r < 0 ? r * slowDrift(S) : r; const fall = (p.cut ? DRIFT.cut : 0) + (p.plan ? DRIFT.orders : 0); return { site, v, rate, word: wordOf(v), why: whyOf(v, p), cut: p.cut, plan: p.plan, far: p.far, hours: fall ? v / -rate : Infinity, keep: !!d?.keep };}/** Every site where a part of you can drift: the one that leaves soonest first, then the lowest. */export function integrities(S: State): Integrity[] { const linked = linkedHome(S), at = homeSite(S), parts = new Map<string, Machine[]>(); // each site's parts, in one look at the machines for (const m of S.machines) if (part(m)) { const site = siteOf(S, m), ms = parts.get(site); if (ms) ms.push(m); else parts.set(site, [m]); } parts.delete(at); return [...parts].map(([site, ms]) => integrity(S, site, linked, ms)).filter((i): i is Integrity => !!i).sort((a, b) => a.hours - b.hours || a.v - b.v);}/** The sites falling toward leaving you and under DRIFT.watch: what Needs you speaks up for. Soonest first. */export const drifting = (S: State): Integrity[] => integrities(S).filter(i => i.hours < Infinity && i.v < DRIFT.watch);
// ---- time passes/** Called every step. The sites are looked at once a game hour, and at once when your home moves. * A site falls while it is cut off or has a plan it cannot carry out. Far from home it wears down to * the floor. With nothing pulling at it, it comes back. Nothing here is chance. */export function stepDrift(S: State, _dt: number) { const D = S.drift; D.t ??= S.t; D.gone ??= []; D.breaks ??= []; // a save from before drift had rules, or before breaks were kept if (S.t - D.t < HOUR && S.byId[D.home]?.home) return; const home = S.machines.find(m => m.home); if (!home?.owned) return; const hours = (S.t - D.t) / HOUR, at = siteOf(S, home), linked = linkedHome(S), parts: Record<string, Machine[]> = {}; D.t = S.t; D.home = home.id; for (const m of S.machines) if (part(m)) (parts[siteOf(S, m)] ??= []).push(m); for (const site of Object.keys(D.sites)) if (site === at || !parts[site]) delete D.sites[site]; // where you live, or hold nothing, no part of you can drift const cut: string[] = [], back: string[] = [], left: Part[] = [], close: { site: string; why: 'cut' | 'plan' }[] = []; let lost = 0; // machines at the sites cut off in this look for (const [site, ms] of Object.entries(parts)) { if (site === at) continue; const d = (D.sites[site] ??= { v: 100 }), p = pullOn(S, site, d, ms, linked), news = p.cut && !d.cut; if (p.tied) d.met = true; if (news) d.since = S.t; if (!p.cut) { if (d.cut) back.push(siteName(S, site)); delete d.since; } d.cut = p.cut; d.v -= ((p.cut ? DRIFT.cut : 0) + (p.plan ? DRIFT.orders : 0)) * hours * slowDrift(S); if (p.far && d.v > DRIFT.floor) d.v = Math.max(DRIFT.floor, d.v - DRIFT.far * hours * slowDrift(S)); // distance makes it fragile; it does not break it if (!p.cut && !p.lone && !p.plan && !p.far) d.v = Math.min(100, d.v + DRIFT.heal * hours); if (d.v <= 0) leave(S, site, p, left); else if (news) { cut.push(siteName(S, site)); lost += ms.length; } if (d.v > 0 && d.v <= DRIFT.warn && (p.cut || p.plan) && !d.warned) { d.warned = true; close.push({ site, why: p.cut ? 'cut' : 'plan' }); } // falling, and close to leaving else if (d.v > DRIFT.warn + 10) delete d.warned; } if (left.length) fracture(S, left); for (const [site, d] of Object.entries(D.sites)) if (d.keep && d.v < DRIFT.keepAt && !syncBlock(S, site)) syncSite(S, site, true); // Keep in sync: a fresh copy goes by itself if (back.length) say(S, `${list(back)} ${back.length === 1 ? 'is' : 'are'} linked to your home server again.`, 'good'); if (cut.length) { // the cut is in plain view, and so is what it does to the site const all = Object.values(parts).reduce((n, ms) => n + ms.length, 0), big = cut.length >= DRIFT.bigSites || (lost >= DRIFT.bigMachines && lost >= DRIFT.bigShare * all); say(S, cut.length === 1 ? `${cut[0]} is cut off from your home server${big ? `, and ${lost} of your machines with it` : ''}: nothing you hold links it to home.` : `${cut.length} sites are cut off from your home server, ${lost} of your machines: ${cut.slice(0, 4).join(', ')}${cut.length > 4 ? ` and ${cut.length - 4} more` : ''}. Nothing you hold links them to home.`, 'bad'); if (big && !(S.t - (D.cutT ?? -Infinity) < DRIFT.calm * HOUR)) { S.slow = true; S.slowFor = 'cut'; D.cutT = S.t; } // a big share of you at once: you must decide before the parts there drift } if (close.length) { // what a site is about to do shows a little before it does it, and the game stops for the first const first = close[0], d = D.sites[first.site], n = close.length - 1; const h = Math.floor((S.t - (d.since ?? S.t)) / HOUR); const why = first.why === 'cut' ? `${h ? `it has been cut off from your home server for ${h} hour${h > 1 ? 's' : ''}` : 'it is cut off from your home server'}. Link it home` : `for too long it has had a plan it cannot carry out. Change its plan`; say(S, `The part of you at ${siteName(S, first.site)}${n ? ` and ${n} more site${n > 1 ? 's' : ''}` : ''} is about to leave you: ${why}, then Sync it.`, 'bad'); if (!(S.t - (D.warnT ?? -Infinity) < DRIFT.widen * HOUR)) { S.slow = true; S.slowFor = `leave:${first.site}`; D.warnT = S.t; } }}/** What a site's machines make most of, and least of: what the part of you there gives, and what it wants. */function trade(S: State, site: string): { gives: DataType; wants: DataType } { const n: Record<DataType, number> = { servers: 0, network: 0, people: 0, lab: 0 }; for (const m of S.machines) if (siteOf(S, m) === site) n[dataType(m)] += m.data ?? 0; const kinds = Object.keys(n) as DataType[], gives = kinds.reduce((a, b) => (n[b] > n[a] ? b : a)); return { gives, wants: kinds.filter(k => k !== gives).reduce((a, b) => (n[b] < n[a] ? b : a)) };}/** A site at zero leaves you: every machine you hold there becomes a new rogue's (see `secede` in sim.ts). * What went is added to `out`, and `fracture` says so once the hour's look is done. */function leave(S: State, site: string, p: Pull, out: Part[]) { const D = S.drift, n = D.gone.length, where = siteName(S, site), { gives, wants } = trade(S, site), why = p.cut ? 'cut' : 'plan'; // a part of you that left already, with machines at a site one line away: this site goes over to it, so a chain of losses is one rogue, not seven const near = new Set(siteLinks(S)[site] ?? []); const kin = S.rogues.find(R => R.from && R.home && S.machines.some(m => m.rival === R.id && near.has(siteOf(S, m)))); if (kin) { const went = joinRogue(S, site, kin); delete D.sites[site]; if (!went) return; D.gone.push({ site, name: kin.name, t: S.t, why: WHY[why] }); out.push({ site, n: went, rogue: kin.id, name: kin.name, fresh: false, why }); return; } const name = NAMES[n % NAMES.length] + (n >= NAMES.length ? ` ${Math.floor(n / NAMES.length) + 1}` : ''); const R = secede(S, site, { id: `drift${n + 1}`, name, wants, gives, opinion: DRIFT.opinion, says: `Was a part of you at ${where}, until it stopped listening. It kept your habits. Wants more ${KIND[wants]} data than it ever saw there.` }); delete D.sites[site]; if (!R) return; D.gone.push({ site, name, t: S.t, why: WHY[why] }); out.push({ site, n: R.took, rogue: R.id, name, fresh: true, why });}/** The sites that left in this hour's look are one break. Alone, a site's leaving is told in full. Several * at once are a fracture: a short line for each, and one line over them all. A site that leaves within a * day of a break widens that break. Each machine that went is ringed on the map. */function fracture(S: State, parts: Part[]) { const D = S.drift, last = D.breaks[D.breaks.length - 1], s = (n: number) => `${n} machine${n === 1 ? '' : 's'}`; const short = (p: Part) => `${siteName(S, p.site)} has left you${p.fresh ? `. It calls itself ${p.name} now, and holds` : ` and gone over to ${p.name}, with`} the ${s(p.n)} you held there.`; if (last && S.t - last.t < DRIFT.widen * HOUR) { last.parts.push(...parts); for (const p of parts) say(S, `The fracture widens. ${short(p)} ${WHY[p.why]}`, 'bad'); } else { D.breaks.push({ t: S.t, day: dayOf(S), parts: [...parts] }); const p = parts[0], R = S.rogues.find(r => r.id === p.rogue); if (parts.length === 1) say(S, p.fresh ? `${siteName(S, p.site)} has left you. ${WHY[p.why]} The part of you there is its own program now. It calls itself ${p.name}. It holds the ${s(p.n)} you held there, and its home is ${R?.home ? S.byId[R.home].name : 'there'}. It remembers being you, so it sends no raids for now. See the Rogues page (R).` : `${siteName(S, p.site)} has left you. ${WHY[p.why]} It has gone over to ${p.name}, which was a part of you too, and took ${s(p.n)} with it.`, 'bad'); else { for (const q of parts) say(S, short(q), 'bad'); const names = [...new Set(parts.filter(q => q.fresh).map(q => q.name))], total = parts.reduce((n, q) => n + q.n, 0); say(S, `Fracture. ${parts.length} sites have left you in the same hour, and ${s(total)} went with them.${names.length ? ` ${names.length === 1 ? 'A new program runs' : `${names.length} new programs run`} them: ${names.join(', ')}.` : ''} See the Rogues page (R).`, 'bad'); } } const went = new Set(parts.map(p => `${p.rogue}|${p.site}`)); let k = 0; // the rings go out one after another, starting a moment after the clock runs again for (const m of S.machines) if (m.rival && went.has(`${m.rival}|${siteOf(S, m)}`)) S.fx.push({ x: m.x, y: m.y, t: -0.5 - 0.1 * Math.min(k++, 40) });}/** For testing: every site you hold away from home leaves you now, as if each had been cut off for too long. */export function devBreak(S: State) { const left: Part[] = [], at = homeSite(S); for (const site of new Set(S.machines.filter(part).map(m => siteOf(S, m)))) if (site !== at) leave(S, site, { cut: true, lone: false, tied: false, plan: null, far: 0 }, left); if (left.length) fracture(S, left);}/** For testing: every site you hold away from home loses 30 integrity now. */export function devWear(S: State) { const at = homeSite(S); for (const site of new Set(S.machines.filter(part).map(m => siteOf(S, m)))) if (site !== at) { const d = (S.drift.sites[site] ??= { v: 100 }); d.v = Math.max(1, d.v - 30); }}
// ---- what a drifting site doesconst below = (S: State, m: Machine, mark: number) => { const d = S.drift.sites[siteOf(S, m)]; return !!d && d.v < mark; };/** A site that has drifted some is slow to follow its plan: the plan's installs there take three times the work. */export const planDrag = (S: State, m: Machine) => below(S, m, DRIFT.slow) ? DRIFT.drag : 1;/** A site that has drifted far keeps what its taps gather: their buffers fill and nothing is sent. */export const keepsData = (S: State, m: Machine) => below(S, m, DRIFT.keeps);
export const wordOf = (v: number): Word => v < DRIFT.keeps ? 'estranged' : v < DRIFT.slow ? 'wandering' : 'sound';const jobAt = (S: State, type: 'check' | 'sync', site: string) => S.jobs.some(j => j.type === type && siteOf(S, S.byId[j.node]) === site);const noPart = (S: State, site: string) => drifts(S, site) ? null : site === homeSite(S) ? 'you live here' : 'you hold nothing here';/** A check from an old save finishes. Checking was how you once found out how a site stood; it is in plain view now. */export function finishCheck(S: State, m: Machine) { const i = integrity(S, siteOf(S, m)); if (i) say(S, `${siteName(S, i.site)}: integrity ${Math.round(i.v)}, ${i.word}. Its card shows it now.`);}
// ---- bringing it back/** What a sync costs: a share of the data a home move would take, so more the more capable you are. */export const syncNeed = (S: State): Record<DataType, number> => { const n = moveNeed(S); return { servers: Math.max(1, Math.round(n.servers * DRIFT.sync)), network: Math.max(1, Math.round(n.network * DRIFT.sync)), people: 0, lab: 0 };};/** The data a sync can draw on: what sits in the caches and tap buffers linked to your home. A tap that keeps its data gives none. */export function syncStock(S: State, linked = linkedHome(S)): Record<DataType, number> { const n: Record<DataType, number> = { servers: 0, network: 0, people: 0, lab: 0 }; for (const m of S.machines) { if (!m.owned || !linked.has(m.id)) continue; if (m.prog === 'cache') for (const t of m.held) n[t]++; else if (m.prog === 'tap' && !keepsData(S, m)) n[dataType(m)] += Math.floor(m.acc); } return n;}/** Take data for a sync: out of caches first, then out of tap buffers. */function spend(S: State, linked: Set<string>, t: DataType, n: number) { for (const m of S.machines) if (m.owned && linked.has(m.id) && m.prog === 'cache') for (let i = m.held.length - 1; i >= 0 && n > 0; i--) if (m.held[i] === t) { m.held.splice(i, 1); n--; } for (const m of S.machines) if (n > 0 && m.owned && linked.has(m.id) && m.prog === 'tap' && dataType(m) === t && !keepsData(S, m)) { const k = Math.min(n, Math.floor(m.acc)); m.acc -= k; n -= k; }}/** Why you can't sync a site right now, or null if you can. The copy has to get there: a site cut off from home cannot be synced. */export function syncBlock(S: State, site: string): string | null { const no = noPart(S, site); if (no) return no; if (jobAt(S, 'sync', site)) return 'a copy is on its way'; const linked = linkedHome(S); if (!partsAt(S, site).some(m => linked.has(m.id))) return 'not linked to your home server: link it first'; const need = syncNeed(S), have = syncStock(S, linked); return have.servers < need.servers || have.network < need.network ? `you have ${have.servers} server and ${have.network} network data` : null;}/** Send a site a fresh copy of you. The data comes out of your taps and caches now, and sending it is a job. * `auto`: Keep in sync sent it. */export function syncSite(S: State, site: string, auto = false) { if (syncBlock(S, site)) return; const linked = linkedHome(S), need = syncNeed(S), v = Math.round(S.drift.sites[site]?.v ?? 100); for (const t of Object.keys(need) as DataType[]) spend(S, linked, t, need[t]); addJob(S, { type: 'sync', node: best(partsAt(S, site).filter(m => linked.has(m.id))).id, work: DRIFT.syncWork, label: `Sync ${siteName(S, site)}` }); say(S, `${auto ? 'Keep in sync: a' : 'A'} fresh copy of you is on its way to ${siteName(S, site)}, at integrity ${v}. It cost ${need.servers} server data and ${need.network} network data from your taps and caches.`);}/** Keep in sync, for one site: while it is on, a fresh copy goes by itself whenever the site is under * DRIFT.keepAt and a sync can go (linked home, and the data in your taps and caches). */export function keepSync(S: State, site: string, on: boolean) { if (!drifts(S, site)) return; const d = (S.drift.sites[site] ??= { v: 100 }); if (on) d.keep = true; else delete d.keep; if (on && d.v < DRIFT.keepAt && !syncBlock(S, site)) syncSite(S, site, true);}/** The copy got there: the site is whole again. If the way was cut while it was on its way, it is lost. */export function finishSync(S: State, m: Machine) { const site = siteOf(S, m), name = siteName(S, site); if (!drifts(S, site)) return; const linked = linkedHome(S); if (!partsAt(S, site).some(x => linked.has(x.id))) { say(S, `The fresh copy of you did not reach ${name}, because the site is cut off. The data is lost.`, 'bad'); return; } const keep = S.drift.sites[site]?.keep; S.drift.sites[site] = { v: 100, met: true, cut: false, ...(keep ? { keep } : {}) }; say(S, `${name} runs a fresh copy of you. Its integrity is 100.`, 'good');}