// Le macchinine sulla pista col gesso (Andrea, 28 ottobre): sul parcheggio sotto casa, a Cardetole, si disegnava col // gesso la pista e si davano spinte a turno alle Micro Machines; la macchinina doveva restare in pista: tutta fuori, si // torna da dove si era partiti. Si gioca contro il Chiaru, un giro; vince chi taglia per primo il traguardo. // // Regole: una spinta a testa, prima Andrea e poi il Chiaru, così a fine giro hanno spinto lo stesso numero di volte. // Se tagliano il traguardo tutti e due nello stesso giro di spinte vince chi è andato più in là. Le penalità, coi nomi // alla francese che si usavano (Andrea, 29 ottobre), e per tutte si torna dove si era prima della spinta: // - fuori: la macchinina esce tutta dal gesso (anche se ce la manda l'altra, urtandola); // - tagliè: esce dalla parte di dentro di una curva, o salta l'erba di mezzo per tagliare la strada; // - cappottè: un urto forte la ribalta; // - accompagnè: il dito l'ha accompagnata invece di darle il colpetto (tenuto premuto oltre la carica piena). // Unità: i pixel del primo piano 640×360; la pista si disegna sopra al parcheggio di Codex. // La linea di mezzo, in senso orario a partire dal traguardo; la pista è larga 2·HALF, col gesso sui due bordi. export const CONTROL = [ [150, 68], [300, 62], [450, 66], [540, 80], [586, 122], [580, 172], [536, 196], [470, 196], [414, 178], [356, 168], [318, 196], [326, 240], [388, 262], [480, 260], [556, 262], [598, 290], [588, 324], [530, 334], [400, 334], [250, 332], [130, 326], [74, 296], [58, 230], [62, 150], [88, 96], ]; export const HALF = 22, CAR = { len: 26, wid: 13, r: 6 }, LAPS = 1; // La spinta: velocità di partenza da MIN_SPEED a MAX_SPEED, e l'asfalto che frena di DECEL px/s². export const MIN_SPEED = 60, MAX_SPEED = 400, DECEL = 420, SWING = 1.2, TAP = 0.14, FIXED_STEP = 1 / 120; const REST = 0.7, THINK = 1, RESTITUTION = 0.6, OUT = HALF + CAR.r, WINDOW = 30; // FLIP: la velocità d'urto (px/s) che ribalta la macchinina colpita; ACCOMPAGNA: quanti secondi si può restare alla // carica piena prima che diventi un accompagnè. export const FLIP = 260, ACCOMPAGNA = 0.45; export const FAULTS = { fuori: 'Fuori', taglie: 'Tagliè', cappotte: 'Cappottè', accompagne: 'Accompagnè' }; export const OTHER = { player: 'cpu', cpu: 'player' }; // Quanto è bravo il Chiaru: quanto gli trema il dito (radianti e forza) e fin dove osa spingere (px). `guide`: cosa // vede Andrea mentre mira (tutto il tiro col punto d'arrivo, solo la linea, solo la freccia). export const LEVELS = { bona: { wobble: 0.1, powerNoise: 0.16, ahead: 120, guide: 'full' }, seria: { wobble: 0.05, powerNoise: 0.08, ahead: 170, guide: 'line' }, male: { wobble: 0.022, powerNoise: 0.035, ahead: 200, guide: 'arrow' }, }; const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; } const deviation = c => random(c) + random(c) + random(c) - 1.5; export const powerAt = time => clamp(time / SWING, 0, 1); export const speedFor = power => MIN_SPEED + (MAX_SPEED - MIN_SPEED) * clamp(power, 0, 1); // Quanta strada fa una macchinina spinta a velocità v (l'asfalto frena sempre uguale). export const reach = v => v * v / (2 * DECEL); export const powerFor = distance => clamp((Math.sqrt(2 * DECEL * Math.max(0, distance)) - MIN_SPEED) / (MAX_SPEED - MIN_SPEED), 0, 1); // --- La pista: una curva chiusa (Catmull-Rom sui punti di controllo), ricampionata a passo costante. export function buildTrack(control = CONTROL, step = 2) { const n = control.length, dense = []; for (let i = 0; i < n; i++) { const [p0, p1, p2, p3] = [control[(i - 1 + n) % n], control[i], control[(i + 1) % n], control[(i + 2) % n]]; for (let k = 0; k < 24; k++) { const t = k / 24, t2 = t * t, t3 = t2 * t; const f = j => 0.5 * (2 * p1[j] + (p2[j] - p0[j]) * t + (2 * p0[j] - 5 * p1[j] + 4 * p2[j] - p3[j]) * t2 + (3 * p1[j] - p0[j] - 3 * p2[j] + p3[j]) * t3); dense.push([f(0), f(1)]); } } const cum = [0]; for (let i = 1; i <= dense.length; i++) cum.push(cum[i - 1] + Math.hypot(dense[i % dense.length][0] - dense[i - 1][0], dense[i % dense.length][1] - dense[i - 1][1])); const total = cum.at(-1), count = Math.round(total / step), pts = []; for (let j = 0, i = 0; j < count; j++) { const at = j * total / count; while (cum[i + 1] < at) i++; const a = dense[i], b = dense[(i + 1) % dense.length], t = (at - cum[i]) / (cum[i + 1] - cum[i] || 1); pts.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]); } const tan = pts.map((p, i) => { const a = pts[(i - 1 + count) % count], b = pts[(i + 1) % count], d = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1; return [(b[0] - a[0]) / d, (b[1] - a[1]) / d]; }); return { pts, tan, n: count, step: total / count, length: total }; } export const TRACK = buildTrack(); // Il punto della linea di mezzo più vicino: vicino all'ultimo noto (`hint`), così nei tornanti non si salta dall'altra // parte; senza `hint`, su tutta la pista. `s` è la distanza lungo la pista dal traguardo, `d` quella dalla linea. export function project(track, x, y, hint) { const { pts, n } = track; const from = hint === undefined ? 0 : hint - WINDOW, to = hint === undefined ? n : hint + WINDOW; let best = null; for (let k = from; k < to; k++) { const i = ((k % n) + n) % n, a = pts[i], b = pts[(i + 1) % n]; const dx = b[0] - a[0], dy = b[1] - a[1], len2 = dx * dx + dy * dy || 1; const t = clamp(((x - a[0]) * dx + (y - a[1]) * dy) / len2, 0, 1); const d = Math.hypot(x - a[0] - dx * t, y - a[1] - dy * t); if (!best || d < best.d) best = { i, t, d }; } return { i: best.i, d: best.d, s: (best.i + best.t) * track.step }; } // Tutta fuori: nessun pezzo della macchinina tocca più la pista. export const isOut = d => d > OUT; // Uscita dalla parte di dentro di una curva (verso dove gira la pista): è un tagliè. function inside(track, i, x, y) { const a = track.tan[(i - 6 + track.n) % track.n], b = track.tan[(i + 6) % track.n], turn = a[0] * b[1] - a[1] * b[0]; const [px, py] = track.pts[i], [tx, ty] = track.tan[i], side = tx * (y - py) - ty * (x - px); return Math.abs(turn) > 0.05 && Math.sign(turn) === Math.sign(side); } // La macchinina tiene il conto della strada fatta (`progress`, anche oltre il giro) seguendo la pista da vicino. function follow(track, car) { const p = project(track, car.x, car.y, car.idx); let delta = p.s - car.s; if (delta > track.length / 2) delta -= track.length; else if (delta < -track.length / 2) delta += track.length; car.progress += delta; car.s = p.s; car.idx = p.i; if (isOut(p.d) && !car.fault) car.fault = inside(track, p.i, car.x, car.y) ? 'taglie' : 'fuori'; } function newCar(track, role, back, side) { const i = (track.n - Math.round(back / track.step)) % track.n, [x, y] = track.pts[i], [tx, ty] = track.tan[i]; const car = { role, x: x - ty * side, y: y + tx * side, vx: 0, vy: 0, heading: Math.atan2(ty, tx), idx: i, s: 0, progress: 0, fault: null }; const p = project(track, car.x, car.y); car.idx = p.i; car.s = p.s; car.progress = p.s > track.length / 2 ? p.s - track.length : p.s; return car; } const cars = c => [c.cars.player, c.cars.cpu]; export const moving = c => cars(c).some(car => car.vx || car.vy); // L'urto fra le due macchinine: due dischi uguali, un po' di rimbalzo. Se l'urto è forte, quella colpita (la più // lenta delle due) si ribalta. function collide(a, b, events) { const dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy), min = CAR.r * 2; if (d >= min || d < 1e-6) return; const nx = dx / d, ny = dy / d, push = (min - d) / 2; a.x -= nx * push; a.y -= ny * push; b.x += nx * push; b.y += ny * push; const rel = (a.vx - b.vx) * nx + (a.vy - b.vy) * ny; if (rel <= 0) return; const j = rel * (1 + RESTITUTION) / 2; const hit = Math.hypot(a.vx, a.vy) < Math.hypot(b.vx, b.vy) ? a : b; a.vx -= j * nx; a.vy -= j * ny; b.vx += j * nx; b.vy += j * ny; if (rel > FLIP) hit.fault = 'cappotte'; events?.push({ kind: 'micro-urto', power: Math.min(1, rel / 300) }); } function step(track, list, dt, events) { for (const car of list) { const v = Math.hypot(car.vx, car.vy); if (!v) continue; const nv = Math.max(0, v - DECEL * dt); if (nv < 2) { car.vx = car.vy = 0; continue; } car.vx *= nv / v; car.vy *= nv / v; car.x += car.vx * dt; car.y += car.vy * dt; } collide(list[0], list[1], events); for (const car of list) if (car.vx || car.vy) follow(track, car); } // Le macchinine fra cui scegliere (Andrea, 29 ottobre: «quando si parte fammi scegliere la macchinina»): il disegno di // Codex in `assets/closeups/micro/.png` (e `-grande.png` per la scelta) e il colore della barra in alto. // Il Chiaru tiene la sua blu e gialla; se la prende Andrea, lui prende la rossa. export const CARS = [ { id: 'rossa', name: 'Rossa e bianca', color: '#e0543c' }, { id: 'blu', name: 'Blu e gialla', color: '#3c66d8' }, { id: 'nera', name: 'Nera e oro', color: '#c9a23a' }, { id: 'azzurra', name: 'Bianca e azzurra', color: '#6fb7ff' }, { id: 'arancio', name: 'Arancione', color: '#f08a2c' }, { id: 'viola', name: 'Viola e argento', color: '#9a5ad8' }, ]; export const carImage = (id, big = false) => `assets/closeups/micro/${id}${big ? '-grande' : ''}.png`; // Dove stanno le macchinine nella scelta: due file da tre, al centro del parcheggio. export const choiceAt = i => [160 + (i % 3) * 160, 132 + Math.floor(i / 3) * 88]; export function pick(c, i = c.choice) { if (c.step !== 'choose' || !CARS[i]) return false; const mine = CARS[i].id; c.colors = { player: mine, cpu: mine === 'blu' ? 'rossa' : 'blu' }; c.step = 'aim'; c.t = 0; c.aimLocked = false; // il clic che sceglie non deve fissare anche la mira return true; } export function choose(c, i) { if (c.step !== 'choose' || !Number.isFinite(i)) return false; c.choice = (i + CARS.length) % CARS.length; return true; } const carUnder = (x, y) => CARS.findIndex((_, i) => { const [cx, cy] = choiceAt(i); return Math.abs(x - cx) < 44 && Math.abs(y - cy) < 30; }); export function newMicro(seed = Date.now() >>> 0, def = {}) { const level = LEVELS[def.level] ? def.level : 'seria', track = TRACK; const c = { step: 'choose', choice: 0, colors: null, t: 0, seed, level, skill: LEVELS[level], current: 'player', power: 0.5, angle: 0, holding: false, holdTime: 0, accumulator: 0, events: [], track, laps: LAPS, flicks: { player: 0, cpu: 0 }, line: null, trail: [], returns: [], before: null, plan: null, visual: { time: 0 } }; // In griglia: Andrea all'esterno, il Chiaru all'interno, poco prima del traguardo. c.cars = { player: newCar(track, 'player', 18, -11), cpu: newCar(track, 'cpu', 18, 11) }; c.angle = c.cars.player.heading; // Si parte scegliendo la macchinina, se non è già data (`def.car`). if (def.car) pick(c, CARS.findIndex(x => x.id === def.car)); return c; } export const canThrow = c => c.step === 'aim' && c.current === 'player'; export const finished = (c, car) => car.progress >= c.laps * c.track.length; const snapshot = car => ({ x: car.x, y: car.y, heading: car.heading, idx: car.idx, s: car.s, progress: car.progress }); export function strike(c, who, angle, power) { c.turn = null; const car = c.cars[who], v = speedFor(power); c.before = { player: snapshot(c.cars.player), cpu: snapshot(c.cars.cpu) }; for (const x of cars(c)) x.fault = null; car.vx = Math.cos(angle) * v; car.vy = Math.sin(angle) * v; car.heading = angle; c.returns = c.returns.filter(r => c.visual.time - r.at < 4); if (who === 'player') c.line = null; c.shooter = who; c.angle = angle; c.power = power; c.plan = null; c.trail = [[car.x, car.y]]; c.step = 'rolling'; c.t = c.accumulator = 0; c.flicks[who]++; c.events.push({ kind: 'micro-spinta', power }, { kind: 'micro-strisciata', power }); return true; } // La spinta provata a vuoto su una copia: dove arriva ciascuna macchinina e se qualcuna esce. export function simulate(c, who, angle, power) { const list = cars(c).map(x => ({ ...x, fault: null })), car = list.find(x => x.role === who), v = speedFor(power); car.vx = Math.cos(angle) * v; car.vy = Math.sin(angle) * v; for (let i = 0; i < 6 / FIXED_STEP && list.some(x => x.vx || x.vy); i++) step(c.track, list, FIXED_STEP); return Object.fromEntries(list.map(x => [x.role, x])); } // Il Chiaru guarda la pista davanti a sé: prova dei punti più o meno lontani, al centro e verso i bordi, e per ognuno // anche le spinte storte che gli possono scappare; sceglie quello che in media lo porta più avanti senza uscire. // Le prove si fanno a pezzi (BUDGET per fotogramma): tutte insieme fermerebbero il gioco per un decimo di secondo. const BUDGET = 20; function cpuCandidates(c) { const me = c.cars.cpu, k = c.skill, list = []; for (let ahead = 30; ahead <= k.ahead; ahead += 10) for (const side of [-10, 0, 10]) { const i = (me.idx + Math.round(ahead / c.track.step)) % c.track.n, [px, py] = c.track.pts[i], [tx, ty] = c.track.tan[i]; const x = px - ty * side, y = py + tx * side, angle = Math.atan2(y - me.y, x - me.x), power = powerFor(Math.hypot(x - me.x, y - me.y)); for (const [da, dp] of [[0, 0], [k.wobble, 0], [-k.wobble, 0], [0, k.powerNoise], [0, -k.powerNoise]]) list.push({ angle, power, try: [angle + da, clamp(power + dp, 0, 1)] }); } return list; } export function cpuThink(c, budget = BUDGET) { const t = c.thinking ??= { todo: cpuCandidates(c), scores: new Map() }, me = c.cars.cpu; for (let i = 0; i < budget && t.todo.length; i++) { const x = t.todo.pop(), r = simulate(c, 'cpu', ...x.try).cpu, key = x.angle + '|' + x.power; t.scores.set(key, { angle: x.angle, power: x.power, score: (t.scores.get(key)?.score ?? 0) + (r.fault ? -80 : r.progress - me.progress) }); } if (t.todo.length) return false; const best = [...t.scores.values()].reduce((a, b) => b.score > a.score ? b : a, { score: -Infinity, angle: me.heading, power: 0.3 }); const k = c.skill; c.plan = { angle: best.angle + deviation(c) * k.wobble, power: clamp(best.power + deviation(c) * k.powerNoise, 0.02, 1) }; c.angle = c.plan.angle; c.power = c.plan.power; c.thinking = null; return true; } // Finita la spinta: chi ha fatto un fallo torna al suo posto, e alla fine di ogni giro di spinte si guarda il traguardo. function settle(c) { const names = { player: 'Andrea', cpu: 'il Chiaru' }, outs = cars(c).filter(car => car.fault); // Uscita e ripresa la pista più in là: ha saltato l'erba di mezzo, è un tagliè. for (const car of outs) if (car.fault === 'fuori' && !isOut(project(c.track, car.x, car.y).d)) car.fault = 'taglie'; // Le scritte di un giro di spinte restano insieme fino alla spinta dopo di Andrea: così vede anche le sue. const said = outs.map(car => `${FAULTS[car.fault]}! ${names[car.role][0].toUpperCase() + names[car.role].slice(1)} torna dov'era.`); // Dove è uscita resta una croce, e la macchinina si vede tornare indietro (solo disegno). for (const car of outs) { c.returns.push({ role: car.role, x: car.x, y: car.y, heading: car.heading, fault: car.fault, at: c.visual.time }); Object.assign(car, c.before[car.role], { vx: 0, vy: 0, fault: null }); c.events.push({ kind: 'micro-fuori', power: car.role === 'player' ? 1 : 0.5 }); } // Rimessa al suo posto, non deve stare sopra all'altra. const [a, b] = cars(c), d = Math.hypot(b.x - a.x, b.y - a.y); if (outs.length === 1 && d < CAR.r * 2) { const back = outs[0], other = back === a ? b : a, nx = d ? (back.x - other.x) / d : 1, ny = d ? (back.y - other.y) / d : 0; back.x = other.x + nx * CAR.r * 2; back.y = other.y + ny * CAR.r * 2; } const mine = c.cars[c.shooter]; if (!outs.length && finished(c, mine)) said.push(`${c.shooter === 'player' ? 'Andrea' : 'Il Chiaru'} taglia il traguardo!`); c.line = [c.line, ...said].filter(Boolean).join(' ') || null; if (!outs.includes(mine) && finished(c, mine)) c.events.push({ kind: 'micro-arrivo', power: c.shooter === 'player' ? 1 : 0.5 }); // Il giro di spinte si chiude col Chiaru: solo allora si decide, così hanno spinto tutti e due lo stesso numero di volte. const end = c.laps * c.track.length, over = { player: c.cars.player.progress - end, cpu: c.cars.cpu.progress - end }; if (c.shooter === 'cpu' && (over.player >= 0 || over.cpu >= 0)) { const winner = over.player >= 0 && over.cpu >= 0 ? (Math.abs(over.player - over.cpu) < 1 ? 'draw' : over.player > over.cpu ? 'player' : 'cpu') : over.player >= 0 ? 'player' : 'cpu'; c.step = 'result'; c.result = { winner, flicks: { ...c.flicks }, over }; return; } // Chi ha già tagliato il traguardo aspetta; spinge l'altro. c.current = OTHER[c.shooter]; c.step = 'aim'; c.t = 0; if (c.current === 'player') c.angle = aimAhead(c); } // Quando tocca ad Andrea la mira parte già lungo la pista, un po' più avanti. function aimAhead(c) { const car = c.cars.player, i = (car.idx + Math.round(60 / c.track.step)) % c.track.n, [x, y] = c.track.pts[i]; return Math.atan2(y - car.y, x - car.x); } export function update(c, dt) { if (!Number.isFinite(dt) || dt <= 0) return; dt = Math.min(dt, 0.25); c.t += dt; c.visual.time += dt; if (c.turn && canThrow(c) && !c.holding) { c.turn.time += dt; c.angle += c.turn.dir * dt * (c.turn.fine ? 0.05 : 0.6 + 1.8 * Math.min(1, c.turn.time / 1.2)); } if (c.holding) { c.holdTime += dt; c.charging = c.holdTime >= c.holdDelay; if (c.charging) c.power = c.holdDelay ? Math.min(1, c.previousPower + powerAt(c.holdTime - c.holdDelay)) : powerAt(c.holdTime); // Alla carica piena il dito non deve restare: si conta quanto ci sta, per l'accompagnè. c.overHold = c.charging && c.power >= 1 ? (c.overHold ?? 0) + dt : 0; } if (c.step === 'rolling') { c.accumulator += dt; const list = cars(c), car = c.cars[c.shooter]; while (c.accumulator >= FIXED_STEP) { step(c.track, list, FIXED_STEP, c.events); c.accumulator -= FIXED_STEP; } const last = c.trail.at(-1); if (!last || Math.hypot(last[0] - car.x, last[1] - car.y) > 3) c.trail.push([car.x, car.y]); if (!moving(c)) { c.step = 'settle'; c.t = 0; } } else if (c.step === 'settle' && c.t >= REST) settle(c); else if (c.step === 'aim' && c.current === 'cpu') { if (!c.plan && c.t >= THINK * 0.3) cpuThink(c); if (c.plan && c.t >= THINK) strike(c, 'cpu', c.plan.angle, c.plan.power); } } // --- Comandi del giocatore: gli stessi del biliardo (mira col mouse o col dito, si tiene premuto per la forza). export function aimAt(c, x, y) { if (!canThrow(c) || c.holding || !Number.isFinite(x) || !Number.isFinite(y)) return false; const car = c.cars.player; if (Math.hypot(x - car.x, y - car.y) < CAR.r) return false; c.angle = Math.atan2(y - car.y, x - car.x); return true; } export function aimScreen(c, [x, y]) { if (c.step === 'choose') { const i = carUnder(x, y); return i >= 0 && choose(c, i); } return x >= 0 && x <= 640 && y >= 24 && y <= 360 && aimAt(c, x, y); } export function nudge(c, dir, fine = false) { if (!canThrow(c) || c.holding || !Number.isFinite(dir)) return false; c.angle += dir * (fine ? 0.004 : 0.03); return true; } export function setPower(c, value) { if (!canThrow(c) || c.holding || !Number.isFinite(value)) return false; c.power = clamp(value, 0, 1); return true; } export function shoot(c) { return c.step === 'choose' ? pick(c) : canThrow(c) && !c.holding && strike(c, 'player', c.angle, c.power); } export function turn(c, dir, owner, fine = false) { if (c.step === 'choose') { choose(c, c.choice + dir); return false; } // nella scelta frecce e pulsanti cambiano macchinina if (!canThrow(c) || c.holding || c.turn) return false; c.turn = { dir, owner, fine, time: 0 }; c.angle += dir * (fine ? 0.004 : 0.03); c.aimLocked = true; return true; } export function stopTurn(c, owner) { if (c.turn?.owner !== owner) return false; c.turn = null; return true; } export function key(c, key, down, fine = false) { if (!['ArrowLeft', 'ArrowRight'].includes(key)) return false; if (down) { if (c.turn?.owner === key) c.turn.fine = fine; else turn(c, key === 'ArrowLeft' ? -1 : 1, key, fine); } else stopTurn(c, key); return true; } export function press(c, pointerId) { if (c.step === 'choose') return pick(c); if (!canThrow(c) || c.holding) return false; // Premuto, la forza parte da zero e sale finché non si lascia (Andrea, 29 ottobre): come il dito che si carica. c.turn = null; c.pointerId = pointerId; c.holdDelay = 0; c.previousPower = c.power; c.holding = true; c.charging = false; c.holdTime = 0; c.power = 0; return true; } export function cancel(c, pointerId) { if (pointerId !== undefined && c.pointerId !== pointerId) return false; c.turn = null; c.charging = false; if (!c.holding) return false; c.holding = false; c.power = c.previousPower; c.holdTime = 0; c.overHold = 0; return true; } export function release(c, pointerId) { if (c.pointerId !== pointerId || !c.holding) return false; if (c.holdTime < TAP) return cancel(c, pointerId); // il tocco secco mira soltanto const accompagne = (c.overHold ?? 0) > ACCOMPAGNA; c.holding = c.charging = false; c.overHold = 0; strike(c, 'player', c.angle, c.power); if (accompagne) c.cars.player.fault = 'accompagne'; return true; } // Sta tenendo il dito oltre la carica piena: il disegno lo avvisa prima che diventi un accompagnè. export const overHeld = c => c.holding && (c.overHold ?? 0) > 0; // La guida: dove si fermerebbe la macchinina, se nessuno la tocca, e se ci arriva restando in pista. export function guide(c) { const car = c.cars.player, length = reach(speedFor(c.power)), ux = Math.cos(c.angle), uy = Math.sin(c.angle); let idx = car.idx, out = false; for (let d = 0; d <= length; d += 2) { const p = project(c.track, car.x + ux * d, car.y + uy * d, idx); idx = p.i; if (isOut(p.d)) { out = true; break; } } return { x: car.x + ux * length, y: car.y + uy * length, length, out }; } // Il pulsante del pannello vale anche nella scelta della macchinina. export const canPress = c => canThrow(c) || c.step === 'choose'; export const canAdjust = canPress; export function button(c) { if (c.step === 'choose') return 'Scegli'; if (c.step === 'result') return 'Finita'; if (c.step === 'rolling' || c.step === 'settle') return 'In movimento…'; return c.current === 'cpu' ? 'Spinge il Chiaru…' : 'Spingi'; } // Il perché del risultato, da scrivere (Andrea, 29 ottobre: «ho tagliato prima io, ma ha vinto il Chiaru»). Le // macchinine vere sono lunghe 4 centimetri e mezzo: un pixel è quasi due millimetri. export const CM = 4.5 / CAR.len; export function why(c) { const { winner, flicks, over } = c.result, n = flicks.player; if (over.player >= 0 && over.cpu >= 0) { const cm = Math.max(1, Math.round(Math.abs(over.player - over.cpu) * CM)); if (winner === 'draw') return [`Tutti e due al traguardo alla spinta ${n},`, 'pari al millimetro.']; return [`Tutti e due al traguardo alla spinta ${n}:`, `vince chi va più in là, ${winner === 'player' ? 'Andrea' : 'il Chiaru'} di ${cm} ${cm === 1 ? 'centimetro' : 'centimetri'}.`]; } return winner === 'player' ? [`Andrea al traguardo alla spinta ${n};`, 'al Chiaru non è bastata la sua.'] : [`Il Chiaru al traguardo alla spinta ${n};`, 'Andrea non c\'è arrivato.']; } // Chi è davanti adesso (1 o 2), per strada fatta. export const rank = (c, who) => c.cars[who].progress >= c.cars[OTHER[who]].progress ? 1 : 2; // Quanta pista manca al traguardo, in percentuale, per il tabellone. export const done = (c, who) => clamp(c.cars[who].progress / (c.laps * c.track.length), 0, 1); export function status(c) { if (c.step === 'choose') return `Scegli la macchinina: ${CARS[c.choice].name}. Tocca quella che vuoi, o frecce e Scegli.`; const where = `Andrea ${rank(c, 'player')}° (${Math.round(done(c, 'player') * 100)}%), Chiaru ${rank(c, 'cpu')}° (${Math.round(done(c, 'cpu') * 100)}%).`; if (c.step === 'result') return `${c.result.winner === 'player' ? 'Hai battuto il Chiaru!' : c.result.winner === 'cpu' ? 'Ha vinto il Chiaru.' : 'Arrivati pari.'} ${why(c).join(' ')}`; if (c.step === 'rolling' || c.step === 'settle') return `${where} ${c.line ?? ''}`.trim(); if (c.current === 'cpu') return `${where} Spinge il Chiaru…`; return `${where} ${c.line ? c.line + ' ' : ''}Tocca a te: mira e spingi.`; }