Files
leggenda-del-regolo/tests/briscola.browser.mjs
T

175 lines
11 KiB
JavaScript

// 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(); }