- 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)
153 lines
6.4 KiB
JavaScript
153 lines
6.4 KiB
JavaScript
import puppeteer from 'puppeteer';
|
|
|
|
const URL = 'http://127.0.0.1:4939';
|
|
const results = {};
|
|
const errors = [];
|
|
|
|
async function runOnce(attempt) {
|
|
const browser = await puppeteer.launch({
|
|
headless: true,
|
|
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage',
|
|
'--enable-unsafe-swiftshader', '--use-gl=angle', '--use-angle=swiftshader'],
|
|
});
|
|
const page = await browser.newPage();
|
|
await page.setViewport({ width: 800, height: 520 });
|
|
page.on('pageerror', (e) => errors.push('[pe] ' + e.message.slice(0, 140)));
|
|
try {
|
|
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 45000 });
|
|
await new Promise(r => setTimeout(r, 3500));
|
|
results.boot = 'PASS';
|
|
|
|
// menu screenshot
|
|
try { await page.screenshot({ path: `/tmp/v_menu_${attempt}.png` }); results.shotMenu = 'PASS'; } catch { results.shotMenu = 'FAIL'; }
|
|
|
|
// start game through UI
|
|
await page.evaluate(() => document.getElementById('m-new').click());
|
|
await new Promise(r => setTimeout(r, 500));
|
|
await page.evaluate(() => {
|
|
const i = document.querySelector('#wiz-name'); i.value = 'Verify Shop';
|
|
document.querySelector('#wiz-start').click();
|
|
});
|
|
// lighten GPU load for swiftshader
|
|
await page.evaluate(() => { window.__TS.G.data.settings.quality = 'low'; });
|
|
await new Promise(r => setTimeout(r, 2200));
|
|
results.startGame = 'PASS';
|
|
|
|
// deterministic profitable day
|
|
const day = await page.evaluate(async () => {
|
|
const T = window.__TS, G = T.G;
|
|
const shelves = G.data.furniture.filter(f => f.type === 'shelf');
|
|
T.eco.buyFromSupplier('baker', 'bread', 6);
|
|
T.eco.buyFromSupplier('farmer', 'apple', 6);
|
|
T.eco.stockShelf(shelves[0], 'bread', 6);
|
|
T.eco.stockShelf(shelves[0], 'apple', 6);
|
|
G.data.candidates.push({ id: 'tc', name: 'Tessa', role: 'cashier', skill: 72, traits: ['friendly'], salary: 40, loyalty: 66 });
|
|
T.staff.hireCandidate('tc');
|
|
G.shop.rep = 18;
|
|
G.shop.minutes = 9 * 60 + 1;
|
|
G.shop.isOpen = true; G.shop.phase = 'open';
|
|
let steps = 6000;
|
|
while (steps-- > 0 && G.shop.isOpen) {
|
|
T.day.updateDay(0.005);
|
|
T.ai.spawnTimerTick(0.5);
|
|
for (const c of [...G.customers]) T.ai.updateCustomer(c, 0.35);
|
|
}
|
|
return { served: G.data.today.served, revenue: Math.round(G.data.today.revenue), staff: G.data.staff.length };
|
|
});
|
|
results.simDay = day.served > 0 && day.revenue > 0 ? `PASS (${day.served} served, ${day.revenue}g)` : `FAIL ${JSON.stringify(day)}`;
|
|
|
|
// close-day → report → next day
|
|
const flow = await page.evaluate(() => new Promise(res => {
|
|
window.__TS.emit('requestClose');
|
|
setTimeout(() => {
|
|
document.getElementById('rc-yes')?.click();
|
|
setTimeout(() => {
|
|
const reportVisible = !document.getElementById('modal-root').classList.contains('hidden');
|
|
const stars = document.querySelector('.r-stars')?.textContent.trim();
|
|
document.getElementById('next-day')?.click();
|
|
setTimeout(() => res({ reportVisible, stars, day2: window.__TS.G.shop.day }), 450);
|
|
}, 450);
|
|
}, 400);
|
|
}));
|
|
results.reportFlow = flow.reportVisible && flow.day2 === 2 ? `PASS (${flow.stars})` : `FAIL ${JSON.stringify(flow)}`;
|
|
|
|
// panels
|
|
const panelOk = [];
|
|
for (const id of ['shop', 'inventory', 'suppliers', 'staff', 'town', 'quests', 'analytics', 'goals']) {
|
|
const ok = await page.evaluate((pid) => new Promise(res => {
|
|
document.querySelector(`.nav-btn[data-panel="${pid}"]`).click();
|
|
setTimeout(() => {
|
|
const body = document.querySelector('.side-panel .sp-body');
|
|
res(!!body && body.innerHTML.length > 300);
|
|
}, 300);
|
|
}), id);
|
|
panelOk.push(`${id}:${ok ? '✓' : '✗'}`);
|
|
}
|
|
results.panels = panelOk.join(' ');
|
|
|
|
// negotiation
|
|
const neg = await page.evaluate(() => new Promise(res => {
|
|
document.querySelector('.nav-btn[data-panel="suppliers"]').click();
|
|
setTimeout(() => {
|
|
document.querySelector('[data-neg="baker"]')?.click();
|
|
setTimeout(() => {
|
|
const btns = [...document.querySelectorAll('.choice-btn')];
|
|
[...btns].find(b => b.textContent.includes('Pay immediately'))?.click();
|
|
setTimeout(() => res({ modal: btns.length >= 4 ? 'PASS' : 'FAIL' }), 250);
|
|
}, 250);
|
|
}, 350);
|
|
}));
|
|
results.negotiation = neg.modal;
|
|
|
|
// decorate: place furniture via data path + visuals refresh
|
|
const decor = await page.evaluate(() => {
|
|
const G = window.__TS.G;
|
|
const before = G.data.furniture.length;
|
|
G.addGold(-30);
|
|
G.data.furniture.push({ id: 'vplant', type: 'plant', cx: 3, cz: 3, rot: 0 });
|
|
window.__TS.ai.forceGridRebuild();
|
|
window.__TS.refreshFurniture();
|
|
return G.data.furniture.length === before + 1 ? 'PASS' : 'FAIL';
|
|
});
|
|
results.decorate = decor;
|
|
|
|
// save/load roundtrip
|
|
const sl = await page.evaluate(() => new Promise(res => {
|
|
window.__TS.saveApi.saveTo('slot1');
|
|
const d = window.__TS.saveApi.loadFrom('slot1');
|
|
res(d && d.shop.day === 2 && d.meta.name === 'Verify Shop' ? 'PASS' : `FAIL ${d?.shop?.day}/${d?.meta?.name}`);
|
|
}));
|
|
results.saveLoad = sl;
|
|
|
|
// achievements
|
|
const ach = await page.evaluate(() => {
|
|
const a = window.__TS.saveApi.getAchievements();
|
|
return Object.keys(a).length >= 1 ? `PASS (${Object.keys(a).length} unlocked)` : `FAIL ${Object.keys(a).length}`;
|
|
});
|
|
results.achievements = ach;
|
|
|
|
// gameplay screenshot at busy time
|
|
try {
|
|
await page.evaluate(() => { window.__TS.G.shop.minutes = 12 * 60; window.__TS.G.shop.isOpen = true; window.__TS.G.timeScale = 3; });
|
|
await new Promise(r => setTimeout(r, 2500));
|
|
await page.screenshot({ path: `/tmp/v_game_${attempt}.png` });
|
|
results.shotGame = 'PASS';
|
|
} catch { results.shotGame = 'FAIL'; }
|
|
} catch (e) {
|
|
results.exception = e.message.slice(0, 120);
|
|
} finally {
|
|
await browser.close().catch(() => {});
|
|
}
|
|
}
|
|
|
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
console.log(`\n=== attempt ${attempt} ===`);
|
|
await runOnce(attempt);
|
|
const done = ['simDay', 'reportFlow', 'saveLoad'].every(k => (results[k] || '').startsWith('PASS'))
|
|
&& (results.panels || '').includes('✓') && !(results.panels || '').includes('✗');
|
|
console.log(JSON.stringify(results, null, 1));
|
|
if (done && !results.exception) break;
|
|
}
|
|
console.log('\nPAGEERRORS:', errors.length ? errors.slice(-6).join(' | ') : 'none');
|
|
process.exit(0);
|