Compare commits
6 Commits
85fc7c16bf
...
a7ebb67413
| Author | SHA1 | Date |
|---|---|---|
|
|
a7ebb67413 | |
|
|
25f5e05a96 | |
|
|
4f2d29942d | |
|
|
0740c12ffa | |
|
|
19dff633ce | |
|
|
8dee798ae2 |
|
|
@ -0,0 +1,83 @@
|
|||
<!doctype html>
|
||||
<html><head><meta charset="utf-8">
|
||||
<script type="importmap">{"imports":{"phaser":"/phaser.esm.js"}}</script>
|
||||
<style>html,body{margin:0;background:#111}canvas{display:block}</style>
|
||||
</head><body>
|
||||
<div id="log"></div>
|
||||
<script type="module">
|
||||
import * as Phaser from 'phaser';
|
||||
import JigsawGame from './src/games/jigsaw/JigsawGame.js';
|
||||
|
||||
const log = (m) => { document.getElementById('log').textContent += m + '\n'; console.log('[t]', m); };
|
||||
let failures = 0;
|
||||
const check = (ok, msg) => { if (!ok) { failures++; log('FAIL: ' + msg); } else log('ok: ' + msg); };
|
||||
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const ART = [
|
||||
{ name: 'Alien World', path: 'assets/images/shift/alien-world.png' },
|
||||
{ name: 'Aquaroom', path: 'assets/images/shift/aquaroom.png' },
|
||||
{ name: 'Aztec Warrior', path: 'assets/images/shift/aztec-warrior.png' },
|
||||
{ name: 'Cat On Tiger', path: 'assets/images/shift/cat-on-tiger.png' },
|
||||
{ name: 'Cockpit', path: 'assets/images/shift/cockpit.png' },
|
||||
];
|
||||
|
||||
const config = {
|
||||
type: Phaser.AUTO,
|
||||
width: 1920, height: 1080,
|
||||
parent: document.body,
|
||||
backgroundColor: '#000',
|
||||
scene: [ { key: 'boot', create() {
|
||||
this.cache.json.add('shift-artwork', { artwork: ART });
|
||||
this.cache.json.add('music', { tracks: [] });
|
||||
this.scene.start('jigsaw-game');
|
||||
} }, JigsawGame ],
|
||||
};
|
||||
const game = new Phaser.Game(config);
|
||||
const s = () => game.scene.getScene('jigsaw-game');
|
||||
|
||||
(async () => {
|
||||
for (let i = 0; i < 400 && !s(); i++) await wait(50); // boot → jigsaw start is async
|
||||
if (!s()) throw new Error('jigsaw scene never started');
|
||||
for (let i = 0; i < 400 && !s().menu; i++) await wait(50);
|
||||
check(!!s().menu, 'menu built');
|
||||
|
||||
// Menu is back to the original shape (random-start button reverted).
|
||||
check(s().randomStartButton === undefined, 'randomStartButton is gone (menu as before)');
|
||||
|
||||
// The initial image is random but always a valid artwork entry, and the
|
||||
// preview shown in the menu matches it.
|
||||
const n = s().artwork.length;
|
||||
const i = s().imageIndex;
|
||||
check(Number.isInteger(i) && i >= 0 && i < n, `initial imageIndex ${i} is a valid index (0..${n - 1})`);
|
||||
check(!!ART.find((a) => a.name === s().currentImage().name), 'initial currentImage() is a known artwork entry');
|
||||
|
||||
// Preview must be built for the initial image (async image load).
|
||||
for (let k = 0; k < 200 && !s().previewImg; k++) await wait(25);
|
||||
check(!!s().previewImg, 'initial preview image is displayed');
|
||||
check(s().thumbName && s().thumbName.text === s().currentImage().name, `thumb name matches initial image (${s().currentImage().name})`);
|
||||
|
||||
// Start Puzzle must still start the initial (random) image.
|
||||
s().selectedDiff = 'easy';
|
||||
s().startPuzzle();
|
||||
let playing = false;
|
||||
for (let k = 0; k < 400 && !playing; k++) { playing = s().state === 'playing'; await wait(25); }
|
||||
check(playing, 'Start Puzzle reaches playing state');
|
||||
check(s().pieces.length === 25, '25 pieces built');
|
||||
check(s().imageName === ART[i].name, `playing image matches the initial pick (${ART[i].name})`);
|
||||
|
||||
// The original 🎲 Random button still randomises the preview in the menu.
|
||||
s().toMenu();
|
||||
const before = s().imageIndex;
|
||||
const seen = new Set([before]);
|
||||
for (let k = 0; k < 10; k++) { s().randomImage(); seen.add(s().imageIndex); await wait(10); }
|
||||
check(seen.size >= 2, '🎲 Random button still changes the selected image');
|
||||
|
||||
document.__initIndex = i;
|
||||
log(failures === 0 ? 'ALL PASS' : failures + ' FAILURES');
|
||||
document.title = failures === 0 ? 'PASS' : 'FAIL:' + failures;
|
||||
})().catch((e) => {
|
||||
log('ERROR: ' + ((e && e.stack) || e));
|
||||
document.title = 'FAIL:error';
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
<!doctype html>
|
||||
<html><head><meta charset="utf-8">
|
||||
<script type="importmap">{"imports":{"phaser":"/phaser.esm.js"}}</script>
|
||||
<style>html,body{margin:0;background:#111}canvas{display:block}</style>
|
||||
</head><body>
|
||||
<div id="log"></div>
|
||||
<script type="module">
|
||||
import * as Phaser from 'phaser';
|
||||
import JigsawGame from './src/games/jigsaw/JigsawGame.js';
|
||||
|
||||
const log = (m) => { const el = document.getElementById('log'); el.textContent += m + '\n'; console.log('[harness]', m); };
|
||||
|
||||
const config = {
|
||||
type: Phaser.WEBGL,
|
||||
width: 1920, height: 1080,
|
||||
parent: document.body,
|
||||
backgroundColor: '#000',
|
||||
scene: [ { key:'boot', create(){
|
||||
// Provide the artwork cache so loadArtwork() has a real list.
|
||||
this.cache.json.add('shift-artwork', { artwork: [ { name:'Alien World', path:'assets/images/shift/alien-world.png' } ] });
|
||||
this.cache.json.add('music', { tracks: [] });
|
||||
this.scene.start('jigsaw-game');
|
||||
} }, JigsawGame ],
|
||||
};
|
||||
const game = new Phaser.Game(config);
|
||||
window.__game = game;
|
||||
|
||||
// Helpers exposed for the driver.
|
||||
window.__log = log;
|
||||
window.__scene = () => game.scene.getScene('jigsaw-game');
|
||||
window.__worldToScreen = (wx, wy) => {
|
||||
const cam = window.__scene().cameras.main;
|
||||
return { x: (wx - cam.scrollX) * cam.zoom + cam.x, y: (wy - cam.scrollY) * cam.zoom + cam.y };
|
||||
};
|
||||
window.__pieceAt = (i) => { const s = window.__scene(); const p = s.pieces[i]; return { i, x:p.img.x, y:p.img.y, placed:p.placed, groupLeader: p.group===p, depth:p.img.depth, hasInput: !!(p.img.input&&p.img.input.enabled) }; };
|
||||
window.__pieces = () => window.__scene().pieces.map((p,i)=>({i, x:Math.round(p.img.x), y:Math.round(p.img.y), placed:p.placed, depth:p.img.depth}));
|
||||
window.__startPuzzle = () => { const s = window.__scene(); s.selectedDiff='easy'; s.startPuzzle(); };
|
||||
window.__grabAndDrop = async (pieceIdx, toWorld, steps=12, holdMs=40) => {
|
||||
const s = window.__scene();
|
||||
const p = s.pieces[pieceIdx];
|
||||
const from = { x: p.img.x, y: p.img.y };
|
||||
// mousedown at from
|
||||
const f2s = window.__worldToScreen(from.x, from.y);
|
||||
await __mouseDown(f2s.x, f2s.y);
|
||||
await new Promise(r=>setTimeout(r,holdMs));
|
||||
for (let k=1;k<=steps;k++){
|
||||
const x = from.x + (toWorld.x-from.x)*k/steps, y = from.y + (toWorld.y-from.y)*k/steps;
|
||||
const c = window.__worldToScreen(x,y);
|
||||
await __mouseMove(c.x,c.y);
|
||||
await new Promise(r=>setTimeout(r,8));
|
||||
}
|
||||
await __mouseUp();
|
||||
return { from, to:{x:p.img.x,y:p.img.y}, moved: Math.hypot(p.img.x-from.x,p.img.y-from.y) };
|
||||
};
|
||||
// Low-level mouse dispatch in canvas (client) coordinates.
|
||||
window.__mouseDown = (cx,cy) => { const el=game.canvas; el.dispatchEvent(new MouseEvent('mousedown',{clientX:cx,clientY:cy,bubbles:true,button:0})); };
|
||||
window.__mouseMove = (cx,cy) => { const el=game.canvas; el.dispatchEvent(new MouseEvent('mousemove',{clientX:cx,clientY:cy,bubbles:true})); };
|
||||
window.__mouseUp = (cx=0,cy=0) => { const el=game.canvas; el.dispatchEvent(new MouseEvent('mouseup',{clientX:cx,clientY:cy,bubbles:true,button:0})); };
|
||||
|
||||
log('harness ready');
|
||||
</script>
|
||||
</body></html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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();
|
||||
|
|
@ -503,11 +507,14 @@ export default class JigsawGame extends Phaser.Scene {
|
|||
// piece's hit area. (With the old ±cell box, ~35 neighbour pairs overlapped
|
||||
// and the click resolved to whichever had the highest index — the bottom-
|
||||
// right piece — far away from the cursor.)
|
||||
// Pieces stay interactive only for the hover cursor. The actual grab is
|
||||
// resolved centrally in onTableDown (nearest piece to the pointer) so a
|
||||
// click can never resolve to a distant neighbour's overlapping hit box.
|
||||
// Pieces stay interactive for the hover cursor AND to consume the
|
||||
// pointerdown (forwarding it to the central grab resolver). Without the
|
||||
// pointerdown handler here, a click that lands inside the piece's custom
|
||||
// hit area is consumed by the piece (topOnly=true) and never reaches the
|
||||
// background's onTableDown — leaving the piece un-grabbable.
|
||||
const grab = this.cell * GRAB_FRAC;
|
||||
img.setInteractive({ useHandCursor: true, hitArea: new Phaser.Geom.Rectangle(-grab, -grab, grab * 2, grab * 2), hitAreaCallback: Phaser.Geom.Rectangle.Contains });
|
||||
img.setInteractive({ useHandCursor: true, hitArea: new Phaser.Geom.Rectangle(img.displayOriginX - grab, img.displayOriginY - grab, grab * 2, grab * 2), hitAreaCallback: Phaser.Geom.Rectangle.Contains });
|
||||
img.on('pointerdown', (pointer) => this.onTableDown(pointer));
|
||||
this.pieces.push({ r, c, img, home, placed: false, key, depth: 10 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
// Headless-chromium CDP driver for the jigsaw initial-image test page.
|
||||
// Loads the page repeatedly; each load must end PASS, and the initial image
|
||||
// index (document.__initIndex) must vary across fresh loads.
|
||||
// Usage: node tools/__jig_init_driver.mjs <url> [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(2, parseInt(process.argv[3] || '8', 10));
|
||||
if (!url) { console.error('usage: driver <url> [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 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 {
|
||||
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 ev = (expression) => send('Runtime.evaluate', { expression, returnByValue: true }).then((r) => r.result.value);
|
||||
|
||||
const indexes = [];
|
||||
for (let k = 0; k < loads; k++) {
|
||||
await send('Page.navigate', { url });
|
||||
const t0 = Date.now();
|
||||
let title = '';
|
||||
while (Date.now() - t0 < 60000) {
|
||||
title = (await ev('document.title')) || '';
|
||||
if (title === 'PASS' || title.startsWith('FAIL')) break;
|
||||
await sleep(500);
|
||||
}
|
||||
if (title !== 'PASS') throw new Error(`load #${k}: ${title || 'TIMEOUT'}`);
|
||||
indexes.push(await ev('document.__initIndex'));
|
||||
}
|
||||
|
||||
const valid = indexes.every((i) => Number.isInteger(i) && i >= 0 && i < 5);
|
||||
const distinct = new Set(indexes).size;
|
||||
console.log('initial indexes across fresh loads:', indexes.join(', '));
|
||||
console.log(`all valid: ${valid}, distinct images: ${distinct}/${loads}`);
|
||||
if (!valid || distinct < 2) { console.log('FAIL'); process.exitCode = 1; }
|
||||
else console.log('ALL PASS');
|
||||
} finally {
|
||||
try { ws && ws.close(); } catch {}
|
||||
chrome.kill('SIGKILL');
|
||||
}
|
||||
Loading…
Reference in New Issue