diff --git a/docs/STATO.md b/docs/STATO.md index e47bc9b..07a811d 100644 --- a/docs/STATO.md +++ b/docs/STATO.md @@ -18,6 +18,11 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto, ## Dove siamo arrivati +- **GP di Fortezza, guida semplice (29 ottobre)**: alla bona e Seria si tiene premuto per il gas e lasciando si + frena; lo sterzo segue la strada da solo (scansa le buche, sorpassa, prende la miscela), ma oltre la velocità che la + curva regge il motorino scivola fuori verso la paglia (`SIMPLE` in `gara.js`). Ginocchio in terra resta con lo + sterzo vero. Il pannello nasconde Sterza, Frena, swipe e sensibilità. Test in `gara.test.mjs` e + `npm run browser:gara`. - **La pista col gesso (29 ottobre, modifiche locali)**: le Micro Machines sul parcheggio di Cardetole contro il Chiaru (Matteo Chiarusi, sprite di Codex dalla foto di Andrea, accanto alla pista). Motore in `micro.js`, disegno in `micro-render.js`: pista disegnata a gesso dal codice sopra l'asfalto di Codex (`micro-parcheggio.png`), un giro, diff --git a/index.html b/index.html index 8ad28d6..a0fc64e 100644 --- a/index.html +++ b/index.html @@ -225,7 +225,11 @@ guida mostra la parabola fino al primo urto, Seria solo l’inizio, A fassi male niente.
Il GP di Fortezza: tre giri attorno alle mura coi cinquantini. Con un solo dito: tieni premuto sulla scena per +
Il GP di Fortezza: tre giri attorno alle mura coi cinquantini. Alla bona e Seria la guida è semplice: + tieni premuto sulla scena (o Spazio, o il pulsante Gas) per accelerare e lascia per frenare; lo sterzo segue la + strada da solo, scansa le buche e sorpassa. Ma se entri in curva troppo forte scivoli fuori, e nella paglia ti + fermi: molla prima delle curve.
+Ginocchio in terra si guida davvero. Con un solo dito: tieni premuto sulla scena per il gas e trascina verso la direzione in cui vuoi andare sullo schermo. A destra vai verso destra, anche quando il motorino scende. I piccoli movimenti non sterzano; il cerchietto segue gli swipe lunghi. Alza il dito per mollare il gas e raddrizzare lo sterzo. Funziona anche col mouse premuto.
diff --git a/package.json b/package.json index fad6d1f..4f5b556 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "browser": "node tests/browser.mjs", "browser:biliardo": "node tests/biliardo.browser.mjs", "browser:micro": "node tests/micro.browser.mjs", + "browser:gara": "node tests/gara.browser.mjs", "browser:tressette": "node tests/tressette.browser.mjs", "browser:scopa": "node tests/scopa.browser.mjs", "browser:briscola": "node tests/briscola.browser.mjs", diff --git a/src/engine/gara.js b/src/engine/gara.js index dd55aed..cad5b48 100644 --- a/src/engine/gara.js +++ b/src/engine/gara.js @@ -50,12 +50,17 @@ export const RIDERS = [ { id: 'andrea', name: 'Andrea', bike: 'il Fifty', motore: 3, testa: 3, color: '#d8402f', trim: '#1c1c1c' }, { id: 'arcu', name: 'L\'Arcu', bike: 'il Betino', motore: 1, testa: 5, color: '#2f7a3a', trim: '#e8d9a0' }, ]; -// I livelli: quanto vanno gli altri, quanto si resta fermi nella paglia, e la guida disegnata in pista. +// I livelli: quanto vanno gli altri, quanto si resta fermi nella paglia, e la guida disegnata in pista. `simple`: la +// guida semplice (Andrea, 29 ottobre: dal telefono lo sterzo era ingiocabile): si tiene premuto per il gas e lasciando +// si frena, lo sterzo segue la strada da solo, e chi entra in curva troppo forte scivola fuori. Ginocchio in terra +// tiene lo sterzo vero. export const LEVELS = { - bona: { rivals: 0.74, stun: 0.4, guide: 'zone' }, - seria: { rivals: 0.84, stun: 0.9, guide: 'coni' }, + bona: { rivals: 0.74, stun: 0.4, guide: 'zone', simple: true }, + seria: { rivals: 0.84, stun: 0.9, guide: 'coni', simple: true }, male: { rivals: 0.93, stun: 1.4, guide: null }, }; +// `def.simple` la forza (i test della guida vera la spengono anche alla bona). +export const simple = c => !!c.simple; function catmull(p0, p1, p2, p3, t) { const t2 = t * t, t3 = t2 * t; @@ -154,7 +159,7 @@ export function newGara(seed = Date.now() >>> 0, def = {}) { }); if (!riders.some(r => r.id === 'andrea')) throw new Error('Nella gara manca Andrea'); for (const r of riders) { r.grid = r.s; r.shown = false; } - return { step: 'intro', t: 0, age: 0, seed, level, skill, riders, holding: false, braking: false, keys: { left: false, right: false, gas: new Set(), brake: false }, + return { step: 'intro', t: 0, age: 0, seed, level, skill, simple: def.simple ?? !!skill.simple, riders, holding: false, braking: false, keys: { left: false, right: false, gas: new Set(), brake: false }, pointer: null, toggle: false, brakeButton: false, drag: 0, buttons: 0, anchor: null, finger: null, sens: SWIPE.sens, power: SWIPE.sens, accumulator: 0, events: [], result: null, order: riders.map(r => r.id), taken: {} }; } @@ -258,12 +263,33 @@ export function steering(c) { // La distanza davanti, sul giro, da s a un punto della pista (sempre fra 0 e la lunghezza del giro). export const ahead = (from, to) => ((((to - from) % TRACK.length) + TRACK.length) % TRACK.length); +// Lo sterzo che segue la strada: si guarda un punto più avanti sulla propria corsia e ci si gira (gli avversari, e +// Andrea nella guida semplice). Andando troppo forte in curva lo sterzo non basta e si scivola fuori. +const follow = r => { const aim = pointAt(r.s + 14 + r.v * 0.18, r.lane * 0.6); return clamp(wrapAngle(Math.atan2(aim.y - r.y, aim.x - r.x) - r.a) * 2.6, -1, 1); }; +// La guida semplice di Andrea: lo sterzo scansa le buche, sorpassa e va sulle lattine di miscela; ma oltre la velocità +// che la curva regge (la più stretta nei SIMPLE.window pixel davanti) il motorino scivola verso l'esterno, tanto più +// quanto si esagera (SIMPLE.slide): a tutto gas, nelle curve vere si finisce nella paglia. +export const SIMPLE = { grip: 1, slide: 8, window: 50 }; +export function simpleLimit(r, s = r.s) { + let low = Infinity; + for (let d = -10; d <= SIMPLE.window; d += STEP) low = Math.min(low, limitAt(s + d)); + return low * r.grip * SIMPLE.grip; +} +function autopilot(c, r) { + const next = PICKUPS.find(p => p.kind !== 'boost' && ahead(r.s, p.s) < 30 + r.v * 0.6); + // Chi sta davanti più lento si passa dall'altra parte; le buche si scansano, la miscela si prende. + const slow = c.riders.find(o => o !== r && o.s - r.s > 0 && o.s - r.s < 26 && Math.abs(o.lat - r.lat) < 7 && o.v < r.v + 8); + if (next?.kind === 'buca') r.lane = next.lane > 0 ? -5 : 5; + else if (slow) r.lane = slow.lat > 0 ? -6 : 6; + else if (next?.kind === 'miscela') r.lane = next.lane; + r.excess = Math.max(0, r.v - simpleLimit(r)); + return follow(r); +} // Gli avversari: guardano un punto più avanti sulla loro corsia e ci sterzano; danno gas finché, frenando da qui, // arriverebbero a ogni curva davanti sotto il limite (un po' meno, quanto si fidano). Chi ha poca testa ogni tanto // si fida troppo. function drive(c, r) { - const aim = pointAt(r.s + 14 + r.v * 0.18, r.lane * 0.6); - r.steer = clamp(wrapAngle(Math.atan2(aim.y - r.y, aim.x - r.x) - r.a) * 2.6, -1, 1); + r.steer = follow(r); const curve = CURVES.find(k => { const s = ((r.s % TRACK.length) + TRACK.length) % TRACK.length; return s >= k.start - 60 && s < k.start - 57; }); if (curve && r.lastCurve !== curve) { r.lastCurve = curve; r.risk = random(c) < r.slip ? 1.15 : 1; } const horizon = (r.v * r.v) / (2 * BRAKE) + 20; @@ -284,6 +310,7 @@ function drive(c, r) { function stepRider(c, r, dt) { if (r.finished !== null) { r.gas = false; r.brake = false; drive(c, r); r.gas = false; } else if (r.out > 0) { r.out = Math.max(0, r.out - dt); r.v = 0; r.u = 0; return; } + else if (r.id === 'andrea' && simple(c)) { r.gas = c.holding; r.brake = !c.holding; r.steer = autopilot(c, r); } else if (r.id === 'andrea') { r.gas = c.holding; r.brake = c.braking; r.steer = steering(c); } else drive(c, r); if (r.finished !== null) r.gas = false; @@ -299,6 +326,7 @@ function stepRider(c, r, dt) { const turn = r.steer * TURN * Math.min(1, r.v / 22) * dt; r.a = wrapAngle(r.a + turn); r.u -= r.v * Math.sin(turn) * SLIDE * (r.v / r.vmax) / r.grip; + if (r.excess) r.u -= TRACK.side[r.idx] * r.excess * SIMPLE.slide * dt; // la guida semplice: troppo forte, si allarga r.u *= Math.exp(-GRIP * r.grip * dt); r.x += (Math.cos(r.a) * r.v - Math.sin(r.a) * r.u) * dt; r.y += (Math.sin(r.a) * r.v + Math.cos(r.a) * r.u) * dt; @@ -406,7 +434,7 @@ export function status(c) { const r = presenting(c); return r ? `${r.name} con ${r.bike}. (Un tocco: il prossimo.)` : 'Tutti in griglia!'; } - if (c.step === 'start') return c.t < COUNTDOWN - 0.01 ? `GP di Fortezza: si parte fra ${Math.ceil(COUNTDOWN - c.t)}… Tieni premuto per il gas, sterza con ← →.` : 'Via!'; + if (c.step === 'start') return c.t < COUNTDOWN - 0.01 ? `GP di Fortezza: si parte fra ${Math.ceil(COUNTDOWN - c.t)}… Tieni premuto per il gas, ${simple(c) ? 'lascia per frenare' : 'sterza con ← →'}.` : 'Via!'; if (c.step === 'result') { const who = c.riders.find(r => r.id === c.result.order[0]).name; return c.result.place === 1 ? `Primo al GP di Fortezza! ${LAPS} giri in ${c.result.times.andrea.toFixed(1)} secondi.` : `${c.result.place}° posto. Ha vinto ${who}.`; @@ -414,6 +442,7 @@ export function status(c) { const me = player(c), place = placeOf(c); if (me.out > 0) return me.lastHole !== undefined ? 'Nella buca! Ci si rialza…' : 'Nella paglia! Si riparte…'; if (me.boost > 0) return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. Miscela!`; + if (simple(c)) return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. ${c.holding ? 'Gas!' : 'Freno.'}`; return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. ${c.braking ? 'Freno!' : c.holding ? 'Gas!' : 'Senza gas.'}`; } export const button = c => c.step === 'intro' ? 'Avanti' : c.step === 'result' ? 'Finita' : c.holding ? 'Molla' : 'Gas'; diff --git a/src/main.js b/src/main.js index 5b76f7a..43c02dc 100644 --- a/src/main.js +++ b/src/main.js @@ -606,7 +606,7 @@ const MINI = { micro: { ...micro, back: 'Torna dal Chiaru', label: 'La pista col gesso: muovi il mouse o tocca il parcheggio per mirare, tieni premuto per caricare la spinta e lascia; oppure regola la forza e premi Spingi. Frecce o pulsanti Mira per girare.' }, basket: { ...basket, back: 'Torna in strada', label: 'Fai due tiri: muovi il mouse o tocca per mirare, tieni premuto per caricare la forza, lascia per tirare.' }, // La gara: niente mira né forza, il pulsante alterna gas e freno (chi non può tenere premuto). - gara: { ...gara, toggle: true, back: 'Torna dal Trappola', label: 'GP di Fortezza: tieni premuto sulla scena per il gas e trascina verso la direzione in cui vuoi andare sullo schermo; alza il dito per mollare. Anche col mouse. Oppure Spazio, Invio o ↑ per il gas, ← → per sterzare, ↓ per frenare; disponibili anche i pulsanti Gas, Sterza e Frena.' }, + gara: { ...gara, toggle: true, back: 'Torna dal Trappola', label: 'GP di Fortezza. Alla bona e Seria: tieni premuto sulla scena (o Spazio) per il gas e lascia per frenare, lo sterzo va da solo; in curva troppo forte si esce. Ginocchio in terra: tieni premuto sulla scena per il gas e trascina verso la direzione in cui vuoi andare sullo schermo; alza il dito per mollare. Anche col mouse. Oppure Spazio, Invio o ↑ per il gas, ← → per sterzare, ↓ per frenare; disponibili anche i pulsanti Gas, Sterza e Frena.' }, pesca: { ...pesca, back: 'Torna in Fortezza', label: 'La pesca: tieni premuto per lanciare, premi quando il galleggiante va sotto, tieni premuto per recuperare.' }, }; const miniState = () => running && game?.closeup && MINI[story.closeups[game.closeup.id].type] ? game.closeup : null; @@ -1011,19 +1011,22 @@ function updateUi() { // Il GP di Fortezza: niente forza, e i due pulsanti di mira diventano lo sterzo (si tengono premuti). // Nel GP di Fortezza il cursore della forza diventa la sensibilità dello sterzo col dito (ricordata). const powerLabel = $('bocce-power').closest('label'); - // Ai rinquarti la forza è sempre piena: il cursore non serve. - setProp(powerLabel, 'hidden', def.type === 'rinquarto'); + // Ai rinquarti la forza è sempre piena: il cursore non serve. Nella guida semplice del GP (alla bona e seria) si + // usa solo il gas: niente sensibilità, sterzo, freno né riquadro dello swipe. + const easy = def.type === 'gara' && gara.simple(c); + setProp(powerLabel, 'hidden', def.type === 'rinquarto' || easy); + for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy); const powerName = def.type === 'gara' ? 'Sensibilità ' : 'Forza '; if (powerLabel.firstChild.nodeValue !== powerName) powerLabel.firstChild.nodeValue = powerName; setAttr($('bocce-power'), 'aria-label', def.type === 'gara' ? 'Sensibilità dello sterzo col dito' : 'Forza del lancio'); - setProp($('gara-pad'), 'hidden', def.type !== 'gara'); + setProp($('gara-pad'), 'hidden', def.type !== 'gara' || easy); if (def.type === 'gara' && c.sensLoaded === undefined) { c.sensLoaded = true; try { const v = localStorage.getItem(GARA_SENS); if (v !== null) gara.setPower(c, Number(v) / 100); } catch { /* si resta al valore di partenza */ } } setText($('bocce-left'), def.type === 'gara' ? '← Sterza' : '← Mira'); setText($('bocce-right'), def.type === 'gara' ? 'Sterza →' : 'Mira →'); - setProp($('gara-brake'), 'hidden', def.type !== 'gara'); + setProp($('gara-brake'), 'hidden', def.type !== 'gara' || easy); setProp($('gara-brake'), 'disabled', !(mg.canAdjust?.(c) ?? false) || !!game.speech); setText($('bocce-throw'), mg.button?.(c) ?? (c.step === 'result' ? 'Finita' : c.step === 'rolling' ? 'In movimento…' : c.current === 'cpu' ? `Tira ${def.names.cpu}…` : def.type === 'bocce' ? 'Lancia' : 'Tira')); setProp($('bocce-help'), 'hidden', def.type !== 'bocce'); diff --git a/tests/gara.browser.mjs b/tests/gara.browser.mjs new file mode 100644 index 0000000..e5ae4be --- /dev/null +++ b/tests/gara.browser.mjs @@ -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(); +} diff --git a/tests/gara.test.mjs b/tests/gara.test.mjs index 9a86229..bb42765 100644 --- a/tests/gara.test.mjs +++ b/tests/gara.test.mjs @@ -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); +});