Files

222 lines
13 KiB
JavaScript

import test from 'node:test';
import { existsSync } from 'node:fs';
import assert from 'node:assert/strict';
import * as m from '../src/engine/micro.js';
import * as micro from '../src/engine/micro.js';
import { drawMicro } from '../src/engine/micro-render.js';
import { Game, MINIGAMES } from '../src/engine/game.js';
import { story } from '../src/story/story.js';
import { settle } from './helpers.mjs';
// Una gara con la macchinina già scelta (la rossa), per provare il resto.
const newMicro = (seed, def = {}) => micro.newMicro(seed, { car: 'rossa', ...def });
// Fa andare la spinta in corso (e quella del Chiaru, se tocca a lui) finché non si torna a mirare o è finita.
function run(c, limit = 20000) {
for (let i = 0; i < limit && !(m.canThrow(c) || c.step === 'result'); i++) m.update(c, 1 / 60);
}
// Andrea «prudente»: mira a un punto della linea di mezzo `ahead` pixel più avanti, con la forza giusta per arrivarci.
function flick(c, ahead = 60) {
const car = c.cars.player, i = (car.idx + Math.round(ahead / c.track.step)) % c.track.n, [x, y] = c.track.pts[i];
c.angle = Math.atan2(y - car.y, x - car.x); c.power = m.powerFor(Math.hypot(x - car.x, y - car.y));
assert.ok(m.shoot(c));
}
// Andrea alla bona: la spinta più lunga che la guida dà in pista, puntando avanti lungo la linea di mezzo.
function guided(c) {
for (let ahead = 190; ahead >= 20; ahead -= 10) {
flickAim(c, ahead);
if (!m.guide(c).out) break;
}
assert.ok(m.shoot(c));
}
function flickAim(c, ahead) {
const car = c.cars.player, i = (car.idx + Math.round(ahead / c.track.step)) % c.track.n, [x, y] = c.track.pts[i];
c.angle = Math.atan2(y - car.y, x - car.x); c.power = m.powerFor(Math.hypot(x - car.x, y - car.y));
}
test('la pista: chiusa, larga uguale, e le macchinine partono in pista poco prima del traguardo', () => {
const t = m.TRACK;
assert.ok(t.length > 1500 && t.length < 2500);
for (let i = 0; i < t.n; i++) {
const [x, y] = t.pts[i];
assert.ok(x - m.HALF > 0 && x + m.HALF < 640 && y - m.HALF > 22 && y + m.HALF < 360, `la pista sta nel quadro (${x}, ${y})`);
}
// I tratti lontani lungo la pista non si toccano: fra i due gessi c'è sempre asfalto.
for (let i = 0; i < t.n; i += 3) for (let j = i + 3; j < t.n; j += 3) {
const along = Math.min(j - i, t.n - j + i) * t.step;
if (along < 120) continue;
const d = Math.hypot(t.pts[i][0] - t.pts[j][0], t.pts[i][1] - t.pts[j][1]);
assert.ok(d > 2 * m.HALF + 2 * m.CAR.r, `tratti troppo vicini: ${i} e ${j} (${d.toFixed(1)} px)`);
}
const c = newMicro(1);
for (const car of Object.values(c.cars)) {
assert.ok(!m.isOut(m.project(t, car.x, car.y).d));
assert.ok(car.progress < 0 && car.progress > -30, 'prima del traguardo');
}
});
test('stesso seme: stessa partita; la spinta va lontano quanto dice la guida', () => {
const a = newMicro(4, { level: 'male' }), b = newMicro(4, { level: 'male' });
for (const c of [a, b]) { flick(c); run(c); }
assert.deepEqual(a.cars, b.cars);
const c = newMicro(2, { level: 'bona' }), start = { ...c.cars.player };
c.power = 0.6; const g = m.guide(c);
assert.ok(m.shoot(c)); run(c);
// Il Chiaru ha già spinto: la macchinina di Andrea sta dove diceva la guida (se lui non l'ha urtata).
assert.ok(Math.hypot(c.cars.player.x - g.x, c.cars.player.y - g.y) < 3, 'arriva dove diceva la guida');
assert.ok(Math.abs(Math.hypot(g.x - start.x, g.y - start.y) - m.reach(m.speedFor(0.6))) < 0.01);
});
test('tutta fuori: si torna dove si era prima della spinta, e la guida lo diceva', () => {
const c = newMicro(3, { level: 'bona' }), start = { x: c.cars.player.x, y: c.cars.player.y };
c.angle = -Math.PI / 2; c.power = 1; // dritto verso l'erba in alto
assert.equal(m.guide(c).out, true);
assert.ok(m.shoot(c));
for (let i = 0; i < 2000 && c.step !== 'settle'; i++) m.update(c, 1 / 60);
assert.equal(c.cars.player.fault, 'fuori');
for (let i = 0; i < 200 && c.current === 'player'; i++) m.update(c, 1 / 60);
assert.deepEqual([c.cars.player.x, c.cars.player.y], [start.x, start.y]);
assert.match(c.line, /^Fuori! Andrea torna/);
assert.equal(c.returns.length, 1);
// Nel tornante non si salta dall'altra parte: si esce lo stesso.
assert.ok(c.events.some(e => e.kind === 'micro-fuori'));
});
test('si parte scegliendo la macchinina: tocco, frecce, pulsante; il Chiaru tiene la blu, o la rossa se la blu è presa', () => {
const c = micro.newMicro(1);
assert.equal(c.step, 'choose'); assert.equal(m.canThrow(c), false); assert.equal(m.button(c), 'Scegli');
assert.ok(m.aimScreen(c, m.choiceAt(4))); assert.equal(c.choice, 4);
assert.equal(m.turn(c, 1, 'x'), false); assert.equal(c.choice, 5, 'la freccia passa alla dopo');
m.turn(c, 1, 'x'); assert.equal(c.choice, 0, 'e ricomincia dalla prima');
assert.ok(m.shoot(c)); assert.deepEqual(c.colors, { player: 'rossa', cpu: 'blu' }); assert.equal(c.step, 'aim');
const d = micro.newMicro(1); m.aimScreen(d, m.choiceAt(1)); assert.ok(m.press(d, 7));
assert.deepEqual(d.colors, { player: 'blu', cpu: 'rossa' });
assert.equal(d.holding, false, 'il tocco che sceglie non carica la spinta');
for (const car of m.CARS) assert.ok(existsSync(new URL('../' + m.carImage(car.id, true), import.meta.url)) && existsSync(new URL('../' + m.carImage(car.id), import.meta.url)));
});
test('primo e secondo mentre si va; nel risultato il perché', () => {
const c = newMicro(1);
c.cars.player.progress = 100; c.cars.cpu.progress = 80;
assert.equal(m.rank(c, 'player'), 1); assert.equal(m.rank(c, 'cpu'), 2);
assert.match(m.status(c), /Andrea 1°.*Chiaru 2°/);
c.result = { winner: 'cpu', flicks: { player: 20, cpu: 20 }, over: { player: 5, cpu: 22 } };
assert.deepEqual(m.why(c), ['Tutti e due al traguardo alla spinta 20:', 'vince chi va più in là, il Chiaru di 3 centimetri.']);
c.result = { winner: 'player', flicks: { player: 9, cpu: 9 }, over: { player: 5, cpu: -40 } };
assert.deepEqual(m.why(c), ['Andrea al traguardo alla spinta 9;', 'al Chiaru non è bastata la sua.']);
c.result = { winner: 'draw', flicks: { player: 9, cpu: 9 }, over: { player: 5, cpu: 5.4 } };
assert.match(m.why(c).join(' '), /pari/);
});
test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si torna indietro', () => {
// Tagliè: dal tratto di mezzo della esse, dritti in su attraverso l'erba verso il tratto di sopra.
const t = newMicro(1, { level: 'bona' }), [sx, sy] = t.track.pts[Math.round(t.track.n * 0.48)];
const p = m.project(t.track, sx, sy); Object.assign(t.cars.player, { x: sx, y: sy, idx: p.i, s: p.s, progress: p.s });
t.angle = -Math.PI / 2; t.power = 0.6; assert.ok(m.shoot(t));
for (let i = 0; i < 2000 && t.step !== 'settle'; i++) m.update(t, 1 / 60);
for (let i = 0; i < 200 && t.current === 'player'; i++) m.update(t, 1 / 60);
assert.match(t.line, /^Tagliè! Andrea torna dov'era\./);
assert.deepEqual([t.cars.player.x, t.cars.player.y], [sx, sy]);
// Cappottè: Andrea a tutta forza contro il Chiaru fermo davanti.
const k = newMicro(2, { level: 'bona' }), me = k.cars.player, him = k.cars.cpu, before = { x: him.x, y: him.y };
k.angle = Math.atan2(him.y - me.y, him.x - me.x); k.power = 1; assert.ok(m.shoot(k));
for (let i = 0; i < 2000 && k.step !== 'settle'; i++) m.update(k, 1 / 60);
assert.equal(him.fault, 'cappotte');
for (let i = 0; i < 200 && k.current === 'player'; i++) m.update(k, 1 / 60);
assert.match(k.line, /Cappottè! Il Chiaru torna dov'era\./);
// Torna al suo posto; se lì c'è ora Andrea, si scosta appena quanto basta.
assert.ok(Math.hypot(him.x - before.x, him.y - before.y) <= 2 * m.CAR.r + 1e-9);
// Accompagnè: tenuto premuto ben oltre la carica piena.
const a = newMicro(3), start = { x: a.cars.player.x, y: a.cars.player.y };
assert.ok(m.press(a));
for (let i = 0; i < 120; i++) m.update(a, 1 / 60);
assert.ok(m.overHeld(a));
assert.ok(m.release(a));
for (let i = 0; i < 2000 && a.current === 'player'; i++) m.update(a, 1 / 60);
assert.match(a.line, /^Accompagnè! Andrea torna dov'era\./);
assert.deepEqual([a.cars.player.x, a.cars.player.y], [start.x, start.y]);
// Lasciando in tempo, la spinta piena vale.
const ok = newMicro(3); m.press(ok);
for (let i = 0; i < Math.round((m.SWING + 0.2) * 60); i++) m.update(ok, 1 / 60);
m.release(ok); assert.equal(ok.cars.player.fault, null);
});
test('una partita intera: stesse spinte per tutti e due, un vincitore, e il Chiaru alla bona perde contro chi usa la guida', () => {
const wins = { player: 0, cpu: 0, draw: 0 };
for (const seed of [1, 2, 3]) {
const c = newMicro(seed, { level: 'bona' });
for (let n = 0; n < 80 && c.step !== 'result'; n++) { if (m.canThrow(c)) guided(c); run(c); }
assert.equal(c.step, 'result');
assert.equal(c.result.flicks.player, c.result.flicks.cpu);
assert.ok(m.finished(c, c.cars.player) || m.finished(c, c.cars.cpu));
wins[c.result.winner]++;
}
assert.ok(wins.player >= 2, `alla bona Andrea, con la guida, deve vincere quasi sempre (${JSON.stringify(wins)})`);
// A fassi male il Chiaru fa il giro con meno spinte che alla bona.
const flicks = level => {
const c = newMicro(7, { level });
for (let n = 0; n < 80 && c.step !== 'result'; n++) { if (m.canThrow(c)) { c.angle = c.cars.player.heading; c.power = 0; m.shoot(c); } run(c); }
return c.flicks.cpu;
};
assert.ok(flicks('male') < flicks('bona'));
});
test('i comandi: un tocco mira soltanto, tenere premuto carica, lasciare spinge; la mira si gira con le frecce', () => {
const c = newMicro(5);
assert.ok(m.press(c, 1)); m.update(c, 0.05); assert.ok(m.release(c, 1) === true && c.step === 'aim', 'il tocco breve non spinge');
// Premuto, la forza riparte da zero e sale; un secondo dopo è quasi piena.
assert.ok(m.press(c, 1)); assert.equal(c.power, 0);
m.update(c, 0.3); const early = c.power; assert.ok(c.charging && early > 0.2 && early < 0.3, `forza ${early}`);
for (let i = 0; i < 42; i++) m.update(c, 1 / 60);
assert.ok(c.power > early && c.power < 1);
const before = c.cars.player.x;
assert.ok(m.release(c, 1)); assert.equal(c.step, 'rolling');
m.update(c, 0.2); assert.notEqual(c.cars.player.x, before);
assert.equal(m.press(c, 2), false, 'mentre corre non si spinge');
const d = newMicro(5), a0 = d.angle;
m.key(d, 'ArrowRight', true); m.update(d, 0.5); m.key(d, 'ArrowRight', false);
assert.ok(d.angle > a0);
assert.ok(m.aimScreen(d, [320, 200])); assert.equal(m.aimScreen(d, [320, 10]), false, 'il tabellone non si mira');
assert.ok(m.setPower(d, 0.25)); assert.equal(d.power, 0.25);
});
test('il Chiaru pensa a pezzi: nessun fotogramma lo ferma troppo', () => {
const c = newMicro(6, { level: 'male' });
flick(c);
let worst = 0;
for (let i = 0; i < 600 && !m.canThrow(c); i++) { const t = performance.now(); m.update(c, 1 / 60); worst = Math.max(worst, performance.now() - t); }
assert.ok(worst < 40, `fotogramma da ${worst.toFixed(1)} ms`);
});
test('il disegno: gesso, macchinine, guida e risultato; lo stato non cambia', () => {
const calls = [], ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => calls.push([k, ...args]) });
const c = newMicro(8, { level: 'bona' }), bg = {}, cars = { [m.carImage('rossa')]: { car: 'player' } };
const draw = () => { const before = structuredClone(c); drawMicro(ctx, c, story.closeups.micro, bg, src => cars[src]); assert.deepEqual(c, before); };
draw();
assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
assert.ok(calls.filter(([op, img]) => op === 'drawImage' && img?.car === 'player').length >= 2, 'la macchinina e il suo fantasma');
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'Chiaru'));
c.step = 'result'; c.result = { winner: 'cpu', flicks: { player: 12, cpu: 12 }, over: { player: 3, cpu: 20 } }; calls.length = 0; draw();
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'vince chi va più in là, il Chiaru di 3 centimetri.'));
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'HA VINTO IL CHIARU'));
});
test('il Chiaru al parcheggio di Cardetole: si chiacchiera, si sceglie il livello e si corre', () => {
assert.ok(MINIGAMES.micro);
const g = new Game(story); g.go('cardetole'); settle(g);
// Il clic su tutto il Chiaru (testa, pancia, gambe) è suo, anche dove sta sopra al parcheggio.
for (const y of [222, 260, 300, 314]) assert.equal(g.hotspotAt([520, y])?.id, 'chiaru', `clic a y=${y}`);
g.act('talk', 'chiaru'); 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 gara alle macchinine?!?!')); settle(g);
g.choose(g.options().findIndex(o => o.text === 'No, aspetta.')); settle(g);
assert.equal(g.closeup, null);
g.choose(g.options().findIndex(o => o.text === 'Si fa una gara alle macchinine?!?!')); settle(g);
g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
assert.equal(g.closeup?.id, 'micro'); assert.equal(g.closeup.level, 'male');
g.closeup.step = 'result'; g.closeup.result = { winner: 'player', flicks: { player: 11, cpu: 11 }, over: { player: 4, cpu: -30 } };
g.closeupClose(); settle(g);
assert.ok(g.state.flags.micro_vinto);
});