Le bocce al pallaio contro Mariano: si tiene premuto per la forza, tre bocce a testa, vince il più vicino al pallino; se vinci, «Hai vinto, ti ci porto!» a Brento Sanico (sfondo e Mariano di Codex dalle foto di Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { newBocce, press, release, update, powerAt, score, thrower, left, project, SWING, LANE, BALLS } from '../src/engine/bocce.js';
|
||||
import { story } from '../src/story/story.js';
|
||||
import { Game } from '../src/engine/game.js';
|
||||
import { settle } from './helpers.mjs';
|
||||
|
||||
const run = (c, seconds, dt = 1 / 60) => { for (let t = 0; t < seconds; t += dt) update(c, dt); };
|
||||
// Tiene premuto finché la forza porta la boccia a `u`, poi lascia (nella prima salita della barra).
|
||||
function throwTo(c, u) {
|
||||
assert.ok(press(c));
|
||||
const want = (u - 8) / (LANE - 4);
|
||||
while (c.power < want && c.holdTime < SWING / 2) update(c, 1 / 240);
|
||||
assert.ok(release(c));
|
||||
}
|
||||
|
||||
test('la forza sale e scende: a metà giro è piena, a giro finito torna a zero', () => {
|
||||
assert.equal(powerAt(0), 0);
|
||||
assert.ok(Math.abs(powerAt(SWING / 2) - 1) < 1e-9);
|
||||
assert.ok(powerAt(SWING) < 1e-9);
|
||||
});
|
||||
|
||||
test('una boccia va lontano quanto dice la forza, verso il pallino', () => {
|
||||
const c = newBocce(7);
|
||||
c.jack.u = 95; c.jack.w = 0; // il pallino lontano, così non lo tocca
|
||||
throwTo(c, 50);
|
||||
for (let i = 0; i < 600 && c.step === 'rolling'; i++) update(c, 1 / 60); // fino a boccia ferma, prima che tiri il campione
|
||||
const b = c.balls[0];
|
||||
assert.ok(Math.abs(b.u - 50) < 2.5, `si ferma a ${b.u.toFixed(1)}, non a 50`);
|
||||
assert.ok(Math.abs(b.w) < 4, 'va più o meno dritta');
|
||||
run(c, 1);
|
||||
assert.equal(thrower(c), 'cpu', 'dopo la pausa, tira il campione');
|
||||
});
|
||||
|
||||
test('una boccia che arriva addosso al pallino lo sposta', () => {
|
||||
const c = newBocce(3);
|
||||
c.jack.u = 40; c.jack.w = 0;
|
||||
c.balls.push({ who: 'player', u: 30, w: 0, vu: 30, vw: 0, r: 1.25 });
|
||||
c.step = 'rolling';
|
||||
run(c, 3);
|
||||
assert.ok(c.jack.u > 41, `il pallino resta a ${c.jack.u.toFixed(1)}`);
|
||||
});
|
||||
|
||||
test('il punto: vince la boccia più vicina, e conta quelle più vicine della migliore avversaria', () => {
|
||||
const c = newBocce(1);
|
||||
c.jack.u = 60; c.jack.w = 0;
|
||||
const put = (who, u) => c.balls.push({ who, u, w: 0, vu: 0, vw: 0, r: 1.25 });
|
||||
put('player', 61); put('player', 63); put('player', 80);
|
||||
put('cpu', 64); put('cpu', 70); put('cpu', 30);
|
||||
assert.deepEqual(score(c), { winner: 'player', points: 2 });
|
||||
c.balls[0].u = 75; c.balls[1].u = 76;
|
||||
assert.equal(score(c).winner, 'cpu');
|
||||
});
|
||||
|
||||
test('una mano intera: tre bocce a testa, a turno, poi il risultato', () => {
|
||||
const c = newBocce(42);
|
||||
for (let i = 0; i < 200 && c.step !== 'result'; i++) {
|
||||
if (c.step === 'aim' && thrower(c) === 'player') throwTo(c, c.jack.u);
|
||||
run(c, 0.5);
|
||||
}
|
||||
assert.equal(c.step, 'result');
|
||||
assert.equal(left(c, 'player'), 0);
|
||||
assert.equal(left(c, 'cpu'), 0);
|
||||
assert.equal(c.balls.length, BALLS * 2);
|
||||
assert.ok(['player', 'cpu'].includes(c.result.winner));
|
||||
assert.equal(press(c), false, 'a mano finita non si tira più');
|
||||
});
|
||||
|
||||
test('la prospettiva: lontano è più in alto e più piccolo, al centro della pista', () => {
|
||||
const near = project(0, 0), far = project(LANE, 0);
|
||||
assert.ok(far.y < near.y && far.s < near.s);
|
||||
assert.ok(project(50, 5).x > project(50, -5).x);
|
||||
});
|
||||
|
||||
test('al pallaio: Mariano sfida, la mano si gioca nel primo piano; vincere vale la gloria e la promessa', () => {
|
||||
const g = new Game(story);
|
||||
settle(g);
|
||||
g.go('tennis_chiesa'); settle(g);
|
||||
assert.ok(g.act('talk', 'mariano'));
|
||||
settle(g);
|
||||
g.choose(g.options().findIndex(o => o.text.startsWith('Mariano! Facciamo')));
|
||||
const before = settle(g).map(s => s.text);
|
||||
assert.ok(before.some(t => t.includes('Brento Sanico')), 'la posta la dice prima');
|
||||
assert.equal(g.closeup?.id, 'bocce');
|
||||
assert.equal(g.act('look', 'pallaio'), false, 'durante la partita la scena non risponde');
|
||||
// Si gioca la mano (le bocce di Andrea mirano al pallino); a mano finita il risultato si forza a favore.
|
||||
const c = g.closeup;
|
||||
for (let i = 0; i < 400 && c.step !== 'result'; i++) {
|
||||
if (c.step === 'aim' && thrower(c) === 'player') {
|
||||
g.closeupPress();
|
||||
while (c.power < (c.jack.u - 8) / (LANE - 4) && c.holdTime < SWING / 2) g.update(1 / 240);
|
||||
g.closeupRelease();
|
||||
}
|
||||
g.update(0.25);
|
||||
}
|
||||
assert.equal(c.step, 'result');
|
||||
c.result = { winner: 'player', points: 1 };
|
||||
for (let i = 0; i < 20 && g.closeup; i++) g.update(0.25);
|
||||
assert.equal(g.closeup, null, 'il risultato si chiude da solo');
|
||||
const after = settle(g).map(s => `${s.who}: ${s.text}`);
|
||||
assert.ok(after.includes('mariano: Hai vinto, ti ci porto!'));
|
||||
assert.ok(g.state.flags.bocce_vinte);
|
||||
// Vinto, si chiede la rivincita o Brento Sanico; lasciare la partita a metà non segna niente.
|
||||
g.act('talk', 'mariano'); settle(g);
|
||||
assert.ok(g.options().some(o => o.text === 'La rivincita?') && g.options().some(o => o.text === 'Allora, Brento Sanico?'));
|
||||
g.choose(g.options().findIndex(o => o.text === 'La rivincita?'));
|
||||
settle(g);
|
||||
assert.equal(g.closeup?.id, 'bocce');
|
||||
assert.ok(g.closeupClose());
|
||||
assert.ok(settle(g).some(s => s.text.startsWith('Già stanco?')));
|
||||
});
|
||||
@@ -362,6 +362,34 @@ try {
|
||||
await book.close();
|
||||
}
|
||||
|
||||
// Le bocce al pallaio: col mouse tenuto premuto la forza sale, lasciando la boccia parte; la scena dietro è ferma.
|
||||
{
|
||||
const bocce = await open({ width: 1280, height: 860 });
|
||||
await bocce.click('#splash');
|
||||
await bocce.click('#new-game');
|
||||
await bocce.click('#rione [data-rione="campomigliaio"]');
|
||||
await bocce.click('#skip-intro');
|
||||
await settle(bocce);
|
||||
await bocce.evaluate(() => game.go('tennis_chiesa', [430, 310]));
|
||||
await settle(bocce);
|
||||
await clickScene(bocce, await hotspotCenter(bocce, 'mariano'));
|
||||
await bocce.waitForFunction(() => game.dialogue && !game.busy, null, { timeout: 20000 });
|
||||
await bocce.locator('#choices li', { hasText: 'Facciamo una partita' }).click();
|
||||
await bocce.waitForFunction(() => game.closeup?.id === 'bocce' && game.closeup.step === 'aim' && !game.speech, null, { timeout: 30000 });
|
||||
const r = await bocce.locator('#scene').boundingBox();
|
||||
await bocce.mouse.move(r.x + r.width / 2, r.y + r.height / 2);
|
||||
await bocce.mouse.down();
|
||||
await bocce.waitForTimeout(500);
|
||||
assert.ok(await bocce.evaluate(() => game.closeup.power > 0.2), 'tenendo premuto la forza sale');
|
||||
await bocce.screenshot({ path: `${OUT}/03c-bocce-forza.png` });
|
||||
await bocce.mouse.up();
|
||||
assert.equal(await bocce.evaluate(() => game.closeup.balls.length), 1, 'lasciando, la boccia parte');
|
||||
await bocce.waitForFunction(() => game.closeup.balls.length === 2, null, { timeout: 15000 });
|
||||
await bocce.keyboard.press('Escape');
|
||||
await bocce.waitForFunction(() => game.speech?.text.startsWith('Già stanco?'), null, { timeout: 5000 });
|
||||
await bocce.close();
|
||||
}
|
||||
|
||||
const phone = await open({ width: 390, height: 844 });
|
||||
await phone.click('#splash'); // un clic salta il cartello
|
||||
assert.ok(await phone.isVisible('#title'), 'il clic sul cartello porta al titolo');
|
||||
|
||||
@@ -70,7 +70,7 @@ test('la storia è coerente: scene, oggetti, dialoghi e punti calpestabili esist
|
||||
if (e.closeup) {
|
||||
const c = story.closeups?.[e.closeup];
|
||||
assert.ok(c, `${where}: primo piano ${e.closeup}`);
|
||||
for (const src of c.type === 'torch' ? [c.picture] : [c.closed, c.open]) assert.ok(existsSync(new URL('../' + src, import.meta.url)), `${where}: manca ${src}`);
|
||||
for (const src of c.picture ? [c.picture] : [c.closed, c.open]) assert.ok(existsSync(new URL('../' + src, import.meta.url)), `${where}: manca ${src}`);
|
||||
}
|
||||
if (e.who) assert.ok(story.characters[e.who], `${where}: personaggio ${e.who}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user