/** * Cargo + trade-preview test (dev tool, run with Node — no browser needed): * * node dev/cargo.test.mjs * * The shop's ECONOMIC core, headless: * * - CARGOHOLD (js/economy/CargoHold.js) — the ship's trade-goods hold as a * pure object: fixed bays (one good each), capacity = unlocked bays, * add/remove/count/clear, a capacity drop sheds the oldest goods, save/ * load round-trips, and the invariant items.length ≤ capacity. * - TRADE PREVIEW (js/economy/Economy.js → tradePreview / resultingCargo) * — the shop UI's calculator: sell side priced at sellPrice, buy side at * buyPrice, net = sell − buy, the two caps (credits can't go negative, * the END hold must fit the unlocked bays), "sell 2 to make room to buy * 3" landing in one confirm, and sell qty clamped to what's aboard. * * No Phaser, no scene — plain Node against the real config + a real galaxy. */ process.env.NODE_ENV = 'dev'; import { pathToFileURL } from 'node:url'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import assert from 'node:assert/strict'; 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 { CargoHold } = await import(pathToFileURL(join(__dirname, '../js/economy/CargoHold.js')).href); const E = await import(pathToFileURL(join(__dirname, '../js/economy/Economy.js')).href); const { Galaxy } = await import(pathToFileURL(join(__dirname, '../js/galaxy/Galaxy.js')).href); const goods = config.get('goods.goods', []); assert.ok(goods.length > 0, 'the goods catalog is loaded'); console.log('== CARGOHOLD — the ship\u2019s trade-goods hold =='); // --- construction + capacity -------------------------------------------- { const h = new CargoHold(4); assert.equal(h.capacity, 4, 'capacity = unlocked bays'); assert.equal(h.used, 0, 'starts empty'); assert.equal(h.room, 4, '… with room for all four'); assert.equal(h.isFull(), false, '… and not full'); assert.equal(h.add(['a', 'b']), 2, 'add fills bays, returns how many landed'); assert.deepEqual(h.items, ['a', 'b'], '… in fill order'); assert.equal(h.add(['c', 'd', 'e']), 2, 'add stops when the hold is full (only 2 fit)'); assert.equal(h.used, 4, '… the hold is now full'); assert.equal(h.isFull(), true, '… and reports full'); } // --- duplicates, count, has, remove ------------------------------------- { const h = new CargoHold(6); assert.equal(h.add(['gem', 'gem', 'gem']), 3, 'three of the same good = three bays'); assert.equal(h.count('gem'), 3, 'count() sees the duplicates'); assert.equal(h.has('gem'), true, 'has() is true'); assert.equal(h.remove('gem', 2), 2, 'remove() takes up to N copies'); assert.equal(h.count('gem'), 1, '… one left'); assert.equal(h.remove('gem', 5), 1, '… asking for more than you have removes what\u2019s there'); assert.equal(h.count('gem'), 0, '… now empty of it'); assert.equal(h.has('gem'), false, 'has() is false again'); assert.equal(h.remove('nope', 1), 0, 'removing a good that\u2019s not aboard is a no-op'); } // --- a capacity DROP sheds the oldest goods ----------------------------- { const h = new CargoHold(4); h.add(['a', 'b', 'c', 'd']); assert.equal(h.used, 4, 'full before the drop'); h.capacity = 2; // the ship shrinks its hold assert.equal(h.capacity, 2, 'capacity is now 2'); assert.equal(h.used, 2, '… and it can only keep two bays'); assert.deepEqual(h.items, ['a', 'b'], '… shedding the OLDEST (tail) goods first'); h.capacity = 6; // grow it back — frees bays assert.equal(h.room, 4, 'growing frees the vacated bays'); } // --- set() truncates to capacity; save/load + clone --------------------- { const h = new CargoHold(3); h.set(['a', 'b', 'c', 'd', 'e']); assert.deepEqual(h.items, ['a', 'b', 'c'], 'set() keeps only what fits the capacity'); h.set(['x', 42, null, 'y']); assert.deepEqual(h.items, ['x', 'y'], 'set() filters out non-ids'); const round = CargoHold.fromJSON(h.toJSON()); assert.equal(round.capacity, 3, 'round-trip keeps the capacity'); assert.deepEqual(round.items, ['x', 'y'], '… and the contents'); const base = new CargoHold(2); base.add(['p', 'q']); const copy = base.clone(); copy.remove('p', 1); assert.deepEqual(base.items, ['p', 'q'], 'clone is independent — mutating it leaves the original'); assert.deepEqual(copy.items, ['q']); } console.log('CARGOHOLD: capacity, dups, drop-sheds, save/load, clone: OK'); console.log('\n== TRADE PREVIEW — the shop UI\u2019s calculator =='); // A real galaxy (for the fixed per-shop prices). const galaxy = Galaxy.create('orbit-cargo-math'); const sysId = [...galaxy.byId.keys()][0]; const cls = [...galaxy.economy.classBySystem.get(sysId)].find((c) => c !== 'capital') ?? 'rocky'; const good = goods.find((g) => g.id) ; // any catalog good const goodId = good.id; const maxSlots = config.get('economy.cargo.maxSlots', 16); const CAP = 4; // the player\u2019s starting unlocked bays // --- sell side: priced at sellPrice, net positive ------------------------ { const cargo = [goodId, goodId]; // two aboard const pv = E.tradePreview({ cls, systemId: sysId, galaxy, cargo, sellMap: { [goodId]: 2 }, buyMap: {}, credits: 0, capacity: CAP }); const unit = E.sellPrice(goodId, cls, sysId, galaxy); assert.ok(unit > 0, 'the sell price is positive'); assert.equal(pv.sell.length, 1, 'one sell line'); assert.equal(pv.sell[0].qty, 2, '… for two units'); assert.equal(pv.sell[0].unit, unit, '… at the shop\u2019s fixed sell price'); assert.equal(pv.sellTotal, unit * 2, 'sellTotal = qty × sellPrice'); assert.equal(pv.buyTotal, 0, '… nothing bought'); assert.equal(pv.net, unit * 2, 'net = the sale proceeds'); assert.equal(pv.creditsAfter, unit * 2, '… credits go up by that'); assert.equal(pv.valid, true, 'selling needs no credits and frees bays → valid'); assert.equal(pv.cargoAfter, 0, '… and the hold is empty after'); } // --- buy side: priced at buyPrice, net negative -------------------------- { const pv = E.tradePreview({ cls, systemId: sysId, galaxy, cargo: [], sellMap: {}, buyMap: { [goodId]: 3 }, credits: 100000, capacity: CAP }); const unit = E.buyPrice(goodId, cls, sysId, galaxy); assert.ok(unit > 0, 'the buy price is positive'); assert.equal(pv.buy[0].unit, unit, '… at the shop\u2019s fixed buy price'); assert.equal(pv.buyTotal, unit * 3, 'buyTotal = qty × buyPrice'); assert.equal(pv.net, -unit * 3, 'net = the spend'); assert.equal(pv.creditsAfter, 100000 - unit * 3, '… credits go down by that'); assert.equal(pv.cargoAfter, 3, '… three bays fill'); assert.deepEqual(pv.cargo, [goodId, goodId, goodId], 'resulting hold = the bought goods'); } // --- the two caps: credits and cargo ------------------------------------ { const unit = E.buyPrice(goodId, cls, sysId, galaxy); const cost = unit * 2; // Can't spend into the red. const broke = E.tradePreview({ cls, systemId: sysId, galaxy, cargo: [], sellMap: {}, buyMap: { [goodId]: 2 }, credits: cost - 1, capacity: CAP }); assert.equal(broke.valid, false, 'buying a credit over what you have is INVALID'); assert.ok(broke.errors.includes('insufficient-credits'), '… flagged as insufficient-credits'); assert.equal(broke.creditsAfter, -1, '… creditsAfter would be −1'); // Exactly affording it is valid (the cap is < 0, not ≤). const exact = E.tradePreview({ cls, systemId: sysId, galaxy, cargo: [], sellMap: {}, buyMap: { [goodId]: 2 }, credits: cost, capacity: CAP }); assert.equal(exact.valid, true, 'spending down to exactly 0 is allowed'); assert.equal(exact.creditsAfter, 0, '… and lands on zero'); // Cargo: buying beyond the unlocked bays is invalid. const full = E.tradePreview({ cls, systemId: sysId, galaxy, cargo: [], sellMap: {}, buyMap: { [goodId]: CAP + 1 }, credits: 100000, capacity: CAP }); assert.equal(full.valid, false, 'buying more than the hold can carry is INVALID'); assert.ok(full.errors.includes('cargo-full'), '… flagged as cargo-full'); } // --- the LOOP in one confirm: sell 2 to make room to buy 3 -------------- { // A full 4-bay hold of goodId. Sell 2 (frees 2 bays + credits), buy 3 // (fills 3). End: 4 − 2 + 3 = 5 … that\u2019s over capacity — so use a mix that // stays in the hold: sell 3, buy 3 → end 4 (fits), and the sale funds it. const unit = E.buyPrice(goodId, cls, sysId, galaxy); const sellUnit = E.sellPrice(goodId, cls, sysId, galaxy); const cargo = [goodId, goodId, goodId, goodId]; // full (4) const pv = E.tradePreview({ cls, systemId: sysId, galaxy, cargo, sellMap: { [goodId]: 3 }, buyMap: { [goodId]: 3 }, credits: 0, capacity: CAP, }); assert.equal(pv.cargoAfter, 4, 'end hold = 4 − 3 + 3 = 4 (fits the capacity)'); assert.equal(pv.creditsAfter, sellUnit * 3 - unit * 3, 'net = sale proceeds − the spend'); assert.equal(pv.valid, pv.creditsAfter >= 0 && pv.cargoAfter <= CAP, '… valid iff both caps hold'); // A genuinely room-making trade: sell 2 (frees room) then buy 3 from FULL. const pv2 = E.tradePreview({ cls, systemId: sysId, galaxy, cargo, sellMap: { [goodId]: 2 }, buyMap: { [goodId]: 3 }, credits: 100000, capacity: CAP, }); assert.equal(pv2.cargoAfter, 5, '4 − 2 + 3 = 5 bays wanted…'); assert.equal(pv2.valid, false, '… but that\u2019s OVER the 4-bay hold → cargo-full'); // …and the SAME trade with one fewer buy now fits (room was the blocker). const pv3 = E.tradePreview({ cls, systemId: sysId, galaxy, cargo, sellMap: { [goodId]: 2 }, buyMap: { [goodId]: 2 }, credits: 100000, capacity: CAP, }); assert.equal(pv3.cargoAfter, 4, '4 − 2 + 2 = 4 → fits'); assert.equal(pv3.valid, true, '… so it\u2019s valid'); } // --- sell qty clamps to what\u2019s actually aboard ------------------------ { const cargo = [goodId]; // only ONE aboard const pv = E.tradePreview({ cls, systemId: sysId, galaxy, cargo, sellMap: { [goodId]: 9 }, buyMap: {}, credits: 0, capacity: CAP }); assert.equal(pv.sell[0].qty, 1, 'asking to sell 9 of a good you have one of sells ONE'); assert.equal(pv.sellTotal, E.sellPrice(goodId, cls, sysId, galaxy), '… priced for the one unit'); assert.equal(pv.cargoAfter, 0, '… hold ends empty'); } // --- a mixed trade (different goods, sell + buy at once) ---------------- { const a = goods[0].id; const b = goods[1].id; const cargo = [a, a, b]; // two a, one b const pv = E.tradePreview({ cls, systemId: sysId, galaxy, cargo, sellMap: { [a]: 2 }, buyMap: { [b]: 2 }, credits: 100000, capacity: CAP, }); assert.equal(pv.sellTotal, 2 * E.sellPrice(a, cls, sysId, galaxy), 'a\u2019s sale at its sell price'); assert.equal(pv.buyTotal, 2 * E.buyPrice(b, cls, sysId, galaxy), 'b\u2019s purchase at its buy price'); assert.equal(pv.net, pv.sellTotal - pv.buyTotal, 'net = sale − purchase'); assert.equal(pv.cargoAfter, 3 - 2 + 2, 'end hold = 3 − 2 + 2 = 3'); assert.deepEqual([...pv.cargo].sort(), [b, b, b].sort(), 'resulting hold = the one leftover b + two bought b\u2019s'); } console.log('TRADE PREVIEW: sell/buy pricing, both caps, loop, clamps, mix: OK'); console.log('\nAll cargo checks passed \u2714');