Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
208 lines
12 KiB
JavaScript
208 lines
12 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('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 } }; calls.length = 0; draw();
|
|
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 } };
|
|
g.closeupClose(); settle(g);
|
|
assert.ok(g.state.flags.micro_vinto);
|
|
});
|