orbit/dev/trade-scout.test.mjs

157 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* TradeScout test (dev tool — run headless with the Phaser loader):
*
* node --import ./dev/phaser-loader.mjs dev/trade-scout.test.mjs
*
* The PURE strategy layer behind the shop's goods inspector (tap a BUY row's
* box → the cargo grid is swapped for the row's strategy report):
*
* - THE REPORT: identity (name/category/tier/flavor) + local prices +
* producer ("GROWN ON") + demand ("SIGHT ON") classes with labels.
* - CHARTED EXITS: only systems the run has ENTERED count; the current
* system always counts (we're here); a system is an exit only if it
* actually BUYS the good (wants it, or is a capital); ranked by its
* BEST class's sell price, top-N kept.
* - PROFIT: exit price − the shop's local buy price (the row's price);
* maxProfit drives the panel's bars.
* - CAPITALS: a visited capital is always an exit (the universal sink).
* - DETERMINISM: same inputs ⇒ identical report.
*/
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));
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);
const { scoutGood, goodCatalog, sellsHere } = await import(pathToFileURL(join(__dirname, '../js/economy/TradeScout.js')).href);
let failures = 0;
const check = (label, cond) => {
console.log(`${cond ? '✔' : '✘ FAIL'} ${label}`);
if (!cond) failures++;
};
const g = Galaxy.create('orbit-trade-scout-test');
const goods = config.get('goods.goods', []);
const classOf = (sysId) => [...g.economy.classBySystem.get(sysId)];
// A real shop to scout from: the home system (always charted, terran world).
const sysId = g.homeSystemId;
const cls = 'terran';
const stock = E.shopFor(cls, sysId, g).buy;
const row = stock.find((s) => sellsHere(s.id, classOf(sysId).find((c) => c !== 'capital')) ) ?? stock[0];
// ---------------------------------------------------------------------------
// 1. The report — identity + local prices + class info
// ---------------------------------------------------------------------------
{
const d = scoutGood(row.id, { galaxy: g, systemId: sysId, cls, buyUnit: row.price, visited: [sysId] });
check('report exists for a stock good', !!d);
check('identity: id + name', d.id === row.id && d.name.length > 0);
check('identity: flavor from the catalog', d.flavor === (goodCatalog(row.id).flavor ?? ''));
check('identity: category + tier from the catalog', d.category === goodCatalog(row.id).category && d.tier === goodCatalog(row.id).tier);
check('localBuy = the row price (the profit baseline)', d.localBuy === row.price);
const wantsHere = sellsHere(row.id, cls);
check(`localSell ${wantsHere ? 'priced (this class wants it)' : 'null (this class does not want it)'}`,
wantsHere ? (d.localSell != null && d.localSell > 0) : d.localSell == null);
check('producer classes listed with labels', d.producer.every((c) => c.cls && c.label.length > 0));
check('demand classes listed with labels', d.demand.every((c) => c.cls && c.label.length > 0));
check('unknown good → null report', scoutGood('no-such-good', { galaxy: g, systemId: sysId, cls }) == null);
}
// ---------------------------------------------------------------------------
// 2. CharteD exits — only visited systems, only buyers, ranked, top-N
// ---------------------------------------------------------------------------
{
// Pick a demand class of this good and find a charted system carrying it.
const wantCls = (goodCatalog(row.id).demand ?? [])[0];
const exitSys = wantCls
? [...g.byId.keys()].find((id) => g.economy.classBySystem.get(id).has(wantCls))
: null;
const visited = [sysId, exitSys].filter(Boolean);
const d = scoutGood(row.id, { galaxy: g, systemId: sysId, cls, buyUnit: row.price, visited, topN: 3 });
check('exits are a list of { systemId, systemName, cls, price, profit, hops }',
d.exits.every((e) => e.systemId && e.systemName && e.cls && Number.isFinite(e.price) && Number.isFinite(e.profit) && e.clsLabel));
check('every exit is a CHARTED (visited) system', d.exits.every((e) => visited.includes(e.systemId)));
check('no exit for an UNCHARTED system', (() => {
const others = [...g.byId.keys()].filter((id) => !visited.includes(id));
const d2 = scoutGood(row.id, { galaxy: g, systemId: sysId, cls, buyUnit: row.price, visited: [sysId] });
return d2.exits.every((e) => visited.includes(e.systemId));
})());
check('ranked by sell price (high → low)', d.exits.every((e, i) => i === 0 || d.exits[i - 1].price >= e.price));
check('top-N honored (≤ 3)', d.exits.length <= 3);
if (exitSys) {
const wantsIt = E.wants(row.id, wantCls);
check(`the charted ${wantCls} system ${wantsIt ? 'IS' : 'is not (capital-only)'} in the exits`,
d.exits.some((e) => e.systemId === exitSys) === wantsIt);
}
check('profit = exit price − local buy', d.exits.every((e) => e.profit === e.price - row.price));
check('maxProfit = the best exit profit', d.maxProfit === Math.max(0, ...d.exits.map((e) => e.profit)));
check('bestExit = the first ranked exit', d.bestExit == null ? d.exits.length === 0 : d.bestExit === d.exits[0]);
}
// ---------------------------------------------------------------------------
// 3. Capitals are always an exit (the universal sink)
// ---------------------------------------------------------------------------
{
const capId = [...g.byId.keys()].find((id) => g.economy.classBySystem.get(id).has('capital'));
const visited = [sysId, capId].filter(Boolean);
const d = scoutGood(row.id, { galaxy: g, systemId: sysId, cls, buyUnit: row.price, visited });
check('a charted CAPITAL always ranks as an exit', d.exits.some((e) => e.systemId === capId && g.economy.classBySystem.get(capId).has('capital')));
}
// ---------------------------------------------------------------------------
// 4. Determinism — same inputs ⇒ identical report
// ---------------------------------------------------------------------------
{
const o = { galaxy: g, systemId: sysId, cls, buyUnit: row.price, visited: [...g.byId.keys()].slice(0, 5) };
const a = JSON.stringify(scoutGood(row.id, { ...o }));
const b = JSON.stringify(scoutGood(row.id, { ...o }));
check('same inputs ⇒ identical report', a === b);
}
// ---------------------------------------------------------------------------
// 5. A good that is produced locally sells ABOVE its local buy (the loop)
// ---------------------------------------------------------------------------
{
// Pick a good this class PRODUCES (in the stock) — somewhere far it must
// sell for more than it costs here (hop term), when that far system is
// charted.
const produced = stock.find((s) => g.economy.classBySystem.get(sysId).has(cls) && E.produces(s.id, cls));
if (produced) {
const far = [...g.byId.keys()].find((id) => {
const c = classOf(id);
return c.some((x) => sellsHere(produced.id, x)) &&
(g.economy.producerHops[produced.id][id] ?? 0) > 1;
});
if (far) {
const d = scoutGood(produced.id, { galaxy: g, systemId: sysId, cls, buyUnit: produced.price, visited: [sysId, far] });
const exit = d.exits.find((e) => e.systemId === far);
check(`the far charted exit sells ${produced.id} above its local buy (the loop)`,
!!exit && exit.price > produced.price);
} else {
check('far exit found (skipped — no suitable system)', true); // informational
}
} else {
check('a locally produced stock good exists (skipped)', true); // informational
}
}
console.log(failures === 0 ? '\nAll TradeScout checks passed ✔' : `\n${failures} FAILURES`);
process.exit(failures === 0 ? 0 : 1);