import test from 'node:test'; import assert from 'node:assert/strict'; import { newBasket, press, release, cancel, update, shoot, setAngle, setPower, aimScreen, nudge, arc, status, canThrow, releasePoint, powerAt, SWING, BALLS, SPOTS, LEVELS, RIM, BOARD, FLOOR, BALL, ANGLE } from '../src/engine/basket.js'; import { story } from '../src/story/story.js'; import { Game } from '../src/engine/game.js'; import { settle } from './helpers.mjs'; const fly = c => { for (let i = 0; i < 2000 && c.step === 'flight'; i++) update(c, 1 / 120); return c.ball; }; const rest = c => { for (let i = 0; i < 200 && c.step === 'settle'; i++) update(c, 1 / 60); }; // Cerca un tiro che entra dalla postazione di adesso: la mano ferma dei test. function goodShot(c) { for (let angle = 40; angle <= 70; angle += 1) for (let power = 0.2; power <= 1; power += 0.02) { const probe = structuredClone(c); setAngle(probe, angle); setPower(probe, power); shoot(probe); if (fly(probe).scored) return { angle, power }; } return null; } test('la forza sale finché si tiene premuto; un tocco breve non spreca il pallone', () => { const c = newBasket(1); assert.equal(powerAt(SWING / 2), 0.5); assert.equal(powerAt(SWING * 2), 1); assert.ok(press(c)); update(c, 0.05); assert.ok(release(c)); // troppo breve: annullato assert.equal(c.thrown, 0); assert.equal(c.power, 0.5, 'la forza torna com\'era'); assert.ok(press(c)); update(c, 0.7); assert.ok(release(c)); assert.equal(c.thrown, 1); assert.equal(c.step, 'flight'); assert.ok(Math.abs(c.ball.vx) > 0 && c.ball.vy < 0, 'la palla parte in alto verso il canestro'); assert.ok(!press(c), 'in volo non si carica'); assert.ok(!cancel(c)); }); test('la mira: l\'angolo segue il punto toccato, resta fra i limiti, non si tira all\'indietro', () => { const c = newBasket(2), r = releasePoint(c); assert.ok(aimScreen(c, [r.x + 100, r.y - 100])); assert.equal(Math.round(c.angle), 45); assert.ok(aimScreen(c, [r.x + 10, r.y - 400])); assert.equal(c.angle, ANGLE.max, 'non oltre il massimo'); assert.ok(aimScreen(c, [r.x + 300, r.y])); assert.equal(c.angle, ANGLE.min, 'non sotto il minimo'); assert.ok(!aimScreen(c, [r.x - 50, r.y - 50]), 'dietro di lui niente'); assert.ok(nudge(c, 1)); assert.equal(c.angle, ANGLE.min + 2); assert.ok(nudge(c, 1, true)); assert.equal(c.angle, ANGLE.min + 2.5); press(c); assert.ok(!aimScreen(c, [r.x + 100, r.y - 100]), 'mentre si carica la mira è ferma'); assert.ok(!setPower(c, 0.3)); }); test('la parabola della guida comincia dalle mani e scende per la gravità', () => { const c = newBasket(3), r = releasePoint(c), pts = arc(c, 1); assert.deepEqual(pts[0], r); assert.ok(pts[5].y < r.y, 'prima sale'); assert.ok(pts.at(-1).y > pts[5].y, 'poi scende'); }); test('da ogni postazione esiste un tiro che entra, e un tiro entrato conta canestro e punti', () => { for (const [i, s] of SPOTS.entries()) { const c = newBasket(10 + i); c.spot = i; const shot = goodShot(c); assert.ok(shot, `nessun tiro entra ${s.name}`); setAngle(c, shot.angle); setPower(c, shot.power); shoot(c); const b = fly(c); assert.ok(b.scored && b.dead); assert.ok(c.events.some(e => e.kind === 'basket-swish' || e.kind === 'basket-in'), 'si sente la retina'); rest(c); assert.equal(c.baskets, 1); assert.equal(c.points, s.points); assert.equal(c.spot, Math.min(i + 1, SPOTS.length - 1), 'dopo un canestro si arretra'); assert.equal(c.step, 'aim'); } }); test('un tiro corto batte sul campo e rimbalza; uno lungo prende il tabellone e torna indietro', () => { const c = newBasket(4); c.spot = 3; // da tre, piano: non arriva nemmeno al palo setAngle(c, 45); setPower(c, 0); shoot(c); let b = fly(c); assert.ok(!b.scored && b.bounces > 0, 'rimbalza per terra'); assert.ok(c.events.some(e => e.kind === 'basket-bounce')); assert.ok(b.y + BALL <= FLOOR + 0.01, 'non sprofonda'); const d = newBasket(5); d.spot = 1; setAngle(d, 30); setPower(d, 1); shoot(d); b = fly(d); assert.ok(b.board, 'ha preso il tabellone'); assert.ok(c.events.every(e => e.kind !== 'basket-board') && d.events.some(e => e.kind === 'basket-board')); assert.ok(b.x < BOARD.x, 'non lo attraversa'); }); test('la palla rimbalza sul ferro, e da sotto non è canestro', () => { const c = newBasket(6); c.ball = { x: RIM.x1 - 2, y: RIM.y - 60, vx: 0, vy: 150, time: 0, bounces: 0, rim: false, board: false, under: false, scored: false, dead: false, spin: 0 }; c.step = 'flight'; for (let i = 0; i < 60; i++) update(c, 1 / 120); assert.ok(c.ball.rim, 'il davanti del ferro l\'ha toccata'); assert.ok(c.events.some(e => e.kind === 'basket-rim')); const d = newBasket(7); d.ball = { x: (RIM.x1 + RIM.x2) / 2, y: RIM.y + 20, vx: 0, vy: -200, time: 0, bounces: 0, rim: false, board: false, under: false, scored: false, dead: false, spin: 0 }; d.step = 'flight'; fly(d); assert.ok(d.ball.under && !d.ball.scored, 'sale attraverso l\'anello e ricade: non vale'); }); test('dieci palloni, poi il risultato: la gloria dipende dal livello', () => { for (const [level, { goal }] of Object.entries(LEVELS)) { const c = newBasket(8, { level }); let made = 0; for (let i = 0; i < BALLS; i++) { assert.ok(canThrow(c)); const shot = made < goal ? goodShot(c) : { angle: 45, power: 0 }; // quanto basta per la gloria, poi a vuoto setAngle(c, shot.angle); setPower(c, shot.power); shoot(c); if (fly(c).scored) made++; rest(c); } assert.equal(c.step, 'result'); assert.equal(c.thrown, BALLS); assert.ok(!canThrow(c)); assert.equal(c.result.baskets, goal); assert.equal(c.result.winner, 'player', level); assert.match(status(c), /canestri su 10/); assert.ok(!shoot(c), 'finita'); } const miss = newBasket(9, { level: 'bona' }); for (let i = 0; i < BALLS; i++) { setAngle(miss, 45); setPower(miss, 0); shoot(miss); fly(miss); rest(miss); } assert.equal(miss.result.winner, 'cpu'); assert.equal(miss.spot, 0, 'senza canestri non ci si muove'); }); test('nella storia: dal campo di Via Cipriani si sceglie il livello e parte il gioco; la gloria lascia il flag', () => { const g = new Game(story); g.go('via_cipriani', [340, 316]); settle(g); g.act('use', 'campo_basket'); settle(g); assert.ok(g.dialogue, 'si decide come tirare'); g.choose(g.options().findIndex(o => o.text === 'Due tiri seri.')); settle(g); assert.equal(g.closeup?.id, 'basket'); assert.equal(g.closeup.level, 'seria'); g.closeup.result = { winner: 'player', baskets: 6, points: 13, goal: 5 }; g.closeup.step = 'result'; g.closeupClose(); settle(g); assert.ok(g.state.flags.basket_vinto, 'la gloria resta'); assert.ok(!g.closeup); });