diff --git a/__jig_random_test.html b/__jig_random_test.html new file mode 100644 index 0000000..c3699eb --- /dev/null +++ b/__jig_random_test.html @@ -0,0 +1,104 @@ + + + + + +
+ + diff --git a/src/games/jigsaw/JigsawGame.js b/src/games/jigsaw/JigsawGame.js index 2fd4e80..9b18385 100644 --- a/src/games/jigsaw/JigsawGame.js +++ b/src/games/jigsaw/JigsawGame.js @@ -153,6 +153,18 @@ export default class JigsawGame extends Phaser.Scene { this.loadPreview(this.currentImage()); } + // One-click "random puzzle": pick a random image (never the current one when + // there's a choice) and start the game with it. The chosen difficulty stays. + startRandomPuzzle() { + if (this.artwork.length > 1) { + let i = this.imageIndex; + while (i === this.imageIndex) i = Math.floor(Math.random() * this.artwork.length); + this.imageIndex = i; + this.loadPreview(this.currentImage()); + } + this.startPuzzle(); + } + loadPreview(item) { if (this.previewImg) { this.previewImg.destroy(); this.previewImg = null; } if (this.thumbBorder) { this.thumbBorder.destroy(); this.thumbBorder = null; } @@ -344,7 +356,7 @@ export default class JigsawGame extends Phaser.Scene { buildMenu() { this.menu = this.add.container(0, 0).setDepth(8000); - const W = 1160, H = 820, cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2 + 12; + const W = 1160, H = 890, cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2 + 12; const panel = this.add.rectangle(cx, cy, W, H, 0x17130c, 0.96); const frame = this.add.graphics(); frame.lineStyle(3, COLORS.accent, 0.85).strokeRoundedRect(cx - W / 2, cy - H / 2, W, H, 20); @@ -383,6 +395,8 @@ export default class JigsawGame extends Phaser.Scene { // Start const start = this.mkButton(this.menu, 'Start Puzzle ▸', cx, cy + 315, 320, 74, () => this.startPuzzle(), { fontSize: 30, bg: COLORS.gold }); this.startButton = start; + // One-click random: pick a random picture and start immediately. + this.randomStartButton = this.mkButton(this.menu, '🎲 Start Random', cx, cy + 404, 300, 58, () => this.startRandomPuzzle(), { fontSize: 24 }); // Load the first preview this.selectDifficulty('easy'); diff --git a/tools/__jig_random_driver.mjs b/tools/__jig_random_driver.mjs new file mode 100644 index 0000000..a994f53 --- /dev/null +++ b/tools/__jig_random_driver.mjs @@ -0,0 +1,72 @@ +// Headless-chromium CDP driver for the jigsaw random-start test page. +// Usage: node tools/__jig_random_driver.mjs +import { spawn } from 'node:child_process'; +import { setTimeout as sleep } from 'node:timers/promises'; + +const url = process.argv[2]; +if (!url) { console.error('usage: driver '); 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 s = d.toString(); if (!/Fontconfig|dbus|DBus|ozone/i.test(s)) process.stderr.write(s); }); + +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 { + // Wait for the debug endpoint. + 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'); + await send('Page.navigate', { url }); + + const t0 = Date.now(); + let title = ''; + while (Date.now() - t0 < 240000) { + const { result: { value } } = await send('Runtime.evaluate', { expression: 'document.title', returnByValue: true }); + title = value || ''; + if (title === 'PASS' || title.startsWith('FAIL')) break; + await sleep(500); + } + + const { result: { value: logText } } = await send('Runtime.evaluate', { expression: 'document.getElementById("log").textContent', returnByValue: true }); + console.log('===== page log ====='); + console.log(logText.trimEnd()); + console.log('===== result: ' + (title || 'TIMEOUT') + ' ====='); + process.exitCode = title === 'PASS' ? 0 : 1; +} finally { + try { ws && ws.close(); } catch {} + chrome.kill('SIGKILL'); +}