Il flipper rivisto con la revisione di Codex: punti solo a un urto nuovo, sottopassi contro gli attraversamenti, molla che arriva sempre in campo (senza cancelletto), salvataggio col tempo della simulazione e in vista, corsie che contano attraversando la riga, traguardo acquisito appena raggiunto, annullare non lancia, due dita e tasto destro, pulsanti dei flipper sempre attivi

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 20:24:01 +01:00
co-authored by Claude Fable 5.1
parent 7076123fcf
commit 1fc446d02a
14 changed files with 386 additions and 208 deletions
+36 -15
View File
@@ -1,5 +1,6 @@
// Il flipper del River nel browser vero: dal bar, la molla col mouse e col dito, i flipper coi pulsanti e con le
// frecce, il risultato. npm run browser:flipper — schermate in /tmp/regolo-flipper.
// Il flipper del River nel browser vero: dal bar, la molla col mouse e col dito, i flipper con due dita sulla scena,
// coi pulsanti e con le frecce, la pausa che lascia tutto, il risultato. npm run browser:flipper — schermate in
// /tmp/regolo-flipper.
import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict';
@@ -28,43 +29,63 @@ try {
assert.equal(await page.locator('#bocce-throw').textContent(), 'Lancia');
assert.ok(await page.locator('#flipper-help').isVisible());
assert.equal(await page.locator('#bocce-left').textContent(), '← Flipper');
assert.ok(!(await page.locator('#bocce-left').isDisabled()), 'i pulsanti dei flipper sono attivi anche prima del lancio');
await page.screenshot({ path: `${out}/${name}-tavola.png` });
const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height];
// La molla: col mouse tenendo premuto sulla scena, col dito il pulsante Lancia.
if (mobile) await page.locator('#bocce-throw').tap();
else {
const cdp = mobile ? await page.context().newCDPSession(page) : null;
const touch = (type, points) => cdp.send('Input.dispatchTouchEvent', { type, touchPoints: points.map(([x, y]) => ({ x, y })) });
// La molla: tenendo premuto sulla scena, col mouse e col dito.
if (mobile) {
await touch('touchStart', [P(440, 320)]);
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' });
await touch('touchEnd', []);
} else {
await page.mouse.move(...P(440, 320)); await page.mouse.down();
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' });
await page.mouse.up();
}
await page.waitForFunction(() => game.closeup && !game.closeup.ball?.waiting, null, { timeout: 5000 });
// I flipper: col dito i pulsanti tenuti premuti, col mouse la metà della scena e poi le frecce.
// Due dita sulla scena: sinistra, poi anche destra, poi si alza la sinistra e la destra resta su.
if (mobile) {
const b = await page.locator('#bocce-left').boundingBox();
const cdp = await page.context().newCDPSession(page);
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: b.x + b.width / 2, y: b.y + b.height / 2 }] });
await touch('touchStart', [P(250, 200)]);
await page.waitForFunction(() => game.closeup.flippers.left.held.length > 0);
await touch('touchStart', [P(250, 200), P(400, 200)]);
await page.waitForFunction(() => game.closeup.flippers.right.held.length > 0 && game.closeup.flippers.left.held.length > 0, null, { timeout: 3000 });
await page.screenshot({ path: `${out}/${name}-flipper.png` });
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
await touch('touchEnd', [P(400, 200)]);
await page.waitForFunction(() => game.closeup.flippers.left.held.length === 0 && game.closeup.flippers.right.held.length > 0, null, { timeout: 3000 });
await touch('touchEnd', []);
await page.waitForFunction(() => game.closeup.flippers.right.held.length === 0);
// I pulsanti tenuti premuti.
const b = await page.locator('#bocce-left').boundingBox();
await touch('touchStart', [[b.x + b.width / 2, b.y + b.height / 2]]);
await page.waitForFunction(() => game.closeup.flippers.left.held.length > 0);
await touch('touchEnd', []);
await page.waitForFunction(() => game.closeup.flippers.left.held.length === 0);
} else {
await page.mouse.move(...P(400, 200)); await page.mouse.down();
await page.waitForFunction(() => game.closeup.flippers.right.held.length > 0);
await page.mouse.down({ button: 'right' });
await page.waitForFunction(() => game.closeup.flippers.right.held.length > 1, null, { timeout: 3000 });
await page.screenshot({ path: `${out}/${name}-flipper.png` });
await page.mouse.up();
await page.mouse.up(); await page.mouse.up({ button: 'right' });
await page.waitForFunction(() => game.closeup.flippers.right.held.length === 0);
await page.keyboard.down('ArrowLeft');
await page.waitForFunction(() => game.closeup.flippers.left.held.length > 0);
await page.keyboard.up('ArrowLeft');
// La pausa lascia tutto: tornando, il flipper è giù anche se il tasto era premuto.
await page.click('#pause-button');
await page.waitForFunction(() => game.closeup.flippers.left.held.length === 0);
await page.keyboard.up('ArrowLeft');
await page.click('#resume');
await page.waitForFunction(() => document.querySelector('#pause').hidden);
}
// La fine: si fa finire la partita e si guarda il quadro.
await page.evaluate(() => { const c = game.closeup; c.step = 'result'; c.ball = null; c.result = { winner: 'player', score: 7350, goal: c.skill.goal }; });
await page.evaluate(() => { const c = game.closeup; c.score = 7350; c.step = 'result'; c.ball = null; c.result = { winner: 'player', score: 7350, goal: c.skill.goal }; });
await page.waitForFunction(() => document.querySelector('#bocce-throw').textContent === 'Finita');
assert.match(await page.locator('#bocce-status').textContent(), /7350 punti/);
await page.screenshot({ path: `${out}/${name}-fine.png` });
await page.locator('#closeup-close').click();
await page.waitForFunction(() => game.state.flags.flipper_vinto);
if (mobile) await page.locator('#closeup-close').click(); else await page.keyboard.press('Enter'); // Invio chiude il risultato
await page.waitForFunction(() => game.state.flags.flipper_vinto && !game.closeup);
await page.close();
}
assert.deepEqual(errors, []);
+92 -46
View File
@@ -7,6 +7,7 @@ import { story } from '../src/story/story.js';
import { settle } from './helpers.mjs';
const run = (c, secs) => { for (let i = 0; i < secs * 60; i++) m.update(c, 1 / 60); };
const put = (c, ball) => Object.assign(c.ball, ball);
// Il giocatore automatico: alza il flipper quando la pallina gli scende addosso, come un umano attento.
function autoplay(c, secs = 600) {
for (let i = 0; i < secs * 60 && c.step === 'play'; i++) {
@@ -21,94 +22,140 @@ function autoplay(c, secs = 600) {
}
}
test('la tavola: tutto sta dentro il campo, le corsie sotto l\'arco, i funghi sui dischi del disegno', () => {
test('la tavola: tutto sta dentro il campo, le corsie coprono tutta la larghezza sotto l\'arco, i funghi sui dischi del disegno', () => {
const c = m.newFlipper(1);
for (const w of c.walls) for (const [x, y] of [w.a, w.b]) assert.ok(x >= m.FIELD.x0 && x <= m.FIELD.x1 && y >= m.FIELD.y0 && y <= m.FIELD.y1, `${w.a}-${w.b}`);
for (const b of c.bumpers) assert.ok(b.x > m.FIELD.x0 + 20 && b.x < m.LANE.x0 - 20);
// I tre dischi dipinti da Codex (misurati sull'immagine): i funghi ci stanno sopra, a meno di 3 pixel.
for (const [b, [x, y]] of c.bumpers.map((b, i) => [b, [[281, 120], [360, 120], [320, 153]][i]])) assert.ok(Math.hypot(b.x - x, b.y - y) < 3);
assert.equal(c.lanes.length, 4); assert.deepEqual(c.lanes.map(l => l.rione), m.RIONI);
assert.equal(c.lanes[0].x0, m.FIELD.x0); assert.equal(c.lanes[3].x1, m.LANE.x0);
for (let i = 1; i < 4; i++) assert.equal(c.lanes[i].x0, c.lanes[i - 1].x1, 'corsie contigue');
assert.equal(c.targets.length, 6); assert.equal(c.balls, m.BALLS);
assert.ok(m.waiting(c) && m.canThrow(c) && c.ball.x > m.LANE.x0, 'la pallina aspetta sulla molla');
});
test('il lancio: la molla manda la pallina su per la corsia, oltre il cancelletto, e non torna più nella corsia', () => {
const c = m.newFlipper(2, { level: 'bona' });
assert.ok(m.launch(c, 0.8)); assert.ok(!m.waiting(c));
let top = 999, left = false;
for (let i = 0; i < 180; i++) { m.update(c, 1 / 60); top = Math.min(top, c.ball?.y ?? 999); if (c.ball && c.ball.x < m.LANE.x0 - 30) left = true; }
assert.ok(top < 40, `è arrivata in cima (${top.toFixed(0)})`);
assert.ok(left, 'è uscita dalla corsia');
// Un lancio fiacco ricasca sulla molla e si rilancia.
const d = m.newFlipper(3, { level: 'bona' }); m.launch(d, 0); run(d, 3);
test('il lancio: anche la molla più fiacca arriva in campo, quella forte fa il giro dell\'arco; la ricaduta si rilancia', () => {
for (const level of ['bona', 'male']) for (const power of [0, 0.5, 1]) {
const c = m.newFlipper(2, { level }); assert.ok(m.launch(c, power)); assert.ok(!m.waiting(c));
let top = 999, out = false;
for (let i = 0; i < 180; i++) { m.update(c, 1 / 60); top = Math.min(top, c.ball?.y ?? 999); if (c.ball && c.ball.x < m.LANE.x0 - 30) out = true; }
assert.ok(top < 50, `${level} forza ${power}: in cima (${top.toFixed(0)})`);
assert.ok(out, `${level} forza ${power}: entrata in campo`);
}
// Una pallina ricaduta nella corsia si assesta e si rilancia, senza perderla.
const d = m.newFlipper(3, { level: 'bona' }); m.launch(d, 0.5); put(d, { x: 440, y: 200, vx: 0, vy: 50 }); run(d, 3);
assert.ok(m.waiting(d) && d.balls === m.BALLS, 'tornata sulla molla, nessuna pallina persa');
});
test('la pallina persa: tre palline, poi il risultato; il salvataggio vale una volta sola', () => {
test('la pallina persa: tre palline, poi il risultato; il salvataggio vale una volta sola e non dipende dal fotogramma', () => {
const c = m.newFlipper(4, { level: 'seria' });
for (let n = 3; n >= 1; n--) {
assert.equal(c.balls, n);
m.launch(c, 0.9); c.saveUntil = 0; // niente salvataggio: si prova sotto
c.ball.x = 320; c.ball.y = 320; c.ball.vx = 0; c.ball.vy = 200; run(c, 1);
put(c, { x: 320, y: 320, vx: 0, vy: 200 }); run(c, 1);
}
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'cpu'); assert.ok(!m.canThrow(c));
assert.match(m.status(c), /Ne servivano 12000/);
assert.match(m.status(c), /Ne servivano 10000/);
const d = m.newFlipper(5, { level: 'bona' });
m.launch(d, 0.9); d.ball.x = 320; d.ball.y = 320; d.ball.vx = 0; d.ball.vy = 200; run(d, 1);
m.launch(d, 0.9); assert.ok(m.saveLeft(d) > 7); put(d, { x: 320, y: 320, vx: 0, vy: 200 }); run(d, 1);
assert.ok(m.waiting(d) && d.balls === 3, 'salvata: la pallina torna sulla molla');
assert.match(d.line ?? '', /salvata/);
m.launch(d, 0.9); d.ball.x = 320; d.ball.y = 320; d.ball.vx = 0; d.ball.vy = 200; run(d, 1);
m.launch(d, 0.9); assert.equal(m.saveLeft(d), 0); put(d, { x: 320, y: 320, vx: 0, vy: 200 }); run(d, 1);
assert.equal(d.balls, 2, 'la seconda volta è persa davvero');
// Lo stesso quarto di secondo, a passi piccoli o in un fotogramma solo: stesso esito (Codex).
for (const dts of [[1 / 240, 60], [1 / 60, 15], [0.25, 1]]) {
const e = m.newFlipper(6, { level: 'seria' }); m.launch(e, 0.8); e.sim = 2.9; e.saveUntil = 3; put(e, { x: 320, y: 344, vx: 0, vy: 100 });
for (let i = 0; i < dts[1]; i++) m.update(e, dts[0]);
assert.equal(e.balls, 3, `salvata con passi da ${dts[0]}`);
}
});
test('il flipper alzato rimanda su la pallina; abbassato la lascia scendere', () => {
test('il flipper alzato rimanda su la pallina, anche a tutta velocità (niente attraversamenti); abbassato la lascia scendere', () => {
const up = m.newFlipper(6, { level: 'seria' }); m.launch(up, 0.9);
Object.assign(up.ball, { x: 300, y: 300, vx: 0, vy: 150 });
m.hold(up, 'left', 'test', true);
put(up, { x: 300, y: 300, vx: 0, vy: 150 }); m.hold(up, 'left', 'test', true);
let best = 0; for (let i = 0; i < 60; i++) { m.update(up, 1 / 60); best = Math.min(best, up.ball?.vy ?? 0); }
assert.ok(best < -250, `tirata su (vy ${best.toFixed(0)})`);
assert.ok(up.events.some(e => e.kind === 'flipper-flip'));
// Il caso riprodotto da Codex: pallina a 980 px/s sulla punta del flipper che si alza.
const fast = m.newFlipper(6, { level: 'seria' }); m.launch(fast, 0.9);
put(fast, { x: 313.438, y: 323.71, vx: -469.837, vy: 860.031 }); m.hold(fast, 'left', 'test', true);
m.update(fast, m.STEP);
assert.ok(fast.ball.vy < 0 && fast.ball.y < 324, `respinta, non passata sotto (y ${fast.ball.y.toFixed(1)}, vy ${fast.ball.vy.toFixed(0)})`);
const down = m.newFlipper(6, { level: 'seria' }); m.launch(down, 0.9);
Object.assign(down.ball, { x: 300, y: 300, vx: 0, vy: 150 }); down.saveUntil = 0;
run(down, 2);
put(down, { x: 300, y: 300, vx: 0, vy: 150 }); down.saveUntil = 0; run(down, 2);
assert.equal(down.balls, 2, 'senza flipper, giù nella Sieve');
});
test('funghi, bersagli, Regolo e corsie fanno punti; le quattro corsie insieme il Palio', () => {
test('funghi, bersagli, Regolo e corsie fanno punti solo a un urto nuovo; le quattro corsie insieme il Palio', () => {
const c = m.newFlipper(7, { level: 'bona' }); m.launch(c, 0.9);
const b = c.bumpers[0];
Object.assign(c.ball, { x: b.x, y: b.y + b.r + 6, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
put(c, { x: b.x, y: b.y + b.r + 6, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
assert.equal(c.score, m.SCORE.bumper); assert.ok(c.ball.vy > 0 && b.hit > 0, 'respinta');
assert.ok(c.events.some(e => e.kind === 'flipper-fungo'));
c.score = 0;
for (const t of c.targets.filter(k => k.bank === 'pieve')) { const before = c.score; Object.assign(c.ball, { x: t.a[0] + 10, y: (t.a[1] + t.b[1]) / 2, vx: -300, vy: 0 }); m.update(c, 1 / 60); m.update(c, 1 / 60); assert.ok(c.score > before, `caduto il ${t.i}`); }
assert.equal(c.score, 3 * m.SCORE.target + m.SCORE.bank);
assert.ok(c.targets.filter(k => k.bank === 'pieve').every(t => !t.down), 'la fila si rialza');
assert.match(c.line, /Pieve/);
c.score = 0;
Object.assign(c.ball, { x: c.regolo.x, y: c.regolo.y + 12, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
// La pallina appoggiata sul Regolo (Codex: 240.000 punti in un secondo): non conta.
c.score = 0; put(c, { x: c.regolo.x, y: c.regolo.y - c.regolo.r - m.R, vx: 0, vy: 0 });
for (let i = 0; i < 240; i++) m.update(c, m.STEP);
assert.equal(c.score, 0, 'il contatto che dura non fa punti');
put(c, { x: c.regolo.x, y: c.regolo.y + 12, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
assert.equal(c.score, m.SCORE.regolo);
c.score = 0;
for (const l of c.lanes) { Object.assign(c.ball, { x: l.x, y: l.y - 10, vx: 0, vy: 100 }); m.update(c, 1 / 60); m.update(c, 1 / 60); assert.ok(l.lit || c.lanes.every(k => !k.lit), l.rione); }
assert.equal(c.score, 4 * m.SCORE.lane + m.SCORE.palio);
for (const t of c.targets.filter(k => k.bank === 'pieve')) { const before = c.score; put(c, { x: t.a[0] + 10, y: (t.a[1] + t.b[1]) / 2, vx: -300, vy: 0 }); m.update(c, 1 / 60); m.update(c, 1 / 60); assert.ok(c.score > before, `caduto il ${t.i}`); }
assert.equal(c.score, 3 * m.SCORE.target + m.SCORE.bank);
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/);
// 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]]) {
put(c, { x, y: c.lanes[i].y - 3, vx: 0, vy: 150 }); run(c, 0.1);
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');
// 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);
});
test('comandi: tenere premuto carica la molla e lasciare lancia; in gioco la metà della scena è il flipper; tasti', () => {
test('comandi: la molla si carica e si lancia lasciando; annullare non lancia; due dita, pulsanti e tasti', () => {
const c = m.newFlipper(8, { level: 'bona' });
m.aimScreen(c, [440, 320]); assert.ok(m.press(c, 1)); run(c, 0.6);
assert.ok(c.holding && c.power > 0.4 && c.power < 0.6, `molla a ${c.power.toFixed(2)}`);
assert.ok(!m.release(c, 2), 'un altro dito non lancia');
assert.ok(m.release(c, 1)); assert.ok(!m.waiting(c));
m.cancel(c, 1); assert.ok(m.waiting(c) && !c.holding, 'annullata la carica: niente lancio');
m.aimScreen(c, [440, 320]); m.press(c, 1); run(c, 0.6); assert.ok(m.release(c, 1)); assert.ok(!m.waiting(c));
m.aimScreen(c, [250, 200]); assert.ok(m.press(c, 3)); assert.ok(c.flippers.left.held.includes(3));
m.aimScreen(c, [400, 200]); assert.ok(m.press(c, 4)); assert.ok(c.flippers.right.held.includes(4));
assert.ok(m.release(c, 3)); assert.ok(!c.flippers.left.held.length && c.flippers.right.held.includes(4));
assert.ok(m.cancel(c, 4)); assert.ok(!c.flippers.right.held.length);
// I pulsanti e le frecce.
assert.ok(m.turn(c, -1, 'button')); assert.ok(c.flippers.left.held.length); m.stopTurn(c, 'button'); assert.ok(!c.flippers.left.held.length);
m.cancel(c); assert.ok(!c.flippers.right.held.length, 'annullare tutto (pausa) lascia i flipper');
// I pulsanti e le frecce; il pulsante da tastiera dà un colpo breve che la fisica vede.
assert.ok(m.turn(c, -1, 'button')); assert.ok(c.flippers.left.held.length); assert.ok(m.stopTurn(c, 'button')); assert.ok(!c.flippers.left.held.length);
m.turn(c, 1, 'click'); m.stopTurn(c, 'click'); assert.ok(c.flippers.right.held.includes('click'));
const a0 = c.flippers.right.angle; run(c, 0.1); assert.notEqual(c.flippers.right.angle, a0, 'il flipper si è mosso'); run(c, 0.3); assert.ok(!c.flippers.right.held.length);
assert.ok(m.key(c, 'ArrowRight', true)); assert.ok(c.flippers.right.held.length); m.key(c, 'ArrowRight', false); assert.ok(!c.flippers.right.held.length);
assert.ok(!m.key(c, 'x', true));
// Spazio: carica e lancia; Invio: lancia con la forza del cursore.
assert.ok(!m.setPower(c, 0.3), 'in gioco il cursore della molla non vale');
// Spazio: carica e lancia; Invio: lancia con la forza del cursore; a partita finita i tasti non sono suoi.
const d = m.newFlipper(9); assert.ok(m.key(d, ' ', true)); run(d, 0.3); assert.ok(d.holding); m.key(d, ' ', false); assert.ok(!m.waiting(d));
const e = m.newFlipper(10); assert.ok(m.setPower(e, 0.5)); assert.equal(m.button(e), 'Lancia'); m.key(e, 'Enter', true); assert.ok(!m.waiting(e)); assert.equal(m.button(e), 'In gioco…');
e.step = 'result'; e.result = { winner: 'cpu', score: 0, goal: 10000 }; assert.ok(!m.key(e, ' ', true) && !m.key(e, 'Enter', true));
});
test('il traguardo è acquisito appena raggiunto: anche uscendo prima, si vince; la trattenuta sul flipper non viene scossa', () => {
const c = m.newFlipper(11, { level: 'bona' }); m.launch(c, 0.9);
c.score = c.skill.goal - 100; put(c, { x: c.regolo.x, y: c.regolo.y + 12, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
assert.ok(c.result && c.result.winner === 'player' && c.step === 'play', 'traguardo fatto, si continua');
assert.match(m.status(c), /record/);
const g = new Game(story); g.go('via_provinciale'); settle(g);
g.act('use', 'river'); settle(g); g.choose(g.options().findIndex(o => o.text.startsWith('Una partita alla bona'))); settle(g);
g.closeup.score = 7000; g.closeup.result = { winner: 'player', score: 7000, goal: 6000 };
g.closeupClose(); settle(g);
assert.ok(g.state.flags.flipper_vinto, 'uscito dopo il traguardo: vinto');
// La pallina trattenuta sul flipper alzato resta lì (niente scossa): è una mossa del giocatore.
const h = m.newFlipper(12, { level: 'bona' }); m.launch(h, 0.9); m.hold(h, 'left', 't', true); run(h, 0.5);
put(h, { x: 296, y: 300, vx: 0, vy: 0 }); run(h, 4);
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('giocando con attenzione, alla bona si arriva al traguardo; nessuna partita si incastra', () => {
@@ -119,7 +166,7 @@ test('giocando con attenzione, alla bona si arriva al traguardo; nessuna partita
assert.equal(c.step, 'result', `seme ${seed}: finita`);
if (c.result.winner === 'player') won++;
}
assert.ok(won >= 4, `vinte ${won} su 8`);
assert.ok(won >= 5, `vinte ${won} su 8`);
});
test('il disegno del flipper non cambia lo stato', () => {
@@ -129,23 +176,22 @@ test('il disegno del flipper non cambia lo stato', () => {
const draw = () => { const before = structuredClone(c); drawFlipper(ctx, c, story.closeups.flipper, bg); assert.deepEqual(c, before); };
draw(); assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'FLIPPER DEL RIVER'));
m.launch(c, 0.8); run(c, 1); draw();
m.launch(c, 0.8); run(c, 1); calls.length = 0; draw();
assert.ok(calls.some(([op, t]) => op === 'fillText' && /^SALVATA \d s$/.test(t)), 'il salvataggio si vede');
c.step = 'result'; c.result = { winner: 'player', score: 7000, goal: 6000 }; calls.length = 0; draw();
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'TRAGUARDO FATTO!'));
});
test('al River: «Usa» o «Apri» il bar, il livello, la partita; vincendo il flag', () => {
test('al River: «Usa» o «Apri» il bar, il livello, la partita; uscendo senza traguardo, la battuta di chi lascia', () => {
const g = new Game(story); g.go('via_provinciale'); settle(g);
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);
assert.equal(g.closeup?.id, 'flipper'); assert.equal(g.closeup.level, 'seria');
g.closeup.step = 'result'; g.closeup.result = { winner: 'player', score: 13000, goal: 12000 };
g.closeupClose(); settle(g);
assert.ok(g.state.flags.flipper_vinto);
g.closeupClose();
assert.equal(g.queue[0]?.say, story.closeups.flipper.quit[0].say);
settle(g);
g.act('open', 'river'); settle(g);
g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
assert.equal(g.closeup?.level, 'male');
g.closeupClose();
assert.equal(g.queue[0]?.say, story.closeups.flipper.quit[0].say);
});