Lavori in corso (non pubblicati): Bertovo al tavolo di mezzo coi rinquarti (Codex), Bini, Yuri, il Gianno e la Brauda in gara, l'Audi «Let it be», sensibilità e riquadro dello swipe, pulsanti su iPhone (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-28 23:36:05 +01:00
co-authored by Claude Opus 5.5
parent 6585c9ffcb
commit bf22dc2511
54 changed files with 954 additions and 117 deletions
+138
View File
@@ -0,0 +1,138 @@
// I rinquarti di Bertovo: posizioni coperte, un solo tentativo per posizione.
import * as billiard from './biliardo.js';
const { LEN, WID, R, PIN, simulate, predict } = billiard;
export { aimAt, aimScreen, nudge, setPower, press, release, cancel, shoot, canThrow } from './biliardo.js';
export const LEVELS = {
bona: { positions: 6, cushions: 1, glory: 3, guideBounces: 2, guideLength: Infinity },
seria: { positions: 6, cushions: 2, glory: 3, guideBounces: 1, guideLength: 65 },
male: { positions: 6, cushions: 3, glory: 2, guideBounces: 0, guideLength: 0 },
};
export const canAdjust = c => billiard.canThrow(c) && !c.holding;
export const aimFree = c => !billiard.canThrow(c);
export const button = c => c.step === 'result' ? 'Finita' : c.step === 'aim' ? 'Tira' : 'In movimento…';
const ball = (role, x, y) => ({ role, x, y, vx: 0, vy: 0 });
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
const attempt = () => ({ cushions: 0, decided: false, point: false, reason: null });
// Conta solo le sponde della propria biglia prima del primo ostacolo.
function contact(result, required, { kind, a, b }) {
if (result.decided || (a.role !== 'player' && b?.role !== 'player')) return;
if (kind === 'rail') { result.cushions++; return; }
result.decided = true;
const other = a.role === 'player' ? b : a;
result.point = kind === 'ball' && other.role === 'cpu' && result.cushions >= required;
result.reason = result.point ? null : kind === 'ball' && other.role === 'cpu' ? 'poche sponde' : 'disturbo toccato';
}
export function tryShot(c, angle, power) {
const result = attempt();
simulate(c, 'player', angle, power, {
onContact: event => contact(result, c.skill.cushions, event),
stop: () => result.decided,
});
if (!result.decided) result.reason = 'bersaglio mancato';
return result;
}
// Distanza dal segmento dei centri, con i raggi sommati (biglia in movimento inclusa).
export function directCovered(c) {
const a = c.balls.player, b = c.balls.cpu, dx = b.x - a.x, dy = b.y - a.y, d2 = dx * dx + dy * dy;
if (!d2) return false;
const obstacles = [...Object.values(c.balls).filter(b => !['player', 'cpu'].includes(b.role)).map(b => ({ ...b, radius: R })),
...c.pins.filter(p => !p.down).map(p => ({ ...p, radius: PIN }))];
return obstacles.some(p => {
const t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / d2;
return t > 0 && t < 1 && Math.hypot(p.x - a.x - t * dx, p.y - a.y - t * dy) < R + p.radius;
});
}
export function solve(c) {
// Angoli intercalati: si esplora subito tutto il tavolo, poi si affina a mezzo grado.
for (let offset = 0; offset < 8; offset++) for (let i = offset; i < 720; i += 8) {
const angle = i * Math.PI / 360;
for (const power of [1, 0.82, 0.65]) {
if (tryShot(c, angle, power).point) return { angle, power };
}
}
return null;
}
function position(c) {
for (;;) {
const place = role => ball(role, 20 + random(c) * (LEN - 40), 18 + random(c) * (WID - 36));
const player = place('player'), cpu = place('cpu');
if (Math.hypot(cpu.x - player.x, cpu.y - player.y) < 70) continue;
const t = 0.3 + random(c) * 0.4;
const blocker = ball('blocker', player.x + (cpu.x - player.x) * t, player.y + (cpu.y - player.y) * t);
c.balls = { player, cpu, blocker, extra: place('extra') };
c.pins = Array.from({ length: 2 }, () => ({ x: 20 + random(c) * (LEN - 40), y: 18 + random(c) * (WID - 36), red: false, down: false }));
const objects = [...Object.values(c.balls).map(b => ({ ...b, radius: R })), ...c.pins.map(p => ({ ...p, radius: PIN }))];
if (objects.some((a, i) => objects.slice(i + 1).some(b => Math.hypot(a.x - b.x, a.y - b.y) < a.radius + b.radius + 2))) continue;
if (!directCovered(c)) continue;
const solution = solve(c);
if (!solution) continue;
c.solution = solution;
break;
}
c.position++;
c.step = 'aim'; c.t = c.accumulator = 0; c.power = 0.75; c.holding = false; c.aimLocked = false;
c.shot = c.pending = c.visual.shot = null; c.windup = 0;
c.angle = Math.atan2(c.balls.cpu.y - c.balls.player.y, c.balls.cpu.x - c.balls.player.x);
c.attempt = attempt();
c.line = 'Bertovo: «Oh tirami questo rinquarto!?»';
}
export function newRinquarto(seed = Date.now() >>> 0, def = {}) {
const level = LEVELS[def.level] ? def.level : 'seria';
const c = { ...billiard.newBiliardo(seed), level, skill: LEVELS[level], position: 0, points: 0, history: [] };
position(c);
return c;
}
export function update(c, dt) {
if (!Number.isFinite(dt) || dt <= 0) return;
dt = Math.min(dt, 0.25);
if (c.step === 'result') {
// L’ultimo birillo può essere ancora in caduta: il riepilogo ferma la partita, non l’animazione.
c.visual.time += dt;
return;
}
if (c.step === 'settle') {
c.t += dt;
// I birilli finiscono di cadere anche mentre si legge l'esito del tiro.
c.visual.time += dt;
if (c.t >= 1.8) position(c);
return;
}
billiard.update(c, dt, event => contact(c.attempt, c.skill.cushions, event));
if (c.step !== 'settle') return;
if (!c.attempt.decided) c.attempt.reason = 'bersaglio mancato';
c.points += Number(c.attempt.point);
c.history.push({ ...c.attempt });
c.line = c.attempt.point ? 'Punto!' : `Niente: ${c.attempt.reason}.`;
if (c.position === c.skill.positions) {
c.step = 'result';
c.result = { winner: c.points >= c.skill.glory ? 'player' : 'cpu', points: c.points };
}
}
// La guida si ferma al contatto, al limite di lunghezza o alla sponda consentita.
export function guide(c) {
if (!c.skill.guideLength) return [];
const source = predict(c).points, points = [source[0]];
let remaining = c.skill.guideLength;
for (const end of source.slice(1, c.skill.guideBounces + 1)) {
const start = points.at(-1), dx = end[0] - start[0], dy = end[1] - start[1], distance = Math.hypot(dx, dy);
if (distance > remaining) { points.push([start[0] + dx * remaining / distance, start[1] + dy * remaining / distance]); break; }
points.push(end); remaining -= distance;
}
return points;
}
export function status(c) {
const prefix = `Posizione ${c.position} di ${c.skill.positions}. Punti: ${c.points}.`;
if (c.step === 'result') return `${prefix} ${c.line} ${c.result.winner === 'player' ? 'Gloria!' : 'Sfida persa.'} Obiettivo: ${c.skill.glory}.`;
if (c.step === 'rolling') return `${prefix} Sponde: ${c.attempt.cushions}/${c.skill.cushions}.`;
return `${prefix} ${c.line} ${c.skill.cushions === 1 ? 'Serve almeno 1 sponda.' : `Servono almeno ${c.skill.cushions} sponde.`}`;
}