From e3d5d8b0d37a01cbaf94c007b0353cd2e1b74d2a Mon Sep 17 00:00:00 2001 From: Andrea Manzi Date: Thu, 29 Oct 2026 10:20:46 +0100 Subject: [PATCH] =?UTF-8?q?Macchinine:=20primo=20e=20secondo=20accanto=20a?= =?UTF-8?q?i=20nomi=20mentre=20si=20va;=20nel=20risultato=20il=20perch?= =?UTF-8?q?=C3=A9=20(chi=20=C3=A8=20arrivato=20a=20quale=20spinta,=20e=20s?= =?UTF-8?q?e=20ci=20arrivano=20tutti=20e=20due=20vince=20chi=20va=20pi?= =?UTF-8?q?=C3=B9=20in=20l=C3=A0,=20di=20quanti=20centimetri)=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 --- src/engine/micro-render.js | 24 +++++++++++++++--------- src/engine/micro.js | 20 +++++++++++++++++--- tests/micro.test.mjs | 18 ++++++++++++++++-- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/engine/micro-render.js b/src/engine/micro-render.js index 633b36a..6a75eb3 100644 --- a/src/engine/micro-render.js +++ b/src/engine/micro-render.js @@ -1,4 +1,4 @@ -import { HALF, CAR, CARS, canThrow, guide, done, overHeld, carImage, choiceAt } from './micro.js'; +import { HALF, CAR, CARS, canThrow, guide, done, overHeld, carImage, choiceAt, why, rank } from './micro.js'; import { text, panel } from './bocce-render.js'; const NAME = { player: 'Andrea', cpu: 'Chiaru' }; @@ -124,21 +124,27 @@ export function drawMicro(ctx, c, def, bg, image) { const level = { bona: 'alla bona', seria: 'seria', male: 'a fassi male' }[c.level]; text(ctx, `${level} · spinte ${c.flicks.player}`, 8, 16, 7, '#d9ccba'); const tint = id => CARS.find(x => x.id === id).color; - for (const [who, x0, color] of [['player', 168, tint(c.colors.player)], ['cpu', 404, tint(c.colors.cpu)]]) { + for (const [who, x0, color] of [['player', 200, tint(c.colors.player)], ['cpu', 430, tint(c.colors.cpu)]]) { + // Primo o secondo, mentre si va (Andrea, 29 ottobre). + const place = rank(c, who); + text(ctx, `${place}°`, x0 - 4, 11, 9, place === 1 ? '#ffe28a' : '#a99d86', 'right'); text(ctx, NAME[who], x0, 11, 8, '#fff0c8'); - ctx.fillStyle = '#3a372e'; ctx.fillRect(x0 + 52, 7, 150, 8); - ctx.fillStyle = color; ctx.fillRect(x0 + 52, 7, Math.round(150 * done(c, who)), 8); - ctx.strokeStyle = '#897b57'; ctx.lineWidth = 1; ctx.strokeRect(x0 + 52.5, 7.5, 149, 7); + ctx.fillStyle = '#3a372e'; ctx.fillRect(x0 + 52, 7, 140, 8); + ctx.fillStyle = color; ctx.fillRect(x0 + 52, 7, Math.round(140 * done(c, who)), 8); + ctx.strokeStyle = '#897b57'; ctx.lineWidth = 1; ctx.strokeRect(x0 + 52.5, 7.5, 139, 7); } const turn = c.step === 'result' ? null : c.current === 'cpu' ? 'Spinge il Chiaru…' : c.step === 'aim' ? 'Tocca a te: mira e spingi' : null; const line = overHeld(c) ? 'Lascia! Sennò è accompagnè' : c.line ?? turn; if (line) { ctx.fillStyle = '#17160fd0'; ctx.fillRect(170, 24, 300, 14); text(ctx, line, 320, 31, 8, /^(Fuori|Tagliè|Cappottè|Accompagnè)!/.test(c.line ?? '') ? '#ffbf9f' : '#9fd0ff', 'center'); } if (c.step === 'result') { const w = c.result.winner, title = w === 'player' ? 'HAI BATTUTO IL CHIARU!' : w === 'cpu' ? 'HA VINTO IL CHIARU' : 'ARRIVATI PARI'; - panel(ctx, 170, 140, 300, 72); - text(ctx, title, 320, 160, 12, w === 'player' ? '#b9e5b3' : '#efd49e', 'center'); - text(ctx, `Un giro, ${c.result.flicks.player} spinte a testa`, 320, 182, 8, '#efd49e', 'center'); - text(ctx, 'Torna dal Chiaru per uscire.', 320, 198, 8, '#d9ccba', 'center'); + // Il perché, in due righe: chi è arrivato a quale spinta, e di quanto è andato più in là. + const [first, second] = why(c); + panel(ctx, 130, 132, 380, 92); + text(ctx, title, 320, 150, 12, w === 'player' ? '#b9e5b3' : '#efd49e', 'center'); + text(ctx, first, 320, 172, 8, '#efd49e', 'center'); + text(ctx, second, 320, 186, 8, '#efd49e', 'center'); + text(ctx, 'Torna dal Chiaru per uscire.', 320, 208, 8, '#d9ccba', 'center'); } ctx.restore(); } diff --git a/src/engine/micro.js b/src/engine/micro.js index 5ea3b0e..f662295 100644 --- a/src/engine/micro.js +++ b/src/engine/micro.js @@ -265,7 +265,7 @@ function settle(c) { 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 } }; + c.step = 'result'; c.result = { winner, flicks: { ...c.flicks }, over }; return; } // Chi ha già tagliato il traguardo aspetta; spinge l'altro. @@ -389,12 +389,26 @@ export function button(c) { 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 ${Math.round(done(c, 'player') * 100)}%, Chiaru ${Math.round(done(c, 'cpu') * 100)}% del giro.`; - if (c.step === 'result') return `${where} ${c.result.winner === 'player' ? 'Hai battuto il Chiaru!' : c.result.winner === 'cpu' ? 'Ha vinto il Chiaru.' : 'Arrivati pari.'}`; + 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.`; diff --git a/tests/micro.test.mjs b/tests/micro.test.mjs index cd0e92c..5a443df 100644 --- a/tests/micro.test.mjs +++ b/tests/micro.test.mjs @@ -96,6 +96,19 @@ test('si parte scegliendo la macchinina: tocco, frecce, pulsante; il Chiaru tien for (const car of m.CARS) assert.ok(existsSync(new URL('../' + m.carImage(car.id, true), import.meta.url)) && existsSync(new URL('../' + m.carImage(car.id), import.meta.url))); }); +test('primo e secondo mentre si va; nel risultato il perché', () => { + const c = newMicro(1); + c.cars.player.progress = 100; c.cars.cpu.progress = 80; + assert.equal(m.rank(c, 'player'), 1); assert.equal(m.rank(c, 'cpu'), 2); + assert.match(m.status(c), /Andrea 1°.*Chiaru 2°/); + c.result = { winner: 'cpu', flicks: { player: 20, cpu: 20 }, over: { player: 5, cpu: 22 } }; + assert.deepEqual(m.why(c), ['Tutti e due al traguardo alla spinta 20:', 'vince chi va più in là, il Chiaru di 3 centimetri.']); + c.result = { winner: 'player', flicks: { player: 9, cpu: 9 }, over: { player: 5, cpu: -40 } }; + assert.deepEqual(m.why(c), ['Andrea al traguardo alla spinta 9;', 'al Chiaru non è bastata la sua.']); + c.result = { winner: 'draw', flicks: { player: 9, cpu: 9 }, over: { player: 5, cpu: 5.4 } }; + assert.match(m.why(c).join(' '), /pari/); +}); + test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si torna indietro', () => { // Tagliè: dal tratto di mezzo della esse, dritti in su attraverso l'erba verso il tratto di sopra. const t = newMicro(1, { level: 'bona' }), [sx, sy] = t.track.pts[Math.round(t.track.n * 0.48)]; @@ -184,7 +197,8 @@ test('il disegno: gesso, macchinine, guida e risultato; lo stato non cambia', () assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg)); assert.ok(calls.filter(([op, img]) => op === 'drawImage' && img?.car === 'player').length >= 2, 'la macchinina e il suo fantasma'); assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'Chiaru')); - c.step = 'result'; c.result = { winner: 'cpu', flicks: { player: 12, cpu: 12 } }; calls.length = 0; draw(); + c.step = 'result'; c.result = { winner: 'cpu', flicks: { player: 12, cpu: 12 }, over: { player: 3, cpu: 20 } }; calls.length = 0; draw(); + assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'vince chi va più in là, il Chiaru di 3 centimetri.')); assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'HA VINTO IL CHIARU')); }); @@ -201,7 +215,7 @@ test('il Chiaru al parcheggio di Cardetole: si chiacchiera, si sceglie il livell g.choose(g.options().findIndex(o => o.text === 'Si fa una gara alle macchinine?!?!')); settle(g); g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g); assert.equal(g.closeup?.id, 'micro'); assert.equal(g.closeup.level, 'male'); - g.closeup.step = 'result'; g.closeup.result = { winner: 'player', flicks: { player: 11, cpu: 11 } }; + g.closeup.step = 'result'; g.closeup.result = { winner: 'player', flicks: { player: 11, cpu: 11 }, over: { player: 4, cpu: -30 } }; g.closeupClose(); settle(g); assert.ok(g.state.flags.micro_vinto); });