Biliardo più preciso (revisione di Codex): urti calcolati nel punto esatto di contatto, anche nei colpi di taglio e veloci; la guida simula il tiro vero e mostra le due biglie dopo l'urto e i falli; la mira resta ferma andando sui comandi; frecce e Maiusc per la mira fine; comandi che non coprono il tavolo; test nel browser con mouse, touch e telefono. Più un tetto agli urti per passo, così due biglie a contatto non possono bloccare il gioco

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
This commit is contained in:
2026-10-25 23:12:46 +01:00
co-authored by Claude Opus 5.5
parent ddc877a2d1
commit 9eabd5c6e1
10 changed files with 434 additions and 71 deletions
+126
View File
@@ -0,0 +1,126 @@
// Regressioni della mira con veri eventi browser, su desktop e touch.
// npm run browser:biliardo — schermate in /tmp/regolo-biliardo.
import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs';
const out = process.env.OUT ?? '/tmp/regolo-biliardo';
mkdirSync(out, { recursive: true });
const server = await staticServer();
let browser;
const errors = [];
try {
browser = await chromium.launch({ args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
for (const [name, mobile, viewport] of [
['desktop', false, { width: 1280, height: 860 }],
['touch', true, { width: 844, height: 390 }],
['portrait', true, { width: 390, height: 844 }],
]) {
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
page.on('pageerror', e => errors.push(e.message));
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('circolo_4_maggio'); settle(game);
game.act('talk', 'ing_tozzi'); settle(game);
game.choose(game.options().findIndex(o => o.text.includes('una partita ai birilli'))); settle(game);
});
await page.waitForSelector('#bocce-controls:not([hidden])');
const canvas = await page.locator('#scene').boundingBox();
for (const id of ['#sound', '#corner']) {
const box = await page.locator(id).boundingBox();
assert.ok(box.y + box.height <= canvas.y, `${id} non deve coprire il punteggio`);
}
const exit = await page.locator('#closeup-close').boundingBox();
assert.ok(exit.y >= canvas.y + canvas.height, 'uscita fuori dal tavolo');
await page.waitForTimeout(1600); // completa la dissolvenza della sala prima delle schermate
const reset = () => page.evaluate(async () => {
const { newBiliardo } = await import('/src/engine/biliardo.js');
game.closeup = { ...newBiliardo(42), id: 'biliardo' };
});
const state = () => page.evaluate(() => ({ angle: game.closeup.angle, power: game.closeup.power, step: game.closeup.step }));
const position = async () => {
const r = await page.locator('#scene').boundingBox();
const [x, y] = await page.evaluate(async () => {
const { VIEW } = await import('/src/engine/biliardo.js');
const b = game.closeup.balls.cpu;
return [VIEW.x + b.x * VIEW.k, VIEW.y + (b.y + 3) * VIEW.k];
});
return { x: r.x + x / 640 * r.width, y: r.y + y / 360 * r.height, r };
};
const target = await position();
if (mobile) await page.touchscreen.tap(target.x, target.y);
else await page.mouse.click(target.x, target.y);
assert.equal((await state()).step, 'aim', 'un tocco sceglie la mira senza tirare');
assert.equal((await state()).power, 0.45, 'il tocco conserva la forza impostata');
// Il passaggio del mouse verso i comandi attraversa il panno.
const fixed = (await state()).angle;
await page.mouse.move(target.r.x + target.r.width * 0.55, target.r.y + target.r.height * 0.8, { steps: 8 });
assert.equal((await state()).angle, fixed, 'spostarsi verso Tira non deve cambiare la mira');
await page.keyboard.press('ArrowRight');
const nudged = (await state()).angle;
assert.ok(Math.abs(nudged - fixed - 0.002) < 1e-10);
await page.keyboard.press('Shift+ArrowLeft');
assert.ok(Math.abs((await state()).angle - nudged + 0.0005) < 1e-10);
await page.locator('#bocce-power').fill('60');
const prepared = await state();
await page.screenshot({ path: `${out}/${name}-mira.png` });
const expected = await page.evaluate(async () => {
const { simulate } = await import('/src/engine/biliardo.js');
const c = game.closeup;
return simulate(c, 'player', c.angle, c.power);
});
if (mobile) await page.tap('#bocce-throw'); else await page.click('#bocce-throw');
assert.equal((await state()).step, 'rolling');
assert.equal((await state()).angle, prepared.angle);
const score = await page.evaluate(async () => {
const { update, judge, FIXED_STEP } = await import('/src/engine/biliardo.js');
const c = game.closeup;
for (let i = 0; i < 10000 && c.step === 'rolling'; i++) update(c, FIXED_STEP);
return judge(c.shot);
});
assert.deepEqual(score, expected, 'il tiro dai comandi segue la simulazione');
await reset();
const hold = await position();
const cdp = mobile ? await page.context().newCDPSession(page) : null;
if (mobile) await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: hold.x, y: hold.y }] });
else { await page.mouse.move(hold.x, hold.y); await page.mouse.down(); }
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.25);
const heldAngle = (await state()).angle;
if (mobile) await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x: hold.x - 30, y: hold.y + 20 }] });
else await page.mouse.move(hold.x - 30, hold.y + 20, { steps: 4 });
assert.equal((await state()).angle, heldAngle, 'il movimento durante la carica non sposta la mira');
if (mobile) await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
else await page.mouse.up();
assert.equal((await state()).step, 'rolling');
assert.equal((await state()).angle, heldAngle);
// Un gesto interrotto non deve lasciare un tiro in carica.
await reset();
if (mobile) await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: hold.x, y: hold.y }] });
else { await page.mouse.move(hold.x, hold.y); await page.mouse.down(); }
await page.waitForFunction(() => game.closeup.holding);
if (mobile) await cdp.send('Input.dispatchTouchEvent', { type: 'touchCancel', touchPoints: [] });
else { await page.locator('#scene').dispatchEvent('pointercancel', { pointerId: 1 }); await page.mouse.up(); }
assert.equal((await state()).step, 'aim');
assert.equal((await state()).power, 0.45);
assert.equal(await page.evaluate(() => game.closeup.holding), false);
await page.keyboard.press('Escape');
assert.equal(await page.evaluate(() => game.closeup), null);
await page.close();
console.log(`${name}: mira, ritocchi, forza, tiro, carica e annullamento OK`);
}
assert.deepEqual(errors, [], 'nessun errore JavaScript nel browser');
console.log(`Schermate: ${out}`);
} finally {
await browser?.close();
server.kill();
}