I rinquarti di Bertovo: gara a punti (Bertovo tira dopo Andrea, preciso secondo il livello), allenamento, posizioni misurate, traiettoria con le sponde e Prossima, comandi sicuri col dito (revisione di Codex); la Brauda in gara; voce di Bertovo (Andrea)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdCPptNRwbVzjzHbohi
This commit is contained in:
2026-10-28 23:55:47 +01:00
co-authored by Claude Opus 5.5
parent bf22dc2511
commit 086e4682d4
24 changed files with 506 additions and 422 deletions
+143 -262
View File
@@ -1,294 +1,175 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as r from '../src/engine/rinquarto.js';
import { R, PIN, LEN, WID, predict, FIXED_STEP, VIEW, simulate, reach, speedFor } from '../src/engine/biliardo.js';
import { pinFall } from '../src/engine/biliardo-render.js';
import { FIXED_STEP, LONG_PRESS } from '../src/engine/biliardo.js';
import { drawRinquarto } from '../src/engine/rinquarto-render.js';
import { Game, MINIGAMES } from '../src/engine/game.js';
import { story } from '../src/story/story.js';
import { settle } from './helpers.mjs';
function roll(c, frames = [1 / 60]) {
for (let i = 0; i < 10000 && c.step === 'rolling'; i++) r.update(c, frames[i % frames.length]);
assert.ok(['settle', 'result'].includes(c.step), 'il tiro si deve fermare');
// Tira (Andrea col tiro dato, o Bertovo da sé) e aspetta l'esito.
function roll(c) {
for (let i = 0; i < 20000 && c.step !== 'review' && c.step !== 'result'; i++) r.update(c, 1 / 60);
assert.ok(['review', 'result'].includes(c.step), 'il tiro si deve fermare');
}
function next(c) {
for (let i = 0; i < 20 && c.step === 'settle'; i++) r.update(c, 0.25);
assert.ok(['aim', 'result'].includes(c.step));
}
function play(c, shot = c.solution) {
Object.assign(c, shot);
assert.ok(r.shoot(c));
assert.equal(r.shoot(c), false, 'un solo tiro per posizione');
function andrea(c, angle = c.solution.angle) { c.angle = angle; assert.ok(r.shoot(c)); roll(c); return c.history.at(-1); }
const samePlace = (a, b) => JSON.stringify(Object.values(a.balls).map(x => [x.role, x.x, x.y])) === JSON.stringify(Object.values(b.balls).map(x => [x.role, x.x, x.y]));
test('stesso seme e livello: stesse posizioni e soluzioni', () => {
const a = r.newRinquarto(11, { level: 'seria' }), b = r.newRinquarto(11, { level: 'seria' });
assert.ok(samePlace(a.start, b.start)); assert.deepEqual(a.solution, b.solution);
assert.notDeepEqual(r.newRinquarto(12, { level: 'seria' }).solution, a.solution);
});
test('ogni posizione: diretta coperta, una finestra buona larga quanto chiede il livello, e la soluzione fa punto davvero', () => {
for (const level of ['bona', 'seria', 'male']) {
const c = r.newRinquarto(3, { level }), widths = [];
for (let p = 1; p <= 3; p++) {
assert.equal(c.position, p);
assert.ok(r.directCovered(c), `${level} ${p}: diretta coperta`);
const shot = r.tryShot(c, c.solution.angle);
assert.ok(shot.point, `${level} ${p}: la soluzione fa punto`);
assert.ok(shot.cushions >= c.skill.cushions);
if (level === 'bona') assert.equal(c.solution.cushions, 1, 'alla bona c\'è sempre un rinquarto a una sponda');
widths.push(c.solution.width);
andrea(c); r.advance(c); roll(c); r.advance(c);
}
assert.ok(widths.every(w => w >= c.skill.width[1]), `${level}: finestre ${widths}`);
}
});
test('la diretta coperta guarda il primo impatto, non solo la vicinanza alla linea (rilievo 8 di Codex)', () => {
const at = (role, x, y) => ({ role, x, y, vx: 0, vy: 0 });
const free = { balls: { player: at('player', 50, 50), cpu: at('cpu', 100, 50), blocker: at('blocker', 99, 56.05) }, pins: [] };
assert.equal(r.directCovered(null, free), false, 'il bersaglio si tocca prima');
const covered = { balls: { player: at('player', 50, 50), cpu: at('cpu', 100, 50), blocker: at('blocker', 75, 52) }, pins: [] };
assert.equal(r.directCovered(null, covered), true);
});
test('la diretta tocca il disturbo; meno sponde del richiesto non valgono', () => {
const c = r.newRinquarto(5, { level: 'seria' });
const direct = r.tryShot(c, Math.atan2(c.balls.cpu.y - c.balls.player.y, c.balls.cpu.x - c.balls.player.x));
assert.equal(direct.point, false); assert.equal(direct.reason, 'disturbo toccato');
const bona = r.newRinquarto(5, { level: 'bona' }), one = r.tryShot(bona, bona.solution.angle);
assert.ok(one.point);
assert.equal(r.tryShot({ ...bona, skill: r.LEVELS.male }, bona.solution.angle).point, one.cushions >= 3);
});
test('a turno: prima Andrea, poi il tavolo torna com\'era e tira Bertovo; l\'esito resta finché non si va avanti', () => {
const c = r.newRinquarto(8, { level: 'bona' });
assert.equal(r.shooter(c), 'andrea'); assert.ok(r.canThrow(c)); assert.equal(r.button(c), 'Tira');
const mine = andrea(c);
assert.equal(c.step, 'review'); assert.equal(mine.who, 'andrea'); assert.ok(mine.point);
assert.equal(c.score.andrea, 1); assert.ok(c.trail.length > 5, 'la traiettoria resta');
assert.equal(mine.marks.length, mine.cushions, 'le sponde hanno il loro punto'); assert.ok(mine.hit);
for (let i = 0; i < 300; i++) r.update(c, 1 / 60);
assert.equal(c.step, 'review', 'non va avanti da sé'); assert.equal(r.button(c), 'Prossima');
assert.ok(r.press(c), 'un tocco va avanti');
assert.equal(r.shooter(c), 'bertovo'); assert.equal(c.step, 'think');
assert.ok(samePlace(c, c.start), 'il tavolo è tornato com\'era');
assert.equal(r.canThrow(c), false, 'mentre tira lui i comandi sono fermi'); assert.equal(r.shoot(c), false);
assert.match(r.status(c), /Tira Bertovo/);
roll(c);
}
test('stesso seme e livello riproducono tutte le sei posizioni e le soluzioni', () => {
for (const level of Object.keys(r.LEVELS)) {
const a = r.newRinquarto(123, { level }), b = r.newRinquarto(123, { level });
for (let i = 1; i <= a.skill.positions; i++) {
assert.equal(a.position, i);
assert.deepEqual(a, b);
assert.match(r.status(a), /Bertovo: «Oh tirami questo rinquarto!\?»/);
play(a); play(b); next(a); next(b);
}
assert.deepEqual(a, b);
}
assert.notDeepEqual(r.newRinquarto(12).balls, r.newRinquarto(13).balls);
assert.equal(c.history.at(-1).who, 'bertovo'); assert.equal(c.score.bertovo, Number(c.history.at(-1).point));
r.advance(c);
assert.equal(c.position, 2); assert.equal(r.shooter(c), 'andrea'); assert.equal(c.step, 'aim');
});
test('ogni posizione è coperta geometricamente e il solver trova un tiro che fa punto davvero', () => {
for (const level of Object.keys(r.LEVELS)) for (const seed of [0, 1, 42, 0xffffffff]) {
const c = r.newRinquarto(seed, { level });
for (let i = 0; i < c.skill.positions; i++) {
assert.ok(r.directCovered(c));
// Verifica indipendente: il disturbo sul segmento sta fra i due centri.
const a = c.balls.player, b = c.balls.cpu, d = c.balls.blocker;
const distance = Math.hypot(b.x - a.x, b.y - a.y);
const area = Math.abs((b.x - a.x) * (d.y - a.y) - (b.y - a.y) * (d.x - a.x));
assert.ok(area / distance < 2 * R);
assert.ok(Math.hypot(d.x - a.x, d.y - a.y) + Math.hypot(b.x - d.x, b.y - d.y) <= distance + 1e-6);
for (const ball of Object.values(c.balls)) {
assert.ok(ball.x >= R && ball.x <= LEN - R && ball.y >= R && ball.y <= WID - R);
for (const other of Object.values(c.balls)) if (other !== ball)
assert.ok(Math.hypot(ball.x - other.x, ball.y - other.y) > 2 * R);
}
const before = structuredClone(c), solution = r.solve(c);
assert.ok(solution);
assert.deepEqual(c, before, 'la ricerca non cambia lo stato né il seme');
const expected = r.tryShot(c, solution.angle, solution.power);
assert.ok(expected.point && expected.cushions >= c.skill.cushions);
assert.equal(r.tryShot(c, c.angle, 1).point, false, 'il diretto è coperto');
const points = c.points;
play(c, solution);
assert.deepEqual(c.attempt, expected);
assert.equal(c.points, points + 1);
next(c);
}
}
});
const clear = level => {
const c = r.newRinquarto(42, { level });
c.balls = {
player: { role: 'player', x: 80, y: 50, vx: 0, vy: 0 },
cpu: { role: 'cpu', x: 80, y: 100, vx: 0, vy: 0 },
};
c.pins = [];
return c;
};
test('un diretto libero non vale; una sponda vale solo alla bona, senza recuperare dopo il primo contatto', () => {
assert.deepEqual(Object.values(r.LEVELS).map(k => k.cushions), [1, 2, 3]);
for (const level of Object.keys(r.LEVELS)) {
const c = clear(level);
assert.equal(r.directCovered(c), false);
const direct = r.tryShot(c, Math.PI / 2, 1);
assert.equal(direct.cushions, 0);
assert.equal(direct.point, false);
assert.equal(direct.reason, 'poche sponde');
const bank = r.tryShot(c, -Math.PI / 2, 0.7);
assert.equal(bank.cushions, 1);
assert.equal(bank.point, level === 'bona');
play(c, { angle: -Math.PI / 2, power: 0.7 });
assert.equal(c.points, Number(level === 'bona'));
assert.deepEqual(c.attempt, bank);
}
});
test('biglie e birilli di disturbo prima del bersaglio annullano il tiro', () => {
for (const kind of ['ball', 'pin']) {
const c = clear('bona');
if (kind === 'ball') c.balls.extra = { role: 'extra', x: 80, y: 25, vx: 0, vy: 0 };
else c.pins.push({ x: 80, y: 25, down: false });
const result = r.tryShot(c, -Math.PI / 2, 1);
assert.equal(result.point, false);
assert.equal(result.reason, 'disturbo toccato');
play(c, { angle: -Math.PI / 2, power: 1 });
assert.equal(c.points, 0);
assert.equal(c.attempt.reason, result.reason);
}
});
test('un disturbo dopo il bersaglio non annulla un punto già fatto', () => {
const c = clear('bona');
// Dopo la sponda alta la bianca colpisce la gialla, che abbatte il birillo.
c.pins = [{ x: 80, y: 120, down: false }];
const events = [];
simulate(c, 'player', -Math.PI / 2, 1, { onContact: e => events.push({ kind: e.kind, role: e.a.role }) });
assert.ok(events.findIndex(e => e.kind === 'pin') > events.findIndex(e => e.kind === 'ball'));
play(c, { angle: -Math.PI / 2, power: 1 });
assert.ok(c.pins[0].down, 'il contatto successivo avviene davvero');
assert.equal(c.points, 1);
assert.equal(c.attempt.reason, null);
});
test('durante l’esito continua la caduta dei birilli, senza assegnare altri punti', () => {
const c = clear('bona');
play(c, { angle: -Math.PI / 2, power: 0.7 });
const p = { down: true, visualDownAt: c.visual.time };
c.pins.push(p);
const history = structuredClone(c.history), points = c.points;
r.update(c, 0.12);
assert.ok(Math.abs(pinFall(p, c.visual.time) - 0.5) < 1e-10);
r.update(c, 0.12);
assert.ok(Math.abs(pinFall(p, c.visual.time) - 1) < 1e-10);
assert.equal(c.step, 'settle');
assert.equal(c.points, points);
assert.deepEqual(c.history, history);
});
test('un birillo colpito appena prima dell’arresto finisce di cadere anche nel riepilogo finale', () => {
const c = clear('bona');
c.position = c.skill.positions;
// Urto reale negli ultimi centimetri di un tiro debole: il riepilogo arriva durante la caduta.
c.pins = [{ x: 80 + reach(speedFor(0)) + R + PIN - 0.01, y: 50, down: false }];
play(c, { angle: 0, power: 0 });
test('alla fine vince chi ha più punti; può finire pari', () => {
const c = r.newRinquarto(8, { level: 'bona' });
for (let p = 0; p < c.skill.positions; p++) { andrea(c, c.solution.angle + Math.PI); r.advance(c); roll(c); r.advance(c); }
assert.equal(c.step, 'result');
const p = c.pins[0], before = structuredClone(c);
assert.ok(p.down && pinFall(p, c.visual.time) < 1);
r.update(c, 0.25);
assert.equal(pinFall(p, c.visual.time), 1);
before.visual.time = c.visual.time;
assert.deepEqual(c, before, 'avanza solo l’animazione, senza nuovi tiri o punti');
for (const dt of [0, -1, NaN, Infinity]) r.update(c, dt);
assert.deepEqual(c, before, 'tempi non validi non cambiano il riepilogo');
});
test('gloria alla soglia di LEVELS, sconfitta un punto sotto: la sfida dura sempre tutte le posizioni', () => {
for (const level of Object.keys(r.LEVELS)) for (const win of [true, false]) {
const c = r.newRinquarto(17, { level }), target = c.skill.glory - Number(!win);
for (let i = 0; i < c.skill.positions; i++) {
play(c, i < target ? c.solution : { angle: c.angle, power: 1 });
if (i < c.skill.positions - 1) assert.equal(c.result, undefined);
next(c);
}
assert.equal(c.history.length, c.skill.positions);
assert.equal(c.points, target);
assert.equal(c.result.winner, win ? 'player' : 'cpu');
assert.match(r.status(c), win ? /Gloria!/ : /Sfida persa/);
const final = structuredClone(c);
r.update(c, 10);
final.visual.time = c.visual.time;
assert.deepEqual(c, final, 'il riepilogo rimane visibile');
assert.equal(r.press(c), false);
assert.equal(c.score.andrea, 0);
assert.equal(c.result.winner, c.score.bertovo > 0 ? 'cpu' : 'draw');
assert.match(r.status(c), c.score.bertovo > 0 ? /Ha vinto Bertovo/ : /Pari/);
for (const [a, b, w] of [[3, 2, 'player'], [2, 3, 'cpu'], [2, 2, 'draw']]) {
const d = { ...c, step: 'review', last: true, round: 1, position: c.skill.positions, score: { andrea: a, bertovo: b } };
r.advance(d); assert.equal(d.result.winner, w);
}
});
test('guida lunga fino alla seconda sponda, corta fino a 65 cm, assente a fassi male', () => {
const c = clear('bona');
c.balls.cpu.x = 200; c.angle = -Math.PI / 2; c.power = 1;
assert.deepEqual(r.guide(c), predict(c).points.slice(0, 3));
assert.equal(r.guide(c).length, 3);
c.skill = r.LEVELS.seria;
const short = r.guide(c);
assert.ok(short.length <= 2);
assert.ok(Math.hypot(short[1][0] - short[0][0], short[1][1] - short[0][1]) <= 65);
c.skill = r.LEVELS.male;
assert.deepEqual(r.guide(c), []);
});
test('la guida si ferma al primo disturbo o all’arresto della biglia', () => {
const c = clear('bona');
c.angle = -Math.PI / 2; c.power = 1;
c.balls.extra = { role: 'extra', x: 80, y: 25, vx: 0, vy: 0 };
let points = r.guide(c);
assert.equal(points.length, 2);
assert.ok(Math.abs(points[1][1] - (25 + 2 * R)) < 1e-7);
delete c.balls.extra;
c.angle = 0; c.power = 0;
points = r.guide(c);
assert.equal(points.length, 2);
assert.ok(points[1][0] > 80 && points[1][0] < 100, 'la guida non arriva alla sponda se manca forza');
assert.equal(points[1][1], 50);
});
test('comandi, suoni e fisica restano coerenti a frequenze diverse', () => {
const states = [];
for (const frames of [[1 / 30], [1 / 144], [0.008, 0.042, 0.09]]) {
const c = r.newRinquarto(42);
assert.ok(r.canAdjust(c));
r.press(c); r.update(c, 0.05); r.release(c);
assert.equal(c.step, 'aim'); assert.equal(c.power, 0.75);
r.press(c); r.update(c, 0.2);
assert.equal(r.aimScreen(c, [VIEW.x + 100, VIEW.y + 100]), false);
assert.equal(r.canAdjust(c), false);
r.cancel(c);
Object.assign(c, c.solution); r.shoot(c); roll(c, frames);
assert.ok(c.events.some(e => e.kind === 'biliardo-cue'));
assert.ok(c.events.some(e => e.kind === 'biliardo-rail'));
assert.ok(c.events.some(e => e.kind === 'biliardo-hit'));
states.push({ balls: c.balls, pins: c.pins.map(({ visualDownAt, ...p }) => p), points: c.points, attempt: c.attempt });
test('l\'allenamento: tira prima Bertovo, sempre giusto, poi Andrea; niente punti, finisce pari', () => {
const c = r.newRinquarto(2, { level: 'allenamento' });
assert.equal(r.shooter(c), 'bertovo'); assert.equal(c.step, 'think');
for (let p = 0; p < c.skill.positions; p++) {
roll(c); assert.ok(c.history.at(-1).point, 'Bertovo fa vedere il rinquarto');
r.advance(c); assert.equal(r.shooter(c), 'andrea');
andrea(c); r.advance(c);
}
for (const state of states.slice(1)) assert.deepEqual(state, states[0]);
assert.deepEqual(c.score, { andrea: 0, bertovo: 0 });
assert.equal(c.result.winner, 'draw'); assert.ok(c.result.training);
});
test('dialogo di Bertovo: si chiacchiera, lui lancia la sfida, poi tre livelli e No, primo piano e battute di uscita; lui sta al tavolo di mezzo del Circolo', () => {
test('Bertovo è più preciso salendo di livello', () => {
const spread = level => {
const c = r.newRinquarto(4, { level: 'bona' }); c.skill = r.LEVELS[level];
const errors = Array.from({ length: 400 }, () => r.bertovoPlan(c).angle - c.solution.angle);
return Math.sqrt(errors.reduce((a, e) => a + e * e, 0) / errors.length);
};
const [bona, seria, male] = ['bona', 'seria', 'male'].map(spread);
assert.ok(bona > seria && seria > male, `${bona} ${seria} ${male}`);
assert.equal(spread('allenamento'), 0);
});
test('i comandi: un tocco mira e basta, tenendo premuto si tira; il secondo dito non tira; le frecce girano finché tenute', () => {
const c = r.newRinquarto(6, { level: 'bona' });
r.aimAt(c, c.balls.cpu.x, c.balls.cpu.y + 20);
assert.ok(r.press(c, 1)); r.update(c, 0.15); r.release(c, 1);
assert.equal(c.step, 'aim', 'un tocco normale non brucia la posizione');
assert.ok(r.press(c, 1)); r.update(c, 0.1);
assert.equal(r.release(c, 2), false, 'il secondo dito non lascia il tiro del primo');
assert.equal(c.step, 'aim');
for (let t = 0; t < LONG_PRESS; t += 0.1) r.update(c, 0.1);
assert.ok(c.charging, 'tenendo premuto la stecca si carica');
r.release(c, 1); assert.equal(c.step, 'rolling'); assert.equal(c.power, 1, 'forza sempre piena');
const d = r.newRinquarto(6, { level: 'bona' }), a0 = d.angle;
assert.ok(r.key(d, 'ArrowRight', true)); r.update(d, 0.5);
const a1 = d.angle; assert.ok(a1 > a0 + 0.2, 'tenendo la freccia gira, e accelera');
r.key(d, 'ArrowRight', false); r.update(d, 0.5); assert.equal(d.angle, a1, 'lasciandola si ferma');
r.key(d, 'ArrowLeft', true, true); r.update(d, 0.5); r.key(d, 'ArrowLeft', false);
assert.ok(a1 - d.angle < 0.05, 'con Maiusc i passi sono fini');
assert.equal(r.setPower(d, 0.2), false); assert.equal(d.power, 1);
});
test('dialogo di Bertovo: si chiacchiera, la sfida o l\'allenamento; lui sta al tavolo di mezzo del Circolo', () => {
assert.ok(MINIGAMES.rinquarto);
assert.equal(story.characters.bertovo.sprite, 'assets/characters/bertovo.json');
// Solo al Circolo 4 Maggio, e parlandoci si apre il suo dialogo.
const where = Object.entries(story.scenes).filter(([, sc]) => JSON.stringify(sc).includes('bertovo')).map(([id]) => id);
assert.deepEqual(where, ['circolo_4_maggio']);
const circolo = new Game(story); circolo.go('circolo_4_maggio'); settle(circolo);
assert.ok(circolo.people().some(p => p.who === 'bertovo'));
circolo.act('talk', 'bertovo'); settle(circolo);
assert.ok(circolo.dialogue, 'parlandoci si sceglie il livello');
for (const [index, level] of ['bona', 'seria', 'male', null].entries()) {
const g = new Game(story); settle(g);
g.run({ dialogue: 'bertovo' });
// Prima si chiacchiera; la sfida la lancia lui, poi si sceglie il livello.
assert.ok(!g.options().some(o => o.text === 'Seria.'), 'il livello non si sceglie subito');
g.choose(g.options().findIndex(o => o.text === 'Dai, fammi vedere un rinquarto.'));
assert.ok(settle(g).some(s => s.who === 'bertovo' && s.text.includes('Oh tirami questo rinquarto!?')));
assert.equal(g.options().length, 4);
g.choose(index); settle(g);
if (!level) { assert.equal(g.closeup, null); assert.ok(g.dialogue, 'col No si torna a parlare'); continue; }
assert.equal(g.closeup.id, 'rinquarto');
assert.equal(g.closeup.level, level);
assert.ok(g.closeupPress());
g.update(0.2); assert.ok(g.closeupRelease());
assert.equal(g.closeup.step, 'rolling');
g.closeupClose();
assert.ok(settle(g).some(s => s.text === story.closeups.rinquarto.quit[0].say));
}
const g = new Game(story); g.go('circolo_4_maggio'); settle(g);
assert.ok(g.people().some(p => p.who === 'bertovo'));
g.act('talk', 'bertovo'); settle(g);
assert.ok(!g.options().some(o => o.text === 'Seria.'), 'il livello non si sceglie subito');
g.choose(g.options().findIndex(o => o.text === 'Si fa una sfida a rinquarti?'));
assert.ok(settle(g).some(s => s.who === 'bertovo' && s.text.includes('Oh tirami questo rinquarto!?')));
assert.equal(g.options().length, 4);
g.choose(g.options().findIndex(o => o.text.startsWith('No'))); settle(g);
assert.equal(g.closeup, null); assert.ok(g.dialogue, 'col No si torna a parlare');
g.choose(g.options().findIndex(o => o.text === 'Si fa una sfida a rinquarti?')); settle(g);
g.choose(g.options().findIndex(o => o.text === 'Seria.')); settle(g);
assert.equal(g.closeup?.id, 'rinquarto'); assert.equal(g.closeup.level, 'seria');
g.closeup.step = 'result'; g.closeup.result = { winner: 'player', points: { andrea: 4, bertovo: 2 } };
g.closeupClose(); settle(g);
assert.ok(g.state.flags.rinquarti_vinti);
const h = new Game(story); h.go('circolo_4_maggio'); settle(h);
h.act('talk', 'bertovo'); settle(h);
h.choose(h.options().findIndex(o => o.text === 'Dai, fammi vedere un rinquarto.')); settle(h);
assert.equal(h.closeup?.level, 'allenamento');
});
test('il riepilogo resta aperto e uscendo parte la battuta di vittoria o sconfitta', () => {
for (const win of [true, false]) {
const g = new Game(story); settle(g);
g.run({ closeup: 'rinquarto', level: 'bona' });
const c = g.closeup;
for (let i = 0; i < c.skill.positions; i++) {
play(c, win ? c.solution : { angle: c.angle, power: 1 });
next(c);
}
g.update(0.1);
assert.equal(g.closeup, c);
assert.equal(c.result.winner, win ? 'player' : 'cpu');
g.closeupClose();
assert.equal(g.closeup, null);
const line = story.closeups.rinquarto[win ? 'win' : 'lose'][0];
assert.ok(settle(g).some(s => s.text === line.say && s.who === 'bertovo'));
}
});
test('disegno senza sprite, con disturbi e riepilogo, senza modificare lo stato', () => {
test('il disegno: traiettoria con le sponde numerate, riepilogo, e lo stato non cambia', () => {
const calls = [], ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => calls.push([k, ...args]) });
const previous = globalThis.document;
globalThis.document = { createElement: () => ({ getContext: () => ctx }) };
try {
for (const level of Object.keys(r.LEVELS)) {
const c = r.newRinquarto(42, { level }), bg = {};
const draw = () => {
const before = structuredClone(c);
drawRinquarto(ctx, c, story.closeups.rinquarto, bg);
assert.deepEqual(c, before);
};
draw();
r.shoot(c); r.update(c, FIXED_STEP); draw(); roll(c); draw();
c.step = 'result'; c.result = { winner: 'cpu' }; draw();
assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
}
const c = r.newRinquarto(8, { level: 'bona' }), bg = {};
const draw = () => { const before = structuredClone(c); drawRinquarto(ctx, c, story.closeups.rinquarto, bg); assert.deepEqual(c, before); };
draw(); assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
andrea(c); calls.length = 0; draw();
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === '1'), 'la prima sponda è numerata');
assert.ok(calls.some(([op, t]) => op === 'fillText' && /punto!/.test(t)));
c.step = 'result'; c.result = { winner: 'cpu', points: { andrea: 1, bertovo: 3 } }; calls.length = 0; draw();
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'HA VINTO BERTOVO'));
} finally {
if (previous === undefined) delete globalThis.document; else globalThis.document = previous;
}