Macchinine: si sceglie la macchinina, la forza sale da zero tenendo premuto e la spinta parte lasciando; il saluto col Chiaru («un girino alle case rosse» / «Vorrai dire un giochino!» / «Ciao Mattè»), «Si fa una gara alle macchinine?!?!», Andrea chiama Bertovo «Fabri»; musica del GP in prestito (Andrea)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
This commit is contained in:
2026-10-29 10:04:46 +01:00
co-authored by Claude Opus 5.5
parent 4d13a4ef25
commit 44480221db
24 changed files with 161 additions and 57 deletions
+16 -4
View File
@@ -34,19 +34,31 @@ try {
await page.evaluate(async () => {
const { settle } = await import('/tests/helpers.mjs');
game.act('talk', 'chiaru'); settle(game);
game.choose(game.options().findIndex(o => o.text === 'Si corre?')); settle(game);
game.choose(game.options().findIndex(o => o.text === 'Si fa una gara alle macchinine?!?!')); settle(game);
game.choose(game.options().findIndex(o => o.text === 'Alla bona.')); settle(game);
});
await page.waitForSelector('#bocce-controls:not([hidden])');
await page.waitForFunction(async () => {
const { hasBackground } = await import('/src/engine/render.js');
return hasBackground('assets/closeups/micro-parcheggio.png') && hasBackground('assets/closeups/micro/rossa.png');
return hasBackground('assets/closeups/micro-parcheggio.png');
});
assert.equal(await page.locator('#bocce-throw').textContent(), 'Spingi');
// Prima si sceglie la macchinina: la viola, toccandola.
assert.equal(await page.locator('#bocce-throw').textContent(), 'Scegli');
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/micro/viola-grande.png'));
if (name === 'desktop') { await page.waitForTimeout(1600); await page.screenshot({ path: `${out}/${name}-sceglie.png` }); }
{
const box = await page.locator('#scene').boundingBox(), [vx, vy] = [480, 220];
const at = { x: box.x + vx / 640 * box.width, y: box.y + vy / 360 * box.height };
if (mobile) await page.touchscreen.tap(at.x, at.y); else await page.mouse.click(at.x, at.y);
}
assert.deepEqual(await page.evaluate(() => game.closeup.colors), { player: 'viola', cpu: 'blu' });
await page.waitForTimeout(1600);
if (name === 'desktop') await page.screenshot({ path: `${out}/${name}-scelta.png` });
await page.waitForFunction(() => document.querySelector('#bocce-throw').textContent === 'Spingi');
assert.ok(await page.locator('#micro-help').isVisible(), 'il «Come si gioca» delle macchinine');
const exit = await page.locator('#closeup-close').boundingBox(), canvas = await page.locator('#scene').boundingBox();
assert.ok(exit.y >= canvas.y + canvas.height, 'l\'uscita sta fuori dalla pista');
await page.waitForTimeout(1600);
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/micro/viola.png'));
const state = () => page.evaluate(() => ({ angle: game.closeup.angle, power: game.closeup.power, step: game.closeup.step, x: game.closeup.cars.player.x }));
// Un punto della pista davanti alla macchinina di Andrea, in pixel della pagina.
const ahead = await page.evaluate(() => { const c = game.closeup, i = (c.cars.player.idx + 50) % c.track.n; return c.track.pts[i]; });
+39 -17
View File
@@ -1,11 +1,16 @@
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);
@@ -44,7 +49,7 @@ test('la pista: chiusa, larga uguale, e le macchinine partono in pista poco prim
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 = m.newMicro(1);
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');
@@ -52,10 +57,10 @@ test('la pista: chiusa, larga uguale, e le macchinine partono in pista poco prim
});
test('stesso seme: stessa partita; la spinta va lontano quanto dice la guida', () => {
const a = m.newMicro(4, { level: 'male' }), b = m.newMicro(4, { level: 'male' });
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 = m.newMicro(2, { level: 'bona' }), start = { ...c.cars.player };
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).
@@ -64,7 +69,7 @@ test('stesso seme: stessa partita; la spinta va lontano quanto dice la guida', (
});
test('tutta fuori: si torna dove si era prima della spinta, e la guida lo diceva', () => {
const c = m.newMicro(3, { level: 'bona' }), start = { x: c.cars.player.x, y: c.cars.player.y };
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));
@@ -78,9 +83,22 @@ test('tutta fuori: si torna dove si era prima della spinta, e la guida lo diceva
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 = m.newMicro(1, { level: 'bona' }), [sx, sy] = t.track.pts[Math.round(t.track.n * 0.48)];
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);
@@ -88,7 +106,7 @@ test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si
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 = m.newMicro(2, { level: 'bona' }), me = k.cars.player, him = k.cars.cpu, before = { x: him.x, y: him.y };
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');
@@ -97,7 +115,7 @@ test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si
// 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 = m.newMicro(3), start = { x: a.cars.player.x, y: a.cars.player.y };
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));
@@ -106,7 +124,7 @@ test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si
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 = m.newMicro(3); m.press(ok);
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);
});
@@ -114,7 +132,7 @@ test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si
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 = m.newMicro(seed, { level: 'bona' });
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);
@@ -124,7 +142,7 @@ test('una partita intera: stesse spinte per tutti e due, un vincitore, e il Chia
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 = m.newMicro(7, { 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;
};
@@ -132,14 +150,18 @@ test('una partita intera: stesse spinte per tutti e due, un vincitore, e il Chia
});
test('i comandi: un tocco mira soltanto, tenere premuto carica, lasciare spinge; la mira si gira con le frecce', () => {
const c = m.newMicro(5);
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');
assert.ok(m.press(c, 1)); for (let i = 0; i < 60; i++) m.update(c, 1 / 60); assert.ok(c.charging);
// 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 = m.newMicro(5), a0 = d.angle;
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');
@@ -147,7 +169,7 @@ test('i comandi: un tocco mira soltanto, tenere premuto carica, lasciare spinge;
});
test('il Chiaru pensa a pezzi: nessun fotogramma lo ferma troppo', () => {
const c = m.newMicro(6, { level: 'male' });
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); }
@@ -156,7 +178,7 @@ test('il Chiaru pensa a pezzi: nessun fotogramma lo ferma troppo', () => {
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 = m.newMicro(8, { level: 'bona' }), bg = {}, cars = { [story.closeups.micro.cars.player]: { car: 'player' } };
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));
@@ -173,10 +195,10 @@ test('il Chiaru al parcheggio di Cardetole: si chiacchiera, si sceglie il livell
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 corre?')); settle(g);
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 corre?')); settle(g);
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 } };