GP di Fortezza, Seria semplice più difficile (parere di Codex, simulato): sopra il limite in curva si esce di sicuro, la curva pulita dà una spinta, rivali a 0,81; CURVA! e LASCIA! sopra il motorino (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:38:10 +01:00
co-authored by Claude Opus 5.5
parent f8dbd01358
commit c26d888263
5 changed files with 109 additions and 18 deletions
+3 -1
View File
@@ -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 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 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 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 - **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 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, `micro-render.js`: pista disegnata a gesso dal codice sopra l'asfalto di Codex (`micro-parcheggio.png`), un giro,
+3 -1
View File
@@ -228,7 +228,9 @@
<p>Il GP di Fortezza: tre giri attorno alle mura coi cinquantini. <b>Alla bona e Seria</b> la guida è semplice: <p>Il GP di Fortezza: tre giri attorno alle mura coi cinquantini. <b>Alla bona e Seria</b> la guida è semplice:
tieni premuto sulla scena (o Spazio, o il pulsante Gas) per accelerare e lascia per frenare; lo sterzo segue la 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 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.</p> 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ù.</p>
<p><b>Ginocchio in terra</b> si guida davvero. Con un solo dito: tieni premuto sulla scena per <p><b>Ginocchio in terra</b> 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 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 il motorino scende. I piccoli movimenti non sterzano; il cerchietto segue gli swipe lunghi. Alza il dito per
+11 -1
View File
@@ -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 { text, panel } from './bocce-render.js';
import { ambientHash as hash } from './ambient.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) { export function drawGara(ctx, c, def, bg, look = () => null, sheet = () => null) {
ctx.save(); ctx.save();
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); 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); 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.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'); 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); if (c.step === 'result') result(ctx, c);
ctx.restore(); ctx.restore();
} }
+64 -14
View File
@@ -56,7 +56,10 @@ export const RIDERS = [
// tiene lo sterzo vero. // tiene lo sterzo vero.
export const LEVELS = { export const LEVELS = {
bona: { rivals: 0.74, stun: 0.4, guide: 'zone', simple: true }, 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 }, male: { rivals: 0.93, stun: 1.4, guide: null },
}; };
// `def.simple` la forza (i test della guida vera la spengono anche alla bona). // `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'); 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; } 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, 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: {} }; 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à // 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ù // 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. // quanto si esagera (SIMPLE.slide): a tutto gas, nelle curve vere si finisce nella paglia.
export const SIMPLE = { grip: 1, slide: 8, window: 50 }; // Ogni livello può stringere (LEVELS.*.tune). `clean`: il premio della curva pulita, presa senza mai passare il
export function simpleLimit(r, s = r.s) { // 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; let low = Infinity;
for (let d = -10; d <= SIMPLE.window; d += STEP) low = Math.min(low, limitAt(s + d)); for (let d = -10; d <= tune.window; d += STEP) low = Math.min(low, limitAt(s + d));
return low * r.grip * SIMPLE.grip; 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) { function autopilot(c, r) {
const next = PICKUPS.find(p => p.kind !== 'boost' && ahead(r.s, p.s) < 30 + r.v * 0.6); 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; if (next?.kind === 'buca') r.lane = next.lane > 0 ? -5 : 5;
else if (slow) r.lane = slow.lat > 0 ? -6 : 6; else if (slow) r.lane = slow.lat > 0 ? -6 : 6;
else if (next?.kind === 'miscela') r.lane = next.lane; 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); return follow(r);
} }
// Gli avversari: guardano un punto più avanti sulla loro corsia e ci sterzano; danno gas finché, frenando da qui, // 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) { function stepRider(c, r, dt) {
if (r.finished !== null) { r.gas = false; r.brake = false; drive(c, r); r.gas = false; } 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.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); } 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 if (r.id === 'andrea') { r.gas = c.holding; r.brake = c.braking; r.steer = steering(c); }
else drive(c, r); else drive(c, r);
if (r.finished !== null) r.gas = false; 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); 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. // Il freno vince sul gas: chi li tiene tutti e due frena.
if (r.brake) r.v -= BRAKE * dt; 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); 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; else r.v -= COAST * dt;
if (r.v > top) r.v = Math.max(top, r.v - 120 * dt); if (r.v > top) r.v = Math.max(top, r.v - 120 * dt);
r.v = Math.max(0, r.v); r.v = Math.max(0, r.v);
r.boost = Math.max(0, r.boost - dt); 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. // 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; const turn = r.steer * TURN * Math.min(1, r.v / 22) * dt;
r.a = wrapAngle(r.a + turn); r.a = wrapAngle(r.a + turn);
r.u -= r.v * Math.sin(turn) * SLIDE * (r.v / r.vmax) / r.grip; 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.u *= Math.exp(-GRIP * r.grip * dt);
r.x += (Math.cos(r.a) * r.v - Math.sin(r.a) * r.u) * 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; 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) { 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); 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; 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; 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 }); 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); 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.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 (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.'}`; 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'; export const button = c => c.step === 'intro' ? 'Avanti' : c.step === 'result' ? 'Finita' : c.holding ? 'Molla' : 'Gas';
+28 -1
View File
@@ -1,6 +1,6 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; 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 { piloti } from '../src/story/piloti.js';
import { story } from '../src/story/story.js'; import { story } from '../src/story/story.js';
import { Game } from '../src/engine/game.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); 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!');
});