Files
deepgrain-foundry/test/t_econ.js
T
44r0n7andClaude Opus 5.5 b270c4e818 Deepgrain Foundry: move from a claude.ai artifact to a local repository
The game as of artifact version 24, ready for local development with
coding agents.

- Source split from three large files into 52 small ones under src/sim,
  src/render and src/ui, joined in the order in src/manifest.json by
  build.py. The joined code is unchanged apart from comments; every
  headless suite gives the same output, and screenshots in both looks
  are pixel-identical to before.
- Light tidying with no change in behaviour: one movement scan loop,
  shared hazard-stripe and belt shader helpers, unused code removed, a
  shared check helper for the test suites.
- World files can be exported as an ordinary download outside claude.ai.
- Tests: broken old scripts removed, experiments moved to tools/,
  test/run_all.py added. Tools: shot.py (repeatable screenshots),
  where.py, map.py.
- Docs: AGENTS.md, CLAUDE.md, NOTES.md (the owner's to-do inbox),
  docs/PROJECT_MAP.md, DESIGN.md, TESTING.md, systems notes, the
  owner's requests and an agent log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9kLmYADKSEjuwUN3rFxDA
2026-10-23 16:05:31 -04:00

116 lines
9.7 KiB
JavaScript

// Economy: the dock (instant sale), orders, prices, the market dip, money for machines, alloys.
const { load, suite, flatWorld, rect, ticks, count } = require('./harness');
const { run } = load();
const { check, done } = suite();
// a generated world comes with a dock near spawn
run('generateWorld(777, 2048, 1024, null)');
const d = run('JSON.stringify(dock() && { x: dock().x, y: dock().y })');
check('new worlds have a trade dock near spawn', d !== 'null' && Math.abs(JSON.parse(d).x - 1024) < 80, d);
check('starting money and claim debt', run('money') === 250 && run('debt') === 20000);
// goods dropped into the dock sell the moment they reach its grate
run('mkRecent.fill(0); orders = []; nextOrderT = 1e12; EV.length = 0;');
run('{ const dk = dock(); for (let y = dk.y + 2; y < dk.y + 8; y++) for (let x = dk.x + 2; x < dk.x + 22; x++) setCell(y*W+x, (x+y) % 2 ? CUORE : COAL, AMB[y], 0); }');
run('{ const dk = dock(); spawnBar(BARCU, dk.x + 6, dk.y + 2, 0, 0); spawnBar(BARCU, dk.x + 12, dk.y + 2, 0, 0); }');
ticks(run, 90);
const interior = () => run('(() => { const dk = dock(); let n = 0; for (let y = dk.y; y < dk.y + 14; y++) for (let x = dk.x + 2; x < dk.x + 22; x++) if (mat[y*W+x] !== EMPTY) n++; return n; })()');
check('ore, coal and bars are paid for as soon as they drop in', run('money') > 250 + 150 && interior() === 0 && run('ents.length') === 0, `money ${run('money').toFixed(1)}`);
check('a "+$" floater rises from the dock', run('EV.some(e => e.t === "sold" && e.v > 1)'));
// no cap: a whole column of ore poured in over time all sells
run('money = 0; EV.length = 0;');
for (let k = 0; k < 20; k++) {
run('{ const dk = dock(); for (let y = dk.y - 30; y < dk.y - 10; y++) for (let x = dk.x + 2; x < dk.x + 22; x++) if (mat[y*W+x] === EMPTY) setCell(y*W+x, CUORE, AMB[y], 0); }');
ticks(run, 30);
}
ticks(run, 240);
const sold = run('money');
check('8,000 grains poured in over ten seconds all sell, nothing piles up', sold > 2500 && interior() === 0, `$${sold.toFixed(0)}, price now ${(run('priceFactor(CUORE)') * 100).toFixed(0)}%`);
// hot goods wait until they cool
run('{ const dk = dock(); for (let x = dk.x + 2; x < dk.x + 22; x++) setCell((dk.y + 13)*W + x, CUORE, 600, 0); }');
ticks(run, 3);
check('hot goods are not sold until they cool', interior() >= 15, interior());
run('{ const dk = dock(); for (let y = dk.y; y < dk.y + 14; y++) for (let x = dk.x + 2; x < dk.x + 22; x++) setCell(y*W+x, EMPTY, AMB[y], 0); }');
// orders: goods fill a matching order first and the order pays a lump sum
run('mkRecent.fill(0); money = 0; earned = 0; orders = []; ordersDone = 0; EV.length = 0;');
run('orders.push({ id: 1, m: BARCU, qty: 3, got: 0, mult: 2, pay: 240, due: tick + 36000, mins: 10 })');
run('{ const dk = dock(); spawnBar(BARCU, dk.x + 6, dk.y + 2, 0, 0); spawnBar(BARCU, dk.x + 10, dk.y + 2, 0, 0); }');
ticks(run, 90);
check('two bars count toward the order and are not paid yet', run('orders[0].got') === 2 && run('money') === 0, `got ${run('orders[0].got')}, money ${run('money')}`);
run('{ const dk = dock(); spawnBar(BARCU, dk.x + 6, dk.y + 2, 0, 0); spawnBar(BARCU, dk.x + 14, dk.y + 2, 0, 0); }');
ticks(run, 90);
check('the third bar fills it: the lump sum is paid, and the fourth sells at market', run('orders.length') === 0 && run('ordersDone') === 1 && Math.abs(run('money') - 280) < 1, `money ${run('money').toFixed(1)}`);
check('the order-filled event carries its pay, and no truck comes', run('EV.some(e => e.t === "order_done" && e.pay === 240)') && run('typeof truck') === 'undefined');
// an expired order costs nothing: what was delivered is paid at market
run('money = 0; mkRecent.fill(0); orders.push({ id: 2, m: CUORE, qty: 500, got: 0, mult: 2, pay: 1000, due: tick + 400, mins: 1 })');
run('{ const dk = dock(); for (let y = dk.y + 2; y < dk.y + 4; y++) for (let x = dk.x + 2; x < dk.x + 22; x++) setCell(y*W+x, CUORE, AMB[y], 0); }');
ticks(run, 90);
check('ore counts toward its order', run('orders[0].got') === 40 && run('money') === 0, run('orders[0].got'));
ticks(run, 400);
check('when the order runs out, the delivered ore is paid at market', run('orders.length') === 0 && Math.abs(run('money') - 40) < 1 && run('EV.some(e => e.t === "order_expired" && e.got === 40)'), `money ${run('money').toFixed(2)}`);
// buyers post orders for goods you have seen, and they pay well over market
run('orders = []; ordersDone = 0; seen.fill(0); seen[CUORE] = 1; seen[BARCU] = 1; seen[COAL] = 1; nextOrderT = 0;');
ticks(run, 120);
const o1 = JSON.parse(run('JSON.stringify(orders[0] || null)'));
check('a buyer posts an order for something you have found', o1 && [run('CUORE'), run('BARCU'), run('COAL')].includes(o1.m) && o1.pay >= o1.qty * run(`PRICE[${o1.m}]`) * 1.5, JSON.stringify(o1));
let sizes = [];
for (const done of [0, 20]) { run(`ordersDone = ${done}; orders = [];`); let t = 0; for (let k = 0; k < 40; k++) t += run('(() => { const o = makeOrder(); return o ? o.qty * PRICE[o.m] : 0; })()'); sizes.push(t / 40); }
check('orders grow as your standing grows', sizes[1] > sizes[0] * 3 && run('STANDING[standing()]') === 'Export houses', sizes.map(v => v.toFixed(0)).join(' -> '));
run('ordersDone = 0; orders = []; nextOrderT = 0;');
run('for (let k = 0; k < 300; k++) { tick += 1800; ordersTick(); }');
check('there are never more than three open orders', run('orders.length') <= 3 && run('orders.length') >= 1, run('orders.length'));
// the market dips when one thing is sold in bulk, then recovers
run('mkRecent.fill(0)');
const p0 = run('priceNow(BARCU)');
run('for (let k = 0; k < 100; k++) sellUnit(BARCU)');
const p1 = run('priceNow(BARCU)');
run('for (let k = 0; k < 4 * 480; k++) marketTick()'); // four half-lives: 32 minutes
const p2 = run('priceNow(BARCU)');
check('selling 100 copper bars lowers their price, which recovers within half an hour', p1 < p0 * 0.5 && p2 > p0 * 0.9, `${p0} -> ${p1.toFixed(1)} -> ${p2.toFixed(1)}`);
check('dirt earns nothing, and rubble only a pittance (builders pay a few times that)', run('PRICE[LDIRT]') === 0 && run('PRICE[RSTONE]') > 0 && run('PRICE[RSTONE]') < 0.05);
check('refined goods pay more than the raw ore in them', run('PRICE[BARCU] > 13 * PRICE[CUORE] + 13 * PRICE[COAL]') && run('PRICE[BARBZ] > 2 * PRICE[BARCU]'));
// machines cost money, walls do not
flatWorld(run, 240, 160, 100);
run('money = 30; bag[RSTONE] = 100; bagTotal = 100; bot.x = 110; bot.y = 70;');
const r1 = run('JSON.stringify(placeBlocks(BP_BELT, 1, blockLine(28, 20, 31, 20)))');
check('belts cost money: $30 buys two', JSON.parse(r1).n === 2 && run('money') === 6, r1 + ' money ' + run('money'));
check('wall blocks still cost material, not money', run('placeBlock(BP_WALL, RSTONE, 26, 22, false)') === '' && run('money') === 6);
run('removePieceAt(113, 81)');
check('taking a belt back refunds its price', run('money') === 18, run('money'));
// paying the claim
run('money = 25000; debt = 20000;');
run('payClaim(5000)');
check('paying part of the claim', run('debt') === 15000 && run('money') === 20000);
run('payClaim(1e9); checkLedger();');
check('paying it off marks the milestone', run('debt') === 0 && run('!!ledger.claim') && run('money') === 5000);
// alloys: tin into molten copper becomes bronze; zinc makes brass
flatWorld(run, 240, 160, 100);
rect(run, 100, 80, 101, 99, run('BRICK')); rect(run, 140, 80, 141, 99, run('BRICK'));
run('for (let y = 92; y <= 99; y++) for (let x = 102; x <= 139; x++) setCell(y*W+x, MCU, 1200, 0)');
run('for (let y = 86; y <= 87; y++) for (let x = 110; x <= 131; x++) setCell(y*W+x, MSN, 1100, 0)');
run('for (let y=80;y<100;y++) for (let x=100;x<142;x++) wakeHeatXY(x,y)');
for (let k = 0; k < 20; k++) { run('for (let y=80;y<100;y++) for (let x=100;x<142;x++) { const i=y*W+x; if (CASTMETAL[mat[i]] && temp[i] < 1150) temp[i] = 1150; }'); ticks(run, 15); }
const bz = count(run, run('MBZ')), sn = count(run, run('MSN')), cu = count(run, run('MCU'));
check('tin melted into copper mixes into bronze, roughly 1 part tin to 7 copper', bz > 200 && sn <= 12, `bronze ${bz}, tin left ${sn}, copper left ${cu}`);
run('for (let y = 92; y <= 99; y++) for (let x = 102; x <= 139; x++) setCell(y*W+x, MCU, 1200, 0)');
run('for (let y = 86; y <= 87; y++) for (let x = 110; x <= 131; x++) setCell(y*W+x, MZN, 1100, 0)');
for (let k = 0; k < 20; k++) { run('for (let y=80;y<100;y++) for (let x=100;x<142;x++) { const i=y*W+x; if (CASTMETAL[mat[i]] && temp[i] < 1150) temp[i] = 1150; }'); ticks(run, 15); }
check('zinc melted into copper becomes brass', count(run, run('MBS')) > 60, count(run, run('MBS')));
// a bronze ingot casts into bronze bars
flatWorld(run, 240, 160, 100);
run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE)');
run('for (const i of molds[0].cavs[0]) setCell(i, MBZ, 1000, 0)');
ticks(run, 900);
run('collectMold(molds[0])');
check('bronze casts into bronze bars', run('ents.length') === 3 && run('ents.every(e => e.m === BARBZ)'), run('JSON.stringify(ents.map(e => MD[e.m].name))'));
// soft metals are no good for a drill bit
run('placeMoldAt("bit", 20, 22, RSTONE)');
run('for (const i of molds[1].cavs[0]) setCell(i, MAU, 1100, 0)');
ticks(run, 1200);
check('a gold casting is too soft for a drill bit', /too soft/.test(run('collectMold(molds[1])')) && run('player.bitTier') === 0);
// save and load keeps the economy
run('generateWorld(778, 768, 512, null); money = 1234; debt = 5000; earned = 9999; mkRecent[BARCU] = 777; ordersDone = 6;');
run('orders = [{ id: 9, m: BARCU, qty: 10, got: 4, mult: 2.1, pay: 840, due: tick + 9000, mins: 3 }];');
run('globalThis.__s = serializeWorld(); orders = []; ordersDone = 0; deserializeWorld(__s.meta, __s.buffers);');
check('save and load keep money, debt, earnings, market, orders and the dock', run('money') === 1234 && run('debt') === 5000 && run('earned') === 9999 && run('mkRecent[BARCU]') === 777 && run('!!dock()') && run('ordersDone') === 6 && run('orders.length === 1 && orders[0].got === 4 && orders[0].pay === 840'));
done();