Circolo 4 Maggio: dal portone in piazza si entra nella sala dei biliardi (sfondo in stile Lucas fatto da Andrea con Codex) e al primo tavolo si gioca a biliardo coi cinque birilli contro l'Ing. Tozzi (battute provvisorie, sprite da fare). Motore nuovo: fisica delle biglie e delle sponde, birilli che cadono, regole e falli del gioco italiano, avversario che prova i colpi a vuoto un po' per fotogramma; disegno dall'alto con stecca, linea di mira e biglia fantasma; stessi comandi delle bocce (pannello, tastiera, pressione); suoni; test; nelle idee la foto per i rigori
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
// Biliardo all'italiana, cinque birilli, visto dall'alto. Regole da circolo, semplificate:
|
||||
// si colpisce la propria biglia e la si manda sulla biglia avversaria; i birilli abbattuti dalla biglia avversaria
|
||||
// o dal pallino fanno punti a chi tira (bianco 2, rosso 4, il rosso da solo 10), il pallino toccato dalla biglia
|
||||
// avversaria ne vale 3. Fallo, con i punti all'avversario (almeno 2): mancare la biglia avversaria, toccare prima
|
||||
// il pallino, abbattere birilli con la propria biglia. Un colpo a testa; vince chi arriva prima al traguardo.
|
||||
// Unità: centimetri. x lungo il tavolo (0..LEN), y di traverso (0..WID).
|
||||
export const LEN = 284, WID = 142, R = 3.05, PIN = 0.9, GAP = 6.3, TARGET = 20;
|
||||
export const MIN_SPEED = 50, MAX_SPEED = 520, FRICTION = 70, DRAG = 0.35, CUSHION = 0.78;
|
||||
export const SWING = 1.4, FIXED_STEP = 1 / 240;
|
||||
const EPS = 0.5, REST = 0.6, CPU_THINK = 0.2, CPU_SHOT = 1.5;
|
||||
export const OTHER = { player: 'cpu', cpu: 'player' };
|
||||
// Dove sta il tavolo nel primo piano 640×360: angolo in alto a sinistra del panno e pixel per centimetro.
|
||||
export const VIEW = { x: 57, y: 78, k: 1.85 };
|
||||
|
||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||
const deviation = c => random(c) + random(c) + random(c) - 1.5;
|
||||
const ball = (role, x, y) => ({ role, x, y, vx: 0, vy: 0 });
|
||||
const list = c => [c.balls.player, c.balls.cpu, c.balls.pallino];
|
||||
const angleTo = (a, b) => Math.atan2(b.y - a.y, b.x - a.x);
|
||||
|
||||
// Il castello: il rosso in mezzo al tavolo, i quattro bianchi a croce intorno.
|
||||
export function castle() {
|
||||
const cx = LEN / 2, cy = WID / 2;
|
||||
return [[0, 0, true], [-GAP, 0], [GAP, 0], [0, -GAP], [0, GAP]].map(([dx, dy, red]) => ({ x: cx + dx, y: cy + dy, red: !!red, down: false }));
|
||||
}
|
||||
|
||||
export const powerAt = time => clamp(time / SWING, 0, 1);
|
||||
export const speedFor = power => MIN_SPEED + (MAX_SPEED - MIN_SPEED) * power;
|
||||
// Quanta strada fa una biglia lanciata a velocità v, senza urti (attrito costante più un po' di resistenza).
|
||||
export function reach(v) {
|
||||
const k = FRICTION / DRAG, t = Math.log((v + k) / k) / DRAG;
|
||||
return (v + k) / DRAG * (1 - Math.exp(-DRAG * t)) - k * t;
|
||||
}
|
||||
export const canThrow = c => c.step === 'aim' && c.current === 'player';
|
||||
export const moving = c => list(c).some(b => b.vx || b.vy);
|
||||
|
||||
export function newBiliardo(seed = Date.now() >>> 0, def = {}) {
|
||||
const c = { step: 'aim', t: 0, seed, current: 'player', target: def.target ?? TARGET, power: 0.45, angle: 0,
|
||||
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.pins = castle();
|
||||
c.angle = angleTo(c.balls.player, c.balls.cpu);
|
||||
return c;
|
||||
}
|
||||
|
||||
// Valore dei birilli abbattuti in un colpo.
|
||||
export function value(pins) {
|
||||
if (pins.length === 1 && pins[0].red) return 10;
|
||||
return pins.reduce((s, p) => s + (p.red ? 4 : 2), 0);
|
||||
}
|
||||
// Chi prende i punti di un colpo, e quanti.
|
||||
export function judge(shot) {
|
||||
const shooter = shot.shooter, other = OTHER[shooter];
|
||||
const own = shot.pins.filter(p => p.by === shooter);
|
||||
const foul = shot.first === null ? 'miss' : shot.first === 'pallino' ? 'pallino' : own.length ? 'own' : null;
|
||||
if (foul) return { shooter, to: other, points: Math.max(2, value(shot.pins)), foul };
|
||||
return { shooter, to: shooter, points: value(shot.pins) + (shot.pallino ? 3 : 0), foul: null };
|
||||
}
|
||||
|
||||
function cushions(b) {
|
||||
let impact = 0;
|
||||
for (const [p, v, hi] of [['x', 'vx', LEN - R], ['y', 'vy', WID - R]]) {
|
||||
if (b[p] < R || b[p] > hi) {
|
||||
const out = b[p] < R ? b[v] < 0 : b[v] > 0;
|
||||
b[p] = clamp(b[p], R, hi);
|
||||
if (out) { impact = Math.max(impact, Math.abs(b[v])); b[v] *= -CUSHION; }
|
||||
}
|
||||
}
|
||||
return impact;
|
||||
}
|
||||
// Urto fra due biglie uguali, quasi elastico; separa anche quelle ferme e sovrapposte.
|
||||
export function collide(a, b) {
|
||||
const dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy);
|
||||
if (d >= 2 * R) return 0;
|
||||
const nx = d > 1e-9 ? dx / d : 1, ny = d > 1e-9 ? dy / d : 0, overlap = (2 * R - d) / 2;
|
||||
a.x -= nx * overlap; a.y -= ny * overlap; b.x += nx * overlap; b.y += ny * overlap;
|
||||
const closing = (a.vx - b.vx) * nx + (a.vy - b.vy) * ny;
|
||||
if (closing <= 0) return 0;
|
||||
const j = closing * 0.975;
|
||||
a.vx -= j * nx; a.vy -= j * ny; b.vx += j * nx; b.vy += j * ny;
|
||||
return closing;
|
||||
}
|
||||
function touch(shot, a, b) {
|
||||
if (!shot) return;
|
||||
const roles = [a.role, b.role], other = OTHER[shot.shooter];
|
||||
if (shot.first === null && roles.includes(shot.shooter)) shot.first = roles.find(r => r !== shot.shooter);
|
||||
if (roles.includes(other) && roles.includes('pallino')) shot.pallino = true;
|
||||
}
|
||||
function step(w, dt) {
|
||||
let hit = 0, rail = 0;
|
||||
for (const b of w.balls) {
|
||||
const v = Math.hypot(b.vx, b.vy);
|
||||
if (v < EPS) { b.vx = b.vy = 0; continue; }
|
||||
const nv = Math.max(0, v - (FRICTION + DRAG * v) * dt), travel = (v + nv) / 2 * dt;
|
||||
b.x += b.vx / v * travel; b.y += b.vy / v * travel;
|
||||
b.vx *= nv / v; b.vy *= nv / v;
|
||||
rail = Math.max(rail, cushions(b));
|
||||
for (const p of w.pins) {
|
||||
if (p.down || Math.hypot(b.x - p.x, b.y - p.y) >= R + PIN) continue;
|
||||
// Il birillo cade dalla parte opposta a chi lo colpisce; la biglia perde un po' di slancio.
|
||||
Object.assign(p, { down: true, by: b.role, dir: Math.atan2(p.y - b.y, p.x - b.x) });
|
||||
b.vx *= 0.88; b.vy *= 0.88;
|
||||
w.shot?.pins.push(p);
|
||||
if (w.events && w.events.length < 16) w.events.push({ kind: 'biliardo-pin', power: Math.min(1, v / 300) });
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < w.balls.length; i++) for (let j = i + 1; j < w.balls.length; j++) {
|
||||
const s = collide(w.balls[i], w.balls[j]);
|
||||
if (s > 0) { touch(w.shot, w.balls[i], w.balls[j]); hit = Math.max(hit, s); }
|
||||
}
|
||||
if (w.events && w.events.length < 16 && (hit > 12 || rail > 12))
|
||||
w.events.push({ kind: hit >= rail ? 'biliardo-hit' : 'biliardo-rail', power: Math.min(1, Math.max(hit, rail) / 400) });
|
||||
}
|
||||
|
||||
function strike(c, who, angle, power) {
|
||||
const b = c.balls[who], v = speedFor(power);
|
||||
b.vx = Math.cos(angle) * v; b.vy = Math.sin(angle) * v;
|
||||
c.shot = { shooter: who, first: null, pallino: false, pins: [] };
|
||||
c.step = 'rolling'; c.t = c.accumulator = 0; c.angle = angle; c.power = power; c.plan = null;
|
||||
c.events.push({ kind: 'biliardo-cue', power });
|
||||
return true;
|
||||
}
|
||||
// Il colpo provato a vuoto, su una copia del tavolo: serve all'avversario per scegliere.
|
||||
export function simulate(c, who, angle, power) {
|
||||
const balls = list(c).map(b => ({ ...b })), pins = c.pins.map(p => ({ ...p }));
|
||||
const w = { balls, pins, shot: { shooter: who, first: null, pallino: false, pins: [] } };
|
||||
const b = balls.find(x => x.role === who), v = speedFor(power);
|
||||
b.vx = Math.cos(angle) * v; b.vy = Math.sin(angle) * v;
|
||||
for (let i = 0; i < 15 / FIXED_STEP && balls.some(x => x.vx || x.vy); i++) step(w, FIXED_STEP);
|
||||
return judge(w.shot);
|
||||
}
|
||||
|
||||
// I birilli si rialzano; una biglia ferma sopra il posto di un birillo si sposta appena quanto basta.
|
||||
function reset(c) {
|
||||
for (const p of c.pins) { p.down = false; delete p.by; delete p.dir; }
|
||||
for (let pass = 0; pass < 4; pass++) {
|
||||
for (const b of list(c)) for (const p of c.pins) {
|
||||
const d = Math.hypot(b.x - p.x, b.y - p.y), need = R + PIN + 0.3;
|
||||
if (d >= need) continue;
|
||||
const a = d > 1e-6 ? Math.atan2(b.y - p.y, b.x - p.x) : -Math.PI / 2;
|
||||
b.x = p.x + Math.cos(a) * need; b.y = p.y + Math.sin(a) * need;
|
||||
}
|
||||
const all = list(c);
|
||||
for (let i = 0; i < 3; i++) for (let j = i + 1; j < 3; j++) collide(all[i], all[j]);
|
||||
all.forEach(cushions);
|
||||
}
|
||||
}
|
||||
function finish(c) {
|
||||
const j = judge(c.shot);
|
||||
c.points[j.to] += j.points;
|
||||
c.last = { ...j, age: 0 };
|
||||
c.shot = null;
|
||||
reset(c);
|
||||
c.t = 0;
|
||||
if (c.points[j.to] >= c.target) { c.step = 'result'; c.result = { winner: j.to, points: { ...c.points } }; return; }
|
||||
c.current = OTHER[j.shooter];
|
||||
c.step = 'aim';
|
||||
c.angle = angleTo(c.balls[c.current], c.balls[OTHER[c.current]]);
|
||||
}
|
||||
|
||||
// L'avversario prova tanti colpi a vuoto e sceglie quello che rende di più; poi la mano gli trema un poco.
|
||||
// Le prove si fanno a pezzi, un po' per fotogramma: tutte insieme fermerebbero il gioco per un decimo di secondo.
|
||||
function cpuCandidates(c) {
|
||||
const base = angleTo(c.balls.cpu, c.balls.player), angles = [];
|
||||
for (let i = 0; i < 64; i++) angles.push(i / 64 * Math.PI * 2);
|
||||
for (let d = -0.3; d <= 0.3; d += 0.025) angles.push(base + d);
|
||||
return angles.flatMap(angle => [0.3, 0.5, 0.7, 0.9].map(power => ({ angle, power })));
|
||||
}
|
||||
function cpuThink(c, budget = 12) {
|
||||
const t = c.thinking ??= { todo: cpuCandidates(c), best: null };
|
||||
for (let i = 0; i < budget && t.todo.length; i++) {
|
||||
const { angle, power } = t.todo.pop(), j = simulate(c, 'cpu', angle, power), gain = j.to === 'cpu' ? j.points : -j.points;
|
||||
if (!t.best || gain > t.best.gain) t.best = { angle, power, gain };
|
||||
}
|
||||
if (t.todo.length) return;
|
||||
c.plan = { angle: t.best.angle + deviation(c) * (c.wobble ?? 0.03), power: clamp(t.best.power + deviation(c) * 0.05, 0.05, 1) };
|
||||
c.angle = c.plan.angle; c.power = c.plan.power; c.thinking = null;
|
||||
}
|
||||
|
||||
export function update(c, dt) {
|
||||
if (!Number.isFinite(dt) || dt <= 0) return;
|
||||
dt = Math.min(dt, 0.25); c.t += dt;
|
||||
if (c.last) c.last.age += dt;
|
||||
if (c.holding) { c.holdTime += dt; c.power = powerAt(c.holdTime); }
|
||||
if (c.step === 'rolling') {
|
||||
c.accumulator += dt;
|
||||
const w = { balls: list(c), pins: c.pins, shot: c.shot, events: c.events };
|
||||
while (c.accumulator >= FIXED_STEP) { step(w, FIXED_STEP); c.accumulator -= FIXED_STEP; }
|
||||
if (!moving(c)) { c.step = 'settle'; c.t = 0; }
|
||||
} else if (c.step === 'settle' && c.t >= REST) finish(c);
|
||||
else if (c.step === 'aim' && c.current === 'cpu') {
|
||||
if (!c.plan && c.t >= CPU_THINK) cpuThink(c);
|
||||
if (c.plan && c.t >= CPU_SHOT) strike(c, 'cpu', c.plan.angle, c.plan.power);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Comandi del giocatore: gli stessi delle bocce.
|
||||
export function aimAt(c, x, y) {
|
||||
if (!canThrow(c) || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||
const b = c.balls.player;
|
||||
if (Math.hypot(x - b.x, y - b.y) < R) return false;
|
||||
c.angle = Math.atan2(y - b.y, x - b.x); return true;
|
||||
}
|
||||
export function aimScreen(c, [sx, sy]) { return aimAt(c, (sx - VIEW.x) / VIEW.k, (sy - VIEW.y) / VIEW.k); }
|
||||
export function nudge(c, dir) { if (!canThrow(c) || c.holding) return false; c.angle += dir * 0.01; return true; }
|
||||
export function setPower(c, value) {
|
||||
if (!canThrow(c) || c.holding || !Number.isFinite(value)) return false;
|
||||
c.power = clamp(value, 0, 1); return true;
|
||||
}
|
||||
export function shoot(c) { return canThrow(c) && !c.holding && strike(c, 'player', c.angle, c.power); }
|
||||
export function press(c) {
|
||||
if (!canThrow(c) || c.holding) return false;
|
||||
c.previousPower = c.power; c.holding = true; c.holdTime = c.power = 0; return true;
|
||||
}
|
||||
export function cancel(c) {
|
||||
if (!c.holding) return false;
|
||||
c.holding = false; c.power = c.previousPower; c.holdTime = 0; return true;
|
||||
}
|
||||
export function release(c) {
|
||||
if (!c.holding) return false;
|
||||
if (c.holdTime < 0.14) return cancel(c); // un tocco breve mira soltanto
|
||||
c.holding = false; return strike(c, 'player', c.angle, c.power);
|
||||
}
|
||||
|
||||
// Dove andrebbe la biglia: la linea fino al primo urto (con le sponde, fino a dove arriva con quella forza),
|
||||
// la biglia fantasma dove tocca, e verso dove partirebbe quella colpita.
|
||||
export function predict(c) {
|
||||
const me = c.balls[c.current], others = list(c).filter(b => b !== me);
|
||||
let x = me.x, y = me.y, dx = Math.cos(c.angle), dy = Math.sin(c.angle), left = reach(speedFor(c.power));
|
||||
const points = [[x, y]], STEP = 0.5;
|
||||
for (let n = 0; left > 0 && n < 6000; n++) {
|
||||
x += dx * STEP; y += dy * STEP; left -= STEP;
|
||||
if (x < R || x > LEN - R) { x = clamp(x, R, LEN - R); dx = -dx; left *= 0.6; points.push([x, y]); }
|
||||
if (y < R || y > WID - R) { y = clamp(y, R, WID - R); dy = -dy; left *= 0.6; points.push([x, y]); }
|
||||
for (const o of others) {
|
||||
const d = Math.hypot(o.x - x, o.y - y);
|
||||
if (d < 2 * R) { points.push([x, y]); return { points, ghost: { x, y }, hit: o.role, dir: [(o.x - x) / d, (o.y - y) / d] }; }
|
||||
}
|
||||
for (const p of c.pins) if (!p.down && Math.hypot(p.x - x, p.y - y) < R + PIN) { points.push([x, y]); return { points, ghost: { x, y }, hit: 'pin', dir: null }; }
|
||||
}
|
||||
points.push([x, y]);
|
||||
return { points, ghost: null, hit: null, dir: null };
|
||||
}
|
||||
|
||||
const FOULS = { miss: 'biglia avversaria mancata', pallino: 'prima il pallino', own: 'birilli con la propria biglia' };
|
||||
export function lastLine(c, names) {
|
||||
const l = c.last;
|
||||
if (!l) return '';
|
||||
if (l.foul) return `Fallo di ${names[l.shooter]} (${FOULS[l.foul]}): ${l.points} a ${names[l.to]}.`;
|
||||
return l.points ? `${names[l.to]} fa ${l.points} punti!` : `${names[l.shooter]}: nessun punto.`;
|
||||
}
|
||||
export function status(c, names) {
|
||||
if (c.step === 'result') return `Partita a ${names[c.result.winner]}: ${c.points.player} a ${c.points.cpu}.`;
|
||||
if (c.step === 'rolling') return 'Biglie in movimento…';
|
||||
if (c.step === 'settle') return 'Si contano i birilli…';
|
||||
const last = lastLine(c, names);
|
||||
return `${last ? last + ' ' : ''}${names[c.current]} al tiro. (${c.points.player} a ${c.points.cpu}, si va a ${c.target})`;
|
||||
}
|
||||
Reference in New Issue
Block a user