import test from 'node:test'; import assert from 'node:assert/strict'; import * as m from '../src/engine/flipper.js'; import { drawFlipper } from '../src/engine/flipper-render.js'; import { Game } from '../src/engine/game.js'; import { story } from '../src/story/story.js'; import { settle } from './helpers.mjs'; const run = (c, secs) => { for (let i = 0; i < secs * 60; i++) m.update(c, 1 / 60); }; // Il giocatore automatico: alza il flipper quando la pallina gli scende addosso, come un umano attento. function autoplay(c, secs = 600) { for (let i = 0; i < secs * 60 && c.step === 'play'; i++) { if (m.waiting(c)) m.launch(c, 0.75); const b = c.ball; for (const side of ['left', 'right']) { const [px, py] = m.PIVOT[side], dir = side === 'left' ? 1 : -1, rel = (b ? b.x - px : 0) * dir; const near = b && !b.waiting && b.vy > 0 && b.y > py - 34 && b.y < py + 14 && rel > -6 && rel < 44; if (near !== c.flippers[side].held.includes('ai')) m.hold(c, side, 'ai', near); } m.update(c, 1 / 60); } } test('la tavola: tutto sta dentro il campo, le corsie sotto l\'arco, i funghi sui dischi del disegno', () => { const c = m.newFlipper(1); for (const w of c.walls) for (const [x, y] of [w.a, w.b]) assert.ok(x >= m.FIELD.x0 && x <= m.FIELD.x1 && y >= m.FIELD.y0 && y <= m.FIELD.y1, `${w.a}-${w.b}`); for (const b of c.bumpers) assert.ok(b.x > m.FIELD.x0 + 20 && b.x < m.LANE.x0 - 20); assert.equal(c.lanes.length, 4); assert.deepEqual(c.lanes.map(l => l.rione), m.RIONI); assert.equal(c.targets.length, 6); assert.equal(c.balls, m.BALLS); assert.ok(m.waiting(c) && m.canThrow(c) && c.ball.x > m.LANE.x0, 'la pallina aspetta sulla molla'); }); test('il lancio: la molla manda la pallina su per la corsia, oltre il cancelletto, e non torna più nella corsia', () => { const c = m.newFlipper(2, { level: 'bona' }); assert.ok(m.launch(c, 0.8)); assert.ok(!m.waiting(c)); let top = 999, left = false; for (let i = 0; i < 180; i++) { m.update(c, 1 / 60); top = Math.min(top, c.ball?.y ?? 999); if (c.ball && c.ball.x < m.LANE.x0 - 30) left = true; } assert.ok(top < 40, `è arrivata in cima (${top.toFixed(0)})`); assert.ok(left, 'è uscita dalla corsia'); // Un lancio fiacco ricasca sulla molla e si rilancia. const d = m.newFlipper(3, { level: 'bona' }); m.launch(d, 0); run(d, 3); assert.ok(m.waiting(d) && d.balls === m.BALLS, 'tornata sulla molla, nessuna pallina persa'); }); test('la pallina persa: tre palline, poi il risultato; il salvataggio vale una volta sola', () => { const c = m.newFlipper(4, { level: 'seria' }); for (let n = 3; n >= 1; n--) { assert.equal(c.balls, n); m.launch(c, 0.9); c.saveUntil = 0; // niente salvataggio: si prova sotto c.ball.x = 320; c.ball.y = 320; c.ball.vx = 0; c.ball.vy = 200; run(c, 1); } assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'cpu'); assert.ok(!m.canThrow(c)); assert.match(m.status(c), /Ne servivano 12000/); const d = m.newFlipper(5, { level: 'bona' }); m.launch(d, 0.9); d.ball.x = 320; d.ball.y = 320; d.ball.vx = 0; d.ball.vy = 200; run(d, 1); assert.ok(m.waiting(d) && d.balls === 3, 'salvata: la pallina torna sulla molla'); assert.match(d.line ?? '', /salvata/); m.launch(d, 0.9); d.ball.x = 320; d.ball.y = 320; d.ball.vx = 0; d.ball.vy = 200; run(d, 1); assert.equal(d.balls, 2, 'la seconda volta è persa davvero'); }); test('il flipper alzato rimanda su la pallina; abbassato la lascia scendere', () => { const up = m.newFlipper(6, { level: 'seria' }); m.launch(up, 0.9); Object.assign(up.ball, { x: 300, y: 300, vx: 0, vy: 150 }); m.hold(up, 'left', 'test', true); let best = 0; for (let i = 0; i < 60; i++) { m.update(up, 1 / 60); best = Math.min(best, up.ball?.vy ?? 0); } assert.ok(best < -250, `tirata su (vy ${best.toFixed(0)})`); assert.ok(up.events.some(e => e.kind === 'flipper-flip')); const down = m.newFlipper(6, { level: 'seria' }); m.launch(down, 0.9); Object.assign(down.ball, { x: 300, y: 300, vx: 0, vy: 150 }); down.saveUntil = 0; run(down, 2); assert.equal(down.balls, 2, 'senza flipper, giù nella Sieve'); }); test('funghi, bersagli, Regolo e corsie fanno punti; le quattro corsie insieme il Palio', () => { const c = m.newFlipper(7, { level: 'bona' }); m.launch(c, 0.9); const b = c.bumpers[0]; Object.assign(c.ball, { x: b.x, y: b.y + b.r + 6, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60); assert.equal(c.score, m.SCORE.bumper); assert.ok(c.ball.vy > 0 && b.hit > 0, 'respinta'); assert.ok(c.events.some(e => e.kind === 'flipper-fungo')); c.score = 0; for (const t of c.targets.filter(k => k.bank === 'pieve')) { const before = c.score; Object.assign(c.ball, { x: t.a[0] + 10, y: (t.a[1] + t.b[1]) / 2, vx: -300, vy: 0 }); m.update(c, 1 / 60); m.update(c, 1 / 60); assert.ok(c.score > before, `caduto il ${t.i}`); } assert.equal(c.score, 3 * m.SCORE.target + m.SCORE.bank); assert.ok(c.targets.filter(k => k.bank === 'pieve').every(t => !t.down), 'la fila si rialza'); assert.match(c.line, /Pieve/); c.score = 0; Object.assign(c.ball, { x: c.regolo.x, y: c.regolo.y + 12, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60); assert.equal(c.score, m.SCORE.regolo); c.score = 0; for (const l of c.lanes) { Object.assign(c.ball, { x: l.x, y: l.y - 10, vx: 0, vy: 100 }); m.update(c, 1 / 60); m.update(c, 1 / 60); assert.ok(l.lit || c.lanes.every(k => !k.lit), l.rione); } assert.equal(c.score, 4 * m.SCORE.lane + m.SCORE.palio); assert.match(c.line, /PALIO/); assert.ok(c.lanes.every(l => !l.lit), 'si rispegne per un altro giro'); }); test('comandi: tenere premuto carica la molla e lasciare lancia; in gioco la metà della scena è il flipper; tasti', () => { const c = m.newFlipper(8, { level: 'bona' }); m.aimScreen(c, [440, 320]); assert.ok(m.press(c, 1)); run(c, 0.6); assert.ok(c.holding && c.power > 0.4 && c.power < 0.6, `molla a ${c.power.toFixed(2)}`); assert.ok(!m.release(c, 2), 'un altro dito non lancia'); assert.ok(m.release(c, 1)); assert.ok(!m.waiting(c)); m.aimScreen(c, [250, 200]); assert.ok(m.press(c, 3)); assert.ok(c.flippers.left.held.includes(3)); m.aimScreen(c, [400, 200]); assert.ok(m.press(c, 4)); assert.ok(c.flippers.right.held.includes(4)); assert.ok(m.release(c, 3)); assert.ok(!c.flippers.left.held.length && c.flippers.right.held.includes(4)); assert.ok(m.cancel(c, 4)); assert.ok(!c.flippers.right.held.length); // I pulsanti e le frecce. assert.ok(m.turn(c, -1, 'button')); assert.ok(c.flippers.left.held.length); m.stopTurn(c, 'button'); assert.ok(!c.flippers.left.held.length); assert.ok(m.key(c, 'ArrowRight', true)); assert.ok(c.flippers.right.held.length); m.key(c, 'ArrowRight', false); assert.ok(!c.flippers.right.held.length); assert.ok(!m.key(c, 'x', true)); // Spazio: carica e lancia; Invio: lancia con la forza del cursore. const d = m.newFlipper(9); assert.ok(m.key(d, ' ', true)); run(d, 0.3); assert.ok(d.holding); m.key(d, ' ', false); assert.ok(!m.waiting(d)); const e = m.newFlipper(10); assert.ok(m.setPower(e, 0.5)); assert.equal(m.button(e), 'Lancia'); m.key(e, 'Enter', true); assert.ok(!m.waiting(e)); assert.equal(m.button(e), 'In gioco…'); }); test('giocando con attenzione, alla bona si arriva al traguardo; nessuna partita si incastra', () => { let won = 0; for (let seed = 1; seed <= 8; seed++) { const c = m.newFlipper(seed * 7919, { level: 'bona' }); autoplay(c); assert.equal(c.step, 'result', `seme ${seed}: finita`); if (c.result.winner === 'player') won++; } assert.ok(won >= 4, `vinte ${won} su 8`); }); test('il disegno del flipper non cambia lo stato', () => { const calls = [], gradient = { addColorStop() {} }; const ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => { calls.push([k, ...args]); return gradient; }, set: (o, k, v) => { o[k] = v; return true; } }); const c = m.newFlipper(11, { level: 'bona' }), bg = {}; const draw = () => { const before = structuredClone(c); drawFlipper(ctx, c, story.closeups.flipper, bg); assert.deepEqual(c, before); }; draw(); assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg)); assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'FLIPPER DEL RIVER')); m.launch(c, 0.8); run(c, 1); draw(); c.step = 'result'; c.result = { winner: 'player', score: 7000, goal: 6000 }; calls.length = 0; draw(); assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'TRAGUARDO FATTO!')); }); test('al River: «Usa» o «Apri» il bar, il livello, la partita; vincendo il flag', () => { const g = new Game(story); g.go('via_provinciale'); settle(g); g.act('use', 'river'); settle(g); assert.ok(g.options().some(o => o.text === 'Una partita seria.')); g.choose(g.options().findIndex(o => o.text === 'Una partita seria.')); settle(g); assert.equal(g.closeup?.id, 'flipper'); assert.equal(g.closeup.level, 'seria'); g.closeup.step = 'result'; g.closeup.result = { winner: 'player', score: 13000, goal: 12000 }; g.closeupClose(); settle(g); assert.ok(g.state.flags.flipper_vinto); g.act('open', 'river'); settle(g); g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g); assert.equal(g.closeup?.level, 'male'); g.closeupClose(); assert.equal(g.queue[0]?.say, story.closeups.flipper.quit[0].say); });