import test from 'node:test'; import assert from 'node:assert/strict'; import * as m from '../src/engine/bruciate.js'; import { drawBruciate } from '../src/engine/bruciate-render.js'; import { Game } from '../src/engine/game.js'; import { story } from '../src/story/story.js'; import { settle } from './helpers.mjs'; // Gira la manovella tenendola, a `w` radianti al secondo, finché le castagne non paiono cotte (o per `secs` secondi). function crank(c, w, { secs = 60, until = 1.05 } = {}) { c.grab ??= { pointerId: 1, heading: null, last: null }; for (let i = 0; i < secs * 60 && c.step === 'turn' && m.doneness(c) < until; i++) { c.pending = w / 60; m.update(c, 1 / 60); } } function pourAndWait(c) { assert.ok(m.shoot(c), 'si rovescia nel sacco'); for (let i = 0; i < 200 && c.step === 'pour'; i++) m.update(c, 1 / 60); assert.equal(c.step, 'result'); return c.result; } test('girando a modo le bruciate vengono tutte buone, nel tempo del livello', () => { for (const level of ['bona', 'seria', 'male']) { const c = m.newBruciate(42, { level }); crank(c, 4.5); assert.ok(c.t > 6 && c.t < m.LEVELS[level].time, `${level}: cotte in ${c.t.toFixed(1)} s`); const r = pourAndWait(c); assert.equal(r.winner, 'player', `${level}: ${JSON.stringify(r)}`); assert.equal(r.spilled, 0); } }); test('ferme sul fuoco: quelle di sotto carbone, quelle di sopra crude', () => { const c = m.newBruciate(42, { level: 'seria' }); crank(c, 0); const r = pourAndWait(c); assert.equal(r.winner, 'cpu'); assert.ok(r.burnt > 0 && r.raw > 0, JSON.stringify(r)); assert.match(m.status(c), /Ne servivano 15/); }); test('troppo forte è una centrifuga: le castagne volano nel fuoco', () => { const c = m.newBruciate(42, { level: 'seria' }); crank(c, 12, { secs: 4, until: 9 }); assert.ok(c.chestnuts.some(k => k.out), 'qualcuna è volata fuori'); assert.ok(c.flying.length > 0); assert.match(c.line, /centrifuga|fuoco/); assert.ok(c.events.some(e => e.kind === 'bruciate-cade')); assert.equal(m.speedWord(c), 'troppo forte'); }); test('rovesciarle troppo presto: crude; e il pulsante aspetta i primi secondi', () => { const c = m.newBruciate(42, { level: 'bona' }); assert.ok(!m.canThrow(c) && !m.shoot(c), 'appena cominciato non si rovescia'); crank(c, 4.5, { until: 0.4 }); const r = pourAndWait(c); assert.equal(r.winner, 'cpu'); assert.ok(r.raw > 10); }); test('il gesto: i giri, grandi o piccoli e dovunque, fanno girare il cilindro; una riga dritta no', () => { const circles = (c, cx, cy, radius, turns, secs) => { assert.ok(m.press(c, 7)); const steps = Math.round(secs * 60); for (let i = 0; i <= steps; i++) { const a = i / steps * turns * Math.PI * 2; m.aimScreen(c, [cx + Math.cos(a) * radius, cy + Math.sin(a) * radius]); m.update(c, 1 / 60); } }; for (const [x, y, r] of [[506, 190, 30], [200, 300, 80], [320, 180, 15]]) { const c = m.newBruciate(1); m.aimScreen(c, [x + r, y]); circles(c, x, y, r, 2, 2); assert.ok(Math.abs(c.speed) > 4 && Math.abs(c.speed) < 9, `giri di raggio ${r}: ${c.speed.toFixed(2)} rad/s`); assert.ok(Math.abs(c.drum) > 6); // Lasciata la manovella, il cilindro rallenta da sé. assert.ok(m.release(c, 7)); const w = Math.abs(c.speed); for (let i = 0; i < 60; i++) m.update(c, 1 / 60); assert.ok(Math.abs(c.speed) < w * 0.5); } const c = m.newBruciate(1); m.aimScreen(c, [100, 200]); m.press(c, 1); for (let i = 0; i < 120; i++) { m.aimScreen(c, [100 + i * 3, 200]); m.update(c, 1 / 60); } assert.ok(Math.abs(c.speed) < 0.2, 'trascinare dritto non gira'); assert.ok(!m.release(c, 99), 'un altro dito non lascia la manovella'); }); test('senza gesto: Spazio e le frecce danno una spinta, Invio rovescia', () => { const c = m.newBruciate(3); assert.ok(m.key(c, ' ', true)); assert.ok(Math.abs(c.speed) >= m.PUSH); assert.ok(m.key(c, 'ArrowRight', true)); assert.ok(Math.abs(c.speed) >= m.PUSH * 1.5); assert.ok(m.key(c, ' ', false), 'anche lasciando il tasto, il tasto è suo'); assert.ok(!m.key(c, 'x', true)); for (let i = 0; i < 60 * m.MIN_POUR + 2; i++) m.update(c, 1 / 60); m.key(c, 'Enter', true); assert.equal(c.step, 'pour'); }); test('il disegno delle bruciate non cambia lo stato', () => { const calls = [], ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => calls.push([k, ...args]) }); const c = m.newBruciate(7, { level: 'bona' }), bg = {}; const draw = () => { const before = structuredClone(c); drawBruciate(ctx, c, story.closeups.bruciate, 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 === 'GIRA!'), 'da fermo, l\'invito a girare'); assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'COTTURA')); crank(c, 12, { secs: 2, until: 9 }); draw(); crank(c, 4.5); pourAndWait(c); calls.length = 0; draw(); assert.ok(calls.some(([op, t]) => op === 'fillText' && /BRUCIATE/.test(t))); }); test('alla Smarronata: dai volontari, «Vi do una mano alla manovella?», il livello e le bruciate', () => { const g = new Game(story); g.go('smarronata_1993'); settle(g); g.act('talk', 'volontari'); settle(g); assert.ok(g.options().some(o => o.text.startsWith('Che Iddio ve ne renda merito')), 'il ringraziamento resta'); g.choose(g.options().findIndex(o => o.text === 'Vi do una mano alla manovella?')); settle(g); g.choose(g.options().findIndex(o => o.text === 'Seria.')); settle(g); assert.equal(g.closeup?.id, 'bruciate'); assert.equal(g.closeup.level, 'seria'); crank(g.closeup, 4.5); pourAndWait(g.closeup); g.closeupClose(); settle(g); assert.ok(g.state.flags.bruciate_fatte); // Anche usando il cilindro si chiede la manovella; uscendo prima, la battuta di chi lascia. g.act('use', 'bruciatore'); settle(g); g.choose(g.options().findIndex(o => o.text === 'Vi do una mano alla manovella?')); settle(g); g.choose(g.options().findIndex(o => o.text === 'Alla bona.')); settle(g); assert.equal(g.closeup?.id, 'bruciate'); g.closeupClose(); assert.equal(g.queue[0]?.say, story.closeups.bruciate.quit[0].say); });