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)
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user