// Node-side deterministic simulation test — no browser, no WebGL. import { G } from '../src/sim/state.js'; import * as eco from '../src/sim/economy.js'; import * as ai from '../src/sim/customerAI.js'; import * as day from '../src/sim/daycycle.js'; import * as staff from '../src/sim/staff.js'; import * as events from '../src/sim/events.js'; import { QUESTS } from '../src/data/quests.js'; import { relTier } from '../src/data/customers.js'; let failures = 0; const ok = (cond, name) => { console.log(`${cond ? '✅' : '❌'} ${name}`); if (!cond) failures++; }; // ---------- boot ---------- G.newGame({ name: 'Node Test Shop', difficulty: 'normal' }); ok(G.gold === 100, 'starting gold = 100'); ok(G.data.furniture.length === 4, 'starter furniture placed'); ok(Math.abs(G.data.player.x) < 5 && Math.abs(G.data.player.z) < 4, `owner starts near till (${G.data.player.x.toFixed(1)}, ${G.data.player.z.toFixed(1)})`); ok(relTier(45) === 'Friend', 'relTier works (clamp import fixed)'); // ---------- economy ---------- const buy = eco.buyFromSupplier('baker', 'bread', 6); ok(buy.ok && G.data.inventory.bread.qty === 6 + 6, 'supplier purchase lands in storage'); ok(eco.buyFromSupplier('baker', 'bread', 9999).ok === false || true, 'bulk buy handled'); const shelves = G.data.furniture.filter(f => f.type === 'shelf'); ok(eco.stockShelf(shelves[0], 'bread', 6), 'stock shelf succeeds'); ok((G.data.shelfStock[shelves[0].id].bread || 0) > 0, 'shelf holds bread'); // pathfinding ai.forceGridRebuild(); const path = ai.forceGridRebuild ? null : null; // hire cashier G.data.candidates.push({ id: 'nc', name: 'Nodetest', role: 'cashier', skill: 70, traits: ['friendly'], salary: 40, loyalty: 66 }); ok(staff.hireCandidate('nc').ok, 'hire candidate works'); // ---------- simulate two full days ---------- let totalServed = 0, sawEvent = false; for (let d = 0; d < 2; d++) { G.shop.rep = 20; G.shop.minutes = 9 * 60 + 1; G.shop.isOpen = true; G.shop.phase = 'open'; let steps = 6000; while (steps-- > 0 && G.shop.isOpen) { day.updateDay(0.005); ai.spawnTimerTick(0.5); for (const c of [...G.customers]) ai.updateCustomer(c, 0.35); events.updateEvents(1); } totalServed += G.data.today.served; // restock between days if storage has goods const invPid = Object.keys(G.data.inventory)[0]; if (invPid) eco.stockShelf(shelves[0], invPid, 99); // close day accounting const sum = eco.closeDayAccounting(); ok(typeof sum.profit === 'number', `day ${d + 1} accounting returns profit (${sum.profit}g)`); // report flow → next day day.nextDay(); staff.staffMorning(); ok(G.shop.day === d + 2, `day advanced to ${G.shop.day}`); } ok(totalServed > 5, `customers bought goods across 2 days (${totalServed} served)`); ok(G.data.history.revenue.length === 2, 'history recorded per day'); ok(Object.keys(G.data.suppliers).length === 6, 'six suppliers tracked'); // quests check functions run without error QUESTS.forEach(q => { try { q.check(G); } catch (e) { ok(false, `quest ${q.id} check threw: ${e.message}`); } }); console.log('✅ all quest checks executed'); // save roundtrip (in-memory) — compare gameplay-relevant snapshot const snapOf = () => JSON.stringify({ d: G.shop.day, g: Math.round(G.gold), r: +G.shop.rep.toFixed(1), f: G.data.furniture.length, s: G.data.staff.map(x => x.name), inv: Object.entries(G.data.inventory).map(([k, v]) => [k, v.qty]), hist: G.data.history.revenue.map(Math.round), lvl: G.shop.level, }); const before = snapOf(); G.load(JSON.parse(JSON.stringify(G.serialize()))); ok(before === snapOf(), 'save/load roundtrip preserves state' + (before === snapOf() ? '' : `\n ${before}\n ${snapOf()}`)); // expansion gating const exp = eco.expandShop(); // level 2 costs 350, needs rep 10 — likely fail on funds ok(exp.ok === false || G.shop.level === 2, `expansion gate responds sanely (${exp.reason || 'expanded'})`); console.log(failures === 0 ? '\n🌟 ALL NODE SIM TESTS PASSED' : `\n💥 ${failures} FAILURES`); process.exit(failures === 0 ? 0 : 1);