import test from 'node:test'; import assert from 'node:assert/strict'; import { newBocce, press, release, update, powerAt, score, thrower, left, project, SWING, LANE, BALLS } from '../src/engine/bocce.js'; import { story } from '../src/story/story.js'; import { Game } from '../src/engine/game.js'; import { settle } from './helpers.mjs'; const run = (c, seconds, dt = 1 / 60) => { for (let t = 0; t < seconds; t += dt) update(c, dt); }; // Tiene premuto finché la forza porta la boccia a `u`, poi lascia (nella prima salita della barra). function throwTo(c, u) { assert.ok(press(c)); const want = (u - 8) / (LANE - 4); while (c.power < want && c.holdTime < SWING / 2) update(c, 1 / 240); assert.ok(release(c)); } test('la forza sale finché si tiene premuto, fino al massimo: niente tempismo da azzeccare', () => { assert.equal(powerAt(0), 0); assert.ok(Math.abs(powerAt(SWING / 2) - 0.5) < 1e-9); assert.equal(powerAt(SWING), 1); assert.equal(powerAt(SWING * 3), 1, 'oltre il massimo resta piena, non ricomincia'); }); test('una boccia va lontano quanto dice la forza, verso il pallino', () => { const c = newBocce(7); c.jack.u = 95; c.jack.w = 0; // il pallino lontano, così non lo tocca throwTo(c, 50); for (let i = 0; i < 600 && c.step === 'rolling'; i++) update(c, 1 / 60); // fino a boccia ferma, prima che tiri il campione const b = c.balls[0]; assert.ok(Math.abs(b.u - 50) < 2.5, `si ferma a ${b.u.toFixed(1)}, non a 50`); assert.ok(Math.abs(b.w) < 4, 'va più o meno dritta'); run(c, 1); assert.equal(thrower(c), 'cpu', 'dopo la pausa, tira il campione'); }); test('una boccia che arriva addosso al pallino lo sposta', () => { const c = newBocce(3); c.jack.u = 40; c.jack.w = 0; c.balls.push({ who: 'player', u: 30, w: 0, vu: 30, vw: 0, r: 1.25 }); c.step = 'rolling'; run(c, 3); assert.ok(c.jack.u > 41, `il pallino resta a ${c.jack.u.toFixed(1)}`); }); test('il punto: vince la boccia più vicina, e conta quelle più vicine della migliore avversaria', () => { const c = newBocce(1); c.jack.u = 60; c.jack.w = 0; const put = (who, u) => c.balls.push({ who, u, w: 0, vu: 0, vw: 0, r: 1.25 }); put('player', 61); put('player', 63); put('player', 80); put('cpu', 64); put('cpu', 70); put('cpu', 30); assert.deepEqual(score(c), { winner: 'player', points: 2 }); c.balls[0].u = 75; c.balls[1].u = 76; assert.equal(score(c).winner, 'cpu'); }); test('una mano intera: tre bocce a testa, a turno, poi il risultato', () => { const c = newBocce(42); for (let i = 0; i < 200 && c.step !== 'result'; i++) { if (c.step === 'aim' && thrower(c) === 'player') throwTo(c, c.jack.u); run(c, 0.5); } assert.equal(c.step, 'result'); assert.equal(left(c, 'player'), 0); assert.equal(left(c, 'cpu'), 0); assert.equal(c.balls.length, BALLS * 2); assert.ok(['player', 'cpu'].includes(c.result.winner)); assert.equal(press(c), false, 'a mano finita non si tira più'); assert.ok(['player', 'cpu'].includes(c.result.winner) || c.result.winner === null); }); test('la prospettiva: lontano è più in alto e più piccolo, al centro della pista', () => { const near = project(0, 0), far = project(LANE, 0); assert.ok(far.y < near.y && far.s < near.s); assert.ok(project(50, 5).x > project(50, -5).x); }); test('al pallaio: Mariano sfida, la mano si gioca nel primo piano; vincere vale la gloria e la promessa', () => { const g = new Game(story); settle(g); g.go('tennis_chiesa'); settle(g); assert.ok(g.act('talk', 'mariano')); settle(g); g.choose(g.options().findIndex(o => o.text.startsWith('Mariano! Facciamo'))); const before = settle(g).map(s => s.text); assert.ok(before.some(t => t.includes('Brento Sanico')), 'la posta la dice prima'); assert.equal(g.closeup?.id, 'bocce'); assert.equal(g.act('look', 'pallaio'), false, 'durante la partita la scena non risponde'); // Si gioca la mano (le bocce di Andrea mirano al pallino); a mano finita il risultato si forza a favore. const c = g.closeup; for (let i = 0; i < 400 && c.step !== 'result'; i++) { if (c.step === 'aim' && thrower(c) === 'player') { g.closeupPress(); while (c.power < (c.jack.u - 8) / (LANE - 4) && c.holdTime < SWING / 2) g.update(1 / 240); g.closeupRelease(); } g.update(0.25); } assert.equal(c.step, 'result'); c.result = { winner: 'player', points: 1 }; for (let i = 0; i < 20; i++) g.update(0.25); assert.equal(g.closeup, c, 'il risultato resta a schermo finché non si torna al pallaio'); assert.ok(g.closeupClose()); const after = settle(g).map(s => `${s.who}: ${s.text}`); assert.ok(after.includes('mariano: Hai vinto, ti ci porto!')); assert.ok(g.state.flags.bocce_vinte); // Vinto, si chiede la rivincita o Brento Sanico; lasciare la partita a metà non segna niente. g.act('talk', 'mariano'); settle(g); assert.ok(g.options().some(o => o.text === 'La rivincita?') && g.options().some(o => o.text === 'Allora, Brento Sanico?')); g.choose(g.options().findIndex(o => o.text === 'La rivincita?')); settle(g); assert.equal(g.closeup?.id, 'bocce'); assert.ok(g.closeupClose()); assert.ok(settle(g).some(s => s.text.startsWith('Già stanco?'))); });