Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
177 lines
11 KiB
JavaScript
177 lines
11 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import * as r from '../src/engine/rinquarto.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';
|
|
|
|
// Tira (Andrea col tiro dato, o Trappizzio Berti 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 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 Trappizzio Berti; 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 Trappizzio Berti/);
|
|
roll(c);
|
|
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('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');
|
|
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 Trappizzio Berti/ : /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('l\'allenamento: tira prima Trappizzio Berti, 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, 'Trappizzio Berti fa vedere il rinquarto');
|
|
r.advance(c); assert.equal(r.shooter(c), 'andrea');
|
|
andrea(c); r.advance(c);
|
|
}
|
|
assert.deepEqual(c.score, { andrea: 0, bertovo: 0 });
|
|
assert.equal(c.result.winner, 'draw'); assert.ok(c.result.training);
|
|
});
|
|
|
|
test('Trappizzio Berti è 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 Trappizzio Berti: 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');
|
|
const where = Object.entries(story.scenes).filter(([, sc]) => JSON.stringify(sc).includes('bertovo')).map(([id]) => id);
|
|
assert.deepEqual(where, ['circolo_4_maggio']);
|
|
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 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 {
|
|
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 TRAPPIZZIO BERTI'));
|
|
} finally {
|
|
if (previous === undefined) delete globalThis.document; else globalThis.document = previous;
|
|
}
|
|
});
|