{"version":3,"sources":["reachability.mjs"],"sourcesContent":["// WHAT A PLAYER CAN EVER HOLD, derived from the definitions and from the world generator's own rules.\n//\n// One model with two readers. The recipe book and Kubipedia (app.js) use it to decide what to show and what to call\n// \"no source yet\"; tests/recipe-graph.test.mjs holds the content to it, so a recipe added with an input nothing\n// yields fails a test instead of reaching a player as a dead end. The catalogue page (doc/catalogue.html) reads it\n// too, for review, and lists every source here rather than the first.\n//\n// The sources are the ways the game actually hands a player a material, each taken from the code that does it\n// rather than restated here:\n//   surface rocks  the bits rocks drop (fixture-scatter.js FIXTURE_VARIANTS)\n//   world crust    a rocky world's two crust kubes, drawn from pickCrust's density band\n//   ore veins      ore-veins.js's candidates, less the names recipes make, with the SAME station→kube mapping the\n//                  world generator uses (functions/index.js, app.js's system-view options) — so the machines whose\n//                  own cost needs an ore it makes (Iron for the smelter) still find that ore in the ground\n//   asteroid seams transient-shape.js's seam pools, one per asteroid class\n//   comets         every ice: pickIce draws the whole ice pool, and comet seams are ices too\n//   bit structures the bits of trees, crystals, remains and every other organic a world grows, and of its flora\n//                  (organics.mjs, flora.mjs): each material in products.json with source 'life' (tools/gen-life-catalogue.py)\n//   turf           a turf bit of its colour off every turfed regolith (turf.mjs; products with source 'turf')\n// then closed over the recipe graph. Deconstruction is NOT a source: craft() refuses it (DECONSTRUCT_CLOSED in\n// functions/index.js; the graph test fails if that changes without this file changing too). Gases in atmospheres\n// and liquids in pools are not sources either: nothing collects them as items yet.\nimport { veinCandidates, veinExclusions } from './ore-veins.js';\nimport { STATIONS } from './synthesis.mjs';\nimport { FIXTURE_VARIANTS } from './fixture-scatter.js';\nimport { TRANSIENT_TYPES, seamPool } from './transient-shape.js';\n\nexport const CRUST_BAND = { lo: 3.0, hi: 3.99 };            // pickCrust (genDensity.mjs, system-view.js)\n// A station id → the kube that IS that station. The mapping veinExclusions needs to find a machine's own build cost.\nexport const stationKubeName = (id) => (STATIONS[id] || {}).name || null;\n\n// EVERY way the game hands out each name, as { name, via, detail }, in the order the sources are checked. rawSources\n// keeps the first per name; the catalogue page (doc/catalogue.html) shows them all, because a first answer hides the\n// rest — Gold is 'ore veins' there, and nothing would say it also rides in on M-type asteroids. `detail` names which\n// rock or which asteroid class, where there is more than one.\nexport function sourceList({ kubes = [], products = [], recipes = [] }) {\n  const out = [];\n  const add = (n, via, detail = null) => { if (n) out.push({ name: n, via, detail }); };\n  const solid = kubes.filter((k) => k && k.state === 'solid'), ice = kubes.filter((k) => k && k.state === 'ice');\n  for (const v of FIXTURE_VARIANTS) for (const n of Object.keys(v.drop || {})) add(n, 'surface rocks', v.id);\n  for (const k of solid) {\n    const d = parseFloat(k.density || '0');\n    if (d >= CRUST_BAND.lo && d <= CRUST_BAND.hi) add(k.name, 'world crust');\n  }\n  const made = veinExclusions(recipes, stationKubeName);\n  for (const c of veinCandidates(kubes, products)) if (!made.has(c.name)) add(c.name, 'ore veins');\n  for (const t of TRANSIENT_TYPES) for (const k of seamPool(t, false, solid, ice, made)) add(k.name, 'asteroid seams', t.label);\n  for (const k of ice) add(k.name, 'comets');\n  for (const p of products) if (p && p.source === 'life') add(p.name + ' Bit', 'bit structures');\n  for (const p of products) if (p && p.source === 'turf') add(p.name, 'turf');   // a turfed regolith's top, mined (turf.mjs, version 9)\n  for (const p of products) if (p && p.source === 'remnant') add(p.name + ' Bit', 'drained reefs');   // coral out of water ten minutes (reservoirs.mjs)\n  for (const p of products) if (p && p.source === 'seedling') add(p.name, 'felled trees');   // a whole tree felled, and the Loamwrights' gifts (seedlings.mjs)\n  for (const p of products) if (p && p.source === 'blackhole') add(p.name, 'black hole debris');\n  // A NEBULA'S GAS, bottled (the owner's Nebulas round 2, NT2/G5, Oct 29): a Gas Canister fills from a nebula's\n  // cloud with these, and a Toolbench bottles a whole kube of one of them into that gas's kube (functions craft, 'bottle').\n  const NEBULA_GASES = new Set(['Hydrogen', 'Helium', 'Oxygen', 'Nitrogen', 'Neon', 'Argon Gas']);   // (functions NEBULA_CANISTER_GASES)\n  for (const k of kubes) if (k && k.state === 'gas' && NEBULA_GASES.has(k.name)) add(k.name, 'a Gas Canister, bottled');\n  for (const p of products) if (p && p.source === 'fauna') add(p.name, 'animals');   // what a hunt or a shearing gives (alien animals, Oct 28)   // Exotic Matter, in the seams of a black hole's drifting rocks (transient-shape.mjs transientRich, Oct 27)\n  return out;\n}\n\n// name → the first way the game hands it out, for every name a player can get without crafting.\nexport function rawSources(defs) {\n  const src = new Map();\n  for (const s of sourceList(defs)) if (!src.has(s.name)) src.set(s.name, s.via);\n  return src;\n}\n\n// Every name that can ever reach an inventory: the raw sources, closed over the recipe graph (an output becomes\n// obtainable once every input of some recipe that makes it is).\nexport function obtainable(defs) {\n  const set = new Set(rawSources(defs).keys());\n  const recipes = defs.recipes || [];\n  for (let pass = 0; pass <= recipes.length; pass++) {\n    let grew = false;\n    for (const r of recipes) {\n      if (!Object.keys(r.inputs || {}).every((n) => set.has(n))) continue;\n      for (const o of (r.outputs || [])) if (o && o.name && !set.has(o.name)) { set.add(o.name); grew = true; }\n    }\n    if (!grew) break;\n  }\n  // A BASE ITEM IS OBTAINABLE WHEN ANY OF ITS MATERIAL VARIANTS IS (Oct 1): once every bucket recipe makes a bucket of\n  // some metal, \"a Bucket\" is still something a player gets — just never a plain one — and the entry for it stays.\n  const all = [...(defs.kubes || []), ...(defs.products || [])];\n  const byId = new Map(all.filter((d) => Number.isInteger(d.id)).map((d) => [d.id, d.name]));\n  for (const d of all) if (Number.isInteger(d.variantOf) && set.has(d.name)) { const b = byId.get(d.variantOf); if (b) set.add(b); }\n  // …AND A TALL PART'S UPPER HALF (vessels phase 2, Oct 12: products.json `partOf`) comes with its base: placing a Suit\n  // Dock puts its upper half in the cell above. It is never in an inventory, but it is had.\n  for (const d of all) if (typeof d.partOf === 'string' && set.has(d.partOf)) set.add(d.name);\n  return set;\n}\n\n// The inputs of a recipe that nothing in the game yields. Empty means the recipe can be completed from scratch.\nexport function deadInputs(recipe, obt) {\n  return Object.keys((recipe && recipe.inputs) || {}).filter((n) => !obt.has(n));\n}\n\n// ---- HOW FAR AWAY A RECIPE IS, FROM WHAT YOU HOLD (the owner's pick, Oct 6: \"show how many steps away\") ----------\n// The recipe book's \"Has a source\" band said only that a recipe CAN be finished one day, which is true of 114 of 117 and\n// so says nothing. This says how far: the number of crafts that have to come first, counted down the deepest chain of\n// ingredients, using what the player holds right now.\n//   0   you hold every input already;\n//   1   every missing input can be made by one craft from what you hold;\n//   N   …by a chain N crafts deep (the chains of different inputs run side by side, so it is the deepest, not the sum).\n// Quantities count: two Steel short of a recipe is two runs of the Steel recipe, whose inputs have to be held twice\n// over. A missing input that no recipe makes is something to FIND (a rock, an ore, a comet), and it comes back in\n// `gather` rather than as a number of crafts, because no amount of crafting reaches it.\n// Each input's chain is counted on its own, as if it had the bag to itself: two chains that both need Copper are each\n// satisfied by the same Copper. That keeps the number a cheap estimate of distance rather than a plan.\n// `producers` is recipeProducers(recipes); `have(name)` the count held; `raw` the names the world hands out (rawSources),\n// each of which can also simply be found rather than made (Iron Bit drops from rocks; it is also an Iron kube split).\n// Cycles (a kube splits into bits, bits merge into the kube) are cut by never reusing a recipe on the chain that led to\n// it. `maxDepth` bounds the search; a chain deeper than that reports steps: Infinity.\nexport function recipeProducers(recipes) {\n  const m = new Map();\n  for (const r of recipes || []) for (const o of (r && r.outputs) || []) {\n    if (!o || !o.name || !(o.count > 0)) continue;\n    if (!m.has(o.name)) m.set(o.name, []);\n    m.get(o.name).push(r);\n  }\n  return m;\n}\nexport function craftSteps(recipe, have, producers, { raw = null, maxDepth = 6 } = {}) {\n  // Fewer things to go and find beats fewer crafts: a chain you can run from your bag is closer than one short step\n  // that needs an ore you have not got.\n  const better = (a, b) => {\n    if (!b) return true;\n    if ((a.steps === Infinity) !== (b.steps === Infinity)) return b.steps === Infinity;   // a chain too deep to count loses to any other\n    return a.gather.length < b.gather.length || (a.gather.length === b.gather.length && a.steps < b.steps);\n  };\n  const merge = (into, more) => { for (const n of more) if (!into.includes(n)) into.push(n); };\n  const memo = new Map();   // name|qty|depth, for results whose search cut no cycle (only those do not depend on the path)\n  function item(name, qty, depth, path) {\n    const held = have(name) || 0;\n    if (held >= qty) return { steps: 0, gather: [], cut: false };\n    const key = name + '|' + qty + '|' + depth;\n    if (memo.has(key)) return memo.get(key);\n    const makers = producers.get(name) || [];\n    let best = (raw && raw.has(name)) || !makers.length ? { steps: 0, gather: [name] } : null, cut = false;\n    if (depth < maxDepth) for (const p of makers) {\n      if (path.has(p)) { cut = true; continue; }\n      const out = p.outputs.find((o) => o.name === name);\n      const res = run(p, Math.ceil((qty - held) / out.count), depth + 1, path);\n      cut = cut || res.cut;\n      const cand = { steps: res.steps + 1, gather: res.gather };\n      if (better(cand, best)) best = cand;\n    }\n    const res = best ? { steps: best.steps, gather: best.gather, cut } : { steps: Infinity, gather: [], cut };   // made, but deeper than maxDepth\n    if (!cut) memo.set(key, res);\n    return res;\n  }\n  function run(r, runs, depth, path) {\n    const next = new Set(path); next.add(r);\n    let steps = 0, cut = false; const gather = [];\n    for (const n of Object.keys(r.inputs || {})) {\n      const x = item(n, r.inputs[n] * runs, depth, next);\n      steps = Math.max(steps, x.steps); merge(gather, x.gather); cut = cut || x.cut;\n    }\n    return { steps, gather, cut };\n  }\n  const res = run(recipe, 1, 0, new Set());\n  return { steps: res.steps, gather: res.gather };\n}\n"],"mappings":"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","names":["res"],"file":"reachability.mjs"}