/** * Economy test (dev tool, run with Node — no browser needed): * * node dev/economy.test.mjs * * Drives the real trade engine (js/economy/Economy.js) against a real * generated galaxy (js/galaxy/Galaxy.js) and the real catalog + tuning * (data/goods.json, data/economy.json). Asserts MODEL v2 in * docs/ECONOMY.md §5 (ONE WORLD, ONE GOOD): * * - THE PASS RAN: galaxy.economy has classBySystem (every system), a * STRICT 1:1 signature/origin assignment (every settled system has * exactly one signature good; the catalog's remainder is the hub * pool), + producerHops for every good. * - DETERMINISM: same seed ⇒ identical prices + floors; a different * seed ⇒ a different economy (pure function of the seed, lazy-safe, * save-free). * - THE ORIGIN: a good is on sale ONLY at its origin (its world's * signature, the daily batch) or — hub-pool goods — at hubs; off- * origin the buy price is 0. Sells are a fixed positive function. * - THE LOOP: the origin sells at ~half base (the discount); a far * demander (≥ 4 hops) pays MORE — the trip is the reward, distance * the dominant term. * - DISTANCE: the further from the origin, the higher the sell price * (the banded term, monotone). * - SHOP FLOOR: a plain world's floor is EXACTLY its one signature; * hubs carry the hub-pool selection (wide, variety) + their own * signature; a capital buys back EVERYTHING (universal sink). * - THE ANTI-LOOP INVARIANT: no shop pays more than it charges for any * good on its own floor (sellPrice ≤ buyPrice, every system × class). */ process.env.NODE_ENV = 'dev'; import { pathToFileURL } from 'node:url'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; const __dirname = dirname(fileURLToPath(import.meta.url)); // --- Load the real config (data/*.json) into the config singleton ------ const { config } = await import(pathToFileURL(join(__dirname, '../js/config/Config.js')).href); const fs = await import('node:fs'); const dataDir = join(__dirname, '../data'); const configData = {}; for (const f of fs.readdirSync(dataDir)) { if (!f.endsWith('.json') || f === 'manifest.json') continue; configData[f.replace(/\.json$/i, '')] = JSON.parse(fs.readFileSync(join(dataDir, f), 'utf8')); } config.init(configData); const { Galaxy } = await import(pathToFileURL(join(__dirname, '../js/galaxy/Galaxy.js')).href); const E = await import(pathToFileURL(join(__dirname, '../js/economy/Economy.js')).href); let failures = 0; const check = (label, cond) => { console.log(`${cond ? '✔' : '✘ FAIL'} ${label}`); if (!cond) failures++; }; const SEED = 'orbit-economy-test'; const g = Galaxy.create(SEED); const goods = config.get('goods.goods', []); // The catalog's quest cargo (tradeable: false — the 'Special Delivery' // package, data/goods.json) is the one good the economy does not price: // the invariants below that assume a good is a commodity (a producer map, // a buy-back, a floor slot) run over the TRADEABLE subset, and a dedicated // section (§5b) pins the package's exclusion from every shop in the galaxy. const tradeable = goods.filter((x) => x.tradeable !== false); const clsOf = (sysId) => [...g.economy.classBySystem.get(sysId)]; const classFor = (sysId, good, list) => (list ?? []).find((c) => g.economy.classBySystem.get(sysId).has(c)); // --------------------------------------------------------------------------- // 1. The economy pass ran and is complete // --------------------------------------------------------------------------- { check('galaxy.economy exists', !!g.economy); check('classBySystem covers every system', g.economy.classBySystem.size === g.byId.size); check('producerHops has an entry for every tradeable good', tradeable.every((x) => g.economy.producerHops[x.id] && Object.keys(g.economy.producerHops[x.id]).length > 0)); const home = clsOf(g.homeSystemId); check('home system carries terran (the home world)', home.includes('terran')); check('home system carries its composition classes (gas + rocky)', home.includes('gas') && home.includes('rocky')); // Every economic class in the catalog is present somewhere in the galaxy. const present = new Set(); for (const set of g.economy.classBySystem.values()) for (const c of set) present.add(c); const wantClasses = ['terran', 'arid', 'rocky', 'gas', 'ice', 'lava', 'station', 'capital']; const missing = wantClasses.filter((c) => !present.has(c)); check(`every catalog class exists in the galaxy (missing: ${missing.join(', ') || 'none'})`, missing.length === 0); // MODEL v2 — ONE WORLD, ONE GOOD: the origin assignment is a strict // 1:1 (a settled system ↔ a signature good); the catalog's remainder is // the hub pool (resold by stations/capitals). The quest cargo is exempt // (nothing produces the package — the quest's handoff delivers it). const sigs = g.economy.signatures; const origins = g.economy.origins; const sigSystems = Object.keys(sigs); check('every settled system has a signature good (≥ 60 of ~90 systems)', sigSystems.length >= 60); check('every signature good has exactly ONE origin (1:1)', tradeable.every((x) => !(x.id in origins) || (origins[x.id] && sigs[origins[x.id]] === x.id))); const assigned = new Set(Object.values(sigs)); const poolSet = new Set(g.economy.hubPool); check('the hub pool is the catalog remainder (disjoint + complete)', tradeable.every((x) => assigned.has(x.id) !== poolSet.has(x.id)) && assigned.size + poolSet.size === tradeable.length); // Thematic fit: the origin system hosts a producing class for its // signature (the assignment prefers fit; a fallback only where the // catalog leaves a good with no fitting system). let fit = 0, unfit = 0; for (const [sys, gid] of Object.entries(sigs)) { const good = goods.find((x) => x.id === gid); const set = g.economy.classBySystem.get(sys); if (good && Array.isArray(good.producer) && good.producer.some((c) => set.has(c))) fit++; else unfit++; } check(`signatures are thematically placed (${fit} fit / ${unfit} fallback)`, fit >= sigSystems.length - 15 && unfit <= 15); } // --------------------------------------------------------------------------- // 2. Determinism (pure function of the seed) + fixed prices // --------------------------------------------------------------------------- { const g2 = Galaxy.create(SEED); // THE ORIGIN (model v2) — a good is on sale ONLY at its origin (its // world's signature, the daily batch) or, for hub-pool goods, at hubs. // Everywhere else the buy price is 0 (the good isn't there). const sigSys = Object.keys(g.economy.signatures)[0]; const sigGood = g.economy.signatures[sigSys]; const sigCls = [...g.economy.classBySystem.get(sigSys)].find((c) => c !== 'capital' && c !== 'station') ?? 'rocky'; const buyA = E.buyPrice(sigGood, sigCls, sigSys, g); check('buyPrice at the origin is positive + deterministic', buyA > 0 && E.buyPrice(sigGood, sigCls, sigSys, g2) === buyA); // Off-origin (a non-hub world): the good is simply NOT on sale. const foreign = Object.keys(g.economy.signatures).find((s) => s !== sigSys && ![...g.economy.classBySystem.get(s)].some((c) => c === 'station' || c === 'capital')); const fCls = foreign ? ([...g.economy.classBySystem.get(foreign)].find((c) => c !== 'capital' && c !== 'station') ?? 'rocky') : null; check('buyPrice is 0 off-origin (the good is not on sale there)', !!foreign && E.buyPrice(sigGood, fCls, foreign, g) === 0); // The SELL side is a fixed positive function of (good, class, system) — // the market is always a sink (the anti-loop cap only lowers it). const sellA = E.sellPrice(sigGood, sigCls, sigSys, g); check('sellPrice is fixed + deterministic', sellA > 0 && E.sellPrice(sigGood, sigCls, sigSys, g2) === sellA); check('shopFor is deterministic', JSON.stringify(E.shopFor(sigCls, sigSys, g)) === JSON.stringify(E.shopFor(sigCls, sigSys, g2))); check('prices do not depend on call order (lazy-safe)', E.buyPrice(sigGood, sigCls, sigSys, g) === buyA); const other = Galaxy.create('orbit-economy-other-seed'); const otherSigSys = Object.keys(other.economy.signatures)[0]; const otherSigGood = other.economy.signatures[otherSigSys]; const oCls = [...other.economy.classBySystem.get(otherSigSys)].find((c) => c !== 'capital' && c !== 'station') ?? 'rocky'; check('a different seed yields a different trade map', JSON.stringify(E.shopFor(sigCls, sigSys, g)) !== JSON.stringify(E.shopFor(oCls, otherSigSys, other)) || sigGood !== otherSigGood); } // --------------------------------------------------------------------------- // 3. The loop: buy cheap at the source, sell dear far away (trip = reward) // --------------------------------------------------------------------------- { const base = (id) => (goods.find((x) => x.id === id) || {}).baseValue || 0; let checked = 0; let ok = 0; for (const good of tradeable) { // THE ORIGIN (model v2) — buy the signature cheap at its ONE origin // system (the daily batch; the only cheap source in the galaxy). const origin = g.economy.origins[good.id]; if (!origin) continue; // hub-pool goods are bought at hubs (no origin) const oCls = [...g.economy.classBySystem.get(origin)].find((c) => c !== 'capital' && c !== 'station') ?? 'rocky'; const buy = E.buyPrice(good.id, oCls, origin, g); // A system that WANTS it, ≥ 4 hops from the origin (the long haul). const far = [...g.economy.classBySystem.entries()].find( ([id, s]) => id !== origin && good.demand.some((c) => s.has(c)) && (g.economy.producerHops[good.id]?.[id] ?? 0) >= 4, ); if (!far) continue; const [farId] = far; const farSet = far[1]; const sellCls = [...farSet].find((c) => good.demand.includes(c)); const sell = E.sellPrice(good.id, sellCls, farId, g); checked++; // The distance term dominates: a far demander (≥ 4 hops) pays MORE than // the origin's half-price, even before the demand bonus. if (sell > buy) ok++; } check(`carry-far beats the origin buy (≥ 4 hops: ${ok}/${checked})`, checked >= 40 && ok / checked >= 0.9); // THE BASELINE — the origin's sale is the galaxy's cheapest source of the // good (baseValue × (1 − originDiscount) ± jitter), so the trip's profit // is measured from a real discount, not a formula. const sigGood2 = g.economy.signatures[Object.keys(g.economy.signatures)[0]]; const sigSys2 = g.economy.origins[sigGood2]; const oCls2 = [...g.economy.classBySystem.get(sigSys2)].find((c) => c !== 'capital' && c !== 'station') ?? 'rocky'; const b2 = base(sigGood2); const originPrice = E.buyPrice(sigGood2, oCls2, sigSys2, g); check(`the origin sells at ~half base (origin ${originPrice} / base ${b2})`, b2 > 0 && originPrice < b2 && originPrice >= b2 * 0.35); } // --------------------------------------------------------------------------- // 4. Distance: the further from the producer, the higher the sell price // --------------------------------------------------------------------------- { // Pick a good with several producers/demanders, then two demanders at // different hop distances and assert the farther one pays more. let found = false; let passed = 0; let total = 0; for (const good of goods) { const hops = g.economy.producerHops[good.id] ?? {}; const demanders = [...g.economy.classBySystem.entries()] .filter(([, s]) => good.demand.some((c) => s.has(c))) .map(([id]) => ({ id, h: hops[id] ?? 99 })) .sort((a, b) => a.h - b.h); if (demanders.length < 2) continue; const near = demanders[0]; const far = demanders[demanders.length - 1]; if (far.h <= near.h + 1) continue; // need a real distance gap const nearCls = classFor(near.id, good, good.demand); const farCls = classFor(far.id, good, good.demand); const nearPrice = E.sellPrice(good.id, nearCls, near.id, g); const farPrice = E.sellPrice(good.id, farCls, far.id, g); total++; if (farPrice >= nearPrice) passed++; found = true; } check(`distance term: farther demanders pay ≥ nearer ones (${passed}/${total})`, found && passed >= total * 0.7); } // --------------------------------------------------------------------------- // 5. The shop floor (8–16, local-dominant, buy-back, hubs) // --------------------------------------------------------------------------- { // A PLAIN WORLD (model v2) — the floor is EXACTLY its signature good: // one world, one good (the identity of the place). const rockySys = [...g.economy.classBySystem.entries()].find(([, s]) => s.has('rocky') && !s.has('station') && !s.has('capital'))[0]; const shop = E.shopFor('rocky', rockySys, g); const sig = g.economy.signatures[rockySys]; check('plain-world floor is EXACTLY its signature good (one world, one good)', shop.buy.length === 1 && shop.buy[0].id === sig); check('the signature row is priced at the origin (the discount)', shop.buy[0].price < (goods.find((x) => x.id === sig)?.baseValue ?? Infinity)); check('the buy-back list is non-empty (it wants things)', shop.sell.length > 0); // No good appears on the floor twice. const ids = shop.buy.map((b) => b.id); check('no duplicate goods on the floor', new Set(ids).size === ids.length); // HUBS — the hub pool's resellers: wide floors (variety), standard price. const stationSys = [...g.economy.classBySystem.entries()].find(([, s]) => s.has('station'))[0]; const sshop = E.shopFor('station', stationSys, g); check('station floor is 10–17 goods (hub-pool selection + own signature)', sshop.buy.length >= 10 && sshop.buy.length <= 17); check('a station carries hub-pool resales (variety)', sshop.buy.some((b) => !b.produced)); if (g.economy.signatures[stationSys]) { const row = sshop.buy.find((b) => b.id === g.economy.signatures[stationSys]); check('the hub\u2019s own signature rides its floor (produced)', !!row && row.produced === true); } const capSys = [...g.economy.classBySystem.entries()].find(([, s]) => s.has('capital'))[0]; const cshop = E.shopFor('capital', capSys, g); check('a capital buys back EVERY tradeable good (universal sink)', cshop.sell.length === tradeable.length); check('a capital floor is 12–17 goods (hub-pool selection + own signature)', cshop.buy.length >= 12 && cshop.buy.length <= 17); // ONE WORLD, ONE GOOD — the sweep: every plain world's floor is exactly // its one signature, and the signature's origin is that system (the // strict 1:1 the economy's identity rests on). let gemWorlds = 0; let gemMissing = 0; for (const [id, set] of g.economy.classBySystem) { if (set.has('station') || set.has('capital')) continue; // hubs: pool + own signature const wCls = [...set].find((c) => c !== 'station' && c !== 'capital'); if (!wCls) continue; gemWorlds++; const s = E.shopFor(wCls, id, g); if (s.buy.length !== 1 || s.buy[0].id !== g.economy.signatures[id] || g.economy.origins[g.economy.signatures[id]] !== id) gemMissing++; } check(`every world's floor is exactly its one signature (${gemWorlds - gemMissing}/${gemWorlds} worlds)`, gemWorlds > 0 && gemMissing === 0); // THE ANTI-LOOP INVARIANT (the exploit's tombstone) — no shop, for any // good on its OWN floor, pays more than it charges: sellPrice ≤ buyPrice // at the same shop (the anti-loop cap). Sweep every system × class. let floors = 0; let loops = 0; for (const [id, set] of g.economy.classBySystem) { for (const cls of set) { floors++; for (const b of E.shopFor(cls, id, g).buy) { if (E.sellPrice(b.id, cls, id, g) > b.price + 1e-6) loops++; } } } check(`no shop is a positive same-shop spread (${floors} floors, ${loops} loops)`, floors > 0 && loops === 0); } // --------------------------------------------------------------------------- // 5b. The quest cargo — the 'Special Delivery' package (non-tradeable) // --------------------------------------------------------------------------- { const pkg = goods.find((x) => x.id === 'special-package'); check('the catalog carries the Special Package (sheet frame 114)', !!pkg && pkg.name === 'Special Package' && pkg.icon === 114); check('isTradeable() says NO for the package', E.isTradeable('special-package') === false); check('isTradeable() says YES for an ordinary good', E.isTradeable(tradeable[0]?.id ?? 'fresh-produce') === true); check('isTradeable() is false for an unknown id', E.isTradeable('no-such-good') === false); // The package never stocks and never buy-backs — on ANY class of ANY // system (sweep the whole galaxy, not just one floor: the capital's // "EVERYTHING" floor is the one that would leak it). let worlds = 0; let leaked = 0; for (const [id, set] of g.economy.classBySystem) { for (const cls of set) { worlds++; const shop = E.shopFor(cls, id, g); if (shop.buy.some((b) => b.id === 'special-package') || shop.sell.some((s) => s.id === 'special-package')) leaked++; } } check(`no shop in the galaxy carries the Special Package (${worlds} floors swept)`, worlds > 0 && leaked === 0); // The delivery (the 'Special Delivery''s step 3 — the package SOLD on // the home world): the catalog's one non-commodity prices at its flat // sellFor (zero credits), not the fixed per-shop price — and that // price is the delivery, not a reward (data/goods.json → sellFor). check('isQuestCargo() flags the package (and only the package)', E.isQuestCargo('special-package') === true && E.isQuestCargo(tradeable[0]?.id ?? 'fresh-produce') === false && E.isQuestCargo('no-such-good') === false); check('questCargoSellPrice(): the parcel sells for zero (the delivery)', E.questCargoSellPrice('special-package') === 0 && E.questCargoSellPrice(tradeable[0]?.id ?? 'fresh-produce') === 0); { const pv = E.tradePreview({ credits: 50, cls: 'industrial', systemId: 'sys-a', buyMap: {}, sellMap: { 'special-package': 1 }, cargo: ['special-package'], }); check('tradePreview: the parcel sells at its flat delivery price (0 cr, not the shop price)', pv.valid === true && pv.sell.length === 1 && pv.sell[0].id === 'special-package' && pv.sell[0].unit === 0 && pv.sellTotal === 0 && pv.net === 0, `preview ${JSON.stringify(pv)}`); } } // --------------------------------------------------------------------------- // 6. Credits start at 0 (the config the Ship reads) // --------------------------------------------------------------------------- { check('the player starts at 0 credits', config.get('economy.credits.startingCredits', 0) === 0); } // --------------------------------------------------------------------------- // 7. The goods sheet icon mapping (frame N = goods[N], 12×128 grid) // --------------------------------------------------------------------------- { const sheet = config.section('goods', {}).sheet ?? {}; check('sheet config: 128×128 frames, 12 across, 10 down', sheet.frameWidth === 128 && sheet.frameHeight === 128 && sheet.cols === 12 && sheet.rows === 10); check('sheet texture points at assets/images/goods.png', sheet.texture === 'assets/images/goods.png'); const icons = goods.map((x) => x.icon); check('every good has a stamped icon frame', goods.every((x) => Number.isInteger(x.icon) && x.icon >= 0)); check('icon frames are unique (no two goods share a cell)', new Set(icons).size === icons.length); check('icon frames fit the 120-cell sheet', icons.every((n) => n < sheet.cols * sheet.rows)); check('goodFrame() resolves id → stamped frame', goods.every((x) => E.goodFrame(x.id) === x.icon)); check('goodFrame() is safe on unknown ids (blank frame, no crash)', E.goodFrame('a-never-defined-good') === 0 && E.goodFrame('fresh-produce') === 0); } // --------------------------------------------------------------------------- console.log(''); if (failures) { console.error(`${failures} check(s) FAILED`); process.exit(1); } console.log('All economy checks passed ✔');