Il flipper dopo la seconda revisione di Codex: flipper vivi anche sul ponte, imbocco solo in salita, ponti con la stessa energia su e giù e uscita nelle corsie di ritorno, combo che scade, scritte in coda col traguardo davanti, tabellone con l'esito giusto e le cifre che corrono in tempo fisso, tasti del mouse dalla maschera, glifi rifatti

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
2026-10-31 21:43:57 +01:00
co-authored by Claude Fable 5.1
parent 45b8c3d98e
commit 07b6a381c1
6 changed files with 172 additions and 105 deletions
+40 -16
View File
@@ -104,7 +104,7 @@ test('funghi, bersagli, Regolo e corsie fanno punti solo a un urto nuovo; le qua
assert.ok(c.targets.filter(k => k.bank === 'pieve').every(t => t.down), 'la fila resta giù un attimo');
put(c, { x: 260, y: 230, vx: 0, vy: 0 }); run(c, 1);
assert.ok(c.targets.filter(k => k.bank === 'pieve').every(t => !t.down), 'poi si rialza');
assert.match(c.line ?? '', /PIEVE/);
assert.ok([c.line, ...c.lines].some(l => /PIEVE/.test(l ?? '')), 'la scritta della Pieve (in coda dietro al Regolo)');
// Le corsie: contano attraversando la riga, anche fuori centro (Codex: a x=287 non si accendeva).
c.score = 0;
for (const [i, x] of [[0, 230], [1, 287], [2, 335], [3, 420]]) {
@@ -112,7 +112,7 @@ test('funghi, bersagli, Regolo e corsie fanno punti solo a un urto nuovo; le qua
assert.ok(c.lanes[i].lit || c.lanes.every(k => !k.lit), c.lanes[i].rione);
}
assert.ok(c.score >= 4 * m.SCORE.lane + m.SCORE.palio, `corsie e Palio (${c.score}; in più, al massimo, un fungo sfiorato)`);
assert.match(c.line, /PALIO/); assert.ok(c.lanes.every(l => !l.lit), 'si rispegne per un altro giro');
assert.ok([c.line, ...c.lines].some(l => /PALIO/.test(l ?? ''))); assert.ok(c.lanes.every(l => !l.lit), 'si rispegne per un altro giro');
// In salita non conta.
put(c, { x: 287, y: c.lanes[1].y + 6, vx: 0, vy: -150 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
assert.ok(!c.lanes[1].lit);
@@ -158,22 +158,36 @@ test('il traguardo è acquisito appena raggiunto: anche uscendo prima, si vince;
assert.ok(h.ball && !h.ball.waiting && h.ball.y > 280 && h.ball.x < 320, `ancora sul flipper (${h.ball?.x.toFixed(0)}, ${h.ball?.y.toFixed(0)})`);
});
test('i ponti: imboccati forte si sale e si scende dall\'altra parte (e la pallina non urta la tavola sotto); piano si ricasca; tutti e due di fila, i PONTI', () => {
test('i ponti: imboccati forte si sale, si passa sopra la tavola e si scende nella corsia di ritorno; piano si ricasca; di traverso no; tutti e due di fila, i PONTI', () => {
const c = m.newFlipper(13, { level: 'bona' }); m.launch(c, 0.9); c.score = 0;
put(c, { x: 247, y: 250, vx: 0, vy: -420 });
let onRamp = 0, top = 999; for (let i = 0; i < 150; i++) { m.update(c, 1 / 60); if (c.ball.ramp) { onRamp++; top = Math.min(top, c.ball.y); } }
assert.ok(onRamp > 20, 'è salita sul ponte'); assert.ok(top < 50, 'fin sopra la tavola');
assert.ok(!c.ball.ramp && c.ball.x > 380, `scesa dall'altra parte (${c.ball.x.toFixed(0)}, ${c.ball.y.toFixed(0)})`);
assert.equal(c.score, m.SCORE.ramp); assert.match(c.line, /PONTE SINISTRO/); assert.ok(c.ramps[0].lit > 0);
put(c, { x: 247, y: 250, vx: 0, vy: -470 });
let onRamp = 0, top = 999, exit = null; for (let i = 0; i < 150; i++) { m.update(c, 1 / 60); if (c.ball.ramp) { onRamp++; top = Math.min(top, c.ball.y); } else if (onRamp && !exit) exit = { x: c.ball.x, y: c.ball.y }; }
assert.ok(onRamp > 20, 'è salita sul ponte'); assert.ok(top < 40, 'fin sopra la tavola');
assert.ok(exit && exit.x > 400 && exit.x < 420, `scesa nella corsia di ritorno di destra (${exit?.x.toFixed(0)})`);
assert.equal(c.score, m.SCORE.ramp); assert.equal(c.line, 'PONTE SX'); assert.deepEqual(c.lines, ['+15000']); assert.ok(c.ramps[0].lit > 0);
assert.ok(c.events.some(e => e.kind === 'flipper-rampa'));
// Il secondo ponte subito dopo: i PONTI.
put(c, { x: 393, y: 250, vx: 0, vy: -420, ramp: null }); run(c, 2.5);
assert.equal(c.score, 3 * m.SCORE.ramp - m.SCORE.ramp + m.SCORE.ponti, `i ponti (${c.score})`); assert.match(c.line ?? '', /PONTI/);
// Troppo piano: non ci arriva, ricasca dall'imbocco senza punti.
// Mentre è sul ponte i flipper rispondono lo stesso (Codex: restavano congelati).
const f = m.newFlipper(13, { level: 'bona' }); m.launch(f, 0.9); put(f, { x: 247, y: 250, vx: 0, vy: -470 }); run(f, 0.3);
assert.ok(f.ball.ramp, 'sul ponte'); m.turn(f, -1, 'click'); run(f, 0.1);
assert.ok(f.flippers.left.angle < 0.3, `il flipper si è alzato (${f.flippers.left.angle.toFixed(2)})`);
// Il secondo ponte subito dopo: i PONTI, col bonus.
put(c, { x: 393, y: 250, vx: 0, vy: -470, ramp: null }); run(c, 2.5);
assert.equal(c.score, 2 * m.SCORE.ramp + m.SCORE.ponti, `i ponti (${c.score})`);
assert.ok([c.line, ...c.lines].some(l => l === 'I PONTI!') && [c.line, ...c.lines].some(l => /BONUS/.test(l ?? '')));
assert.equal(c.lastRamp, null, 'la combo è spesa');
// Troppo piano: sale un po', non ce la fa, ricasca dall'imbocco senza punti.
const d = m.newFlipper(14, { level: 'bona' }); m.launch(d, 0.9); d.score = 0;
put(d, { x: 247, y: 250, vx: 0, vy: -300 }); let climbed = false; for (let i = 0; i < 90; i++) { m.update(d, 1 / 60); if (d.ball.ramp) climbed = true; }
assert.ok(!d.ball.ramp && d.ball.y > 236, 'ricaduta'); assert.equal(d.score % m.SCORE.ramp === 0 && d.score >= m.SCORE.ramp, false, 'niente punti del ponte');
void climbed;
put(d, { x: 247, y: 250, vx: 0, vy: -335 }); let climbed = false; for (let i = 0; i < 300; i++) { m.update(d, 1 / 60); if (d.ball.ramp) climbed = true; }
assert.ok(climbed, 'è salita'); assert.ok(!d.ball.ramp && d.ball.y > 236, 'ricaduta'); assert.ok(d.score < m.SCORE.ramp, 'niente punti del ponte');
// Di traverso (Codex: sfiorando l'imbocco quasi in orizzontale saliva lo stesso): no.
const e = m.newFlipper(15, { level: 'bona' }); m.launch(e, 0.9); e.score = 0;
put(e, { x: 247, y: 236.1, vx: 500, vy: -30 }); run(e, 0.5);
assert.ok(!e.ball.ramp && e.score < m.SCORE.ramp, 'non è salita');
// La combo scade dopo dodici secondi, e si azzera perdendo la pallina.
const g = m.newFlipper(16, { level: 'bona' }); m.launch(g, 0.9); g.lastRamp = { id: 'sinistra', at: g.sim }; put(g, { x: 320, y: 200, vx: 0, vy: 0 });
run(g, 13); assert.equal(g.lastRamp, null, 'scaduta');
g.lastRamp = { id: 'sinistra', at: g.sim }; g.saveUntil = 0; put(g, { x: 320, y: 330, vx: 0, vy: 300 }); run(g, 1);
assert.equal(g.balls, 2); assert.equal(g.lastRamp, null, 'azzerata con la pallina persa');
});
test('giocando con attenzione, alla bona si arriva al traguardo; nessuna partita si incastra', () => {
@@ -187,7 +201,7 @@ test('giocando con attenzione, alla bona si arriva al traguardo; nessuna partita
assert.ok(won >= 5, `vinte ${won} su 8`);
});
test('il disegno del flipper non cambia lo stato', () => {
test('il disegno del flipper non cambia lo stato; il tabellone dice l\'esito giusto e le cifre corrono', async () => {
const calls = [], gradient = { addColorStop() {} };
const ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => { calls.push([k, ...args]); return gradient; }, set: (o, k, v) => { o[k] = v; return true; } });
const c = m.newFlipper(11, { level: 'bona' }), bg = {};
@@ -198,11 +212,21 @@ test('il disegno del flipper non cambia lo stato', () => {
assert.ok(calls.filter(([op]) => op === 'fillRect').length > 300, 'il tabellone a punti luminosi');
c.step = 'result'; c.result = { winner: 'player', score: 70000, goal: 60000 }; calls.length = 0; draw();
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'TRAGUARDO FATTO!'));
// Perdendo, il tabellone dice FINITA (Codex: diceva «traguardo ok» anche a zero punti).
const l = m.newFlipper(12, { level: 'bona' }); l.step = 'result'; l.ball = null; l.result = { winner: 'cpu', score: 0, goal: 60000 }; calls.length = 0;
drawFlipper(ctx, l, story.closeups.flipper, bg);
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'FINE PARTITA'));
// Le cifre che corrono: in mezzo secondo da dove erano a dove devono arrivare, a scatti di dieci.
const { shownScore } = await import('../src/engine/flipper-render.js');
const d = { from: 1000, to: 51000, t0: 10 };
assert.equal(shownScore(d, 10), 1000); assert.equal(shownScore(d, 10.25) % 10, 0); assert.ok(shownScore(d, 10.25) > 20000 && shownScore(d, 10.25) < 30000); assert.equal(shownScore(d, 10.5), 51000);
});
test('al River: un clic sul bar (o «Usa», «Apri»), il livello, la partita; uscendo senza traguardo, la battuta di chi lascia', () => {
const g = new Game(story); g.go('via_provinciale'); settle(g);
assert.equal(g.hotspot('river').default, 'open');
const [bx, by, bw, bh] = g.hotspot('river').box;
assert.equal(g.hotspotAt([bx + bw / 2, by + bh / 2])?.id, 'river', 'il River si clicca');
g.act('use', 'river'); settle(g);
assert.ok(g.options().some(o => o.text === 'Una partita seria.'));
g.choose(g.options().findIndex(o => o.text === 'Una partita seria.')); settle(g);