Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
119 lines
6.4 KiB
JavaScript
119 lines
6.4 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { newBiliardo, update, shoot, predict, simulate, judge, LEN, WID, VIEW, FIXED_STEP } from '../src/engine/biliardo.js';
|
|
import { drawBiliardo, visualFrame, pinFall, tozziPose, tablePatches, TOZZI, SHOT_DELAY } from '../src/engine/biliardo-render.js';
|
|
import { story } from '../src/story/story.js';
|
|
|
|
test('il panno generato coincide con VIEW, senza buchi fra i ritagli', () => {
|
|
const png = readFileSync(new URL('../' + story.closeups.biliardo.picture, import.meta.url));
|
|
const patches = tablePatches();
|
|
assert.equal(png.readUInt32BE(16), 1672);
|
|
assert.equal(png.readUInt32BE(20), 941);
|
|
for (const [i, expected] of [VIEW.x, VIEW.y, LEN * VIEW.k, WID * VIEW.k].entries())
|
|
assert.ok(Math.abs(patches[12][i + 4] - expected) < 1e-10);
|
|
let area = 0;
|
|
for (const [i, p] of patches.entries()) {
|
|
const [x, y, w, h, dx, dy, dw, dh] = p;
|
|
assert.ok(w > 0 && h > 0 && dw > 0 && dh > 0);
|
|
assert.ok(x >= 0 && y >= 0 && x + w <= 1672 && y + h <= 941);
|
|
assert.ok(dx >= 0 && dy >= 0 && dx + dw <= 640 && dy + dh <= 360);
|
|
if (i % 5 < 4) assert.equal(dx + dw, patches[i + 1][4]);
|
|
if (i < 20) assert.equal(dy + dh, patches[i + 5][5]);
|
|
area += dw * dh;
|
|
}
|
|
assert.ok(Math.abs(area - 640 * 360) < 1e-8);
|
|
});
|
|
|
|
test('la steccata trattiene solo il disegno: velocità immediata e traiettoria invariata', () => {
|
|
const c = newBiliardo(42), start = { ...c.balls.player };
|
|
const expected = simulate(c, 'player', c.angle, c.power);
|
|
shoot(c);
|
|
assert.ok(Math.hypot(c.balls.player.vx, c.balls.player.vy) > 0);
|
|
update(c, SHOT_DELAY / 2);
|
|
assert.notEqual(c.balls.player.x, start.x, 'la fisica è già partita');
|
|
assert.equal(visualFrame(c).balls.player.x, start.x, 'il disegno aspetta la punta della stecca');
|
|
update(c, SHOT_DELAY);
|
|
const before = structuredClone(c);
|
|
const shown = visualFrame(c);
|
|
assert.ok(shown.balls.player.x > start.x);
|
|
assert.ok(shown.balls.player.x < c.balls.player.x);
|
|
assert.deepEqual(c, before, 'leggere il fotogramma non scrive nello stato');
|
|
for (let i = 0; i < 10000 && c.step === 'rolling'; i++) update(c, FIXED_STEP);
|
|
assert.deepEqual(judge(c.shot), expected);
|
|
assert.ok(c.visual.frames.length < 110, 'la cronologia non cresce per tutta la partita');
|
|
while (c.step === 'settle') update(c, FIXED_STEP);
|
|
assert.equal(visualFrame(c).balls, c.balls, 'al nuovo turno torna la posizione corrente');
|
|
});
|
|
|
|
test('caduta progressiva dei birilli, senza alterare guida, punteggio o rialzata', () => {
|
|
const c = newBiliardo(9);
|
|
Object.assign(c.balls.player, { x: LEN / 2 - 60, y: WID / 2 });
|
|
Object.assign(c.balls.cpu, { x: LEN / 2 - 30, y: WID / 2 });
|
|
Object.assign(c.balls.pallino, { x: 20, y: 20 });
|
|
c.angle = 0; c.power = 0.5;
|
|
const expected = simulate(c, 'player', c.angle, c.power), before = structuredClone(c);
|
|
predict(c);
|
|
assert.deepEqual(c, before, 'la previsione non fa partire animazioni');
|
|
shoot(c);
|
|
for (let i = 0; i < 1000 && !c.pins.some(p => p.down); i++) update(c, FIXED_STEP);
|
|
const p = c.pins.find(p => p.down);
|
|
assert.ok(p && Number.isFinite(p.visualDownAt));
|
|
assert.equal(pinFall(p, p.visualDownAt), 0);
|
|
assert.ok(pinFall(p, p.visualDownAt + 0.12) > 0 && pinFall(p, p.visualDownAt + 0.12) < 1);
|
|
assert.equal(pinFall(p, p.visualDownAt + 0.3), 1);
|
|
assert.equal(visualFrame(c).pins.some(p => p.down), false, 'anche la caduta segue il tempo delle biglie visibili');
|
|
for (let i = 0; i < 10000 && c.step === 'rolling'; i++) update(c, FIXED_STEP);
|
|
assert.deepEqual(judge(c.shot), expected);
|
|
while (c.step === 'settle') update(c, FIXED_STEP);
|
|
assert.ok(c.pins.every(p => !p.down && p.visualDownAt === undefined));
|
|
});
|
|
|
|
test('Tozzi pensa, esulta e sbaglia; tutte le pose restano fuori da tavolo e comandi', () => {
|
|
const c = newBiliardo(3);
|
|
assert.equal(tozziPose(c), 'stand');
|
|
c.current = 'cpu'; assert.equal(tozziPose(c), 'chalk');
|
|
for (const [last, pose] of [
|
|
[{ shooter: 'cpu', to: 'cpu', points: 4, foul: null, age: 0 }, 'cheer'],
|
|
[{ shooter: 'cpu', to: 'player', points: 2, foul: 'miss', age: 0 }, 'shrug'],
|
|
[{ shooter: 'cpu', to: 'cpu', points: 0, foul: null, age: 0 }, 'shrug'],
|
|
[{ shooter: 'player', to: 'cpu', points: 2, foul: 'own', age: 0 }, 'cheer'],
|
|
]) { c.last = last; assert.equal(tozziPose(c), pose); }
|
|
c.last.age = 3; assert.equal(tozziPose(c), 'chalk');
|
|
const atlas = JSON.parse(readFileSync(new URL('../assets/characters/ing-tozzi.json', import.meta.url)));
|
|
for (const pose of ['stand', 'chalk', 'cheer', 'shrug']) {
|
|
const f = atlas.frames[pose], left = Math.round(TOZZI.x - f.ax * TOZZI.scale);
|
|
assert.ok(left >= 0 && left + Math.round(f.w * TOZZI.scale) < VIEW.x - 16, pose);
|
|
assert.ok(TOZZI.y - f.ay * TOZZI.scale > 72 && TOZZI.y < 360);
|
|
}
|
|
});
|
|
|
|
test('il disegno usa immagine e atlas, funziona durante il caricamento e non modifica la partita', () => {
|
|
const calls = [], ctx = new Proxy({}, { get: (o, key) => key in o ? o[key] : (...args) => calls.push([key, ...args]) });
|
|
const previous = globalThis.document;
|
|
globalThis.document = { createElement: () => ({ getContext: () => ctx }) };
|
|
const atlas = JSON.parse(readFileSync(new URL('../assets/characters/ing-tozzi.json', import.meta.url)));
|
|
const bg = {}, sheet = { atlas, img: {} }, c = newBiliardo(42);
|
|
try {
|
|
const render = (background, sprite) => {
|
|
const before = structuredClone(c);
|
|
drawBiliardo(ctx, c, story.closeups.biliardo, background, sprite);
|
|
assert.deepEqual(c, before);
|
|
};
|
|
render(); // Il file può non essere ancora arrivato dalla rete.
|
|
calls.length = 0;
|
|
render(bg, sheet);
|
|
assert.ok(calls.some(([op, image]) => op === 'drawImage' && image === bg));
|
|
assert.ok(calls.some(([op, image]) => op === 'drawImage' && image === sheet.img));
|
|
assert.ok(!calls.some(([op]) => op === 'createRadialGradient'), 'biglie senza gradienti lisci');
|
|
shoot(c); update(c, 0.1); render(bg, sheet);
|
|
update(c, 0.07); render(bg, sheet);
|
|
while (c.step === 'rolling') update(c, 1 / 60);
|
|
render(bg, sheet);
|
|
c.step = 'result'; c.result = { winner: 'cpu' }; render(bg, sheet);
|
|
} finally {
|
|
if (previous === undefined) delete globalThis.document;
|
|
else globalThis.document = previous;
|
|
}
|
|
});
|