From 25f5e05a966df59dc495c1442025d82d1f26fe83 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Sun, 30 Aug 2026 21:14:37 -0600 Subject: [PATCH] Start the jigsaw puzzle picker on a random image The menu always opened on the first artwork; now imageIndex is randomised right after loadArtwork(), so each visit to the picker shows a different initial puzzle. The existing Random/prev/next buttons are unchanged (previous random-start button reverted in 4f2d299). Test page + CDP driver verify 8 fresh page loads all land on a valid in-range image with its preview loaded, with variety across loads. --- __jig_init_test.html | 60 +++++++++++++++++++++++ src/games/jigsaw/JigsawGame.js | 4 ++ tools/__jig_init_driver.mjs | 88 ++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 __jig_init_test.html create mode 100644 tools/__jig_init_driver.mjs diff --git a/__jig_init_test.html b/__jig_init_test.html new file mode 100644 index 0000000..7db8b7d --- /dev/null +++ b/__jig_init_test.html @@ -0,0 +1,60 @@ + + + + + +
+ + diff --git a/src/games/jigsaw/JigsawGame.js b/src/games/jigsaw/JigsawGame.js index 2fd4e80..7fccb81 100644 --- a/src/games/jigsaw/JigsawGame.js +++ b/src/games/jigsaw/JigsawGame.js @@ -77,6 +77,10 @@ export default class JigsawGame extends Phaser.Scene { this.selectedDiff = 'easy'; this.loadArtwork(); + // Open the menu on a random picture, not always the first one, so each + // visit to the puzzle picker starts on a different puzzle. (loadArtwork + // guarantees at least one entry, so this index is always valid.) + this.imageIndex = Math.floor(Math.random() * this.artwork.length); this.buildBackground(); this.buildHUD(); this.buildMenu(); diff --git a/tools/__jig_init_driver.mjs b/tools/__jig_init_driver.mjs new file mode 100644 index 0000000..55a5ec2 --- /dev/null +++ b/tools/__jig_init_driver.mjs @@ -0,0 +1,88 @@ +// Headless-chromium CDP driver: loads a page repeatedly and reads the result +// the page publishes in document.title (RESULT: or FAIL:). +// Usage: node tools/__jig_init_driver.mjs [loads=8] +import { spawn } from 'node:child_process'; +import { setTimeout as sleep } from 'node:timers/promises'; + +const url = process.argv[2]; +const loads = Math.max(1, parseInt(process.argv[3] || '8', 10)); +if (!url) { console.error('usage: driver [loads]'); process.exit(2); } + +const BIN = process.env.HOME + '/.cache/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-linux64/chrome-headless-shell'; +const PORT = 9333; +const chrome = spawn(BIN, [ + '--headless', '--no-sandbox', '--disable-gpu', + `--remote-debugging-port=${PORT}`, + 'about:blank', +], { stdio: ['ignore', 'pipe', 'pipe'] }); +chrome.stderr.on('data', (d) => { const t = d.toString(); if (!/Fontconfig|dbus|DBus|ozone|sandbox|WebGL|GL Driver/i.test(t)) process.stderr.write(t); }); + +async function httpJson(path) { + const r = await fetch(`http://127.0.0.1:${PORT}${path}`); + return r.json(); +} + +let ws, idc = 0; +const pending = new Map(); +const send = (method, params = {}) => new Promise((resolve, reject) => { + const id = ++idc; + pending.set(id, { resolve, reject }); + ws.send(JSON.stringify({ id, method, params })); +}); + +try { + let targets = null; + for (let i = 0; i < 60; i++) { + try { targets = await httpJson('/json/list'); break; } catch { await sleep(250); } + } + if (!targets) throw new Error('chrome devtools endpoint never came up'); + const page = targets.find((t) => t.type === 'page'); + if (!page) throw new Error('no page target'); + ws = new WebSocket(page.webSocketDebuggerUrl); + await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; }); + ws.onmessage = (m) => { + const msg = JSON.parse(m.data); + if (msg.id && pending.has(msg.id)) { + const { resolve, reject } = pending.get(msg.id); + pending.delete(msg.id); + if (msg.error) reject(new Error(msg.error.message)); else resolve(msg.result); + } + }; + + await send('Runtime.enable'); + await send('Page.enable'); + + const results = []; + for (let k = 0; k < loads; k++) { + await send('Page.navigate', { url }); + const t0 = Date.now(); + let title = ''; + while (Date.now() - t0 < 30000) { + const out = await send('Runtime.evaluate', { expression: 'document.title', returnByValue: true }); + title = out.result.value || ''; + if (title.startsWith('RESULT:') || title.startsWith('FAIL')) break; + await sleep(250); + } + results.push(title); + } + + let ok = true; + const idxs = []; + for (const t of results) { + if (!t.startsWith('RESULT:')) { ok = false; console.error('bad title: ' + t); continue; } + const r = JSON.parse(t.slice('RESULT:'.length)); + const valid = Number.isInteger(r.idx) && r.idx >= 0 && r.idx < r.n && r.n > 1 && + r.validName && r.preview && r.menuVisible; + if (!valid) { ok = false; console.error('bad result: ' + JSON.stringify(r)); } + idxs.push(r.idx); + } + const distinct = new Set(idxs).size; + console.log(`initial indices over ${loads} fresh page loads: ${idxs.join(', ')}`); + console.log(`distinct initial images: ${distinct}/${loads}`); + if (distinct < 2) { ok = false; console.error('expected variety in the initial image'); } + console.log(ok ? 'ALL PASS' : 'FAIL'); + process.exitCode = ok ? 0 : 1; +} finally { + try { ws && ws.close(); } catch {} + chrome.kill('SIGKILL'); +}