Lavori in corso (non pubblicati): Bertovo al tavolo di mezzo coi rinquarti (Codex), Bini, Yuri, il Gianno e la Brauda in gara, l'Audi «Let it be», sensibilità e riquadro dello swipe, pulsanti su iPhone (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECdCPptNRwbVzjzHbohi
This commit is contained in:
+40
-29
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { newGara, update, press, presenting, INTRO, release, cancel, shoot, key, steerButton, brakeButton, aimScreen, steering, status, button, standings, player, rider, limitAt, pointAt, TRACK, CURVES, PICKUPS, BRAKE, VERGE, LAPS, COUNTDOWN, RIDERS } from '../src/engine/gara.js';
|
||||
import { SWIPE, setPower, newGara, update, press, presenting, INTRO, release, cancel, shoot, key, steerButton, brakeButton, aimScreen, steering, status, button, standings, player, rider, limitAt, pointAt, TRACK, CURVES, PICKUPS, BRAKE, VERGE, LAPS, COUNTDOWN, RIDERS } from '../src/engine/gara.js';
|
||||
import { piloti } from '../src/story/piloti.js';
|
||||
import { story } from '../src/story/story.js';
|
||||
import { Game } from '../src/engine/game.js';
|
||||
@@ -17,6 +17,8 @@ const run = (c, seconds, drive, dt = 1 / 60) => {
|
||||
return c.heard;
|
||||
};
|
||||
const skip = c => { while (c.step === 'intro') press(c); return c; };
|
||||
// Lo sterzo col dito insegue quello voluto: per leggerlo si lascia assestare.
|
||||
const settled = c => { for (let i = 0; i < 400; i++) steering(c); return steering(c); };
|
||||
const race = c => { skip(c); assert.equal(c.step, 'start'); run(c, COUNTDOWN + 0.01); assert.equal(c.step, 'race'); return c; };
|
||||
// Un dito al centro della scena indica la direzione del punto davanti al motorino.
|
||||
const swipeTo = (c, target) => {
|
||||
@@ -93,73 +95,73 @@ test('le fonti del gas sono indipendenti: lasciare un tasto non toglie quello de
|
||||
});
|
||||
|
||||
test('lo sterzo: frecce, pulsanti e il dito che trascina; il mouse che passa senza premere non sterza', () => {
|
||||
const c = race(newGara(3, { riders: RIDERS }));
|
||||
assert.ok(key(c, 'ArrowLeft', true)); assert.equal(steering(c), -1);
|
||||
key(c, 'ArrowLeft', false); assert.equal(steering(c), 0);
|
||||
steerButton(c, 1); assert.equal(steering(c), 1); steerButton(c, 0);
|
||||
const c = race(newGara(3, { riders: RIDERS })); setPower(c, 1);
|
||||
assert.ok(key(c, 'ArrowLeft', true)); assert.equal(settled(c), -1);
|
||||
key(c, 'ArrowLeft', false); assert.equal(settled(c), 0);
|
||||
steerButton(c, 1); assert.equal(settled(c), 1); steerButton(c, 0);
|
||||
// Gas da tastiera e sinistra, il mouse si muove a destra senza premere: lo sterzo resta a sinistra (P1).
|
||||
key(c, 'ArrowUp', true); key(c, 'ArrowLeft', true);
|
||||
aimScreen(c, [300, 100], 5); aimScreen(c, [360, 100], 5);
|
||||
assert.equal(steering(c), -1);
|
||||
assert.equal(settled(c), -1);
|
||||
key(c, 'ArrowUp', false); key(c, 'ArrowLeft', false);
|
||||
// Il dito indica una direzione; un altro puntatore non sposta niente.
|
||||
player(c).a = -0.5 / 2.6;
|
||||
player(c).a = -0.5 / SWIPE.hard;
|
||||
aimScreen(c, [300, 100], 9); press(c, 9);
|
||||
aimScreen(c, [314, 100], 9); assert.ok(Math.abs(steering(c) - 0.5) < 1e-10);
|
||||
aimScreen(c, [250, 100], 4); assert.ok(Math.abs(steering(c) - 0.5) < 1e-10);
|
||||
release(c, 9); assert.equal(steering(c), 0, 'lasciando il dito lo sterzo torna dritto');
|
||||
aimScreen(c, [314, 100], 9); assert.ok(Math.abs(settled(c) - 0.5) < 1e-6);
|
||||
aimScreen(c, [250, 100], 4); assert.ok(Math.abs(settled(c) - 0.5) < 1e-6);
|
||||
release(c, 9); assert.equal(settled(c), 0, 'lasciando il dito lo sterzo torna dritto');
|
||||
const a = player(c).a; key(c, 'ArrowUp', true); key(c, 'ArrowRight', true); run(c, 0.4); assert.notEqual(player(c).a, a, 'sterzando si gira');
|
||||
assert.equal(key(c, 'x', true), false, 'gli altri tasti non sono suoi');
|
||||
});
|
||||
|
||||
test('lo swipe segue lo schermo: scendendo, destra sterza negativo; avanti e piccoli movimenti vanno dritto', () => {
|
||||
const c = race(newGara(3)), r = player(c);
|
||||
const c = race(newGara(3)), r = player(c); setPower(c, 1);
|
||||
r.a = Math.PI / 2;
|
||||
aimScreen(c, [320, 180], 9); press(c, 9);
|
||||
aimScreen(c, [370, 180], 9);
|
||||
assert.equal(steering(c), -1, 'scendendo, la destra dello schermo è a sinistra del motorino');
|
||||
assert.equal(settled(c), -1, 'scendendo, la destra dello schermo è a sinistra del motorino');
|
||||
r.a = 0;
|
||||
assert.ok(Math.abs(steering(c)) < 1e-10, 'col dito fermo lo sterzo si annulla quando il motorino è allineato');
|
||||
assert.ok(Math.abs(settled(c)) < 1e-6, 'col dito fermo lo sterzo si annulla quando il motorino è allineato');
|
||||
r.a = Math.PI / 2;
|
||||
aimScreen(c, [320, 230], 9); assert.ok(Math.abs(steering(c)) < 1e-10, 'swipe in avanti');
|
||||
aimScreen(c, [323, 184], 9); assert.equal(steering(c), 0, '5 px non fanno tremare lo sterzo');
|
||||
aimScreen(c, [320, 230], 9); assert.ok(Math.abs(settled(c)) < 1e-6, 'swipe in avanti');
|
||||
aimScreen(c, [323, 184], 9); assert.equal(settled(c), 0, '5 px non fanno tremare lo sterzo');
|
||||
aimScreen(c, [330, 230], 9);
|
||||
assert.ok(Math.abs(steering(c) + Math.atan2(10, 50) * 2.6) < 1e-10, 'sterzo proporzionale all’angolo');
|
||||
assert.ok(Math.abs(settled(c) + Math.atan2(10, 50) * SWIPE.hard) < 1e-6, 'sterzo proporzionale all’angolo');
|
||||
r.a = -Math.PI + 0.1;
|
||||
aimScreen(c, [270, 180], 9);
|
||||
assert.ok(Math.abs(steering(c) + 0.26) < 1e-10, 'si gira dalla parte più breve anche passando da π a -π');
|
||||
assert.ok(Math.abs(settled(c) + 0.1 * SWIPE.hard) < 1e-6, 'si gira dalla parte più breve anche passando da π a -π');
|
||||
release(c, 9);
|
||||
assert.equal(c.holding, false); assert.equal(steering(c), 0);
|
||||
assert.equal(c.holding, false); assert.equal(settled(c), 0);
|
||||
assert.equal(c.anchor, null); assert.equal(c.finger, null);
|
||||
});
|
||||
|
||||
test('l’origine segue gli swipe lunghi e si può correggere; rilascio e annullamento puliscono il dito', () => {
|
||||
const c = race(newGara(3));
|
||||
const c = race(newGara(3)); setPower(c, 1);
|
||||
player(c).a = 0;
|
||||
aimScreen(c, [100, 100], 9); press(c, 9);
|
||||
for (const x of [200, 300, 400]) {
|
||||
aimScreen(c, [x, 100], 9);
|
||||
assert.deepEqual(c.anchor, [x - 60, 100]);
|
||||
assert.ok(Math.abs(steering(c)) < 1e-10);
|
||||
assert.ok(Math.abs(settled(c)) < 1e-6);
|
||||
}
|
||||
aimScreen(c, [400, 160], 9);
|
||||
assert.ok(Math.abs(Math.hypot(c.finger[0] - c.anchor[0], c.finger[1] - c.anchor[1]) - 60) < 1e-10);
|
||||
assert.equal(steering(c), 1, 'si corregge verso il basso senza arrivare al bordo');
|
||||
assert.ok(Math.abs(Math.hypot(c.finger[0] - c.anchor[0], c.finger[1] - c.anchor[1]) - 60) < 1e-6);
|
||||
assert.equal(settled(c), 1, 'si corregge verso il basso senza arrivare al bordo');
|
||||
key(c, 'ArrowLeft', true); shoot(c); brakeButton(c, true);
|
||||
release(c, 9);
|
||||
assert.equal(steering(c), -1, 'lasciare il dito conserva lo sterzo della tastiera');
|
||||
assert.equal(settled(c), -1, 'lasciare il dito conserva lo sterzo della tastiera');
|
||||
assert.ok(c.holding && c.braking, 'Gas e Frena del pannello restano attivi');
|
||||
key(c, 'ArrowDown', true); brakeButton(c, false);
|
||||
assert.ok(c.braking, 'il freno della tastiera resta attivo');
|
||||
aimScreen(c, [200, 200], 10); press(c, 10);
|
||||
assert.equal(c.drag, 0, 'il nuovo tocco non eredita lo swipe');
|
||||
cancel(c);
|
||||
assert.equal(c.holding, false); assert.equal(c.braking, false); assert.equal(steering(c), 0);
|
||||
assert.equal(c.holding, false); assert.equal(c.braking, false); assert.equal(settled(c), 0);
|
||||
assert.equal(c.anchor, null); assert.equal(c.finger, null);
|
||||
});
|
||||
|
||||
test('alla bona si completano tre giri usando solo un dito, senza tasti o pulsanti', () => {
|
||||
const c = race(newGara(5, { level: 'bona', riders: piloti }));
|
||||
const c = race(newGara(5, { level: 'bona', riders: piloti.filter(p => p.consenso !== null) }));
|
||||
aimScreen(c, [320, 180], 'bot'); press(c, 'bot');
|
||||
for (let t = 0; t < 180 && c.step === 'race'; t += 1 / 60) {
|
||||
swipeTo(c, pointAt(player(c).s + 50));
|
||||
@@ -196,7 +198,7 @@ test('la presentazione: uno alla volta arrivano in griglia col motorino, poi il
|
||||
});
|
||||
|
||||
test('sempre a gas e dritto si finisce nella paglia; con giudizio alla bona si vince', () => {
|
||||
const wild = newGara(5, { level: 'bona', riders: piloti });
|
||||
const wild = newGara(5, { level: 'bona', riders: piloti.filter(p => p.consenso !== null) });
|
||||
skip(wild);
|
||||
run(wild, COUNTDOWN + 60, () => true);
|
||||
assert.ok(wild.heard.has('gara-fuori:andrea'));
|
||||
@@ -204,17 +206,26 @@ test('sempre a gas e dritto si finisce nella paglia; con giudizio alla bona si v
|
||||
assert.equal(wild.step, 'race', 'dritto non si arriva');
|
||||
const heard = new Set();
|
||||
for (const seed of [1, 2, 3, 4]) {
|
||||
const c = newGara(seed, { level: 'bona', riders: piloti });
|
||||
skip(c);
|
||||
const c = newGara(seed, { level: 'bona', riders: piloti.filter(p => p.consenso !== null) });
|
||||
skip(c); setPower(c, 1); // a sensibilità piena: il dito risponde subito
|
||||
for (const k of run(c, COUNTDOWN + 120, careful)) heard.add(k);
|
||||
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'player', `seme ${seed}`);
|
||||
assert.equal(c.result.order[0], 'andrea'); assert.match(status(c), /Primo al GP di Fortezza!/);
|
||||
assert.equal(Object.keys(c.result.times).length, piloti.length);
|
||||
assert.equal(Object.keys(c.result.times).length, piloti.filter(p => p.consenso !== null).length);
|
||||
}
|
||||
assert.ok(['gara-giro:andrea', 'gara-arrivo:andrea', 'gara-turbo:andrea', 'gara-miscela:andrea'].every(k => heard.has(k)), [...heard].join());
|
||||
assert.ok(CURVES.length >= 2 && TRACK.length > 600, 'una pista con delle curve vere');
|
||||
});
|
||||
|
||||
test('alla sensibilità di partenza (bassa) lo stesso pilota non finisce mai nella paglia', () => {
|
||||
for (const seed of [1, 2, 3]) {
|
||||
const c = newGara(seed, { level: 'bona', riders: piloti.filter(p => p.consenso !== null) });
|
||||
skip(c); assert.equal(c.sens, SWIPE.sens);
|
||||
run(c, COUNTDOWN + 120, careful);
|
||||
assert.equal(c.step, 'result'); assert.equal(c.result.offs, 0, `seme ${seed}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('le buche: piano si sobbalza, forte si casca; la lattina di miscela dà lo sprint', () => {
|
||||
const hole = PICKUPS.find(p => p.kind === 'buca'), can = PICKUPS.find(p => p.kind === 'miscela');
|
||||
for (const [speed, fell] of [[20, false], [80, true]]) {
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
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 { 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');
|
||||
}
|
||||
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');
|
||||
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);
|
||||
});
|
||||
|
||||
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 });
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
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 });
|
||||
}
|
||||
for (const state of states.slice(1)) assert.deepEqual(state, states[0]);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
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));
|
||||
}
|
||||
});
|
||||
|
||||
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', () => {
|
||||
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));
|
||||
}
|
||||
} finally {
|
||||
if (previous === undefined) delete globalThis.document; else globalThis.document = previous;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user