GP di Fortezza: guida semplice alla bona e Seria (si tiene premuto per il gas, lasciando si frena, lo sterzo va da solo e troppo forte in curva si scivola nella paglia), per il telefono; Ginocchio in terra resta con lo sterzo vero; prova nel browser del GP (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:18:22 +01:00
co-authored by Claude Opus 5.5
parent 9d2cc2fcb3
commit 1b7511f6ca
7 changed files with 151 additions and 19 deletions
+56
View File
@@ -0,0 +1,56 @@
// Il GP di Fortezza nel browser vero: alla bona e seria la guida semplice (solo gas, lasciando si frena), a ginocchio
// in terra lo sterzo vero. npm run browser:gara — schermate in /tmp/regolo-gara.
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-gara';
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, level] of [
['touch', true, { width: 844, height: 390 }, 'seria'],
['portrait', true, { width: 390, height: 844 }, 'bona'],
['desktop', false, { width: 1280, height: 860 }, 'male'],
]) {
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 level => {
const { settle } = await import('/tests/helpers.mjs');
settle(game); game.run({ closeup: 'gara', level }); settle(game);
}, level);
await page.waitForSelector('#bocce-controls:not([hidden])');
// Salta la presentazione dei piloti e aspetta il via.
await page.evaluate(async () => { const g = await import('/src/engine/gara.js'); while (game.closeup.step === 'intro') g.press(game.closeup); });
await page.waitForFunction(() => game.closeup.step === 'race', null, { timeout: 15000 });
const easy = level !== 'male';
for (const id of ['#bocce-left', '#bocce-right', '#gara-brake', '#gara-pad'])
assert.equal(await page.locator(id).isVisible(), !easy, `${name}: ${id} ${easy ? 'nascosto' : 'visibile'}`);
assert.equal(await page.locator('#bocce-power').isVisible(), !easy, 'la sensibilità solo con lo sterzo vero');
// Il pulsante Gas accende il gas; un altro tocco lo spegne e nella guida semplice si frena.
if (mobile) await page.locator('#bocce-throw').tap(); else await page.locator('#bocce-throw').click();
await page.waitForFunction(() => game.closeup.riders.find(r => r.id === 'andrea').v > 40, null, { timeout: 15000 });
await page.screenshot({ path: `${out}/${name}-${level}.png` });
if (mobile) await page.locator('#bocce-throw').tap(); else await page.locator('#bocce-throw').click();
const v = await page.evaluate(() => game.closeup.riders.find(r => r.id === 'andrea').v);
await page.waitForTimeout(300);
const after = await page.evaluate(() => game.closeup.riders.find(r => r.id === 'andrea').v);
assert.ok(after < v, `${name}: senza gas rallenta (${v.toFixed(0)} → ${after.toFixed(0)})`);
await page.close();
}
assert.deepEqual(errors, []);
console.log(`ok: schermate in ${out}`);
} finally {
await browser?.close();
server.kill();
}
+40 -6
View File
@@ -1,6 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { SWIPE, setPower, newGara, update, press, presenting, INTRO, release, cancel, shoot, key, steerButton, brakeButton, aimScreen, steering, status, button, standings, player, rider, limitAt, pointAt, TRACK, CURVES, PICKUPS, BRAKE, VERGE, LAPS, COUNTDOWN, RIDERS } from '../src/engine/gara.js';
import { SIMPLE, simple, simpleLimit, SWIPE, setPower, newGara, update, press, presenting, INTRO, release, cancel, shoot, key, steerButton, brakeButton, aimScreen, steering, status, button, standings, player, rider, limitAt, pointAt, TRACK, CURVES, PICKUPS, BRAKE, VERGE, LAPS, COUNTDOWN, RIDERS } from '../src/engine/gara.js';
import { piloti } from '../src/story/piloti.js';
import { story } from '../src/story/story.js';
import { Game } from '../src/engine/game.js';
@@ -58,7 +58,7 @@ test('i piloti sono dati: motore da 1 a 6 e testa da 1 a 5 danno velocità, acce
});
test('il semaforo, poi il gas: tenendo premuto si va, lasciando si rallenta, il freno vince sul gas', () => {
const c = newGara(3, { level: 'seria', riders: RIDERS });
const c = newGara(3, { level: 'seria', riders: RIDERS, simple: false });
for (const r of RIDERS) assert.ok(press(c, 7), r.id);
assert.equal(c.step, 'start', 'un tocco per pilota e la presentazione è finita');
assert.equal(c.holding, false, 'senza dare gas');
@@ -161,7 +161,7 @@ test('l’origine segue gli swipe lunghi e si può correggere; rilascio e annull
});
test('alla bona si completano tre giri usando solo un dito, senza tasti o pulsanti', () => {
const c = race(newGara(5, { level: 'bona', riders: piloti.filter(p => p.consenso !== null) }));
const c = race(newGara(5, { level: 'bona', riders: piloti.filter(p => p.consenso !== null), simple: false }));
aimScreen(c, [320, 180], 'bot'); press(c, 'bot');
for (let t = 0; t < 180 && c.step === 'race'; t += 1 / 60) {
swipeTo(c, pointAt(player(c).s + 50));
@@ -198,7 +198,7 @@ test('la presentazione: uno alla volta arrivano in griglia col motorino, poi il
});
test('sempre a gas e dritto si finisce nella paglia; con giudizio alla bona si vince', () => {
const wild = newGara(5, { level: 'bona', riders: piloti.filter(p => p.consenso !== null) });
const wild = newGara(5, { level: 'bona', riders: piloti.filter(p => p.consenso !== null), simple: false });
skip(wild);
run(wild, COUNTDOWN + 60, () => true);
assert.ok(wild.heard.has('gara-fuori:andrea'));
@@ -206,7 +206,7 @@ test('sempre a gas e dritto si finisce nella paglia; con giudizio alla bona si v
assert.equal(wild.step, 'race', 'dritto non si arriva');
const heard = new Set();
for (const seed of [1, 2, 3, 4]) {
const c = newGara(seed, { level: 'bona', riders: piloti.filter(p => p.consenso !== null) });
const c = newGara(seed, { level: 'bona', riders: piloti.filter(p => p.consenso !== null), simple: false });
skip(c); setPower(c, 1); // a sensibilità piena: il dito risponde subito
for (const k of run(c, COUNTDOWN + 120, careful)) heard.add(k);
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'player', `seme ${seed}`);
@@ -219,7 +219,7 @@ test('sempre a gas e dritto si finisce nella paglia; con giudizio alla bona si v
test('alla sensibilità di partenza (bassa) lo stesso pilota non finisce mai nella paglia', () => {
for (const seed of [1, 2, 3]) {
const c = newGara(seed, { level: 'bona', riders: piloti.filter(p => p.consenso !== null) });
const c = newGara(seed, { level: 'bona', riders: piloti.filter(p => p.consenso !== null), simple: false });
skip(c); assert.equal(c.sens, SWIPE.sens);
run(c, COUNTDOWN + 120, careful);
assert.equal(c.step, 'result'); assert.equal(c.result.offs, 0, `seme ${seed}`);
@@ -270,3 +270,37 @@ test('nella storia: dal Trappola l\'Arcu scherza sul Betino, poi la sfida; prima
// Il Betino, lassù: solo Andrea ne parla.
g.act('use', 'betino'); assert.ok(settle(g).every(s => s.who === 'andrea'));
});
// La guida semplice (Andrea, 29 ottobre: dal telefono lo sterzo era ingiocabile): alla bona e seria si tiene premuto
// per il gas e lasciando si frena, lo sterzo va da solo; troppo forte in curva si scivola fuori. Ginocchio in terra no.
test('guida semplice: alla bona e seria sì, a ginocchio in terra no; lasciando si frena e lo sterzo va da solo', () => {
const riders = piloti.filter(p => p.consenso !== null);
assert.equal(simple(newGara(1, { level: 'bona', riders })), true);
assert.equal(simple(newGara(1, { level: 'seria', riders })), true);
assert.equal(simple(newGara(1, { level: 'male', riders })), false);
const c = race(newGara(1, { level: 'bona', riders }));
run(c, 2, () => true);
const v = player(c).v; assert.ok(v > 40);
run(c, 0.3, () => false);
assert.ok(player(c).v < v - BRAKE * 0.3 + 5, 'lasciando frena, non solo rallenta');
assert.match(status(c), /Freno\./);
// Le frecce non sterzano: lo sterzo segue la strada.
key(c, 'ArrowLeft', true); run(c, 2, () => true); key(c, 'ArrowLeft', false);
assert.ok(Math.abs(player(c).lat) < 9, 'resta in strada');
});
test('guida semplice: sempre a tutto gas si finisce nella paglia; mollando prima delle curve no, e alla bona si vince', () => {
const riders = piloti.filter(p => p.consenso !== null);
// Molla quando, frenando da qui, arriverebbe a una curva sopra il limite della guida semplice.
const wise = c => { const r = player(c), h = r.v * r.v / (2 * BRAKE) + 10; for (let d = 0; d <= h; d += 4) { const a = simpleLimit(r, r.s + d); if (r.v * r.v > a * a + 2 * BRAKE * d) return false; } return true; };
for (const seed of [1, 2]) {
const wild = race(newGara(seed, { level: 'bona', riders }));
run(wild, 120, () => true);
assert.ok(wild.result.offs > 0, 'a tutto gas si esce');
const c = race(newGara(seed, { level: 'bona', riders }));
run(c, 120, wise);
assert.equal(c.result.offs, 0); assert.equal(c.result.falls, 0, 'le buche si scansano');
assert.equal(c.result.winner, 'player', `seme ${seed}`);
}
assert.ok(SIMPLE.slide > 0);
});