Files
deepseek 0e7c07533e TINY SHOP — cozy 3D shop-management game (Three.js vertical slice)
- Procedural 3D world: dollhouse shop, town, day/night, weather, seasons
- Customer AI with personalities (story NPCs, thieves, weekly regulars)
- Economy: suppliers, negotiation, pricing psychology, daily accounting
- Staff with traits/loyalty, 8 expansion levels, furniture & decoration
- Events with choices, quests, achievements, 4 difficulties, rival shop
- Animated daily report, analytics, save/load (3 slots + autosave)
- Procedural music & SFX (WebAudio), zero external assets
- Test harnesses: simtest (node), verify/check/e2e (headless browser)
2026-08-23 06:58:24 +00:00

93 lines
4.0 KiB
JavaScript

// 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);