// Partite complete con eventi reali. Si accelerano solo le attese, mai le mosse di Andrea. // npm run browser:briscola — /tmp/regolo-shots/briscola-*.png import { chromium } from 'playwright'; import { mkdirSync, writeFileSync } from 'node:fs'; import assert from 'node:assert/strict'; import { staticServer } from './helpers.mjs'; const out = '/tmp/regolo-shots'; mkdirSync(out, { recursive: true }); const server = await staticServer(), errors = []; let browser; async function clickCard(page, mode, index) { const r = await page.locator('#scene').boundingBox(); const x = r.x + (176 + index * 102 + 42) / 640 * r.width, y = r.y + 290 / 360 * r.height; if (mode === 'touch') await page.touchscreen.tap(x, y); else if (mode === 'keyboard') { await page.keyboard.press(String(index + 1)); assert.equal(await page.evaluate(() => game.closeup.selected), index); await page.keyboard.press('ArrowRight'); await page.keyboard.press('ArrowLeft'); assert.equal(await page.evaluate(() => game.closeup.selected), index); await page.keyboard.press('Enter'); } else await page.mouse.click(x, y); } async function advance(page) { return page.evaluate(async () => { const { canPlay } = await import('/src/engine/briscola.js'); for (let i = 0; i < 6 && !canPlay(game.closeup) && !['count', 'result'].includes(game.closeup.step); i++) game.update(1.3); return game.closeup.step; }); } async function artwork(page) { const pictures = await page.evaluate(async () => { await document.fonts.ready; const { DECK_CARDS, newBriscola } = await import('/src/engine/briscola.js'); const { drawCard, drawBriscola } = await import('/src/engine/briscola-render.js'); const { story } = await import('/src/story/story.js'); const canvas = document.createElement('canvas'), ctx = canvas.getContext('2d'), pictures = {}; for (const [deck, cards] of Object.entries(DECK_CARDS)) { canvas.width = 900; canvas.height = 560; ctx.imageSmoothingEnabled = false; ctx.fillStyle = '#234c40'; ctx.fillRect(0, 0, 900, 560); cards.forEach((card, i) => drawCard(ctx, card, 8 + (i % 10) * 89, 8 + Math.floor(i / 10) * 136)); pictures[`${deck}-mazzo`] = canvas.toDataURL().split(',')[1]; canvas.width = 640; canvas.height = 360; const c = newBriscola(41, { deck }), before = JSON.stringify(c); drawBriscola(ctx, c, story.closeups.briscola); if (before !== JSON.stringify(c)) throw new Error('Il renderer modifica lo stato'); pictures[`${deck}-tavolo`] = canvas.toDataURL().split(',')[1]; } return pictures; }); for (const [name, png] of Object.entries(pictures)) writeFileSync(`${out}/briscola-${name}.png`, Buffer.from(png, 'base64')); } try { browser = await chromium.launch({ args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); const scenarios = ['mouse', 'touch', 'keyboard'].flatMap(mode => [ { name: `desktop-${mode}`, mode, viewport: { width: 1280, height: 860 } }, { name: `telefono-${mode}`, mode, viewport: { width: 390, height: 844 } }, ]); scenarios.push({ name: 'orizzontale-touch', mode: 'touch', viewport: { width: 844, height: 390 } }); for (const { name, mode, viewport } of scenarios) { const page = await browser.newPage({ viewport, hasTouch: true, isMobile: viewport.width < 900 }); page.on('pageerror', e => errors.push(e.message)); page.on('console', e => { if (e.type() === 'error') errors.push(e.text()); }); await page.goto(`http://127.0.0.1:${server.port}/`); await page.click('#splash'); await page.click('#new-game'); await page.click('#rione [data-rione="campomigliaio"]'); await page.click('#skip-intro'); await page.evaluate(async () => { const { settle } = await import('/tests/helpers.mjs'); settle(game); game.go('oratorio'); settle(game); }); await page.waitForTimeout(1500); if (name === 'desktop-mouse') { await page.screenshot({ path: `${out}/briscola-oratorio.png` }); await artwork(page); } await page.evaluate(async () => { const { settle } = await import('/tests/helpers.mjs'); game.act('talk', 'giocatore_carte'); settle(game); }); await page.locator('#choices li', { hasText: 'Una briscola?' }).click(); await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game)); // Sul telefono le napoletane, altrove le toscane: tutti e due i mazzi passano dal dialogo. const deck = name.startsWith('telefono') ? 'napoletane' : 'toscane'; await page.locator('#choices li', { hasText: deck === 'napoletane' ? 'Con le napoletane.' : 'Con le toscane.' }).click(); await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game)); await page.locator('#choices li', { hasText: 'A fassi male.' }).click(); assert.equal(await page.evaluate(async () => { (await import('/tests/helpers.mjs')).settle(game); return game.closeup.deck; }), deck); await page.evaluate(async deck => { const { newBriscola } = await import('/src/engine/briscola.js'); game.closeup = { id: 'briscola', ...newBriscola(91421, { level: 'male', deck }) }; }, deck); await page.waitForSelector('#briscola-controls:not([hidden])'); assert.ok(await page.isHidden('#bocce-controls')); const scene = await page.locator('#scene').boundingBox(), exit = await page.locator('#closeup-close').boundingBox(); assert.ok(exit.y >= scene.y + scene.height, 'uscita fuori dal tavolo'); for (const id of ['#corner', '#sound']) { const r = await page.locator(id).boundingBox(); assert.ok(r.y + r.height <= scene.y, `${id} fuori dal tavolo`); } assert.ok(scene.width * 84 / 640 >= 44, 'carte toccabili sul telefono'); await advance(page); await page.screenshot({ path: `${out}/briscola-${name}-inizio.png` }); if (name === 'desktop-mouse') { // Il tempo premuto non carica nulla; uscire dal bersaglio o annullare non gioca. const r = await page.locator('#scene').boundingBox(), x = r.x + 218 / 640 * r.width, y = r.y + 290 / 360 * r.height; const before = await page.evaluate(() => game.closeup.hands.player.length); await page.mouse.move(x, y); await page.mouse.down(); await page.waitForTimeout(400); assert.equal(await page.evaluate(() => game.closeup.hands.player.length), before); await page.locator('#scene').dispatchEvent('pointercancel', { pointerId: 1 }); await page.mouse.up(); assert.equal(await page.evaluate(() => game.closeup.hands.player.length), before); // Anche la selezione da tastiera sui pulsanti segue il focus. await page.locator('#briscola-card-0').focus(); await page.keyboard.press('ArrowRight'); assert.equal(await page.evaluate(() => game.closeup.selected), 1); assert.equal(await page.evaluate(() => document.activeElement.id), 'briscola-card-1'); await page.locator('#briscola-card-1').evaluate(b => b.blur()); // Muto e pausa fermano anche le attese della partita. await page.keyboard.press('m'); assert.equal(await page.evaluate(() => music.muted), true); assert.equal(await page.evaluate(() => ambience.nodes.size), 0); await page.keyboard.press('m'); await page.click('#pause-button'); const paused = await page.evaluate(() => JSON.stringify(game.closeup)); await page.waitForTimeout(250); assert.equal(await page.evaluate(() => JSON.stringify(game.closeup)), paused); await page.keyboard.press('Escape'); } let played = 0, capturedTrick = false, capturedEnd = false; while (!['count', 'result'].includes(await advance(page))) { const index = await page.evaluate(async () => { const { observation, attentive } = await import('/src/engine/briscola.js'); return attentive(observation(game.closeup, 'player')); }); const before = await page.evaluate(() => game.closeup.hands.player.length); await clickCard(page, mode, index); played++; assert.equal(await page.evaluate(() => game.closeup.hands.player.length), before - 1, 'un gesto gioca esattamente una carta'); if (!capturedTrick) { await page.evaluate(() => { if (game.closeup.step === 'play') game.update(0.75); }); assert.equal(await page.evaluate(() => game.closeup.step), 'trick'); assert.equal(await page.evaluate(() => game.closeup.table.length), 2); await page.waitForTimeout(200); await page.screenshot({ path: `${out}/briscola-${name}-presa.png` }); capturedTrick = true; } if (!capturedEnd && await page.evaluate(() => !game.closeup.stock.length)) { await page.screenshot({ path: `${out}/briscola-${name}-fine-mazzo.png` }); capturedEnd = true; } assert.ok(played <= 20); } assert.equal(played, 20); await page.evaluate(() => game.update(3)); await page.screenshot({ path: `${out}/briscola-${name}-conteggio.png` }); await page.evaluate(() => game.update(15)); await page.waitForFunction(() => game.closeup.step === 'result'); await page.waitForTimeout(50); const result = await page.evaluate(() => game.closeup.result); assert.equal(result.player + result.cpu, 120); await page.screenshot({ path: `${out}/briscola-${name}-risultato.png` }); if (mode === 'keyboard') await page.keyboard.press('Enter'); else if (mode === 'touch') await page.tap('#closeup-close'); else await page.click('#closeup-close'); await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game)); assert.equal(await page.evaluate(() => !!game.state.flags.briscola_vinta), result.winner === 'player'); assert.ok(await page.evaluate(() => game.state.flags.briscola_giocata)); // Rivincita e abbandono con Esc: nessun flag di vittoria regalato. await page.evaluate(async () => { const { settle } = await import('/tests/helpers.mjs'); game.act('talk', 'giocatore_carte'); settle(game); }); await page.locator('#choices li', { hasText: 'La rivincita?' }).click(); await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game)); await page.locator('#choices li', { hasText: 'Con le toscane.' }).click(); await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game)); await page.locator('#choices li', { hasText: 'Alla bona.' }).click(); await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game)); await page.keyboard.press('Escape'); assert.equal(await page.evaluate(() => game.closeup), null); console.log(`${name}: 20 carte giocate, prese, conteggio, risultato e rivincita OK`); await page.close(); } assert.deepEqual(errors, []); console.log(`Schermate: ${out}/briscola-*.png`); } finally { await browser?.close(); server.kill(); }