La prova nel browser in tre processi paralleli (principale, minigiochi, telefoni), ognuno col suo server e il suo Chromium: da 525 a circa 275 secondi, tre giri su tre verdi; --parte= per una sola. Le attese della torcia hanno una scadenza di 30 s invece di 200 giri, che con la macchina carica non bastavano (Andrea)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
2026-11-01 12:35:40 +01:00
co-authored by Claude Opus 5.5
parent cfe7cb7fd6
commit 24e9b8c055
4 changed files with 279 additions and 254 deletions
+275 -250
View File
@@ -1,6 +1,8 @@
// Prova nel browser vero (Chromium headless con swiftshader): titolo, partita giocata con clic veri
// fino alla fine, salvataggio e impaginazione su telefono. Schermate in $OUT (predefinito: /tmp/regolo-shots).
// npm run browser
// npm run browser: le tre parti (--parte=principale, minigiochi, telefoni) girano insieme come processi separati, ognuna
// col suo server e il suo Chromium; la prova passa quasi tutto il tempo ad aspettare animazioni in tempo reale, e così le
// attese si sovrappongono. Una parte sola: node tests/browser.mjs --parte=minigiochi (--media-only: solo multimedia).
import { chromium } from 'playwright';
import { staticServer } from './helpers.mjs';
import { mkdirSync, readFileSync } from 'node:fs';
@@ -12,6 +14,22 @@ import { checkMultimedia } from './multimedia.browser.mjs';
const OUT = process.env.OUT ?? '/tmp/regolo-shots';
mkdirSync(OUT, { recursive: true });
const PARTI = ['principale', 'minigiochi', 'telefoni'];
const PARTE = process.argv.find(a => a.startsWith('--parte='))?.slice('--parte='.length);
if (PARTE && !PARTI.includes(PARTE)) { console.error(`parte sconosciuta: ${PARTE} (${PARTI.join(', ')})`); process.exit(2); }
if (!PARTE && !process.argv.includes('--media-only')) {
const { spawn } = await import('node:child_process');
const t0 = Date.now();
const esiti = await Promise.all(PARTI.map(parte => new Promise(resolve => {
const p = spawn(process.execPath, [process.argv[1], `--parte=${parte}`], { stdio: ['ignore', 'pipe', 'pipe'] });
let resto = '';
const riga = chunk => { const righe = (resto + chunk).split('\n'); resto = righe.pop(); for (const r of righe) console.log(`[${parte}] ${r}`); };
p.stdout.on('data', riga); p.stderr.on('data', riga);
p.on('close', code => { if (resto) console.log(`[${parte}] ${resto}`); resolve({ parte, code, s: Math.round((Date.now() - t0) / 1000) }); });
})));
for (const e of esiti) console.log(`${e.code === 0 ? 'OK ' : 'FAIL'} ${e.parte} (${e.s} s)`);
process.exit(esiti.every(e => e.code === 0) ? 0 : 1);
}
const server = await staticServer(), PORT = server.port;
const browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
const errors = [];
@@ -49,9 +67,261 @@ const hotspotCenter = async (page, id) => {
return c;
};
// Le parti che aprono una pagina propria (col suo salvataggio), ognuna nel suo processo (vedi in cima).
async function minigiochi() {
// Il libro dell'oratorio: preso, il primo piano; si sfoglia coi clic veri, «FINE» senza che nessuno lo dica,
// Esc chiude; dall'inventario «Apri» lo riapre e il pulsante Chiudi lo chiude. Intanto la scena dietro non risponde.
{
const book = await open({ width: 1280, height: 860 });
await book.click('#splash');
await book.click('#new-game');
await book.click('#rione [data-rione="campomigliaio"]');
await book.click('#skip-intro');
await settle(book);
await book.evaluate(() => game.go('oratorio', [540, 300]));
await settle(book);
await book.click('#verbs [data-verb="take"]');
await clickScene(book, await hotspotCenter(book, 'libro_dimenticato'));
await book.waitForFunction(() => game.closeup?.step === 'cover', null, { timeout: 15000 });
await book.waitForTimeout(600);
await book.screenshot({ path: `${OUT}/03a-libro-copertina.png` });
const where = await book.evaluate(() => [game.state.x, game.state.y]);
await clickScene(book, [320, 200]);
assert.equal(await book.evaluate(() => game.closeup.step), 'title', 'un clic apre il libro');
await clickScene(book, [320, 200]);
assert.equal(await book.evaluate(() => game.closeup.step), 'turn', 'un altro gira la pagina');
await book.waitForFunction(() => game.closeup?.step === 'end');
assert.equal(await book.evaluate(() => game.speech), null, '«FINE» non lo dice nessuno');
// Prima la battuta, poi la schermata: con swiftshader una schermata può durare più della battuta.
await book.waitForFunction(() => game.speech?.text === 'Mi ricordavo un finale diverso.', null, { timeout: 5000 });
await book.screenshot({ path: `${OUT}/03b-libro-fine.png` });
assert.deepEqual(await book.evaluate(() => [game.state.x, game.state.y]), where, 'i clic sul libro non muovono Andrea');
await book.keyboard.press('Space');
await book.keyboard.press('Escape');
assert.equal(await book.evaluate(() => game.closeup), null, 'Esc chiude il libro');
await book.waitForSelector('#closeup-close', { state: 'hidden' });
await settle(book);
await book.click('#verbs [data-verb="open"]');
await book.click('#inventory button');
await book.waitForFunction(() => game.closeup?.step === 'cover');
assert.ok(await book.isVisible('#closeup-close'), 'si vede il pulsante Chiudi');
await book.click('#closeup-close');
assert.equal(await book.evaluate(() => game.closeup), null, 'Chiudi chiude il libro');
await book.close();
}
// Le bocce al pallaio: col mouse tenuto premuto la forza sale, lasciando la boccia parte; la scena dietro è ferma.
{
const bocce = await open({ width: 1280, height: 860 });
await bocce.click('#splash');
await bocce.click('#new-game');
await bocce.click('#rione [data-rione="campomigliaio"]');
await bocce.click('#skip-intro');
await settle(bocce);
await bocce.evaluate(() => game.go('tennis_chiesa', [430, 310]));
await settle(bocce);
await clickScene(bocce, await hotspotCenter(bocce, 'mariano'));
await bocce.waitForFunction(() => game.dialogue && !game.busy, null, { timeout: 20000 });
await bocce.locator('#choices li', { hasText: 'Facciamo una partita' }).click();
await bocce.waitForFunction(() => game.options().some(o => o.text === 'Seria.'), null, { timeout: 30000 });
await bocce.locator('#choices li', { hasText: 'Seria.' }).click();
await bocce.waitForFunction(() => game.closeup?.id === 'bocce' && game.closeup.step === 'aim' && !game.speech, null, { timeout: 30000 });
const r = await bocce.locator('#scene').boundingBox();
await bocce.mouse.move(r.x + r.width / 2, r.y + r.height / 2);
await bocce.mouse.down();
await bocce.waitForTimeout(500);
assert.ok(await bocce.evaluate(() => game.closeup.power > 0.2), 'tenendo premuto la forza sale');
await bocce.screenshot({ path: `${OUT}/03c-bocce-forza.png` });
await bocce.mouse.up();
assert.equal(await bocce.evaluate(() => game.closeup.balls.length), 1, 'lasciando, la boccia parte');
await bocce.waitForFunction(() => game.closeup.balls.length === 2, null, { timeout: 15000 });
// Il clic fissa la mira: andando verso la forza o «Lancia» il puntatore attraversa il campo e non la sposta.
await bocce.waitForFunction(() => game.closeup.step === 'aim' && game.closeup.current === 'player', null, { timeout: 30000 });
await bocce.mouse.click(r.x + r.width * 0.45, r.y + r.height * 0.6);
const fixed = await bocce.evaluate(() => JSON.stringify(game.closeup.aim));
await bocce.mouse.move(r.x + r.width * 0.5, r.y + r.height * 0.95, { steps: 8 });
assert.equal(await bocce.evaluate(() => JSON.stringify(game.closeup.aim)), fixed, 'la mira resta dove si è cliccato');
await bocce.keyboard.press('Escape');
await bocce.waitForFunction(() => game.speech?.text.startsWith('Già stanco?'), null, { timeout: 5000 });
await bocce.close();
}
// «Fai due tiri» al campo del Circolo: il mouse mira, tenendo premuto la forza sale, lasciando la palla vola; Esc esce.
{
const k = await open({ width: 1280, height: 860 });
await k.click('#splash');
await k.click('#new-game');
await k.click('#rione [data-rione="campomigliaio"]');
await k.click('#skip-intro');
await settle(k);
await k.evaluate(() => game.go('via_cipriani', [340, 316]));
await settle(k);
await k.evaluate(() => game.act('use', 'campo_basket'));
await k.waitForFunction(() => game.dialogue && !game.busy, null, { timeout: 20000 });
await k.locator('#choices li', { hasText: 'Due tiri seri.' }).click();
await k.waitForFunction(() => game.closeup?.id === 'basket' && game.closeup.step === 'aim' && !game.speech, null, { timeout: 30000 });
const r = await k.locator('#scene').boundingBox();
const at = (x, y) => [r.x + x / 640 * r.width, r.y + y / 360 * r.height];
await k.mouse.move(...at(560, 60));
const high = await k.evaluate(() => game.closeup.angle);
await k.mouse.move(...at(600, 200));
assert.ok(await k.evaluate(() => game.closeup.angle) < high, 'la mira segue il mouse');
await k.mouse.down();
await k.waitForTimeout(500);
assert.ok(await k.evaluate(() => game.closeup.power > 0.2 && game.closeup.holding), 'tenendo premuto la forza sale');
await k.screenshot({ path: `${OUT}/03e-basket-carica.png` });
await k.mouse.up();
assert.equal(await k.evaluate(() => game.closeup.step), 'flight', 'lasciando, la palla parte');
await k.waitForTimeout(350);
await k.screenshot({ path: `${OUT}/03f-basket-volo.png` });
await k.waitForFunction(() => game.closeup.step === 'aim' && game.closeup.thrown === 1, null, { timeout: 20000 });
await k.keyboard.press('Escape');
await k.waitForFunction(() => game.speech?.text.startsWith('Lascio il pallone'), null, { timeout: 5000 });
await k.close();
}
// La pesca al lago dell'Anatre: coi ragazzi in Fortezza «S'ha andà a fallo?!?», poi il lancio tenendo premuto e l'attesa.
{
const k = await open({ width: 1280, height: 860 });
await k.click('#splash');
await k.click('#new-game');
await k.click('#rione [data-rione="campomigliaio"]');
await k.click('#skip-intro');
await settle(k);
await k.evaluate(() => game.go('fortezza', [240, 334]));
await settle(k);
await clickScene(k, await hotspotCenter(k, 'pescatori'));
await k.waitForFunction(() => game.dialogue && !game.busy, null, { timeout: 40000 });
await k.locator('#choices li', { hasText: 'S’ha andà a fallo?!?' }).or(k.locator('#choices li', { hasText: "S'ha andà a fallo?!?" })).first().click();
await k.waitForFunction(() => game.options().some(o => o.text === 'Seria.'), null, { timeout: 30000 });
await k.locator('#choices li', { hasText: 'Seria.' }).click();
await k.waitForFunction(() => game.closeup?.id === 'pesca' && game.closeup.step === 'aim' && !game.speech, null, { timeout: 30000 });
const r = await k.locator('#scene').boundingBox();
await k.mouse.move(r.x + r.width * 0.7, r.y + r.height * 0.7);
assert.ok(await k.evaluate(() => game.closeup.power > 0.5), 'toccando l\'acqua al largo la forza sale');
await k.mouse.down();
await k.waitForTimeout(500);
assert.ok(await k.evaluate(() => game.closeup.holding && game.closeup.power > 0.2), 'tenendo premuto si carica il lancio');
await k.mouse.up();
await k.waitForFunction(() => game.closeup.step === 'wait', null, { timeout: 5000 });
await k.waitForTimeout(600);
await k.screenshot({ path: `${OUT}/03g-pesca-attesa.png` });
assert.equal(await k.textContent('#bocce-throw'), 'Ferra');
await k.keyboard.press('Escape');
await k.waitForFunction(() => game.speech?.text.startsWith('Lascio la canna'), null, { timeout: 5000 });
await k.close();
}
// Il biliardo del Circolo 4 Maggio: la sala, poi la partita; un tocco sul tavolo mira, tenendo premuto la forza sale.
{
const b = await open({ width: 1280, height: 860 });
await b.click('#splash');
await b.click('#new-game');
await b.click('#rione [data-rione="campomigliaio"]');
await b.click('#skip-intro');
await settle(b);
await b.evaluate(() => game.go('circolo_4_maggio'));
await settle(b);
await b.waitForTimeout(1500); // la dissolvenza del cambio scena
await b.screenshot({ path: `${OUT}/03d-circolo.png` });
await clickScene(b, await hotspotCenter(b, 'ing_tozzi'));
await b.waitForFunction(() => game.dialogue && !game.busy, null, { timeout: 20000 });
await b.locator('#choices li', { hasText: 'una partita ai birilli' }).click();
await b.waitForFunction(() => game.options().some(o => o.text === 'Seria.'), null, { timeout: 30000 });
await b.locator('#choices li', { hasText: 'Seria.' }).click();
await b.waitForFunction(() => game.closeup?.id === 'biliardo' && game.closeup.step === 'aim' && !game.speech, null, { timeout: 30000 });
const r = await b.locator('#scene').boundingBox();
const [tx, ty] = await b.evaluate(async () => { const { VIEW } = await import('/src/engine/biliardo.js'); const V = game.closeup.balls.cpu; return [VIEW.x + V.x * VIEW.k, VIEW.y + V.y * VIEW.k]; });
await b.mouse.move(r.x + tx / 640 * r.width, r.y + ty / 360 * r.height);
await b.mouse.down();
await b.waitForTimeout(500);
assert.ok(await b.evaluate(() => game.closeup.power > 0.2), 'tenendo premuto la forza sale');
await b.screenshot({ path: `${OUT}/03e-biliardo-forza.png` });
await b.mouse.up();
assert.equal(await b.evaluate(() => game.closeup.step), 'rolling', 'lasciando, la biglia parte');
await b.waitForFunction(() => game.closeup.step === 'aim' && game.closeup.current === 'cpu' || game.closeup.step === 'rolling' && game.closeup.shot?.shooter === 'cpu', null, { timeout: 20000 });
await b.screenshot({ path: `${OUT}/03f-biliardo-socio.png` });
await b.keyboard.press('Escape');
await b.waitForFunction(() => game.speech?.text.startsWith('Lasci a metà?'), null, { timeout: 5000 });
await b.close();
}
}
async function telefoni() {
const phone = await open({ width: 390, height: 844 });
await phone.click('#splash'); // un clic salta il cartello
assert.ok(await phone.isVisible('#title'), 'il clic sul cartello porta al titolo');
await phone.click('#new-game');
await phone.click('#rione [data-rione="campomigliaio"]');
await phone.click('#skip-intro');
await settle(phone);
const overflow = await phone.evaluate(() => document.documentElement.scrollWidth > innerWidth);
// Ogni verbo si vede tutto: niente pulsanti tagliati dall'inventario.
assert.ok(await phone.evaluate(() => [...document.querySelectorAll('#verbs button')].every(b => b.scrollWidth <= b.clientWidth + 1 && b.getBoundingClientRect().right <= innerWidth)), 'verbi tagliati sul telefono');
assert.equal(overflow, false, 'scorrimento orizzontale su telefono');
await phone.screenshot({ path: `${OUT}/08-telefono.png` });
// Telefono stretto in orizzontale: niente deve allargare la pagina oltre lo schermo (il browser la rimpicciolirebbe
// e taglierebbe verbi e inventario a destra), e la pausa sta tutta dentro la scena.
const small = await browser.newPage({ viewport: { width: 740, height: 330 }, deviceScaleFactor: 2.4, hasTouch: true, isMobile: true });
small.on('pageerror', e => errors.push(e.message));
await small.goto(`http://127.0.0.1:${PORT}/`);
await small.waitForSelector('#splash', { state: 'hidden', timeout: 15000 });
await small.click('#new-game');
await small.click('#rione [data-rione="campomigliaio"]');
await small.click('#skip-intro');
await settle(small);
assert.equal(await small.evaluate(() => innerWidth), 740, 'la pagina è più larga dello schermo del telefono');
await small.click('#pause-button');
assert.ok(await small.evaluate(() => document.getElementById('to-title').getBoundingClientRect().bottom <= document.getElementById('stage').getBoundingClientRect().bottom), 'la pausa esce dalla scena');
await small.screenshot({ path: `${OUT}/08b-telefono-stretto-pausa.png` });
// Telefono in verticale: la Pausa prende tutto lo schermo, e versione e spiegazione non finiscono sopra i verbi.
{
const tall = await browser.newPage({ viewport: { width: 412, height: 915 }, deviceScaleFactor: 2, hasTouch: true, isMobile: true });
await tall.goto(`http://127.0.0.1:${PORT}/`);
await tall.waitForSelector('#splash', { state: 'hidden', timeout: 15000 });
await tall.click('#new-game');
await tall.click('#rione [data-rione="campomigliaio"]');
await tall.click('#skip-intro');
await settle(tall);
await tall.click('#pause-button');
const fit = await tall.evaluate(() => {
const p = document.getElementById('pause'), v = p.querySelector('.version').getBoundingClientRect();
return { inside: p.scrollHeight <= p.clientHeight + 1, top: document.elementFromPoint(v.x + v.width / 2, v.y + v.height / 2)?.closest('#pause') !== null };
});
assert.deepEqual(fit, { inside: true, top: true }, 'in verticale la pausa esce dallo schermo o finisce sotto i verbi');
await tall.close();
}
const landscape = await open({ width: 844, height: 390 });
await landscape.click('#new-game');
await landscape.click('#rione [data-rione="campomigliaio"]');
await landscape.click('#skip-intro');
await settle(landscape);
assert.equal(await landscape.evaluate(() => document.documentElement.scrollWidth > innerWidth), false);
assert.ok(await landscape.evaluate(() => [...document.querySelectorAll('#verbs button')].every(b => b.scrollWidth <= b.clientWidth + 1 && b.getBoundingClientRect().height >= 26)), 'in orizzontale i verbi si leggono interi e il dito li prende');
await landscape.screenshot({ path: `${OUT}/09-orizzontale.png`, fullPage: true });
// Debug (?debug): area calpestabile e hotspot disegnati; Maiusc+clic segna punti senza muovere Andrea, X li azzera.
const dbg = await open({ width: 1280, height: 860 }, '?debug');
await dbg.click('#new-game');
await dbg.click('#rione [data-rione="campomigliaio"]');
await dbg.click('#skip-intro');
await settle(dbg);
const start = await dbg.evaluate(() => [game.state.x, game.state.y]);
await dbg.keyboard.down('Shift');
await clickScene(dbg, [100, 300]);
await clickScene(dbg, [200, 320]);
await dbg.keyboard.up('Shift');
await dbg.mouse.move(700, 400);
await dbg.waitForTimeout(200);
assert.deepEqual(await dbg.evaluate(() => [game.state.x, game.state.y]), start, 'Maiusc+clic non muove Andrea');
await dbg.screenshot({ path: `${OUT}/10-debug.png` });
}
try {
await checkMultimedia(browser, PORT, OUT, errors);
if (!process.argv.includes('--media-only')) {
if (process.argv.includes('--media-only')) await checkMultimedia(browser, PORT, OUT, errors);
else if (PARTE === 'minigiochi') { await minigiochi(); console.log('Browser OK: libro, bocce, tiri, pesca, biliardo.'); }
else if (PARTE === 'telefoni') { await checkMultimedia(browser, PORT, OUT, errors); await telefoni(); console.log('Browser OK: telefoni e debug.'); }
else {
const page = await open({ width: 1280, height: 860 });
// Il cartello d'apertura, poi da solo il titolo.
assert.match(await page.textContent('#splash'), /Manzolo\s*\(Poca\)\s*Art\s*Presents/);
@@ -287,7 +557,7 @@ try {
await page.mouse.move(box.x + at[0] / 640 * box.width, box.y + at[1] / 360 * box.height);
await page.waitForFunction(() => game.closeup?.step === 'reveal');
await page.screenshot({ path: `${OUT}/05b-fessura-${i}.png` });
for (let n = 0; n < 200; n++) {
for (const fine = Date.now() + 30000; Date.now() < fine;) { // con la macchina carica la rivelazione dura di più
if (await page.evaluate(i => game.closeup.found > i, i)) break;
if (await page.evaluate(() => !!game.speech)) await page.keyboard.press('Space');
await page.waitForTimeout(30);
@@ -375,253 +645,8 @@ try {
await page.click('#close-map');
await page.setViewportSize({ width: 1280, height: 860 });
// Il libro dell'oratorio: preso, il primo piano; si sfoglia coi clic veri, «FINE» senza che nessuno lo dica,
// Esc chiude; dall'inventario «Apri» lo riapre e il pulsante Chiudi lo chiude. Intanto la scena dietro non risponde.
{
const book = await open({ width: 1280, height: 860 });
await book.click('#splash');
await book.click('#new-game');
await book.click('#rione [data-rione="campomigliaio"]');
await book.click('#skip-intro');
await settle(book);
await book.evaluate(() => game.go('oratorio', [540, 300]));
await settle(book);
await book.click('#verbs [data-verb="take"]');
await clickScene(book, await hotspotCenter(book, 'libro_dimenticato'));
await book.waitForFunction(() => game.closeup?.step === 'cover', null, { timeout: 15000 });
await book.waitForTimeout(600);
await book.screenshot({ path: `${OUT}/03a-libro-copertina.png` });
const where = await book.evaluate(() => [game.state.x, game.state.y]);
await clickScene(book, [320, 200]);
assert.equal(await book.evaluate(() => game.closeup.step), 'title', 'un clic apre il libro');
await clickScene(book, [320, 200]);
assert.equal(await book.evaluate(() => game.closeup.step), 'turn', 'un altro gira la pagina');
await book.waitForFunction(() => game.closeup?.step === 'end');
assert.equal(await book.evaluate(() => game.speech), null, '«FINE» non lo dice nessuno');
// Prima la battuta, poi la schermata: con swiftshader una schermata può durare più della battuta.
await book.waitForFunction(() => game.speech?.text === 'Mi ricordavo un finale diverso.', null, { timeout: 5000 });
await book.screenshot({ path: `${OUT}/03b-libro-fine.png` });
assert.deepEqual(await book.evaluate(() => [game.state.x, game.state.y]), where, 'i clic sul libro non muovono Andrea');
await book.keyboard.press('Space');
await book.keyboard.press('Escape');
assert.equal(await book.evaluate(() => game.closeup), null, 'Esc chiude il libro');
await book.waitForSelector('#closeup-close', { state: 'hidden' });
await settle(book);
await book.click('#verbs [data-verb="open"]');
await book.click('#inventory button');
await book.waitForFunction(() => game.closeup?.step === 'cover');
assert.ok(await book.isVisible('#closeup-close'), 'si vede il pulsante Chiudi');
await book.click('#closeup-close');
assert.equal(await book.evaluate(() => game.closeup), null, 'Chiudi chiude il libro');
await book.close();
}
// Le bocce al pallaio: col mouse tenuto premuto la forza sale, lasciando la boccia parte; la scena dietro è ferma.
{
const bocce = await open({ width: 1280, height: 860 });
await bocce.click('#splash');
await bocce.click('#new-game');
await bocce.click('#rione [data-rione="campomigliaio"]');
await bocce.click('#skip-intro');
await settle(bocce);
await bocce.evaluate(() => game.go('tennis_chiesa', [430, 310]));
await settle(bocce);
await clickScene(bocce, await hotspotCenter(bocce, 'mariano'));
await bocce.waitForFunction(() => game.dialogue && !game.busy, null, { timeout: 20000 });
await bocce.locator('#choices li', { hasText: 'Facciamo una partita' }).click();
await bocce.waitForFunction(() => game.options().some(o => o.text === 'Seria.'), null, { timeout: 30000 });
await bocce.locator('#choices li', { hasText: 'Seria.' }).click();
await bocce.waitForFunction(() => game.closeup?.id === 'bocce' && game.closeup.step === 'aim' && !game.speech, null, { timeout: 30000 });
const r = await bocce.locator('#scene').boundingBox();
await bocce.mouse.move(r.x + r.width / 2, r.y + r.height / 2);
await bocce.mouse.down();
await bocce.waitForTimeout(500);
assert.ok(await bocce.evaluate(() => game.closeup.power > 0.2), 'tenendo premuto la forza sale');
await bocce.screenshot({ path: `${OUT}/03c-bocce-forza.png` });
await bocce.mouse.up();
assert.equal(await bocce.evaluate(() => game.closeup.balls.length), 1, 'lasciando, la boccia parte');
await bocce.waitForFunction(() => game.closeup.balls.length === 2, null, { timeout: 15000 });
// Il clic fissa la mira: andando verso la forza o «Lancia» il puntatore attraversa il campo e non la sposta.
await bocce.waitForFunction(() => game.closeup.step === 'aim' && game.closeup.current === 'player', null, { timeout: 30000 });
await bocce.mouse.click(r.x + r.width * 0.45, r.y + r.height * 0.6);
const fixed = await bocce.evaluate(() => JSON.stringify(game.closeup.aim));
await bocce.mouse.move(r.x + r.width * 0.5, r.y + r.height * 0.95, { steps: 8 });
assert.equal(await bocce.evaluate(() => JSON.stringify(game.closeup.aim)), fixed, 'la mira resta dove si è cliccato');
await bocce.keyboard.press('Escape');
await bocce.waitForFunction(() => game.speech?.text.startsWith('Già stanco?'), null, { timeout: 5000 });
await bocce.close();
}
// «Fai due tiri» al campo del Circolo: il mouse mira, tenendo premuto la forza sale, lasciando la palla vola; Esc esce.
{
const k = await open({ width: 1280, height: 860 });
await k.click('#splash');
await k.click('#new-game');
await k.click('#rione [data-rione="campomigliaio"]');
await k.click('#skip-intro');
await settle(k);
await k.evaluate(() => game.go('via_cipriani', [340, 316]));
await settle(k);
await k.evaluate(() => game.act('use', 'campo_basket'));
await k.waitForFunction(() => game.dialogue && !game.busy, null, { timeout: 20000 });
await k.locator('#choices li', { hasText: 'Due tiri seri.' }).click();
await k.waitForFunction(() => game.closeup?.id === 'basket' && game.closeup.step === 'aim' && !game.speech, null, { timeout: 30000 });
const r = await k.locator('#scene').boundingBox();
const at = (x, y) => [r.x + x / 640 * r.width, r.y + y / 360 * r.height];
await k.mouse.move(...at(560, 60));
const high = await k.evaluate(() => game.closeup.angle);
await k.mouse.move(...at(600, 200));
assert.ok(await k.evaluate(() => game.closeup.angle) < high, 'la mira segue il mouse');
await k.mouse.down();
await k.waitForTimeout(500);
assert.ok(await k.evaluate(() => game.closeup.power > 0.2 && game.closeup.holding), 'tenendo premuto la forza sale');
await k.screenshot({ path: `${OUT}/03e-basket-carica.png` });
await k.mouse.up();
assert.equal(await k.evaluate(() => game.closeup.step), 'flight', 'lasciando, la palla parte');
await k.waitForTimeout(350);
await k.screenshot({ path: `${OUT}/03f-basket-volo.png` });
await k.waitForFunction(() => game.closeup.step === 'aim' && game.closeup.thrown === 1, null, { timeout: 20000 });
await k.keyboard.press('Escape');
await k.waitForFunction(() => game.speech?.text.startsWith('Lascio il pallone'), null, { timeout: 5000 });
await k.close();
}
// La pesca al lago dell'Anatre: coi ragazzi in Fortezza «S'ha andà a fallo?!?», poi il lancio tenendo premuto e l'attesa.
{
const k = await open({ width: 1280, height: 860 });
await k.click('#splash');
await k.click('#new-game');
await k.click('#rione [data-rione="campomigliaio"]');
await k.click('#skip-intro');
await settle(k);
await k.evaluate(() => game.go('fortezza', [240, 334]));
await settle(k);
await clickScene(k, await hotspotCenter(k, 'pescatori'));
await k.waitForFunction(() => game.dialogue && !game.busy, null, { timeout: 40000 });
await k.locator('#choices li', { hasText: 'S’ha andà a fallo?!?' }).or(k.locator('#choices li', { hasText: "S'ha andà a fallo?!?" })).first().click();
await k.waitForFunction(() => game.options().some(o => o.text === 'Seria.'), null, { timeout: 30000 });
await k.locator('#choices li', { hasText: 'Seria.' }).click();
await k.waitForFunction(() => game.closeup?.id === 'pesca' && game.closeup.step === 'aim' && !game.speech, null, { timeout: 30000 });
const r = await k.locator('#scene').boundingBox();
await k.mouse.move(r.x + r.width * 0.7, r.y + r.height * 0.7);
assert.ok(await k.evaluate(() => game.closeup.power > 0.5), 'toccando l\'acqua al largo la forza sale');
await k.mouse.down();
await k.waitForTimeout(500);
assert.ok(await k.evaluate(() => game.closeup.holding && game.closeup.power > 0.2), 'tenendo premuto si carica il lancio');
await k.mouse.up();
await k.waitForFunction(() => game.closeup.step === 'wait', null, { timeout: 5000 });
await k.waitForTimeout(600);
await k.screenshot({ path: `${OUT}/03g-pesca-attesa.png` });
assert.equal(await k.textContent('#bocce-throw'), 'Ferra');
await k.keyboard.press('Escape');
await k.waitForFunction(() => game.speech?.text.startsWith('Lascio la canna'), null, { timeout: 5000 });
await k.close();
}
// Il biliardo del Circolo 4 Maggio: la sala, poi la partita; un tocco sul tavolo mira, tenendo premuto la forza sale.
{
const b = await open({ width: 1280, height: 860 });
await b.click('#splash');
await b.click('#new-game');
await b.click('#rione [data-rione="campomigliaio"]');
await b.click('#skip-intro');
await settle(b);
await b.evaluate(() => game.go('circolo_4_maggio'));
await settle(b);
await b.waitForTimeout(1500); // la dissolvenza del cambio scena
await b.screenshot({ path: `${OUT}/03d-circolo.png` });
await clickScene(b, await hotspotCenter(b, 'ing_tozzi'));
await b.waitForFunction(() => game.dialogue && !game.busy, null, { timeout: 20000 });
await b.locator('#choices li', { hasText: 'una partita ai birilli' }).click();
await b.waitForFunction(() => game.options().some(o => o.text === 'Seria.'), null, { timeout: 30000 });
await b.locator('#choices li', { hasText: 'Seria.' }).click();
await b.waitForFunction(() => game.closeup?.id === 'biliardo' && game.closeup.step === 'aim' && !game.speech, null, { timeout: 30000 });
const r = await b.locator('#scene').boundingBox();
const [tx, ty] = await b.evaluate(async () => { const { VIEW } = await import('/src/engine/biliardo.js'); const V = game.closeup.balls.cpu; return [VIEW.x + V.x * VIEW.k, VIEW.y + V.y * VIEW.k]; });
await b.mouse.move(r.x + tx / 640 * r.width, r.y + ty / 360 * r.height);
await b.mouse.down();
await b.waitForTimeout(500);
assert.ok(await b.evaluate(() => game.closeup.power > 0.2), 'tenendo premuto la forza sale');
await b.screenshot({ path: `${OUT}/03e-biliardo-forza.png` });
await b.mouse.up();
assert.equal(await b.evaluate(() => game.closeup.step), 'rolling', 'lasciando, la biglia parte');
await b.waitForFunction(() => game.closeup.step === 'aim' && game.closeup.current === 'cpu' || game.closeup.step === 'rolling' && game.closeup.shot?.shooter === 'cpu', null, { timeout: 20000 });
await b.screenshot({ path: `${OUT}/03f-biliardo-socio.png` });
await b.keyboard.press('Escape');
await b.waitForFunction(() => game.speech?.text.startsWith('Lasci a metà?'), null, { timeout: 5000 });
await b.close();
}
const phone = await open({ width: 390, height: 844 });
await phone.click('#splash'); // un clic salta il cartello
assert.ok(await phone.isVisible('#title'), 'il clic sul cartello porta al titolo');
await phone.click('#new-game');
await phone.click('#rione [data-rione="campomigliaio"]');
await phone.click('#skip-intro');
await settle(phone);
const overflow = await phone.evaluate(() => document.documentElement.scrollWidth > innerWidth);
// Ogni verbo si vede tutto: niente pulsanti tagliati dall'inventario.
assert.ok(await phone.evaluate(() => [...document.querySelectorAll('#verbs button')].every(b => b.scrollWidth <= b.clientWidth + 1 && b.getBoundingClientRect().right <= innerWidth)), 'verbi tagliati sul telefono');
assert.equal(overflow, false, 'scorrimento orizzontale su telefono');
await phone.screenshot({ path: `${OUT}/08-telefono.png` });
// Telefono stretto in orizzontale: niente deve allargare la pagina oltre lo schermo (il browser la rimpicciolirebbe
// e taglierebbe verbi e inventario a destra), e la pausa sta tutta dentro la scena.
const small = await browser.newPage({ viewport: { width: 740, height: 330 }, deviceScaleFactor: 2.4, hasTouch: true, isMobile: true });
small.on('pageerror', e => errors.push(e.message));
await small.goto(`http://127.0.0.1:${PORT}/`);
await small.waitForSelector('#splash', { state: 'hidden', timeout: 15000 });
await small.click('#new-game');
await small.click('#rione [data-rione="campomigliaio"]');
await small.click('#skip-intro');
await settle(small);
assert.equal(await small.evaluate(() => innerWidth), 740, 'la pagina è più larga dello schermo del telefono');
await small.click('#pause-button');
assert.ok(await small.evaluate(() => document.getElementById('to-title').getBoundingClientRect().bottom <= document.getElementById('stage').getBoundingClientRect().bottom), 'la pausa esce dalla scena');
await small.screenshot({ path: `${OUT}/08b-telefono-stretto-pausa.png` });
// Telefono in verticale: la Pausa prende tutto lo schermo, e versione e spiegazione non finiscono sopra i verbi.
{
const tall = await browser.newPage({ viewport: { width: 412, height: 915 }, deviceScaleFactor: 2, hasTouch: true, isMobile: true });
await tall.goto(`http://127.0.0.1:${PORT}/`);
await tall.waitForSelector('#splash', { state: 'hidden', timeout: 15000 });
await tall.click('#new-game');
await tall.click('#rione [data-rione="campomigliaio"]');
await tall.click('#skip-intro');
await settle(tall);
await tall.click('#pause-button');
const fit = await tall.evaluate(() => {
const p = document.getElementById('pause'), v = p.querySelector('.version').getBoundingClientRect();
return { inside: p.scrollHeight <= p.clientHeight + 1, top: document.elementFromPoint(v.x + v.width / 2, v.y + v.height / 2)?.closest('#pause') !== null };
});
assert.deepEqual(fit, { inside: true, top: true }, 'in verticale la pausa esce dallo schermo o finisce sotto i verbi');
await tall.close();
}
const landscape = await open({ width: 844, height: 390 });
await landscape.click('#new-game');
await landscape.click('#rione [data-rione="campomigliaio"]');
await landscape.click('#skip-intro');
await settle(landscape);
assert.equal(await landscape.evaluate(() => document.documentElement.scrollWidth > innerWidth), false);
assert.ok(await landscape.evaluate(() => [...document.querySelectorAll('#verbs button')].every(b => b.scrollWidth <= b.clientWidth + 1 && b.getBoundingClientRect().height >= 26)), 'in orizzontale i verbi si leggono interi e il dito li prende');
await landscape.screenshot({ path: `${OUT}/09-orizzontale.png`, fullPage: true });
// Debug (?debug): area calpestabile e hotspot disegnati; Maiusc+clic segna punti senza muovere Andrea, X li azzera.
const dbg = await open({ width: 1280, height: 860 }, '?debug');
await dbg.click('#new-game');
await dbg.click('#rione [data-rione="campomigliaio"]');
await dbg.click('#skip-intro');
await settle(dbg);
const start = await dbg.evaluate(() => [game.state.x, game.state.y]);
await dbg.keyboard.down('Shift');
await clickScene(dbg, [100, 300]);
await clickScene(dbg, [200, 320]);
await dbg.keyboard.up('Shift');
await dbg.mouse.move(700, 400);
await dbg.waitForTimeout(200);
assert.deepEqual(await dbg.evaluate(() => [game.state.x, game.state.y]), start, 'Maiusc+clic non muove Andrea');
await dbg.screenshot({ path: `${OUT}/10-debug.png` });
assert.deepEqual(errors, [], 'errori nella console');
console.log(`Browser OK: soluzione completa, ripresa del salvataggio, telefono. Schermate in ${OUT}`);
console.log(`Browser OK: soluzione completa, ripresa del salvataggio. Schermate in ${OUT}`);
}
assert.deepEqual(errors, [], 'errori nella console');
} finally {
+1 -1
View File
@@ -84,7 +84,7 @@ export async function checkMultimedia(browser, port, out, errors) {
else await page.mouse.move(...await at(target));
await page.waitForFunction(() => game.closeup?.step === 'reveal');
await page.screenshot({ path: `${out}/media-torcia-${mobile ? 'telefono' : 'desktop'}-${i}.png` });
for (let n = 0; n < 200; n++) {
for (const fine = Date.now() + 30000; Date.now() < fine;) { // con la macchina carica la rivelazione dura di più
if (await page.evaluate(i => game.closeup.found > i, i)) break;
if (await page.evaluate(() => !!game.speech)) await page.keyboard.press('Space');
await page.waitForTimeout(25);