La cerbottana: dalla bottega di Pierin di Frilli in piazza lo stucco verde, poi i pallini contro il muro della piazza (fiato, respiro, tosse, bersaglio di gesso, lattine, barattolo appeso; muro di Codex) (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
// La cerbottana nel browser vero: dalla bottega di Pierin di Frilli al muro della piazza; mira, fiato e tiro col mouse
|
||||
// e col dito. npm run browser:cerbottana — schermate in /tmp/regolo-cerbottana.
|
||||
import { chromium } from 'playwright';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import assert from 'node:assert/strict';
|
||||
import { staticServer } from './helpers.mjs';
|
||||
|
||||
const out = process.env.OUT ?? '/tmp/regolo-cerbottana';
|
||||
mkdirSync(out, { recursive: true });
|
||||
const server = await staticServer();
|
||||
let browser;
|
||||
const errors = [];
|
||||
try {
|
||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
||||
for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) {
|
||||
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
await page.goto(`http://127.0.0.1:${server.port}/`);
|
||||
await page.click('#splash'); await page.click('#new-game'); await page.click('#rione [data-rione="campomigliaio"]'); await page.click('#skip-intro');
|
||||
await page.evaluate(async () => {
|
||||
const { settle } = await import('/tests/helpers.mjs');
|
||||
settle(game); game.go('piazza'); settle(game);
|
||||
game.act('talk', 'pierin'); settle(game);
|
||||
game.choose(game.options().findIndex(o => o.text.startsWith('Una cerbottana'))); settle(game);
|
||||
game.choose(game.options().findIndex(o => o.text === 'Seria.')); settle(game);
|
||||
});
|
||||
await page.waitForSelector('#bocce-controls:not([hidden])');
|
||||
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/muro-cerbottana.png'));
|
||||
assert.equal(await page.locator('#bocce-throw').textContent(), 'Spara');
|
||||
assert.ok(await page.locator('#cerbottana-help').isVisible());
|
||||
await page.waitForTimeout(1600);
|
||||
const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height];
|
||||
if (mobile) await page.touchscreen.tap(...P(320, 118)); else { await page.mouse.move(...P(320, 118)); await page.mouse.click(...P(320, 118)); }
|
||||
assert.equal(await page.evaluate(() => game.closeup.step), 'aim', 'un tocco mira soltanto');
|
||||
await page.screenshot({ path: `${out}/${name}-mira.png` });
|
||||
if (mobile) { await page.locator('#bocce-power').fill('70'); await page.locator('#bocce-throw').tap(); }
|
||||
else { await page.mouse.down(); await page.waitForFunction(() => game.closeup.power >= 0.6); await page.mouse.up(); }
|
||||
await page.waitForFunction(() => game.closeup.left === 11 && game.closeup.step === 'aim', null, { timeout: 10000 });
|
||||
await page.screenshot({ path: `${out}/${name}-dopo.png` });
|
||||
await page.close();
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(`ok: schermate in ${out}`);
|
||||
} finally { await browser?.close(); server.kill(); }
|
||||
@@ -0,0 +1,82 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as m from '../src/engine/cerbottana.js';
|
||||
import { drawCerbottana } from '../src/engine/cerbottana-render.js';
|
||||
import { Game, MINIGAMES } from '../src/engine/game.js';
|
||||
import { story } from '../src/story/story.js';
|
||||
import { settle } from './helpers.mjs';
|
||||
|
||||
// Ferma il respiro (il mirino va dove si punta), spara col fiato dato e aspetta che il pallino arrivi.
|
||||
function shot(c, x, y, power = m.BEST) {
|
||||
c.skill = { ...c.skill, sway: 0 }; c.aim = { x, y };
|
||||
assert.ok(m.setPower(c, power)); assert.ok(m.shoot(c));
|
||||
for (let i = 0; i < 200 && c.step === 'flying'; i++) m.update(c, 1 / 60);
|
||||
return c.line;
|
||||
}
|
||||
|
||||
test('col fiato giusto il pallino va dove si punta; poco fiato cade sotto, troppo va sopra; resta attaccato al muro', () => {
|
||||
const c = m.newCerbottana(1, { level: 'seria' });
|
||||
assert.match(shot(c, m.TARGET.x, m.TARGET.y), /10 punti/);
|
||||
assert.equal(c.points, 10); assert.deepEqual(c.stuck.at(-1), { x: m.TARGET.x, y: m.TARGET.y });
|
||||
for (let i = 0; i < 60; i++) m.update(c, 1 / 60);
|
||||
assert.ok(m.landing(c, 0.5).y > m.TARGET.y + 40, 'poco fiato: sotto');
|
||||
assert.ok(m.landing(c, 0.9).y < m.TARGET.y - 40, 'troppo fiato: sopra');
|
||||
shot(c, 320, 200, 0.2);
|
||||
assert.match(c.line, /corto/); assert.equal(c.stuck.length, 1, 'il pallino corto non resta sul muro');
|
||||
});
|
||||
|
||||
test('le lattine cadono, il barattolo appeso c\'è solo a seria e a fassi male; finito lo stucco si guarda il punteggio', () => {
|
||||
const c = m.newCerbottana(2, { level: 'bona' });
|
||||
const k = c.cans[0];
|
||||
assert.match(shot(c, k.x, m.LEDGE - 8), /Lattina giù/);
|
||||
assert.ok(k.down); assert.equal(c.points, m.CAN.points);
|
||||
assert.equal(m.LEVELS.bona.swinger, false); assert.equal(m.LEVELS.seria.swinger, true);
|
||||
// Il barattolo dondola mentre il pallino vola: si mira dove sarà all'arrivo.
|
||||
const s = m.newCerbottana(3, { level: 'seria' }), p = m.swinger({ visual: { time: s.visual.time + m.FLIGHT + 1 / 120 } });
|
||||
assert.match(shot(s, p.x, p.y + m.SWINGER.h / 2), /barattolo appeso/);
|
||||
for (let i = 0; i < 60; i++) m.update(c, 1 / 60);
|
||||
while (c.left > 0) { for (let i = 0; i < 60 && !m.canThrow(c); i++) m.update(c, 1 / 60); shot(c, m.TARGET.x, m.TARGET.y); }
|
||||
for (let i = 0; i < 60; i++) m.update(c, 1 / 60);
|
||||
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'player');
|
||||
assert.equal(c.stuck.length, m.PELLETS - 1);
|
||||
});
|
||||
|
||||
test('i comandi: un tocco mira, tenere premuto prende fiato da zero, lasciare spara; troppo a fiato pieno si tossisce', () => {
|
||||
const c = m.newCerbottana(4);
|
||||
assert.ok(m.aimScreen(c, [200, 100])); assert.deepEqual(c.aim, { x: 200, y: 100 });
|
||||
assert.ok(m.press(c, 1)); m.update(c, 0.05); assert.equal(m.release(c, 1), true); assert.equal(c.step, 'aim', 'il tocco breve non spara');
|
||||
assert.ok(m.press(c, 1)); assert.equal(c.power, 0);
|
||||
for (let i = 0; i < 40; i++) m.update(c, 1 / 60);
|
||||
assert.ok(c.charging && c.power > 0.5 && c.power < 0.8);
|
||||
assert.ok(m.release(c, 1)); assert.equal(c.step, 'flying'); assert.equal(c.left, m.PELLETS - 1);
|
||||
const d = m.newCerbottana(5); m.press(d, 1);
|
||||
for (let i = 0; i < 60 * (m.SWING + m.COUGH + 0.2); i++) m.update(d, 1 / 60);
|
||||
assert.equal(d.holding, false); assert.ok(d.events.some(e => e.kind === 'cerbottana-tosse'));
|
||||
for (let i = 0; i < 60; i++) m.update(d, 1 / 60);
|
||||
assert.match(d.line, /Coff/); assert.equal(d.left, m.PELLETS - 1);
|
||||
assert.ok(m.key(d, 'ArrowUp', true)); assert.equal(d.aim.y, m.TARGET.y - 4);
|
||||
});
|
||||
|
||||
test('il disegno: muro, gesso, lattine, pallini; 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.newCerbottana(6, { level: 'seria' }), bg = {};
|
||||
const draw = () => { const before = structuredClone(c); drawCerbottana(ctx, c, story.closeups.cerbottana, 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 === 'LA CERBOTTANA'));
|
||||
c.step = 'result'; c.result = { winner: 'player', points: 44, goal: 40 }; calls.length = 0; draw();
|
||||
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'CENTRATO!'));
|
||||
});
|
||||
|
||||
test('da Pierin di Frilli, in piazza: lo stucco, il livello e il muro', () => {
|
||||
assert.ok(MINIGAMES.cerbottana);
|
||||
const g = new Game(story); g.go('piazza'); settle(g);
|
||||
const h = g.hotspot('pierin');
|
||||
assert.equal(g.hotspotAt([h.box[0] + h.box[2] / 2, h.box[1] + h.box[3] / 2]).id, 'pierin');
|
||||
g.act('talk', 'pierin'); settle(g);
|
||||
g.choose(g.options().findIndex(o => o.text.startsWith('Una cerbottana'))); settle(g);
|
||||
g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
|
||||
assert.equal(g.closeup?.id, 'cerbottana'); assert.equal(g.closeup.level, 'male');
|
||||
g.closeup.step = 'result'; g.closeup.result = { winner: 'player', points: 52, goal: 50 };
|
||||
g.closeupClose(); settle(g);
|
||||
assert.ok(g.state.flags.cerbottana_vinta);
|
||||
});
|
||||
Reference in New Issue
Block a user