From c26d888263099003a851570edd396215cf8779e9 Mon Sep 17 00:00:00 2001 From: Andrea Manzi Date: Thu, 29 Oct 2026 10:38:10 +0100 Subject: [PATCH] =?UTF-8?q?GP=20di=20Fortezza,=20Seria=20semplice=20pi?= =?UTF-8?q?=C3=B9=20difficile=20(parere=20di=20Codex,=20simulato):=20sopra?= =?UTF-8?q?=20il=20limite=20in=20curva=20si=20esce=20di=20sicuro,=20la=20c?= =?UTF-8?q?urva=20pulita=20d=C3=A0=20una=20spinta,=20rivali=20a=200,81;=20?= =?UTF-8?q?CURVA!=20e=20LASCIA!=20sopra=20il=20motorino=20(Andrea)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ --- docs/STATO.md | 4 +- index.html | 4 +- src/engine/gara-render.js | 12 +++++- src/engine/gara.js | 78 ++++++++++++++++++++++++++++++++------- tests/gara.test.mjs | 29 ++++++++++++++- 5 files changed, 109 insertions(+), 18 deletions(-) diff --git a/docs/STATO.md b/docs/STATO.md index 07a811d..79f0d5d 100644 --- a/docs/STATO.md +++ b/docs/STATO.md @@ -22,7 +22,9 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto, 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`. + `npm run browser:gara`. Seria più difficile (parere di Codex, simulato): sopra il 110% del limite in curva si esce + di sicuro (stun 1,2 s), la curva pulita dà una spinta, rivali a 0,81; CURVA! (preavviso) e LASCIA! sul motorino. + In simulazione: chi frena giusto 1° in 46 s, tutto gas 4°-5° in 55-59 s. - **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 a0fc64e..8b1ad4e 100644 --- a/index.html +++ b/index.html @@ -228,7 +228,9 @@

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.

+ fermi: molla prima delle curve. Sopra il motorino «CURVA!» avvisa che ne arriva una, «LASCIA!» dice quando + mollare. A Seria chi entra in curva troppo forte va fuori di sicuro, e chi la prende giusta fa la «curva pulita» + ed esce con una spinta in più.

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 diff --git a/src/engine/gara-render.js b/src/engine/gara-render.js index 53c2de1..9c24bc8 100644 --- a/src/engine/gara-render.js +++ b/src/engine/gara-render.js @@ -1,4 +1,4 @@ -import { TRACK, CURVES, PICKUPS, BRAKE, LAPS, COUNTDOWN, ROAD, VERGE, CAN, SWIPE, pointAt, standings, player, lapOf, presenting, placeOf, INTRO } from './gara.js'; +import { TRACK, CURVES, PICKUPS, BRAKE, LAPS, COUNTDOWN, ROAD, VERGE, CAN, SWIPE, pointAt, standings, player, lapOf, presenting, placeOf, INTRO, mustLift, PREAVVISO } from './gara.js'; import { text, panel } from './bocce-render.js'; import { ambientHash as hash } from './ambient.js'; @@ -216,6 +216,12 @@ function result(ctx, c) { }); } +// Una scritta sopra il motorino di Andrea, su un fondo scuro perché si legga anche sullo sterrato e sul telefono. +function tag(ctx, line, r, color) { + const x = Math.round(r.x), y = Math.round(r.y) - 20, w = line.length * 9 + 10; + ctx.fillStyle = 'rgba(23,22,15,0.85)'; ctx.fillRect(x - w / 2, y - 7, w, 14); + text(ctx, line, x, y, 10, color, 'center'); +} export function drawGara(ctx, c, def, bg, look = () => null, sheet = () => null) { ctx.save(); if (bg) ctx.drawImage(bg, 0, 0, 640, 360); @@ -232,6 +238,10 @@ export function drawGara(ctx, c, def, bg, look = () => null, sheet = () => null) const me = player(c); if (c.step === 'race' && me.out > 0) text(ctx, me.lastHole !== undefined ? 'NELLA BUCA!' : 'NELLA PAGLIA!', 320, 180, 10, '#ffe28a', 'center'); if (c.step === 'race' && me.boost > 0 && me.out <= 0) text(ctx, 'MISCELA!', 320, 180, 8, '#ffb030', 'center'); + // La guida semplice: LASCIA! sopra il motorino quando serve frenare, e la curva pulita. + if (c.step === 'race' && c.holding && mustLift(c)) { if (Math.floor(c.age * 8) % 2) tag(ctx, 'LASCIA!', me, '#ff6a4a'); } + else if (c.step === 'race' && c.holding && mustLift(c, PREAVVISO)) tag(ctx, 'CURVA!', me, '#ffe28a'); + if (c.step === 'race' && c.clean !== undefined && c.t - c.clean < 1.2) text(ctx, 'CURVA PULITA!', 320, 180, 9, '#b9e5b3', 'center'); if (c.step === 'result') result(ctx, c); ctx.restore(); } diff --git a/src/engine/gara.js b/src/engine/gara.js index cad5b48..f8bef22 100644 --- a/src/engine/gara.js +++ b/src/engine/gara.js @@ -56,7 +56,10 @@ export const RIDERS = [ // tiene lo sterzo vero. export const LEVELS = { bona: { rivals: 0.74, stun: 0.4, guide: 'zone', simple: true }, - seria: { rivals: 0.84, stun: 0.9, guide: 'coni', simple: true }, + // Seria (Andrea, 29 ottobre: «chi non rallenta in curva deve andare fuori»; valori proposti da Codex e simulati): + // si scivola di più, sopra il limite in curva si esce per forza, e la curva pulita dà una spinta. + seria: { rivals: 0.81, stun: 1.2, guide: 'coni', simple: true, + tune: { slide: 12, clean: { min: 0.85, slack: 2, time: 0.6, accel: 1.2 }, force: { over: 1.1, hold: 0.18, apex: 1.15, time: 0.3, guard: 0.6 } } }, male: { rivals: 0.93, stun: 1.4, guide: null }, }; // `def.simple` la forza (i test della guida vera la spengono anche alla bona). @@ -159,7 +162,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, simple: def.simple ?? !!skill.simple, 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, tune: { ...SIMPLE, ...skill.tune }, 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: {} }; } @@ -269,11 +272,53 @@ const follow = r => { const aim = pointAt(r.s + 14 + r.v * 0.18, r.lane * 0.6); // 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) { +// Ogni livello può stringere (LEVELS.*.tune). `clean`: il premio della curva pulita, presa senza mai passare il +// limite (`slack` px/s di tolleranza) e senza frenare troppo (almeno `min` del limite): per `time` secondi in uscita +// si accelera di più (`accel`), senza superare la velocità massima. `force`: l'uscita sicura, sopra `over` volte il +// limite per `hold` secondi (o `apex` volte nel punto più stretto) si va nella paglia in `time` secondi, una volta per +// curva; rientrati, `guard` secondi di tregua. +export const SIMPLE = { grip: 1, slide: 8, window: 50, clean: null, force: null }; +export function simpleLimit(r, s = r.s, tune = SIMPLE) { 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; + for (let d = -10; d <= tune.window; d += STEP) low = Math.min(low, limitAt(s + d)); + return low * r.grip * tune.grip; +} +// La curva in corso: dentro si tiene il peggio dell'eccesso e la velocità più bassa (per il premio), e si guarda se +// si è andati troppo forte abbastanza a lungo (per l'uscita sicura). +function judgeCurve(c, r, dt) { + const tune = c.tune, s = ((r.s % TRACK.length) + TRACK.length) % TRACK.length; + const k = CURVES.find(k => s >= k.start - 20 && s <= k.end + 10); + if (k) { + const w = r.curve?.k === k ? r.curve : (r.curve = { k, excess: 0, slow: Infinity, over: 0, fault: false }); + w.excess = Math.max(w.excess, r.excess); w.slow = Math.min(w.slow, r.v); + const f = tune.force, limit = limitAt(k.apex) * r.grip * tune.grip; + if (f && !w.fault && !(r.guard > 0) && r.out === 0) { + w.over = r.v > limit * f.over ? w.over + dt : 0; + if (w.over >= f.hold || (Math.abs(s - k.apex) < STEP * 2 && r.v > limit * f.apex)) { w.fault = true; r.forced = f.time; } + } + return; + } + const w = r.curve; r.curve = null; + if (!w || !tune.clean || w.fault) return; + const limit = limitAt(w.k.apex) * r.grip * tune.grip; + if (w.excess <= tune.clean.slack && w.slow >= limit * tune.clean.min && r.out === 0) { + r.push = tune.clean.time; r.cleans = (r.cleans ?? 0) + 1; + c.events.push({ kind: 'gara-giro', who: r.id, power: 1 }); c.clean = c.t; + } +} +// Andrea, nella guida semplice, deve lasciare entro `lead` secondi per arrivare alle curve davanti sotto il limite. +// Sopra il motorino: CURVA! in giallo con PREAVVISO secondi d'anticipo, poi LASCIA! in rosso quando è il momento (sul +// telefono non serve l'istante preciso, e lasciare troppo presto, che qui vuol dire frenare, non conviene). +export const PREAVVISO = 0.7, LIFT = 0; +export function mustLift(c, lead = LIFT) { + const r = player(c); + if (!simple(c) || c.step !== 'race' || r.out > 0) return false; + const horizon = r.v * r.v / (2 * BRAKE) + r.v * lead + 10; + for (let d = 0; d <= horizon; d += 4) { + const a = simpleLimit(r, r.s + d, c.tune), room = Math.max(0, d - r.v * lead); + if (r.v > a && r.v * r.v > a * a + 2 * BRAKE * room) return true; + } + return false; } function autopilot(c, r) { const next = PICKUPS.find(p => p.kind !== 'boost' && ahead(r.s, p.s) < 30 + r.v * 0.6); @@ -282,7 +327,7 @@ function autopilot(c, r) { 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)); + r.excess = Math.max(0, r.v - simpleLimit(r, r.s, c.tune)); return follow(r); } // Gli avversari: guardano un punto più avanti sulla loro corsia e ci sterzano; danno gas finché, frenando da qui, @@ -309,24 +354,28 @@ 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.out > 0) { r.out = Math.max(0, r.out - dt); r.v = 0; r.u = 0; if (!r.out && c.tune.force) r.guard = c.tune.force.guard; return; } + else if (r.id === 'andrea' && simple(c)) { r.gas = c.holding; r.brake = !c.holding; r.steer = autopilot(c, r); judgeCurve(c, r, dt); } 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; const grass = Math.abs(r.lat) > ROAD, top = r.vmax * (r.boost > 0 ? BOOST.speed : 1) * (grass ? 0.5 : 1); // Il freno vince sul gas: chi li tiene tutti e due frena. - if (r.brake) r.v -= BRAKE * dt; - else if (r.gas) r.v += r.accel * dt * (1 - 0.4 * r.v / top) * (r.boost > 0 ? 1.6 : 1); + if (r.brake) { r.v -= BRAKE * dt; r.push = 0; } + else if (r.gas) r.v += r.accel * dt * (1 - 0.4 * r.v / top) * (r.boost > 0 ? 1.6 : 1) * (r.push > 0 ? c.tune.clean.accel : 1); else r.v -= COAST * dt; if (r.v > top) r.v = Math.max(top, r.v - 120 * dt); r.v = Math.max(0, r.v); r.boost = Math.max(0, r.boost - dt); + if (r.push) r.push = Math.max(0, r.push - dt); + if (r.guard) r.guard = Math.max(0, r.guard - dt); // Lo sterzo: da fermi non si gira; in curva una parte della velocità resta per traverso e si scivola fuori. 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 + if (r.excess) r.u -= TRACK.side[r.idx] * r.excess * c.tune.slide * dt; // la guida semplice: troppo forte, si allarga + // L'uscita sicura: verso l'esterno della curva, e lo sterzo non la salva. + if (r.forced > 0) { r.forced = Math.max(0, r.forced - dt); r.u = -(r.curve?.k.side ?? TRACK.side[r.idx]) * 90; r.v = Math.max(r.v, 30); } 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; @@ -335,7 +384,8 @@ function stepRider(c, r, dt) { if (Math.abs(r.lat) > VERGE) { const h = TRACK.heading[r.idx], p = TRACK.pts[r.idx], side = Math.sign(r.lat), back = side * (VERGE - 1); r.x = p[0] - Math.sin(h) * back; r.y = p[1] + Math.cos(h) * back; r.lat = back; r.u = 0; - if (r.v > 40 && r.finished === null) { + if ((r.v > 40 || r.forced > 0) && r.finished === null) { + r.forced = 0; r.out = c.skill.stun * (r.id === 'andrea' ? 1 : 0.7); r.offs++; r.risk = 1; c.events.push({ kind: 'gara-fuori', who: r.id, power: r.id === 'andrea' ? 1 : 0.4 }); } @@ -442,7 +492,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.'}`; + if (simple(c)) return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. ${c.clean !== undefined && c.t - c.clean < 1.5 ? 'Curva pulita!' : mustLift(c) && c.holding ? 'Lascia!' : mustLift(c, PREAVVISO) && c.holding ? 'Curva!' : 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/tests/gara.test.mjs b/tests/gara.test.mjs index bb42765..c64f4ab 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 { 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 { SIMPLE, simple, simpleLimit, mustLift, PREAVVISO, 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'; @@ -304,3 +304,30 @@ test('guida semplice: sempre a tutto gas si finisce nella paglia; mollando prima } assert.ok(SIMPLE.slide > 0); }); + +test('Seria semplice: chi non rallenta in curva va fuori e perde, chi frena giusto fa le curve pulite e vince; LASCIA! dice quando', () => { + const riders = piloti.filter(p => p.consenso !== null); + 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, c.tune); 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: 'seria', riders })); + run(wild, 120, () => true); + const good = race(newGara(seed, { level: 'seria', riders })); + run(good, 120, wise); + assert.ok(wild.result.offs >= 4, `a tutto gas fuori in quasi ogni curva (${wild.result.offs})`); + assert.ok(wild.result.place >= 3); + assert.equal(good.result.offs, 0); assert.equal(good.result.winner, 'player', `seme ${seed}`); + assert.ok(player(good).cleans >= 5, 'le curve pulite'); + assert.ok(wild.result.times.andrea - good.result.times.andrea > 6, 'tutto gas costa più di 6 secondi'); + } + // Chi segue LASCIA! guida come chi frena giusto; CURVA! arriva prima. + const c = race(newGara(1, { level: 'seria', riders })); + let early = 0, said = false; + for (let t = 0; t < 20; t += 1 / 60) { + c.holding = !mustLift(c); + if (mustLift(c, PREAVVISO) && !mustLift(c)) early++; + update(c, 1 / 60); + if (c.holding && mustLift(c)) said ||= /Lascia!/.test(status(c)); + } + assert.ok(early > 20, `il preavviso CURVA! viene prima di LASCIA! (${early} fotogrammi)`); + assert.ok(said, 'il pannello dice Lascia!'); +});