- 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)
98 lines
3.7 KiB
JavaScript
98 lines
3.7 KiB
JavaScript
import puppeteer from 'puppeteer';
|
|
import { PNG } from 'pngjs';
|
|
import fs from 'fs';
|
|
|
|
const URL = 'http://127.0.0.1:4939';
|
|
const browser = await puppeteer.launch({
|
|
headless: true,
|
|
args: ['--no-sandbox', '--disable-setuid-sandbox', '--enable-unsafe-swiftshader', '--use-gl=angle', '--use-angle=swiftshader'],
|
|
});
|
|
const page = await browser.newPage();
|
|
await page.setViewport({ width: 1024, height: 600 });
|
|
|
|
const errors = [];
|
|
const logs = [];
|
|
page.on('console', (msg) => {
|
|
const t = `[${msg.type()}] ${msg.text()}`;
|
|
logs.push(t);
|
|
if (msg.type() === 'error') errors.push(t);
|
|
});
|
|
page.on('pageerror', (err) => errors.push('[pageerror] ' + err.message));
|
|
|
|
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 45000 });
|
|
await new Promise(r => setTimeout(r, 4000));
|
|
|
|
// sanity: dom state
|
|
const state = await page.evaluate(() => ({
|
|
title: document.title,
|
|
hasMenu: !!document.getElementById('menu'),
|
|
hasNewBtn: !!document.getElementById('m-new'),
|
|
uiHidden: document.getElementById('ui')?.classList.contains('hidden'),
|
|
canvases: [...document.querySelectorAll('canvas')].map(c => ({ w: c.width, h: c.height, id: c.id || '(main)' })),
|
|
webgl: (() => { try { const c = document.createElement('canvas'); return !!c.getContext('webgl2'); } catch { return false; } })(),
|
|
}));
|
|
console.log('STATE:', JSON.stringify(state, null, 1));
|
|
|
|
async function analyze(file, label) {
|
|
const buf = fs.readFileSync(file);
|
|
const png = PNG.sync.read(buf);
|
|
const { width: w, height: h, data } = png;
|
|
const region = (x0, y0, x1, y1) => {
|
|
let r = 0, g = 0, b = 0, n = 0;
|
|
for (let y = y0; y < y1; y += 4) for (let x = x0; x < x1; x += 4) {
|
|
const i = (y * w + x) * 4;
|
|
r += data[i]; g += data[i + 1]; b += data[i + 2]; n++;
|
|
}
|
|
return [Math.round(r / n), Math.round(g / n), Math.round(b / n)];
|
|
};
|
|
const px = (x, y) => { const i = (y * w + x) * 4; return [data[i], data[i + 1], data[i + 2]]; };
|
|
// variance sample across whole frame
|
|
let sum = 0, sum2 = 0, n = 0;
|
|
for (let y = 0; y < h; y += 6) for (let x = 0; x < w; x += 6) {
|
|
const [r, g, b] = px(x, y);
|
|
const l = (r + g + b) / 3;
|
|
sum += l; sum2 += l * l; n++;
|
|
}
|
|
const mean = sum / n;
|
|
const std = Math.sqrt(sum2 / n - mean * mean);
|
|
console.log(`${label}: mean=${mean.toFixed(1)} std=${std.toFixed(1)} top=${region(0, 0, w, 60)} center=${region(w / 3, h / 3, 2 * w / 3, 2 * h / 3)} bottom=${region(0, h - 80, w, h)}`);
|
|
}
|
|
|
|
await page.screenshot({ path: '/tmp/shot_menu.png' });
|
|
await analyze('/tmp/shot_menu.png', 'MENU ');
|
|
|
|
// proceed into game
|
|
try {
|
|
await page.evaluate(() => document.getElementById('m-new').click());
|
|
await new Promise(r => setTimeout(r, 500));
|
|
await page.evaluate(() => {
|
|
const inp = document.querySelector('#wiz-name');
|
|
if (inp) inp.value = 'The Gilded Carrot';
|
|
document.querySelector('#wiz-start')?.click();
|
|
});
|
|
await new Promise(r => setTimeout(r, 2500));
|
|
await page.screenshot({ path: '/tmp/shot_game.png' });
|
|
await analyze('/tmp/shot_game.png', 'GAME ');
|
|
|
|
await page.evaluate(() => window.__TS.G.timeScale = 3);
|
|
await new Promise(r => setTimeout(r, 6000));
|
|
await page.screenshot({ path: '/tmp/shot_busy.png' });
|
|
await analyze('/tmp/shot_busy.png', 'BUSY ');
|
|
|
|
const gameInfo = await page.evaluate(() => ({
|
|
day: window.__TS.G.shop.day,
|
|
clock: Math.round(window.__TS.G.shop.minutes),
|
|
customers: window.__TS.G.customers.length,
|
|
gold: Math.round(window.__TS.G.gold),
|
|
phase: window.__TS.G.shop.phase,
|
|
isOpen: window.__TS.G.shop.isOpen,
|
|
}));
|
|
console.log('SIM:', JSON.stringify(gameInfo));
|
|
} catch (e) {
|
|
console.log('FLOW ERROR:', e.message);
|
|
}
|
|
|
|
console.log('ERRORS(' + errors.length + '):');
|
|
errors.slice(0, 12).forEach(e => console.log(' ' + e.slice(0, 300)));
|
|
await browser.close();
|