Esito che vale e si salva appena deciso, per tutti i minigiochi (anche tornando al titolo dalla schermata del risultato); briscola: uscire durante il conteggio non perde più la vittoria; tressette: si esce ai 21 col primo evento che ci porta, accusa compresa (prima vinceva sempre Andrea se ci arrivavano tutte e due), il carico che fa vincere si dà anche se è il 2, accuse che non escono dal tavolo, convenzioni scritte nell'aiuto; scopa: primiera valutata per quel che migliora, finale esatto a mazzo finito, presa sulla tavola al rilascio con scelta e conferma, frecce e Invio sempre sulla presa scelta. Tastiera: Invio e Spazio giocano sempre la carta scelta. Tavola descritta a parole. Classi CSS comuni ai tre giochi. Video: player nel gioco (pulsante «Guarda il video» nel «Come si gioca», compare quando il video esiste), video/narra.py per la voce del narratore, pezzi comuni dei copioni e il copione della briscola; scaletta in video/SCALETTA-CARTE.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
154 lines
9.9 KiB
JavaScript
154 lines
9.9 KiB
JavaScript
// Scopa ai 11 nel browser: carte con mouse, tocco e tastiera, scelta fra più prese; si accelerano solo le attese.
|
|
// npm run browser:scopa — /tmp/regolo-shots/scopa-*.png
|
|
import { chromium } from 'playwright';
|
|
import { mkdirSync } 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 tap(page, mode, selector) {
|
|
if (mode === 'touch') { const r = await page.locator(selector).boundingBox(); await page.touchscreen.tap(r.x + r.width / 2, r.y + r.height / 2); }
|
|
else await page.click(selector);
|
|
}
|
|
async function at(page, mode, [px, py]) {
|
|
// Sul telefono i pulsanti stanno sotto il tavolo: dopo averli toccati la pagina può essere scesa.
|
|
await page.locator('#scene').scrollIntoViewIfNeeded();
|
|
const r = await page.locator('#scene').boundingBox(), x = r.x + px / 640 * r.width, y = r.y + py / 360 * r.height;
|
|
if (mode === 'touch') await page.touchscreen.tap(x, y); else await page.mouse.click(x, y);
|
|
}
|
|
async function playCard(page, mode, index) {
|
|
if (mode === 'keyboard') {
|
|
await page.keyboard.press(String(index + 1));
|
|
assert.equal(await page.evaluate(() => game.closeup.selected), index);
|
|
await page.keyboard.press('Enter');
|
|
return;
|
|
}
|
|
const p = await page.evaluate(async i => {
|
|
const { handX, HAND } = await import('/src/engine/scopa-render.js');
|
|
return [handX(game.closeup.hands.player.length, i) + 32, HAND.y + 46];
|
|
}, index);
|
|
await at(page, mode, p);
|
|
}
|
|
async function wait(page) {
|
|
return page.evaluate(async () => {
|
|
const { canPlay } = await import('/src/engine/scopa.js');
|
|
// Un tocco mentre qualcuno parla salta la battuta, non gioca: la battuta si salta qui.
|
|
for (let i = 0; i < 60; i++) {
|
|
if (game.speech) game.skip();
|
|
else if (canPlay(game.closeup) || ['smazzata', 'result', 'choose'].includes(game.closeup.step)) break;
|
|
else game.update(1.4);
|
|
}
|
|
return game.closeup.step;
|
|
});
|
|
}
|
|
try {
|
|
browser = await chromium.launch({ args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
|
const scenarios = [
|
|
{ name: 'desktop-mouse', mode: 'mouse', viewport: { width: 1280, height: 860 }, full: true },
|
|
{ name: 'telefono-touch', mode: 'touch', viewport: { width: 390, height: 844 } },
|
|
{ name: 'desktop-keyboard', mode: 'keyboard', viewport: { width: 1280, height: 860 } },
|
|
{ name: 'orizzontale-touch', mode: 'touch', viewport: { width: 844, height: 390 } },
|
|
];
|
|
for (const { name, mode, viewport, full } of scenarios) {
|
|
const page = await browser.newPage({ viewport, hasTouch: true, isMobile: viewport.width < 900 });
|
|
page.on('pageerror', e => errors.push(`${name}: ${e.message}`));
|
|
page.on('console', e => { if (e.type() === 'error') errors.push(`${name}: ${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); game.act('talk', 'giocatore_carte'); settle(game); });
|
|
await page.locator('#choices li', { hasText: 'Una scopa?' }).click();
|
|
await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game));
|
|
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: 'Seria.' }).click();
|
|
await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game));
|
|
assert.deepEqual(await page.evaluate(() => [game.closeup.deck, game.closeup.level]), [deck, 'seria']);
|
|
await page.waitForSelector('#scopa-controls:not([hidden])');
|
|
assert.ok(await page.isHidden('#tressette-controls')); assert.ok(await page.isHidden('#briscola-controls'));
|
|
await wait(page);
|
|
await page.screenshot({ path: `${out}/scopa-${name}-inizio.png` });
|
|
// Una volta per scenario: tavola preparata con due prese possibili per il 7, scelta toccando la tavola,
|
|
// con un pulsante o con frecce e Invio.
|
|
await page.evaluate(async () => {
|
|
// Una partita nuova in cui esce Andrea, così in tavola ci sono proprio quattro carte.
|
|
const { newScopa } = await import('/src/engine/scopa.js'), old = game.closeup;
|
|
let seed = 1, c;
|
|
do c = newScopa(seed++, { level: old.level, deck: old.deck }); while (c.current !== 'player');
|
|
const pool = [...c.stock, ...c.table, c.hands.player[0]];
|
|
const free = v => pool.splice(pool.findIndex(a => a.value === v), 1)[0];
|
|
c.table = [free(3), free(4), free(2), free(5)]; c.hands.player[0] = free(7); c.stock = pool;
|
|
game.closeup = { id: old.id, ...c };
|
|
});
|
|
// La tavola preparata non perde né duplica carte.
|
|
const ids = await page.evaluate(() => { const c = game.closeup; return [...c.stock, ...c.table, ...c.hands.player, ...c.hands.cpu, ...c.taken.player, ...c.taken.cpu].map(a => a.id); });
|
|
assert.equal(new Set(ids).size, 40); assert.equal(ids.length, 40);
|
|
await playCard(page, mode, 0);
|
|
assert.equal(await page.evaluate(() => game.closeup.step), 'choose');
|
|
await page.waitForSelector('#scopa-options:not([hidden])');
|
|
await page.screenshot({ path: `${out}/scopa-${name}-scelta.png` });
|
|
// Tastiera (Codex): fuoco sulla prima presa, freccia a destra, Invio: deve partire la seconda.
|
|
if (mode === 'keyboard') { await page.locator('#scopa-options button:first-child').focus(); await page.keyboard.press('ArrowRight'); await page.keyboard.press('Enter'); }
|
|
else if (mode === 'touch') {
|
|
// Il 5 in tavola sta solo nella presa 2+5.
|
|
const p = await page.evaluate(async () => {
|
|
const { tableXY } = await import('/src/engine/scopa-render.js'), c = game.closeup;
|
|
const i = c.table.findIndex(a => a.value === 5), [x, y] = tableXY(c.table.length, i); return [x + 21, y + 30];
|
|
});
|
|
// Il primo tocco sceglie la presa che lo contiene (evidenziata), il secondo la conferma.
|
|
await at(page, mode, p);
|
|
assert.equal(await page.evaluate(() => game.closeup.step === 'choose' && game.closeup.choice.selected), 1);
|
|
await at(page, mode, p);
|
|
} else await tap(page, mode, '#scopa-options button:nth-child(2)');
|
|
assert.equal(await page.evaluate(() => game.closeup.step), 'take');
|
|
assert.deepEqual(await page.evaluate(() => game.closeup.last.captured.map(a => a.value).sort()), [2, 5]);
|
|
await page.screenshot({ path: `${out}/scopa-${name}-presa.png` });
|
|
let played = 1;
|
|
for (let steps = 0; steps < 400; steps++) {
|
|
const step = await wait(page);
|
|
if (step === 'result') break;
|
|
if (step === 'smazzata') {
|
|
await page.screenshot({ path: `${out}/scopa-${name}-smazzata.png` });
|
|
if (!full) break;
|
|
if (mode === 'keyboard') await page.keyboard.press('Enter'); else await tap(page, mode, '#scopa-next');
|
|
continue;
|
|
}
|
|
if (step !== 'play' && step !== 'choose') throw new Error('partita ferma in ' + step);
|
|
if (step === 'choose') {
|
|
if (mode === 'keyboard') await page.keyboard.press('Enter'); else await tap(page, mode, '#scopa-options button:first-child');
|
|
continue;
|
|
}
|
|
const move = await page.evaluate(async () => {
|
|
const { observation, choose } = await import('/src/engine/scopa.js');
|
|
return choose(observation(game.closeup, 'player'), 'male', 3);
|
|
});
|
|
const before = await page.evaluate(() => game.closeup.hands.player.length);
|
|
await playCard(page, mode, move.index); played++;
|
|
const after = await page.evaluate(() => game.closeup.step === 'choose' ? -1 : game.closeup.hands.player.length);
|
|
// Il gioco intanto va avanti da solo: dopo l'ultima carta la mano può essere già ridata.
|
|
assert.ok(after === -1 || after === before - 1 || (before === 1 && after === 3), `un gesto gioca una carta (${name}: ${before} → ${after})`);
|
|
}
|
|
const count = await page.evaluate(() => game.closeup.summary ? game.closeup.summary.rows : null);
|
|
assert.ok(count, 'conteggio della smazzata');
|
|
if (full) {
|
|
assert.equal(await page.evaluate(() => game.closeup.step), 'result');
|
|
await page.screenshot({ path: `${out}/scopa-${name}-risultato.png` });
|
|
const winner = await page.evaluate(() => game.closeup.result.winner);
|
|
await page.keyboard.press('Enter');
|
|
await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game));
|
|
assert.equal(await page.evaluate(() => game.closeup), null);
|
|
assert.equal(await page.evaluate(() => !!game.state.flags.scopa_vinta), winner === 'player');
|
|
} else {
|
|
await page.keyboard.press('Escape');
|
|
assert.equal(await page.evaluate(() => game.closeup), null);
|
|
assert.equal(await page.evaluate(() => !!game.state.flags.scopa_vinta), false);
|
|
}
|
|
console.log(`${name}: ${played} carte giocate, presa scelta fra due${full ? ', partita ai 11 finita' : ', una smazzata'} OK`);
|
|
await page.close();
|
|
}
|
|
assert.deepEqual(errors, []); console.log(`Schermate: ${out}/scopa-*.png`);
|
|
} finally { await browser?.close(); server.kill(); }
|