Biliardo dell'Ing. Tozzi: il tiro pieno va più forte (720 cm/s; i rinquarti restano a 520) e il test nel browser segue i comandi nuovi (Andrea)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdCPptNRwbVzjzHbohi
This commit is contained in:
2026-10-29 00:11:02 +01:00
co-authored by Claude Opus 5.5
parent a11a6ef59f
commit 381254ed4d
4 changed files with 21 additions and 12 deletions
+8 -5
View File
@@ -30,7 +30,10 @@ export function castle() {
} }
export const powerAt = time => clamp(time / SWING, 0, 1); export const powerAt = time => clamp(time / SWING, 0, 1);
export const speedFor = power => MIN_SPEED + (MAX_SPEED - MIN_SPEED) * power; // Il tiro più forte: al biliardo dell'Ing. Tozzi si arriva a STRONG (Andrea, 29 ottobre: «più potenza»); i rinquarti
// restano a MAX_SPEED, perché le loro posizioni sono misurate a quella forza.
export const STRONG = 720;
export const speedFor = (power, max = MAX_SPEED) => MIN_SPEED + (max - MIN_SPEED) * power;
// Quanta strada fa una biglia lanciata a velocità v, senza urti (attrito costante più un po' di resistenza). // Quanta strada fa una biglia lanciata a velocità v, senza urti (attrito costante più un po' di resistenza).
export function reach(v) { export function reach(v) {
const k = FRICTION / DRAG, t = Math.log((v + k) / k) / DRAG; const k = FRICTION / DRAG, t = Math.log((v + k) / k) / DRAG;
@@ -48,7 +51,7 @@ export const LEVELS = {
}; };
export function newBiliardo(seed = Date.now() >>> 0, def = {}) { export function newBiliardo(seed = Date.now() >>> 0, def = {}) {
const level = LEVELS[def.level] ? def.level : 'seria'; const level = LEVELS[def.level] ? def.level : 'seria';
const c = { step: 'aim', t: 0, seed, level, skill: LEVELS[level], current: 'player', target: def.target ?? TARGET, power: 0.45, angle: 0, const c = { step: 'aim', t: 0, seed, level, skill: LEVELS[level], current: 'player', target: def.target ?? TARGET, maxSpeed: def.maxSpeed ?? STRONG, power: 0.45, angle: 0,
holding: false, holdTime: 0, accumulator: 0, events: [], points: { player: 0, cpu: 0 }, last: null, shot: null, plan: null }; holding: false, holdTime: 0, accumulator: 0, events: [], points: { player: 0, cpu: 0 }, last: null, shot: null, plan: null };
c.balls = { player: ball('player', LEN * 0.18, WID * 0.5), cpu: ball('cpu', LEN * 0.81, WID * 0.25), pallino: ball('pallino', LEN * 0.88, WID * 0.7) }; c.balls = { player: ball('player', LEN * 0.18, WID * 0.5), cpu: ball('cpu', LEN * 0.81, WID * 0.25), pallino: ball('pallino', LEN * 0.88, WID * 0.7) };
c.pins = castle(); c.pins = castle();
@@ -171,7 +174,7 @@ function step(w, dt) {
export function strike(c, who, angle, power) { export function strike(c, who, angle, power) {
c.turn = null; c.turn = null;
const b = c.balls[who], v = speedFor(power); const b = c.balls[who], v = speedFor(power, c.maxSpeed);
c.visual.shot = { at: c.visual.time, angle, power, x: b.x, y: b.y }; c.visual.shot = { at: c.visual.time, angle, power, x: b.x, y: b.y };
c.pending = { ball: b, vx: Math.cos(angle) * v, vy: Math.sin(angle) * v, power }; c.pending = { ball: b, vx: Math.cos(angle) * v, vy: Math.sin(angle) * v, power };
c.windup = WINDUP; c.windup = WINDUP;
@@ -183,7 +186,7 @@ export function strike(c, who, angle, power) {
export function simulate(c, who, angle, power, { onContact, stop } = {}) { export function simulate(c, who, angle, power, { onContact, stop } = {}) {
const balls = list(c).map(b => ({ ...b })), pins = c.pins.map(p => ({ ...p })); const balls = list(c).map(b => ({ ...b })), pins = c.pins.map(p => ({ ...p }));
const w = { balls, pins, onContact, shot: { shooter: who, first: null, pallino: false, pins: [] } }; const w = { balls, pins, onContact, shot: { shooter: who, first: null, pallino: false, pins: [] } };
const b = balls.find(x => x.role === who), v = speedFor(power); const b = balls.find(x => x.role === who), v = speedFor(power, c.maxSpeed);
b.vx = Math.cos(angle) * v; b.vy = Math.sin(angle) * v; b.vx = Math.cos(angle) * v; b.vy = Math.sin(angle) * v;
for (let i = 0; i < 15 / FIXED_STEP && !stop?.() && balls.some(x => x.vx || x.vy); i++) step(w, FIXED_STEP); for (let i = 0; i < 15 / FIXED_STEP && !stop?.() && balls.some(x => x.vx || x.vy); i++) step(w, FIXED_STEP);
return judge(w.shot); return judge(w.shot);
@@ -343,7 +346,7 @@ export function predict(c) {
const cached = predictions.get(c); const cached = predictions.get(c);
if (cached?.key === key) return cached.result; if (cached?.key === key) return cached.result;
const balls = list(c).map(b => ({ ...b })), me = balls.find(b => b.role === c.current); const balls = list(c).map(b => ({ ...b })), me = balls.find(b => b.role === c.current);
const v = speedFor(c.power); const v = speedFor(c.power, c.maxSpeed);
me.vx = Math.cos(c.angle) * v; me.vy = Math.sin(c.angle) * v; me.vx = Math.cos(c.angle) * v; me.vy = Math.sin(c.angle) * v;
const result = { points: [[me.x, me.y]], ghost: null, hit: null, dir: null, outgoing: [] }; const result = { points: [[me.x, me.y]], ghost: null, hit: null, dir: null, outgoing: [] };
const tracks = []; const tracks = [];
+1 -1
View File
@@ -152,7 +152,7 @@ function nextPosition(c) {
export function newRinquarto(seed = Date.now() >>> 0, def = {}) { export function newRinquarto(seed = Date.now() >>> 0, def = {}) {
const level = LEVELS[def.level] ? def.level : 'seria'; const level = LEVELS[def.level] ? def.level : 'seria';
const c = { ...billiard.newBiliardo(seed), level, skill: LEVELS[level], position: 0, score: { andrea: 0, bertovo: 0 }, history: [], const c = { ...billiard.newBiliardo(seed, { maxSpeed: billiard.MAX_SPEED }), level, skill: LEVELS[level], position: 0, score: { andrea: 0, bertovo: 0 }, history: [],
order: LEVELS[level].training ? ['bertovo', 'andrea'] : ['andrea', 'bertovo'], ready: [], gen: null, fixedPower: 1, trail: [], last: false }; order: LEVELS[level].training ? ['bertovo', 'andrea'] : ['andrea', 'bertovo'], ready: [], gen: null, fixedPower: 1, trail: [], last: false };
nextPosition(c); nextPosition(c);
return c; return c;
+6 -3
View File
@@ -128,11 +128,13 @@ try {
const fixed = (await state()).angle; 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 }); 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'); assert.equal((await state()).angle, fixed, 'spostarsi verso Tira non deve cambiare la mira');
// Le frecce girano finché sono giù (sempre più svelte); un colpo secco è un passo, con Maiusc un passo fine.
await page.keyboard.press('ArrowRight'); await page.keyboard.press('ArrowRight');
const nudged = (await state()).angle; const nudged = (await state()).angle;
assert.ok(Math.abs(nudged - fixed - 0.002) < 1e-10); assert.ok(nudged - fixed >= 0.03 - 1e-9 && nudged - fixed < 0.1, `passo: ${nudged - fixed}`);
await page.keyboard.press('Shift+ArrowLeft'); await page.keyboard.press('Shift+ArrowLeft');
assert.ok(Math.abs((await state()).angle - nudged + 0.0005) < 1e-10); const fine = nudged - (await state()).angle;
assert.ok(fine >= 0.0005 - 1e-9 && fine < 0.01, `passo fine: ${fine}`);
await page.locator('#bocce-power').fill('60'); await page.locator('#bocce-power').fill('60');
const prepared = await state(); const prepared = await state();
await page.screenshot({ path: `${out}/${name}-mira.png` }); await page.screenshot({ path: `${out}/${name}-mira.png` });
@@ -158,7 +160,8 @@ try {
const cdp = mobile ? await page.context().newCDPSession(page) : null; 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 }] }); 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(); } else { await page.mouse.move(hold.x, hold.y); await page.mouse.down(); }
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.25); // Si tiene premuto: dopo LONG_PRESS la stecca comincia a caricare.
await page.waitForFunction(() => game.closeup.holding && game.closeup.charging);
const heldAngle = (await state()).angle; const heldAngle = (await state()).angle;
if (mobile) await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x: hold.x - 30, y: hold.y + 20 }] }); 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 }); else await page.mouse.move(hold.x - 30, hold.y + 20, { steps: 4 });
+6 -3
View File
@@ -1,6 +1,6 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { newBiliardo, update, press, release, cancel, setPower, shoot, judge, value, reach, speedFor, predict, castle, aimAt, aimScreen, nudge, canThrow, collide, simulate, LEN, WID, R, VIEW, SWING, powerAt, FIXED_STEP, CUSHION, WINDUP } from '../src/engine/biliardo.js'; import { newBiliardo, update, press, release, cancel, setPower, shoot, judge, value, reach, speedFor, predict, castle, aimAt, aimScreen, nudge, canThrow, collide, simulate, LEN, WID, R, VIEW, SWING, powerAt, FIXED_STEP, CUSHION, WINDUP, STRONG } from '../src/engine/biliardo.js';
import { story } from '../src/story/story.js'; import { story } from '../src/story/story.js';
import { Game } from '../src/engine/game.js'; import { Game } from '../src/engine/game.js';
import { settle } from './helpers.mjs'; import { settle } from './helpers.mjs';
@@ -34,6 +34,9 @@ test('la forza: sale finché si tiene premuto; a metà la biglia fa circa un tav
assert.ok(reach(speedFor(0.5)) > LEN * 0.9 && reach(speedFor(0.5)) < LEN * 1.3, `a metà forza: ${reach(speedFor(0.5)).toFixed(0)} cm`); assert.ok(reach(speedFor(0.5)) > LEN * 0.9 && reach(speedFor(0.5)) < LEN * 1.3, `a metà forza: ${reach(speedFor(0.5)).toFixed(0)} cm`);
assert.ok(reach(speedFor(1)) > LEN * 2); assert.ok(reach(speedFor(1)) > LEN * 2);
assert.ok(reach(speedFor(0)) < 30, 'piano piano si sposta appena'); assert.ok(reach(speedFor(0)) < 30, 'piano piano si sposta appena');
// All'Ing. Tozzi il tiro pieno va più forte (Andrea, 29 ottobre); i rinquarti restano alla forza di prima.
assert.ok(reach(speedFor(1, STRONG)) > reach(speedFor(1)) * 1.3);
assert.equal(newBiliardo(1).maxSpeed, STRONG);
}); });
test('un colpo dritto sulla biglia avversaria la manda avanti, poi tocca all\'altro', () => { test('un colpo dritto sulla biglia avversaria la manda avanti, poi tocca all\'altro', () => {
@@ -126,14 +129,14 @@ test('la guida e il tiro finiscono nello stesso punto, anche dopo sponde oblique
const end = p.points.at(-1); const end = p.points.at(-1);
shoot(c); roll(c); shoot(c); roll(c);
near(c.balls.player.x, end[0]); near(c.balls.player.y, end[1]); near(c.balls.player.x, end[0]); near(c.balls.player.y, end[1]);
if (angle === 0) near(c.balls.player.x - x, reach(speedFor(power)), 0.01); if (angle === 0) near(c.balls.player.x - x, reach(speedFor(power, c.maxSpeed)), 0.01);
} }
}); });
test('la sponda smorza la componente normale: anche la guida cambia angolo al rimbalzo', () => { test('la sponda smorza la componente normale: anche la guida cambia angolo al rimbalzo', () => {
const c = emptyTable(); const c = emptyTable();
Object.assign(c.balls.player, { x: 270, y: 30 }); Object.assign(c.balls.player, { x: 270, y: 30 });
c.angle = Math.PI / 4; c.power = 0.3; c.angle = Math.PI / 4; c.power = 0.2; // la stessa velocità di 0,3 prima del tiro più forte
const p = predict(c), [start, rail, end] = p.points; const p = predict(c), [start, rail, end] = p.points;
assert.equal(p.points.length, 3); assert.equal(p.points.length, 3);
near(rail[0], LEN - R); near(rail[0], LEN - R);