Briscola all'oratorio: alle tavolate il giocatore di carte (segnaposto, sprite di Codex) sfida Andrea a briscola in due. Si sceglie il mazzo, toscane (cuori, mattoni, fiori, picche; gobbo, donna e regio) o napoletane, e il livello: «Alla bona», «Seria», «A fassi male» (conta le carte uscite, finale perfetto a mazzo finito, mai la mano altrui). Regole fedeli, conteggio delle prese a fine partita, rivincita, suoni di carte, comandi con mouse, tocco e tastiera. Carte e dorso col Regolo serpentello disegnati da Codex; motore e prove nel browser rivisti da Claude
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
// 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(); }
|
||||
Reference in New Issue
Block a user