/** * 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 — plus the * per-bay PURCHASE-COST ledger (parallel to the items: the credits/unit * actually paid at the shop, or null where a bay has no cost on file), * kept aligned by every mutation. * - 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']); } // --- the PURCHASE-COST ledger (parallel to items) ------------------------ { const h = new CargoHold(4); assert.equal(h.costAt(0), null, 'an empty bay has no cost on file'); assert.equal(h.totalCost(), 0, '… and nothing paid'); h.add('frost-diamond', 30); h.add(['iron-ore', 'iron-ore'], 21); assert.deepEqual(h.costs, [30, 21, 21], 'add() stamps the paid unit cost onto every bay added'); assert.equal(h.costAt(1), 21, 'costAt() reads the bay\u2019s recorded cost'); assert.equal(h.totalCost(), 72, 'totalCost() = the credits actually paid'); assert.equal(h.costBays, 3, 'costBays counts the recorded bays'); assert.deepEqual(h.list(), [ { id: 'frost-diamond', cost: 30 }, { id: 'iron-ore', cost: 21 }, { id: 'iron-ore', cost: 21 }, ], 'list() pairs each bay with its cost'); h.remove('iron-ore'); assert.deepEqual(h.costs, [30, 21], 'remove() drops the sold bay\u2019s cost with the good'); h.add('grain-crates'); assert.deepEqual(h.costs, [30, 21, null], 'a bay added without a price reads a null cost (the package handoff)'); assert.equal(h.totalCost(), 51, '… and null-cost bays count as 0 (never invented)'); } // --- the cost ledger follows the mutations ------------------------------- { // a capacity drop sheds the bay\u2019s cost with its good const h = new CargoHold(4); h.add(['a', 'b', 'c', 'd'], 10); h.capacity = 2; assert.equal(h.used, 2); assert.deepEqual(h.costs, [10, 10], 'a shed bay takes its cost with it'); // set() keeps the two arrays aligned (a save/load round-trip) const s = new CargoHold(3); s.set(['a', 'b', 'c'], [5, null]); assert.deepEqual(s.items, ['a', 'b', 'c']); assert.deepEqual(s.costs, [5, null, null], 'set() aligns costs to the kept bays (short → null)'); const round = CargoHold.fromJSON(s.toJSON()); assert.deepEqual(round.costs, [5, null, null], 'save/load round-trips the ledger'); const legacy = CargoHold.fromJSON({ capacity: 3, items: ['a', 'b'] }); assert.deepEqual(legacy.costs, [null, null], 'a save predating the ledger loads with no costs on file'); // dropFirstNot — the package handoff makes room (the package outranks trade) const d = new CargoHold(2); d.add(['trade-a', 'trade-b'], 9); assert.equal(d.dropFirstNot('special-package'), 'trade-a', 'it sheds the first bay that isn\u2019t the package'); assert.equal(d.dropFirstNot('special-package'), 'trade-b', '… then the next'); assert.equal(d.dropFirstNot('special-package'), null, '… a hold of nothing else reads null'); assert.deepEqual(d.costs, [], '… and the shed costs leave with the goods'); d.add('special-package'); assert.deepEqual(d.costs, [null], 'the package itself arrives with no cost on file'); // a bad cost never leaks a NaN into the ledger const bad = new CargoHold(2); bad.add('x', 'not-a-number'); bad.add('y', -5); assert.deepEqual(bad.costs, [null, null], 'non-finite / negative costs normalize to null'); } // --- costsFor — the sale's per-bay PROFIT BASIS (FIFO, matches remove) ---- { const h = new CargoHold(6); h.add(['gem', 'gem', 'gem'], 30); // three bays of 'gem' at 30 each h.add(['iron-ore', 'iron-ore'], 21); // two bays of 'iron-ore' at 21 each h.add('gem'); // one 'gem' with NO cost on file assert.deepEqual(h.items, ['gem', 'gem', 'gem', 'iron-ore', 'iron-ore', 'gem']); assert.deepEqual(h.costs, [30, 30, 30, 21, 21, null]); // costsFor returns the FIRST n bays of the good, in fill order (the same // bays remove() would take) — each its recorded cost or null. assert.deepEqual(h.costsFor('gem', 2), [30, 30], 'two gem bays → their costs, in order'); assert.deepEqual(h.costsFor('gem', 5), [30, 30, 30, null], 'asking for more returns all that exist (incl. the null-cost bay)'); assert.deepEqual(h.costsFor('iron-ore', 1), [21], 'a different good → its own cost'); assert.deepEqual(h.costsFor('nope', 3), [], 'a good that\u2019s not aboard → no basis'); assert.deepEqual(h.costsFor('gem', 0), [], 'n = 0 → nothing'); assert.deepEqual(h.costsFor('gem', 'not-a-number'), [], 'a bad n → nothing (no throw)'); // … and it is READ-ONLY (the hold is untouched). assert.deepEqual(h.items, ['gem', 'gem', 'gem', 'iron-ore', 'iron-ore', 'gem'], 'costsFor never mutates the hold'); assert.deepEqual(h.costs, [30, 30, 30, 21, 21, null], '… the ledger is unchanged too'); // remove() then agrees: the same FIFO bays leave with the same costs. h.remove('gem', 2); assert.deepEqual(h.items, ['gem', 'iron-ore', 'iron-ore', 'gem']); assert.deepEqual(h.costs, [30, 21, 21, null]); } console.log('CARGOHOLD: capacity, dups, drop-sheds, save/load, clone, costsFor: OK'); console.log('\n== TRADE PREVIEW — the shop UI\u2019s calculator =='); // A real galaxy (for the fixed per-shop prices). Model v2: a good is only // on sale at its ORIGIN (the world's signature, batch-limited) or at a HUB // (hub-pool goods) — the preview's fixtures pick the shop's own floor rows. const galaxy = Galaxy.create('orbit-cargo-math'); const sysId = Object.keys(galaxy.economy.signatures)[0]; // any settled system const sigId = galaxy.economy.signatures[sysId]; // its signature good (on its floor) const cls = [...galaxy.economy.classBySystem.get(sysId)].find((c) => c !== 'capital') ?? 'rocky'; const goodId = sigId; // THE good this shop sells (origin batch price) const maxSlots = config.get('economy.cargo.maxSlots', 16); const CAP = 4; // the player's 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) ---------------- { // A HUB sells two different goods (its own signature at the origin price // + hub-pool goods at standard) — the mixed buy needs both on one floor. const hubSys = [...galaxy.economy.classBySystem.entries()].find(([, s]) => s.has('station'))[0]; const hubCls = 'station'; const hubShop = E.shopFor(hubCls, hubSys, galaxy); const a = hubShop.buy.find((r) => r.produced)?.id ?? hubShop.buy[0].id; // the hub's own signature (origin price) const b = hubShop.buy.find((r) => r.id !== a)?.id ?? hubShop.buy[0].id; // a hub-pool resale (standard price) const cargo = [a, a, b]; // two a, one b const pv = E.tradePreview({ cls: hubCls, systemId: hubSys, galaxy, cargo, sellMap: { [a]: 2 }, buyMap: { [b]: 2 }, credits: 100000, capacity: CAP, }); assert.equal(pv.sellTotal, 2 * E.sellPrice(a, hubCls, hubSys, galaxy), 'a\u2019s sale at its sell price'); assert.equal(pv.buyTotal, 2 * E.buyPrice(b, hubCls, hubSys, 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'); } // --- THE BATCH CAP (model v2 — the origin's remaining daily stock) ------- { // The origin world sells its signature in a DAILY BATCH (Stock.js): // a buy over the remaining batch is INVALID (the UI clamps the stepper // at the same number; this is the backstop). Hubs' resale is unlimited // (no cap in stockCaps → the same buy stands). const cap = 2; // the shop's remaining batch (a partial day) const over = E.tradePreview({ cls, systemId: sysId, galaxy, cargo: [], sellMap: {}, buyMap: { [goodId]: cap + 1 }, credits: 100000, capacity: CAP, stockCaps: { [goodId]: cap }, }); assert.equal(over.valid, false, 'buying over the remaining batch is INVALID'); assert.ok(over.errors.includes('insufficient-stock'), '… flagged as insufficient-stock'); const exact = E.tradePreview({ cls, systemId: sysId, galaxy, cargo: [], sellMap: {}, buyMap: { [goodId]: cap }, credits: 100000, capacity: CAP, stockCaps: { [goodId]: cap }, }); assert.equal(exact.valid, true, 'buying EXACTLY the remaining batch is valid'); // No cap for this good here (a hub resells unlimited) → the same buy stands. const hubSys = [...galaxy.economy.classBySystem.entries()].find(([, s]) => s.has('station'))[0]; const hubShop = E.shopFor('station', hubSys, galaxy); const hubGood = hubShop.buy.find((r) => !r.produced)?.id ?? hubShop.buy[0].id; const free = E.tradePreview({ cls: 'station', systemId: hubSys, galaxy, cargo: [], sellMap: {}, buyMap: { [hubGood]: cap + 5 }, credits: 100000, capacity: CAP + 10, }); assert.equal(free.valid, true, 'hubs\u2019 resale is unlimited (no stock cap)'); // A BUYBACK (buyOverrides) never draws on the batch (it returns what the // shop JUST bought from the player — the neutral undo; the UI's stepper // already clamps the buyback qty to what was sold this session, and the // engine's backstop must not block it). const bb = E.tradePreview({ cls, systemId: sysId, galaxy, cargo: [goodId, goodId], sellMap: {}, buyMap: { [goodId]: 2 }, buyOverrides: { [goodId]: E.sellPrice(goodId, cls, sysId, galaxy) }, credits: 100000, capacity: CAP + 5, stockCaps: { [goodId]: 0 }, }); assert.equal(bb.buy[0].unit, E.sellPrice(goodId, cls, sysId, galaxy), 'the buyback is priced at the sale price (neutral undo)'); assert.equal(bb.valid, true, '… and the dry batch never blocks a buyback'); } console.log('TRADE PREVIEW: sell/buy pricing, both caps, loop, clamps, mix: OK'); console.log('\n== PROFIT XP — 1 XP per 1 credit above the good\u2019s purchase cost =='); // tradeProfitXp(sell, costsFor) is the pure core of the sale's XP: each bay // earns max(0, sell unit − its recorded cost); bays with no cost on file // earn nothing; the total is whole credits, clamped ≥ 0. { // A single good sold 2 bays at 50, bought at 30 and 20 → (50−30)+(50−20)=50. const sell = [{ id: 'gem', unit: 50, qty: 2 }]; const costsFor = (id, n) => (id === 'gem' ? [30, 20].slice(0, n) : []); assert.equal(E.tradeProfitXp(sell, costsFor), 50, 'profit = Σ max(0, unit − cost) per bay (20 + 30)'); // Sold at/below cost earns NOTHING (no negative XP, no loss). assert.equal(E.tradeProfitXp([{ id: 'gem', unit: 30, qty: 2 }], costsFor), 10, 'at-cost bay earns 0; the 20-cost bay earns its 10 margin'); assert.equal(E.tradeProfitXp([{ id: 'gem', unit: 20, qty: 2 }], costsFor), 0, 'a sale below every cost earns 0'); // A bay with no cost on file (null) earns nothing — no basis, no profit. const someCost = (id, n) => (id === 'gem' ? [null, 10].slice(0, n) : []); assert.equal(E.tradeProfitXp([{ id: 'gem', unit: 50, qty: 2 }], someCost), 40, 'the null-cost bay earns 0; the 10-cost bay earns 40'); // A good not aboard / no cost basis at all → 0. assert.equal(E.tradeProfitXp([{ id: 'gem', unit: 50, qty: 2 }], () => []), 0, 'no cost on file anywhere → 0'); assert.equal(E.tradeProfitXp([{ id: 'gem', unit: 50, qty: 2 }], null), 0, 'a missing costsFor → 0 (never throws)'); // Multiple goods sum independently; each bay is clamped to its own margin. const multi = (id, n) => ({ gem: [10, 60], ore: [40] }[id]?.slice(0, n) ?? []); const sell2 = [ { id: 'gem', unit: 50, qty: 2 }, // (50−10)=40 + (50−60)→0 = 40 { id: 'ore', unit: 50, qty: 1 }, // (50−40)=10 = 10 ]; assert.equal(E.tradeProfitXp(sell2, multi), 50, 'each good/bay earns its own margin; losses don\u2019t offset gains'); // Fractional credits floor to whole XP; the total is never negative. assert.equal(E.tradeProfitXp([{ id: 'gem', unit: 50.9, qty: 1 }], () => [50.2]), 0, 'sub-1-credit margins floor out (50.9−50.2 < 1 → 0)'); assert.equal(E.tradeProfitXp([], () => [10]), 0, 'an empty sale earns nothing'); assert.equal(E.tradeProfitXp(null, () => [10]), 0, 'a null sale earns nothing (no throw)'); } // The REAL seam: CargoHold.costsFor is the basis tradeProfitXp reads, and // together they price the exact bays the sale removes (FIFO). { const h = new CargoHold(4); h.add(['gem', 'gem'], 30); // bought at 30 each h.add(['gem', 'gem']); // two more with NO cost on file (a handoff) const sellLine = { id: 'gem', unit: 50, qty: 2 }; // the shop sells the FIRST two (30-cost) const xp = E.tradeProfitXp([sellLine], (id, n) => h.costsFor(id, n)); assert.equal(xp, 40, 'the two 30-cost bays earn (50−30)×2 = 40; the null-cost bays are untouched'); // … and selling the null-cost bays (FIFO after the two priced ones) earns 0. h.remove('gem', 2); assert.deepEqual(h.costs, [null, null], '… the two priced bays are gone'); assert.equal(E.tradeProfitXp([sellLine], (id, n) => h.costsFor(id, n)), 0, '… the remaining null-cost bays earn nothing'); } console.log('PROFIT XP: per-bay margin, loss/none→0, multi-good, floor, FIFO basis: OK'); console.log('\nAll cargo checks passed \u2714');