Files
leggenda-del-regolo/tests/gara.test.mjs
T

196 lines
12 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { newGara, update, press, presenting, INTRO, release, shoot, key, steerButton, 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';
import { settle } from './helpers.mjs';
// Gli eventi sonori di ogni gara si ricordano nella gara stessa (`c.heard`, «tipo» e «tipo:pilota»): un test non
// vede quelli degli altri.
const run = (c, seconds, drive, dt = 1 / 60) => {
c.heard ??= new Set();
for (let t = 0; t < seconds && c.step !== 'result'; t += dt) {
if (drive && c.step === 'race') c.holding = drive(c);
update(c, dt); for (const e of c.events.splice(0)) { c.heard.add(e.kind); c.heard.add(`${e.kind}:${e.who}`); }
}
return c.heard;
};
const skip = c => { while (c.step === 'intro') press(c); return c; };
const race = c => { skip(c); assert.equal(c.step, 'start'); run(c, COUNTDOWN + 0.01); assert.equal(c.step, 'race'); return c; };
// Il pilota di giudizio: guarda avanti e ci sterza (scansando buche e chi gli sta davanti), e dà gas finché,
// frenando, arriverebbe a ogni curva un po' sotto il limite. Usa i comandi del giocatore (il trascinamento).
const careful = c => {
const r = player(c), here = ((r.s % TRACK.length) + TRACK.length) % TRACK.length;
const next = PICKUPS.find(p => p.kind !== 'boost' && p.s - here > -8 && p.s - here < 60);
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);
const lane = next?.kind === 'buca' ? (next.lane > 0 ? -6 : 6) : slow ? (slow.lat > 0 ? -6 : 6) : next ? next.lane : 0;
const ahead = pointAt(r.s + 14 + r.v * 0.18, lane), want = Math.atan2(ahead.y - r.y, ahead.x - r.x) - r.a;
// Come un dito premuto che trascina: lo zero dove si è premuto, di lato quanto serve.
if (c.pointer === null) { aimScreen(c, [320, 180], 'bot'); press(c, 'bot'); }
aimScreen(c, [320 + 28 * Math.max(-1, Math.min(1, Math.atan2(Math.sin(want), Math.cos(want)) * 2.6)), 180], 'bot');
const horizon = r.v * r.v / (2 * BRAKE) + 20;
let brake = false;
for (let d = 0; d <= horizon; d += 4) { const a = limitAt(r.s + d) * 0.85; if (r.v * r.v > a * a + 2 * BRAKE * d) { brake = true; break; } }
key(c, 'ArrowDown', brake);
return c.holding;
};
test('i piloti sono dati: motore e testa da 1 a 5 danno velocità, accelerazione e prudenza', () => {
const slow = rider({ motore: 1, testa: 5 }), fast = rider({ motore: 5, testa: 1 });
assert.ok(fast.vmax > slow.vmax && fast.accel > slow.accel && fast.grip < slow.grip);
assert.ok(fast.slip > slow.slip && fast.caution > slow.caution);
assert.deepEqual(rider({ motore: 9, testa: -2 }).motore, 5, 'fuori scala si ferma ai bordi');
// Ogni pilota della storia ha nome, motorino e colore; Andrea c'è una volta sola.
for (const p of piloti) { assert.ok(p.id && p.name && p.bike && /^#[0-9a-f]{6}$/i.test(p.color), p.id); }
assert.equal(piloti.filter(p => p.id === 'andrea').length, 1);
assert.throws(() => newGara(1, { riders: [{ id: 'arcu', name: 'L\'Arcu' }] }), /manca Andrea/);
const c = newGara(1, { riders: piloti });
assert.equal(c.riders.length, piloti.length);
assert.deepEqual(c.riders.map(r => r.s), piloti.map((_, k) => -16 - 14 * Math.floor(k / 2)), 'due per fila, dietro il traguardo');
assert.deepEqual(c.riders.map(r => Math.sign(r.lat)), piloti.map((_, k) => k % 2 ? 1 : -1));
});
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 });
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');
assert.ok(press(c, 7)); assert.equal(c.holding, true);
const start = player(c).s;
const heard = run(c, COUNTDOWN - 0.1);
assert.equal(c.step, 'start'); assert.equal(player(c).s, start, 'prima del via non si muove nessuno');
run(c, 0.15);
assert.equal(c.step, 'race'); assert.ok(heard.has('gara-semaforo') && heard.has('gara-via'));
run(c, 1);
const v = player(c).v; assert.ok(v > 20);
release(c, 7); run(c, 0.2);
assert.ok(player(c).v < v, 'senza gas rallenta');
assert.match(status(c), /Giro 1 di 3/); assert.equal(button(c), 'Gas');
// Gas e freno insieme: frena (P1 della revisione di Codex).
key(c, 'ArrowUp', true); run(c, 0.5); const fast = player(c).v;
key(c, 'ArrowDown', true); run(c, 0.1);
assert.ok(c.holding && c.braking); assert.ok(player(c).v < fast - 10, 'col gas tenuto il freno frena');
key(c, 'ArrowDown', false); key(c, 'ArrowUp', false);
shoot(c); assert.equal(c.holding, true); assert.equal(button(c), 'Molla');
shoot(c); assert.equal(c.holding, false);
});
test('le fonti del gas sono indipendenti: lasciare un tasto non toglie quello del dito o degli altri tasti', () => {
const c = race(newGara(3, { riders: RIDERS }));
key(c, ' ', true); key(c, 'ArrowUp', true); key(c, 'ArrowUp', false);
assert.equal(c.holding, true, 'Spazio ancora giù');
press(c, 1); key(c, ' ', false);
assert.equal(c.holding, true, 'il dito ancora giù');
assert.equal(press(c, 2), false, 'un secondo dito sulla scena non conta');
release(c, 2); assert.equal(c.holding, true, 'e lasciandolo non toglie il gas al primo');
release(c, 1); assert.equal(c.holding, false);
shoot(c); key(c, ' ', true); key(c, ' ', false); assert.equal(c.holding, true, 'il pulsante Gas resta acceso');
});
test('lo sterzo: frecce, pulsanti e il dito che trascina; il mouse che passa senza premere non sterza', () => {
const c = race(newGara(3, { riders: RIDERS }));
assert.ok(key(c, 'ArrowLeft', true)); assert.equal(steering(c), -1);
key(c, 'ArrowLeft', false); assert.equal(steering(c), 0);
steerButton(c, 1); assert.equal(steering(c), 1); steerButton(c, 0);
// Gas da tastiera e sinistra, il mouse si muove a destra senza premere: lo sterzo resta a sinistra (P1).
key(c, 'ArrowUp', true); key(c, 'ArrowLeft', true);
aimScreen(c, [300, 100], 5); aimScreen(c, [360, 100], 5);
assert.equal(steering(c), -1);
key(c, 'ArrowUp', false); key(c, 'ArrowLeft', false);
// Il dito premuto: il punto dove preme è lo zero, di lato si sterza; un altro puntatore non sposta niente.
aimScreen(c, [300, 100], 9); press(c, 9);
aimScreen(c, [314, 100], 9); assert.equal(steering(c), 0.5);
aimScreen(c, [250, 100], 4); assert.equal(steering(c), 0.5);
release(c, 9); assert.equal(steering(c), 0, 'lasciando il dito lo sterzo torna dritto');
const a = player(c).a; key(c, 'ArrowUp', true); key(c, 'ArrowRight', true); run(c, 0.4); assert.notEqual(player(c).a, a, 'sterzando si gira');
assert.equal(key(c, 'x', true), false, 'gli altri tasti non sono suoi');
});
test('la presentazione: uno alla volta arrivano in griglia col motorino, poi il semaforo', () => {
const c = newGara(2, { riders: piloti });
assert.equal(c.step, 'intro'); assert.equal(presenting(c).id, piloti[0].id);
assert.ok(c.riders.every(r => !r.shown), 'all\'inizio la griglia è vuota');
assert.match(status(c), new RegExp(`${piloti[0].name} con ${piloti[0].bike}`));
run(c, INTRO.each + INTRO.ride / 2);
assert.ok(c.riders[0].shown && c.riders[1].shown && !c.riders[2].shown);
assert.equal(c.riders[0].s, c.riders[0].grid, 'il primo è già al suo posto');
assert.ok(c.riders[1].s < c.riders[1].grid, 'il secondo sta arrivando');
assert.equal(presenting(c).id, piloti[1].id);
assert.ok(c.heard.has('gara-motore:' + piloti[0].id) && c.heard.has('gara-motore:' + piloti[1].id));
run(c, piloti.length * INTRO.each + INTRO.after - (INTRO.each + INTRO.ride / 2) + 0.05);
assert.equal(c.step, 'start');
assert.ok(c.riders.every(r => r.shown && r.s === r.grid && r.v === 0), 'tutti fermi in griglia');
// Un tocco: chi arriva è subito al suo posto, e tocca al prossimo.
const d = newGara(2, { riders: piloti });
run(d, 0.3); assert.ok(d.riders[0].s < d.riders[0].grid);
assert.ok(press(d)); run(d, 0.01);
assert.equal(presenting(d).id, piloti[1].id); assert.equal(d.riders[0].s, d.riders[0].grid);
assert.equal(d.step, 'intro'); assert.equal(button(d), 'Avanti');
});
test('sempre a gas e dritto si finisce nella paglia; con giudizio alla bona si vince', () => {
const wild = newGara(5, { level: 'bona', riders: piloti });
skip(wild);
run(wild, COUNTDOWN + 60, () => true);
assert.ok(wild.heard.has('gara-fuori:andrea'));
assert.ok(Math.abs(player(wild).lat) <= VERGE, 'la paglia rimanda in strada');
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 });
skip(c);
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}`);
assert.equal(c.result.order[0], 'andrea'); assert.match(status(c), /Primo al GP di Fortezza!/);
assert.equal(Object.keys(c.result.times).length, piloti.length);
}
assert.ok(['gara-giro:andrea', 'gara-arrivo:andrea', 'gara-turbo:andrea', 'gara-miscela:andrea'].every(k => heard.has(k)), [...heard].join());
assert.ok(CURVES.length >= 2 && TRACK.length > 600, 'una pista con delle curve vere');
});
test('le buche: piano si sobbalza, forte si casca; la lattina di miscela dà lo sprint', () => {
const hole = PICKUPS.find(p => p.kind === 'buca'), can = PICKUPS.find(p => p.kind === 'miscela');
for (const [speed, fell] of [[20, false], [80, true]]) {
const c = newGara(1, { riders: RIDERS }), r = player(c);
c.step = 'race'; Object.assign(r, { s: hole.s - 4, idx: Math.floor(hole.s / 2), v: speed, a: hole.heading });
const p = pointAt(hole.s - 4, hole.lane); r.x = p.x; r.y = p.y;
const heard = run(c, 0.2);
assert.equal(r.falls, fell ? 1 : 0, `a ${speed}`);
assert.ok(heard.has(fell ? 'gara-buca:andrea' : 'gara-sobbalzo:andrea'));
}
const c = newGara(1, { riders: RIDERS }), r = player(c);
c.step = 'race'; Object.assign(r, { s: can.s - 3, idx: Math.floor(can.s / 2), v: 40, a: can.heading });
const p = pointAt(can.s - 3, can.lane); r.x = p.x; r.y = p.y;
run(c, 0.2);
assert.ok(r.boost > 0 && c.taken[can.id] !== undefined, 'la miscela dà lo sprint');
});
test('la classifica: chi è arrivato davanti, poi per strada fatta', () => {
const c = newGara(1, { riders: piloti });
c.riders[0].s = 50; c.riders[1].s = 80; c.riders[2].finished = 30;
assert.deepEqual(standings(c).slice(0, 3).map(r => r.id), [c.riders[2].id, c.riders[1].id, c.riders[0].id]);
});
test('nella storia: dal Trappola l\'Arcu scherza sul Betino, poi la sfida; prima del via prende la rincorsa; la vittoria lascia il flag', () => {
const g = new Game(story);
g.go('via_liguri_magelli', [352, 256]); settle(g);
g.act('talk', 'arcu'); settle(g);
g.choose(g.options().findIndex(o => o.text.startsWith('Ma ti ho visto du')));
const joke = settle(g).map(s => `${s.who ?? 'andrea'}: ${s.text}`);
assert.ok(joke.includes('arcu: In salita si spinge.'));
g.choose(g.options().findIndex(o => o.text.startsWith('Si fa una gara coi motorini?'))); settle(g);
g.choose(g.options().findIndex(o => o.text === 'Seria.'));
const before = settle(g).map(s => s.text);
assert.ok(before.some(t => t.includes('prendo la rincorsa')));
assert.equal(g.closeup?.id, 'gara'); assert.equal(g.closeup.level, 'seria');
assert.deepEqual(g.closeup.riders.map(r => r.id), piloti.filter(p => p.consenso !== null).map(p => p.id));
assert.ok(!g.closeup.riders.some(r => r.id === 'puntella'), 'senza consenso non corre');
g.closeup.result = { winner: 'player', place: 1, times: {}, order: ['andrea'], offs: 0 }; g.closeup.step = 'result';
g.closeupClose(); const after = settle(g).map(s => s.text);
assert.ok(g.state.flags.gara_vinta);
assert.ok(after.some(t => t.includes('Primo!')));
// Il Betino, lassù: solo Andrea ne parla.
g.act('use', 'betino'); assert.ok(settle(g).every(s => s.who === 'andrea'));
});