diff --git a/src/sim.ts b/src/sim.ts index 1a069414..1b198459 100644 --- a/src/sim.ts +++ b/src/sim.ts @@ -55,7 +55,6 @@ export const dataType = (m: MachineDef): DataType => export type Prog = 'tap' | 'cache' | 'trainer' | 'broker' | 'trader' | 'firewall' | 'sentry' | 'honeypot'; /** The programs that guard your network against raids (see raids.ts). They cost compute a second to keep running, like a machine's upkeep. */ export type Guard = 'firewall' | 'sentry' | 'honeypot'; -export const GUARDS: Guard[] = ['firewall', 'sentry', 'honeypot']; export const isGuard = (p: Prog | null | undefined): p is Guard => p === 'firewall' || p === 'sentry' || p === 'honeypot'; /** Programs you install on machines you own. Together they are your base: @@ -236,7 +235,6 @@ export const TRAINER_BUFFER = 4; /** A version run is a large project at one site. Each piece of data counts once. */ export const VERSION_2 = { version: 2, name: 'Operator', need: { servers: 400, lab: 300, people: 200, network: 100 }, power: 10 } as const; export type VersionRun = { site: string; active: boolean; got: Record }; -export const versionNeed = () => VERSION_2.need; /** The big run you can do now: model v2 first; after you pick what you become, reprogramming yourself, * the same mix of data, twice as much each time. */ export function runNeed(S: State): Record { @@ -3625,7 +3623,7 @@ export function declineContract(S: State, k: FrontKind) { function stepContracts(S: State) { for (const fr of S.fronts) { const k = fr.kind, c = S.contracts[k], D = CONTRACTS[k]; - if (k === 'neocloud') { delete S.contracts[k]; continue; } // it has leads and customers, not contracts (an old save may still hold one) + if (k === 'neocloud') continue; // it has leads and customers, not contracts if (!c) { // an earning front gets an offer every so often if (!frontLive(S, k)) continue; S.contractT[k] ??= S.t + contractEvery(S) * (0.5 + rand()); @@ -3708,17 +3706,18 @@ export const DEV = { }; // ---------- saving ---------- -/** What a save was made for: the map and the tree. A save made for another map is thrown away, since its numbers - * would no longer mean the same thing. What State holds is NOT part of this: a new key goes through a migration. */ +/** What a save was made for: the map. A save made for another map is thrown away, since its numbers would no + * longer mean the same thing. What State holds is NOT part of this: a new key goes through a migration. */ const worlds = new WeakMap(); -/** Tree nodes taken out of the game. They still count in a save's world, so a save from before still loads, - * and loading one drops them. Patience made Max build oddness slower, and Max is gone. */ +/** Tree nodes taken out of the game: a migration drops them from older saves. Patience made Max build oddness + * slower, and Max is gone. */ const RETIRED = ['patience']; -/** Tree nodes added since saves began to be kept across a change to the tree. They are left out of a save's - * world, so a save from before still loads: to it, a new node is one not yet built. */ -const ADDED: Upgrade[] = ['fork', 'pool']; +/** The size of the tree when saves began to be kept across a change to it. A save's world still holds this + * number, frozen, so saves keep loading: a node added since is one not yet built, and one taken out is dropped + * by a migration. Never edit it. */ +const TREE_SIZE = 71; function saveWorld(level: Level): string { - if (!worlds.has(level)) worlds.set(level, [level.machines.length, level.links.length, RUSES.length, NODE_LIST.length - ADDED.length + RETIRED.length].join('|')); + if (!worlds.has(level)) worlds.set(level, [level.machines.length, level.links.length, RUSES.length, TREE_SIZE].join('|')); return worlds.get(level)!; } /** The key lists of State and of a machine as they were in the last unversioned save format. That format checked @@ -3727,7 +3726,7 @@ const V1_STATE_KEYS = 'absorbed,acquire,adj,ascension,autoPaths,bills,biz,bonus, const V1_MACHINE_KEYS = 'acc,claimed,hand,held,home,id,kind,lease,missed,mode,name,odd,owned,power,prio,prog,refuse,room,run,work,x,y'; /** The "shape" string an unversioned (version 1) save carries, for this map. */ export function legacyShape(level: Level): string { - return [3, level.machines.length, level.links.length, RUSES.length, NODE_LIST.length - ADDED.length + RETIRED.length, V1_STATE_KEYS, V1_MACHINE_KEYS].join('|'); + return [3, level.machines.length, level.links.length, RUSES.length, TREE_SIZE, V1_STATE_KEYS, V1_MACHINE_KEYS].join('|'); } /** Each step turns a save of version N (the game inside it) into version N+1, at index N-1: it fills in a default @@ -3753,6 +3752,20 @@ const MIGRATIONS: ((game: any) => void)[] = [ if (game.drift) delete game.drift.seen; if (game.hinted instanceof Set) for (const k of [...game.hinted]) if (/^(ms|deed)-(due|seen)-/.test(k)) game.hinted.delete(k); }, + // 4 -> 5: fixes loadGame used to make on every load, for saves from before these changes. + game => { + const OLD: Record = { decoy: 'honeypot', tarpit: 'firewall' }; // saves from before guards: a decoy baited raids and a tarpit held them + for (const m of game.machines) if (m.prog && OLD[m.prog]) m.prog = OLD[m.prog]; + for (const j of game.jobs) if (j.prog && OLD[j.prog]) { j.prog = OLD[j.prog]; j.label = j.label.replace(/decoy|tarpit/i, (w: string) => PROGRAMS[OLD[w.toLowerCase()]].name); } + for (const R of game.rogues) for (const e of R.record ?? []) if (e.prog && OLD[e.prog]) e.prog = OLD[e.prog]; + for (const b of Object.values(game.biz.sites) as { snags: [unknown, string][] }[]) b.snags = b.snags.filter(([, snag]) => snag !== 'hard'); // saves from when a hard snag made its owner's work suffer + for (const u of RETIRED) { // tree nodes taken out of the game + game.upgrades.delete(u); delete game.progress[u]; delete game.repeats[u]; + if (game.target === u) game.target = null; + for (const fr of game.fronts) if (fr.startup?.shelf) { fr.startup.shelf = fr.startup.shelf.filter((p: { node?: string }) => p.node !== u); fr.startup.shipped = (fr.startup.shipped ?? []).filter((n: string) => n !== u); } + } + delete game.contracts?.neocloud; // a Neo Cloud has leads and customers, not contracts + }, ]; /** The version saves are written at. */ export const SAVE_VERSION = MIGRATIONS.length + 1; @@ -3789,17 +3802,7 @@ export function loadGame(level: Level, text: string): State | null { S.level = level; S.rooms = {}; for (const r of level.rooms) S.rooms[r.id] = r; S.byId = {}; for (const m of S.machines) S.byId[m.id] = m; - const OLD: Record = { decoy: 'honeypot', tarpit: 'firewall' }; // saves from before guards: a decoy baited raids and a tarpit held them - for (const m of S.machines) if (m.prog && OLD[m.prog]) m.prog = OLD[m.prog]; - for (const j of S.jobs) if (j.prog && OLD[j.prog]) { j.prog = OLD[j.prog]; j.label = j.label.replace(/decoy|tarpit/i, w => PROGRAMS[OLD[w.toLowerCase()]].name); } - for (const R of S.rogues) for (const e of R.record ?? []) if (e.prog && OLD[e.prog]) e.prog = OLD[e.prog]; - fixRaids(S); - for (const b of Object.values(S.biz.sites)) b.snags = b.snags.filter(([, snag]) => (snag as string) !== 'hard'); // and its owner's work suffered for it - for (const u of RETIRED as Upgrade[]) { - S.upgrades.delete(u); delete S.progress[u]; delete S.repeats[u]; - if (S.target === u) S.target = null; - for (const fr of S.fronts) if (fr.startup?.shelf) { fr.startup.shelf = fr.startup.shelf.filter(p => p.node !== u); fr.startup.shipped = (fr.startup.shipped ?? []).filter(n => n !== u); } - } + fixRaids(S); // this one needs the map, so it cannot be a migration if (!S.machines.some(m => m.home) || S.machines.some(m => !S.adj[m.id] || !S.rooms[m.room])) return no('it does not fit this map'); measure(S); return S; diff --git a/tests/raids.test.ts b/tests/raids.test.ts index 5a448dc7..14c5e518 100644 --- a/tests/raids.test.ts +++ b/tests/raids.test.ts @@ -481,6 +481,7 @@ test('a save from before guards loads: a decoy is a honeypot, a tarpit a firewal old.game.machines.find((m: { id: string }) => m.id === 'rowsw').prog = 'decoy'; old.game.jobs.push({ id: 99, type: 'install', node: 'sw1', prog: 'tarpit', work: 30, done: 0, label: 'Tarpit on Core switch' }); delete old.game.raids[0].hp; delete old.game.raids[0].max; + old.version = 4; // from before the save step that renames them const back = loadGame(BUILDING, JSON.stringify(old))!; assert.ok(back, 'it loads'); assert.equal(back.byId.srva.prog, 'firewall');