From 4a193ed19cd4f35bc6618209f3708211d83f73ba Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 14:02:34 +0200 Subject: [PATCH 01/11] =?UTF-8?q?docs(pzkko):=20il=20prompt=20di=20=C2=ABv?= =?UTF-8?q?irtual=C2=BB,=20le=20scommesse=20sul=20calcio=20inventato?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La specifica di Alberto per il nuovo gioco: il campionato infinito a cicli di cinque minuti, le squadre storpiate, le quote che seguono la forza vera e i risultati a caso, lo schermino da tamagotchi. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015VZM7WrmYAstfLRGvRUqFy --- .../pzkko/games/PROMPT-virtual.md | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 albertopecchini.it/pzkko/games/PROMPT-virtual.md diff --git a/albertopecchini.it/pzkko/games/PROMPT-virtual.md b/albertopecchini.it/pzkko/games/PROMPT-virtual.md new file mode 100644 index 00000000..e02b73f8 --- /dev/null +++ b/albertopecchini.it/pzkko/games/PROMPT-virtual.md @@ -0,0 +1,168 @@ +# Prompt — «Virtual», le scommesse sul calcio inventato di pzkko.it + +## Il gioco in una frase + +Un nuovo gioco di pzkko.it, `games/virtual/`, nella stessa finestra degli altri +(roulette, blackjack, mine, dado, moneta): un campionato di calcio **inventato e +infinito**, una partita sola alla volta, quattro minuti per scommettere e un minuto +di partita animata in uno schermino in stile tamagotchi. Si gioca coi **$pzk** di +`games/pzk.js`: niente soldi veri, mai. + +## Prima di scrivere una riga + +1. Leggi `pzkko/README.md` per intero, poi apri e studia **almeno due giochi già + fatti** (consiglio `games/moneta/` e `games/mine/`) e `games/pzk.js`, + `games/crt.js`/`crt.css`, `wallet/`. Il nuovo gioco deve sembrare nato insieme a + loro: stessa finestra, stessa apertura dal muro, stesse fiches, stesso modo di + prendere e restituire i $pzk (sessione da 1.000 o conto del wallet), stesso + rispetto di `prefers-reduced-motion`, stesso italiano nei commenti e nei testi. +2. Guarda come la home aggancia i giochi (volantino sul muro, apertura della + finestra, regia) e aggancia `virtual` allo stesso modo. +3. Niente build, niente framework: file statici come il resto di `pzkko/`. + +## Il calendario: la partita di ogni minuto, uguale per tutti + +- Il tempo è diviso in **cicli di 5 minuti** allineati all'orologio: + - minuti 0–4 del ciclo: **scommesse aperte** (es. 12:55:00 → 12:58:59); + - ultimo minuto: **partita in diretta** (12:59:00 → 12:59:59); + - a 13:00:00 la partita è finita, si paga e si apre la successiva + (13:00 → 13:04 scommesse, 13:04 → 13:05 partita, e così via all'infinito). + - Nota per te: «ogni minuto c'è una partita sola» significa che sullo schermo + c'è sempre **una sola** partita, mai due insieme; il ritmo è 4+1. +- **Tutto è deterministico dall'orario**: l'indice della partita è + `Math.floor(Date.now() / 300000)`; da quell'indice, con un PRNG seminato + (stile `regole.js` delle mine, che già decide le mine dal seme), escono + squadre, quote, meteo e l'intera cronaca minuto per minuto. Così due persone + sullo stesso sito vedono la **stessa partita** nello stesso momento, chi apre + il gioco a metà partita la vede dal punto giusto, e un ricaricamento non cambia + niente. Mostra sempre un conto alla rovescia grande e chiaro («si chiude tra + 2:41», «IN DIRETTA 34'», «FINALE»). +- Le scommesse si accettano solo nella finestra dei 4 minuti; a partita iniziata + il pulsante si spegne. Le scommesse fatte restano visibili nella schedina e si + pagano al fischio finale anche se la finestra è stata chiusa (salvale come fanno + gli altri giochi; se l'utente torna dopo, trova l'esito). + +## Le squadre: Serie A e Serie B storpiate + +- Tutte le squadre di Serie A e Serie B 2026/27 con **nomi storpiati ma + riconoscibili** (stile videogioco senza licenza: «Juvenclub», «Inder», + «Milano Rossonera», «Napule», «Atalanto», «Fiorentoina», «Mantua»…, inventane + di divertenti ma non offensivi) e **stemmi originali disegnati da te** in SVG o + canvas: stessi colori sociali e lo stesso «spirito» (una zebra stilizzata, un + biscione, un giglio), ma **mai** il logo vero, mai i marchi registrati, mai i + nomi veri dei giocatori. Cognomi dei giocatori inventati anche quelli. +- Ogni squadra ha: nome storpiato, sigla di 3 lettere, città, colori maglia + casa/trasferta (le maglie si vedono addosso agli omini in campo), stemma, e una + **forza** da 1 a 100 presa dalla realtà (le big alte, le neopromosse e le + squadre di B basse). +- Gli accoppiamenti: casuali dal seme, di solito A contro A o B contro B, ogni + tanto un incrocio A–B (che dà quote più sbilanciate e più divertenti). + +## Le quote: seguono la realtà, i risultati no + +Questo è il cuore, rispettalo alla lettera: + +- **Le quote seguono la forza reale**: la squadra forte è quotata bassa, quella + scarsa alta; fattore campo leggero per chi gioca in casa; un pizzico di rumore + dal seme così due partite uguali non hanno quote identiche. +- **I risultati sono completamente casuali** e **non** dipendono dalla forza: il + risultato esce da una distribuzione indipendente dalle squadre (gol per + squadra, per esempio, con una Poisson di media fissa ~1,35 uguale per tutte). + Quindi la sfavorita vince spesso quanto la favorita: è voluto, è la battuta del + gioco. Scrivilo in una riga nella schermata delle regole («le quote copiano la + Serie A vera, i risultati no»). +- Le quote devono comunque essere **proporzionate e credibili** come quelle di un + bookmaker: calcola le probabilità implicite dalla forza, applica un margine + (~6–8%), arrotonda a due decimali, mai sotto 1,05, coerenti tra loro (1 X 2 che + sommano a ~106%, Over e Under della stessa soglia complementari). + +### Le scommesse possibili + +Solo queste, niente altro: + +- **1 X 2** (esito finale). +- **Over / Under** sul totale dei gol con soglie **1,5 · 2,5 · 3,5 · 4,5** + (le «maggiore/minore di 2, 3, 4…» della richiesta originale: le soglie a mezzo + gol evitano il pareggio della scommessa). + +Si possono mettere più puntate sulla stessa partita (anche 1 e Over 2,5 insieme), +con le fiches dei $pzk come negli altri giochi; vincita = puntata × quota. +Schedina chiara: cosa hai giocato, quota, possibile vincita, esito. + +## La partita: un minuto di schermino tamagotchi + +Uno **schermino LCD/pixel** (canvas, rendering `image-rendering: pixelated`, +palette ridotta, animazioni a pochi fotogrammi, «clean da tamagotchi»: carino, +leggibile, mai confuso) dentro la finestra del gioco, con sotto o accanto il +tabellone (stemmi, nomi, punteggio, minuto, cartellini). + +- **Campo da calcio a 5 in erba**, con le righe, le due porte, e i giocatori a + **cinque per parte** con le **maglie della propria squadra** (colori casa e, + se si somigliano, trasferta per gli ospiti) e un portiere di colore diverso. +- Il minuto vero di partita corre come **90' + recupero compressi in 60 + secondi**. La cronaca è una lista di eventi decisi dal seme prima che la + partita inizi; l'animazione li mette in scena all'istante giusto. +- **Eventi animati**, ognuno con una piccola sequenza riconoscibile: + gol (esultanza, rete che si gonfia, scritta GOL), tiro fuori / palo / parata + (quasi gol), rimessa laterale, fallo, **cartellino giallo**, **cartellino + rosso** (il giocatore esce e la squadra resta in quattro), **rigore** + (dischetto, rincorsa, gol o parata), **infortunio** (giocatore a terra, barella + pixel, cambio), fischio d'inizio, intervallo, fischio finale. Rara ma possibile: + la **vittoria a tavolino** (partita sospesa, 3–0 a tavolino scritto sul + tabellone; decidi tu una causa buffa e innocua, tipo «nebbia», «blackout dei + riflettori», «invasione di nutrie»). Le scommesse si pagano sul risultato + ufficiale, tavolino compreso. +- **Telecamera che si muove**: segue la palla con un po' di inerzia, zooma sulle + azioni pericolose; **inquadrature diverse** che cambiano (campo largo, mezzo + campo, dietro la porta per i rigori, primo piano sul cartellino, replay rapido + del gol a velocità ridotta). +- **Contorno**: spalti con **tifosi pixel** nei colori delle due squadre che si + alzano ed esultano al gol, sbandieratori, cori come onde di pixel; panchine. +- **Meteo della città di chi gioca in casa**, deciso dal seme ma **coerente col + clima e la stagione reale** di quella città nel giorno di oggi (a ottobre a + Bergamo può piovere o esserci nebbia, a Palermo è sereno e caldo, al nord + d'inverno può nevicare; se sei in dubbio usa la media climatica della zona). + Effetti: sereno, nuvole, pioggia (pozzanghere e scivolate), nebbia (si vede + poco, bellissimo nello schermino), neve (palla arancione), vento. **Luce in + base all'ora vera**: partite serali coi riflettori accesi. + Ma il meteo **non cambia mai il risultato**: è solo scena. +- Suoni opzionali coerenti con `suono/` (fischietto, boato, «uuuh» sul palo), + sintetizzati come il resto del sito, spenti se l'utente ha spento il suono. + +## Durante le scommesse (i 4 minuti) + +Lo schermino non è vuoto: squadre che si scaldano, tifosi che entrano, meteo +già visibile, presentazione della partita (stemmi, forza, ultimi «risultati» +inventati, che ovviamente non contano). Tutto deve far venire voglia di giocare +ma senza schermate rumorose. + +## Qualità e dettagli che contano + +- Funziona bene da **telefono** (molti aprono pzkko.it da lì): schermino che si + ridimensiona tenendo i pixel netti, pulsanti grandi, niente scroll orizzontale. +- Prestazioni: un solo canvas, `requestAnimationFrame`, si ferma quando la + finestra è chiusa o la scheda nascosta (riprende al punto giusto grazie al + determinismo). +- `prefers-reduced-motion`: niente telecamera che si muove, solo il tabellone e + le scritte degli eventi. +- Le regole del gioco in una schermata breve e simpatica nello stesso tono degli + altri giochi, con la frase su quote vere e risultati a caso, e che si gioca coi + $pzk, che non valgono niente fuori da pzkko.it. +- Nessuna immagine generata da un modello è necessaria: disegna tutto in canvas/SVG. + Se proprio ti serve un'immagine, segui il metodo di `tools/pzkko/fogli.js`, ma + preferisci di no. + +## Consegna + +1. Lavora su un ramo `pzkko/virtual`, a partire da `main` aggiornato. +2. Aggiorna `pzkko/README.md` con una voce `games/virtual/` nello stesso stile + delle altre (cosa fa ogni file), e la `regia` se gli altri giochi ci compaiono. +3. Provalo davvero: apri la pagina in locale (`npm run dev:pzkko` dalla radice), + apri il gioco, guarda almeno un ciclo completo scommesse → partita → pagamento, + verifica che due schede aperte vedano la stessa partita e che le quote siano + sensate (favorita bassa, sfavorita alta) mentre i risultati su ~1.000 partite + simulate siano indipendenti dalla forza. Controlla anche il telefono + (viewport stretto). +4. Commit chiari in italiano, come quelli già nel repo; push del ramo e apri una + pull request verso `main` con una descrizione breve in italiano di cosa c'è e + come provarlo. Non fare merge su `main` da solo. From b9710fccd153d679bc114e62de3fa5765c05b33d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 14:02:34 +0200 Subject: [PATCH 02/11] feat(pzkko): virtual, le scommesse sul campionato di calcio inventato MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - partita.js: il calendario a cicli di cinque minuti allineati all'orologio (quattro di scommesse, uno di diretta), tutto dal seme dell'indice Math.floor(Date.now() / 300000): squadre, quote, meteo, formazioni e la cronaca minuto per minuto. Le quote dalla forza vera (gol attesi, due Poisson, margine 6,5%, mai sotto 1,05, 1 X 2 al 106,5%), il risultato da un altro seme con una Poisson di media fissa 1,35: la sfavorita vince quanto la favorita. Una su 120 a tavolino, per nebbia, nutrie o blackout - squadre.js: le quaranta di A e B 2026/27 coi nomi storpiati (Juvenclub, Inder, Napule, Mantua…), colori, forza, stemmi originali in SVG e cognomi inventati - schermo.js: lo schermino a pixel (192 × 120 per un fattore intero, pixelated): il campo a cinque, gli omini con le maglie, i tifosi che saltano, la telecamera con inerzia e zoom, i rigori da dietro la porta, il replay del gol, i cartellini, la barella, il meteo coerente col clima della città e la luce dell'ora vera - virtual.js e virtual.css: la finestra come gli altri giochi, le quote stampate, la schedina salvata nel browser e pagata al fischio finale anche a finestra chiusa, i $pzk sul tavolo di pzk.js - tools/pzkko/virtual/prova.mjs: 5.000 partite senza browser, nessun errore (quote ↔ forza −0,77; forza ↔ gol +0,01; favorita netta vince il 38,8%, la sfavorita il 35,7%, p = 0,54) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015VZM7WrmYAstfLRGvRUqFy --- .../pzkko/games/virtual/partita.js | 410 +++++++++++ .../pzkko/games/virtual/schermo.js | 689 ++++++++++++++++++ .../pzkko/games/virtual/squadre.js | 128 ++++ .../pzkko/games/virtual/virtual.css | 164 +++++ .../pzkko/games/virtual/virtual.js | 445 +++++++++++ .../tools/pzkko/virtual/prova.mjs | 139 ++++ 6 files changed, 1975 insertions(+) create mode 100644 albertopecchini.it/pzkko/games/virtual/partita.js create mode 100644 albertopecchini.it/pzkko/games/virtual/schermo.js create mode 100644 albertopecchini.it/pzkko/games/virtual/squadre.js create mode 100644 albertopecchini.it/pzkko/games/virtual/virtual.css create mode 100644 albertopecchini.it/pzkko/games/virtual/virtual.js create mode 100644 albertopecchini.it/tools/pzkko/virtual/prova.mjs diff --git a/albertopecchini.it/pzkko/games/virtual/partita.js b/albertopecchini.it/pzkko/games/virtual/partita.js new file mode 100644 index 00000000..7470dbbe --- /dev/null +++ b/albertopecchini.it/pzkko/games/virtual/partita.js @@ -0,0 +1,410 @@ +// Il calendario e le partite di «Virtual», il campionato inventato di pzkko.it. Niente disegno qui: la finestra +// (virtual.js) e lo schermino (schermo.js) chiedono a questo modulo cosa succede, e la prova +// (tools/pzkko/virtual/prova.mjs) lo fa girare su migliaia di partite senza browser. +// +// IL TEMPO. Il tempo è diviso in cicli di cinque minuti allineati all'orologio: i primi quattro minuti del ciclo le +// scommesse sono aperte, l'ultimo minuto la partita è in diretta (90' più il recupero compressi in 60 secondi), allo +// scoccare del ciclo dopo è finita, si paga e si apre la successiva. L'indice della partita è Math.floor(Date.now() / +// 300000): da quell'indice, con un generatore seminato, escono le squadre, le quote, il meteo e tutta la cronaca. Così +// chiunque apra pzkko.it vede LA STESSA partita nello stesso momento, chi arriva a metà la vede dal punto giusto e +// ricaricare non cambia niente. +// +// LE QUOTE SEGUONO LA REALTÀ, I RISULTATI NO. Le quote si calcolano dalla forza vera delle squadre (squadre.js: le big +// alte, le neopromosse e la Serie B basse) con un fattore campo leggero e un pizzico di rumore dal seme, come farebbe un +// bookmaker: gol attesi dalla differenza di forza, probabilità di 1 X 2 e dei totali da una Poisson, un margine del +// 6,5%, arrotondate ai centesimi e mai sotto 1,05. Il risultato invece esce da una Poisson di media fissa (GOL_MEDI, +// 1,35 a squadra) uguale per tutte, che della forza non sa niente: la sfavorita vince quanto la favorita. È voluto, è la +// battuta del gioco. Anche il meteo è solo scena: non tocca il risultato. + +import { SQUADRE, SERIE_A, SERIE_B, cognome } from './squadre.js' + +export const CICLO = 5 * 60 * 1000 // ms +export const SCOMMESSE = 4 * 60 * 1000 // i primi quattro minuti del ciclo +export const DURATA = CICLO - SCOMMESSE // il minuto della partita +export const GOL_MEDI = 1.35 +export const MARGINE = 0.065 +export const QUOTA_MINIMA = 1.05 +export const CAMPO = 4 // il fattore campo, in punti di forza +export const SOGLIE = [1.5, 2.5, 3.5, 4.5] +/** Le puntate possibili: 1 X 2 e over/under sulle quattro soglie. */ +export const PUNTATE = [ + { id: '1', nome: '1', dice: 'vince chi gioca in casa' }, + { id: 'X', nome: 'X', dice: 'pareggio' }, + { id: '2', nome: '2', dice: 'vince chi gioca fuori' }, + ...SOGLIE.flatMap((s) => [{ id: `O${s}`, nome: `over ${s.toLocaleString('it-IT')}`, dice: `più di ${s.toLocaleString('it-IT')} gol in tutto` }, { id: `U${s}`, nome: `under ${s.toLocaleString('it-IT')}`, dice: `meno di ${s.toLocaleString('it-IT')} gol in tutto` }]), +] +export const PER_PUNTATA = new Map(PUNTATE.map((p) => [p.id, p])) + +// ── l'orologio del campionato ──────────────────────────────────────────────── +export const indiceAdesso = (now = Date.now()) => Math.floor(now / CICLO) +/** Dove siamo nel ciclo: { indice, fase: 'scommesse' | 'partita', manca (ms alla chiusura o alla fine), t (secondi di partita, 0–60) }. */ +export function fase(now = Date.now()) { + const indice = Math.floor(now / CICLO), dentro = now - indice * CICLO + if (dentro < SCOMMESSE) return { indice, fase: 'scommesse', manca: SCOMMESSE - dentro, t: 0 } + return { indice, fase: 'partita', manca: CICLO - dentro, t: (dentro - SCOMMESSE) / 1000 } +} + +// ── il generatore seminato (mulberry32) ────────────────────────────────────── +/** Un generatore riproducibile: rand() dà un numero fra 0 e 1, sempre gli stessi per lo stesso seme. */ +export function generatore(seme) { + let a = (seme ^ 0x9e3779b9) >>> 0 + return () => { + a = (a + 0x6d2b79f5) >>> 0 + let t = a + t = Math.imul(t ^ (t >>> 15), t | 1) + t ^= t + Math.imul(t ^ (t >>> 7), t | 61) + return ((t ^ (t >>> 14)) >>> 0) / 4294967296 + } +} +// due semi diversi per la stessa partita: uno per squadre, quote e scena (che DEVONO dipendere dall'indice), uno a parte +// per il risultato. Così è anche evidente nel codice che il risultato non legge niente delle squadre +const semeScena = (indice) => (Math.imul(indice, 2654435761) ^ 0x5bd1e995) >>> 0 +const semeRisultato = (indice) => (Math.imul(indice ^ 0x7f4a7c15, 1597334677) + 0x3c6ef372) >>> 0 + +/** Una Poisson di media `media`, con rand. */ +export function poisson(media, rand) { + const L = Math.exp(-media) + let k = 0, p = rand() + while (p > L) { k++; p *= rand() } + return k +} + +// ── le probabilità da cui nascono le quote ─────────────────────────────────── +const fattoriale = (n) => { let f = 1; for (let i = 2; i <= n; i++) f *= i; return f } +const pPoisson = (l, k) => (Math.exp(-l) * l ** k) / fattoriale(k) +/** + * Dai gol attesi delle due squadre, le probabilità di 1, X, 2 e di over/under per ogni soglia (Poisson indipendenti, + * fino a 10 gol a testa). È il modello del bookmaker: con le forze vere dà quote credibili. + */ +export function probabilita(lc, lo) { + let p1 = 0, pX = 0, p2 = 0 + const tot = new Array(21).fill(0) + for (let a = 0; a <= 10; a++) for (let b = 0; b <= 10; b++) { + const p = pPoisson(lc, a) * pPoisson(lo, b) + if (a > b) p1 += p; else if (a === b) pX += p; else p2 += p + tot[a + b] += p + } + const s = p1 + pX + p2 + const over = {}, under = {} + for (const soglia of SOGLIE) { + let o = 0 + for (let n = Math.ceil(soglia); n <= 20; n++) o += tot[n] + over[soglia] = o / s; under[soglia] = 1 - o / s + } + return { 1: p1 / s, X: pX / s, 2: p2 / s, over, under } +} +/** I gol attesi che il bookmaker dà a casa e ospite dalle loro forze (già col rumore). */ +export function golAttesi(forzaCasa, forzaOspite) { + const d = (forzaCasa + CAMPO - forzaOspite) / 100 + return { lc: GOL_MEDI * Math.exp(1.25 * d), lo: GOL_MEDI * Math.exp(-1.25 * d) } +} +const quota = (p) => Math.max(QUOTA_MINIMA, Math.round(100 / (p * (1 + MARGINE))) / 100) +/** Le quote dalle probabilità: margine, centesimi, mai sotto 1,05. */ +export function quoteDa(P) { + const q = { 1: quota(P[1]), X: quota(P.X), 2: quota(P[2]) } + for (const s of SOGLIE) { q[`O${s}`] = quota(P.over[s]); q[`U${s}`] = quota(P.under[s]) } + return q +} + +// ── il tempo di gioco: 90' + recupero in 60 secondi ────────────────────────── +// primo tempo 0–27 s (0'–45'), intervallo 27–29,5 s, secondo tempo 29,5–57 s (45'–90'), recupero 57–60 s (90+1'…90+3') +export const T = { fine1: 27, via2: 29.5, fine2: 57, fine: 60 } +/** Il minuto da scrivere sul tabellone a t secondi: { min, extra, scritta } (scritta: «34'», «90+2'», «INT», «FINE»). */ +export function minuto(t) { + if (t < 0) return { min: 0, extra: 0, scritta: "0'" } + if (t < T.fine1) { const m = Math.floor((t / T.fine1) * 45) + 1; return { min: m, extra: 0, scritta: `${m}'` } } + if (t < T.via2) return { min: 45, extra: 0, scritta: 'INT', intervallo: true } + if (t < T.fine2) { const m = 45 + Math.floor(((t - T.via2) / (T.fine2 - T.via2)) * 45) + 1; return { min: m, extra: 0, scritta: `${m}'` } } + if (t < T.fine) { const e = Math.floor(((t - T.fine2) / (T.fine - T.fine2)) * 3) + 1; return { min: 90, extra: e, scritta: `90+${e}'` } } + return { min: 90, extra: 3, scritta: 'FINE', finita: true } +} +/** Il secondo di partita in cui cade il minuto m (1–93), con una frazione a caso dentro il minuto. */ +function secondoDelMinuto(m, frazione = 0.5) { + if (m <= 45) return ((m - 1 + frazione) / 45) * T.fine1 + if (m <= 90) return T.via2 + ((m - 46 + frazione) / 45) * (T.fine2 - T.via2) + return T.fine2 + ((m - 91 + frazione) / 3) * (T.fine - T.fine2) +} + +// ── il meteo della città di casa, coerente col clima e con la stagione ────── +// Per ogni clima, mese per mese, quanto spesso c'è sereno, nuvole, pioggia, nebbia, neve, vento (pesi). Medie climatiche a +// occhio, non previsioni: a ottobre a Bergamo può piovere o esserci nebbia, a Palermo è sereno e caldo, al nord d'inverno +// può nevicare. È solo scena: il risultato non lo guarda. +const METEO = ['sereno', 'nuvole', 'pioggia', 'nebbia', 'neve', 'vento'] +// ser nuv pio neb nev ven +const CLIMA = { + nord: { + inverno: [20, 25, 15, 28, 8, 4], primavera: [35, 30, 25, 4, 0, 6], estate: [60, 20, 14, 0, 0, 6], autunno: [25, 25, 22, 22, 1, 5], + temp: { inverno: [0, 7], primavera: [10, 20], estate: [22, 33], autunno: [8, 18] }, + }, + alpi: { + inverno: [35, 25, 5, 5, 28, 2], primavera: [35, 30, 20, 2, 8, 5], estate: [50, 25, 20, 0, 0, 5], autunno: [35, 25, 20, 5, 10, 5], + temp: { inverno: [-4, 5], primavera: [6, 17], estate: [16, 28], autunno: [4, 15] }, + }, + liguria: { + inverno: [35, 30, 25, 0, 2, 8], primavera: [40, 30, 22, 0, 0, 8], estate: [65, 20, 8, 0, 0, 7], autunno: [30, 25, 35, 0, 0, 10], + temp: { inverno: [6, 12], primavera: [12, 20], estate: [22, 30], autunno: [11, 19] }, + }, + centro: { + inverno: [35, 30, 22, 6, 2, 5], primavera: [45, 28, 20, 1, 0, 6], estate: [70, 18, 7, 0, 0, 5], autunno: [38, 25, 28, 4, 0, 5], + temp: { inverno: [4, 12], primavera: [11, 21], estate: [22, 33], autunno: [10, 20] }, + }, + sud: { + inverno: [45, 28, 18, 1, 1, 7], primavera: [58, 24, 11, 0, 0, 7], estate: [85, 10, 2, 0, 0, 3], autunno: [52, 24, 17, 0, 0, 7], + temp: { inverno: [8, 15], primavera: [13, 23], estate: [24, 34], autunno: [14, 23] }, + }, +} +const stagione = (mese) => (mese === 11 || mese <= 1 ? 'inverno' : mese <= 4 ? 'primavera' : mese <= 7 ? 'estate' : 'autunno') +// l'ora e il mese di Mantova (Europe/Rome) dell'istante +function oraRoma(ms) { + const p = new Intl.DateTimeFormat('en-GB', { timeZone: 'Europe/Rome', hour: 'numeric', minute: 'numeric', month: 'numeric', hour12: false }).formatToParts(new Date(ms)) + const v = (t) => Number(p.find((x) => x.type === t)?.value) + return { ora: v('hour') % 24 + v('minute') / 60, mese: v('month') - 1 } +} +// tramonto e alba a occhio per l'Italia del nord, mese per mese (ora locale): basta per decidere se i riflettori sono accesi +const TRAMONTO = [17.0, 17.7, 18.3, 20.0, 20.6, 21.1, 21.1, 20.6, 19.7, 18.7, 17.0, 16.8] +const ALBA = [7.9, 7.4, 6.7, 6.8, 6.0, 5.6, 5.8, 6.3, 6.9, 7.4, 7.1, 7.8] +function meteoDi(squadra, inizio, rand) { + const { ora, mese } = oraRoma(inizio) + const c = CLIMA[squadra.clima], st = stagione(mese), pesi = c[st] + let r = rand() * pesi.reduce((a, b) => a + b, 0), tipo = 'sereno' + for (let i = 0; i < pesi.length; i++) { if (r < pesi[i]) { tipo = METEO[i]; break } r -= pesi[i] } + const [tmin, tmax] = c.temp[st] + // la temperatura: fra minima e massima della stagione, più fresca di notte e col maltempo + let temp = tmin + (tmax - tmin) * (0.25 + 0.5 * rand()) + const notte = ora < ALBA[mese] || ora >= TRAMONTO[mese] + const sera = !notte && ora >= TRAMONTO[mese] - 0.75 // la luce bassa prima del tramonto + if (notte) temp -= 3 + if (tipo === 'pioggia' || tipo === 'nebbia') temp -= 2 + if (tipo === 'neve') temp = Math.min(temp, 1) + return { tipo, temp: Math.round(temp), notte, sera, forza: 0.5 + rand() * 0.5, mese, ora } +} + +// ── le cause buffe della vittoria a tavolino ───────────────────────────────── +export const CAUSE = [ + 'blackout dei riflettori', 'invasione di nutrie', 'il pallone è finito nel canale e non ce n\'era un altro', 'gli ospiti hanno sbagliato stadio', + 'l\'arbitro ha perso il fischietto', 'il bar dello stadio ha finito le brioche', 'un drone del fantacalcio sul campo', 'la squadra di casa è rimasta chiusa negli spogliatoi', +] +const CAUSA_NEBBIA = 'nebbia fitta: non si vede la porta' + +// ── la partita ─────────────────────────────────────────────────────────────── +const cache = new Map() +/** + * La partita con quell'indice: squadre, quote, meteo, formazioni, le ultime cinque «uscite» inventate, il risultato + * ufficiale e la cronaca (eventi con il secondo in cui cadono). Tutto dal seme: uguale per chiunque la chieda. + */ +export function partita(indice) { + let p = cache.get(indice) + if (p) return p + p = costruisci(indice) + if (cache.size > 40) cache.delete(cache.keys().next().value) + cache.set(indice, p) + return p +} + +function costruisci(indice) { + const rand = generatore(semeScena(indice)) + const inizio = indice * CICLO + SCOMMESSE // il fischio d'inizio, in ms + // le squadre: di solito A contro A o B contro B, una su sette un incrocio + const A = SQUADRE.filter((s) => s.serie === SERIE_A), B = SQUADRE.filter((s) => s.serie === SERIE_B) + const r = rand() + let lista1, lista2 + if (r < 0.52) lista1 = lista2 = A + else if (r < 0.86) lista1 = lista2 = B + else if (rand() < 0.5) { lista1 = A; lista2 = B } else { lista1 = B; lista2 = A } + const casa = lista1[Math.floor(rand() * lista1.length)] + let ospite = lista2[Math.floor(rand() * lista2.length)] + while (ospite === casa) ospite = lista2[Math.floor(rand() * lista2.length)] + + // le quote: dalla forza vera, col fattore campo e un filo di rumore (così due partite uguali non hanno quote identiche) + const fc = casa.forza + (rand() - 0.5) * 6, fo = ospite.forza + (rand() - 0.5) * 6 + const { lc, lo } = golAttesi(fc, fo) + const prob = probabilita(lc, lo) + const quote = quoteDa(prob) + + const meteo = meteoDi(casa, inizio, rand) + const formazioni = { casa: formazione(rand), ospite: formazione(rand) } + const ultime = { casa: Array.from({ length: 5 }, () => 'VNP'[Math.floor(rand() * 3)]), ospite: Array.from({ length: 5 }, () => 'VNP'[Math.floor(rand() * 3)]) } + // le maglie: gli ospiti giocano in trasferta se la loro prima maglia somiglia a quella di casa + const maglie = { casa: { colori: casa.casa, disegno: casa.disegno }, ospite: simili(casa.casa, ospite.casa) ? { colori: ospite.fuori, disegno: 'pieno' } : { colori: ospite.casa, disegno: ospite.disegno } } + if (simili(maglie.casa.colori, maglie.ospite.colori)) maglie.ospite = { colori: ospite.fuori, disegno: 'pieno' } // (fuori è sempre diversa da casa) + const portieri = { casa: portiere(maglie.casa.colori, maglie.ospite.colori), ospite: portiere(maglie.ospite.colori, maglie.casa.colori) } + + // ── IL RISULTATO: dal suo seme, con la Poisson fissa. Non legge niente di quanto sopra ── + const rr = generatore(semeRisultato(indice)) + const golCasa = poisson(GOL_MEDI, rr), golOspite = poisson(GOL_MEDI, rr) + const tavolino = rr() < 1 / 120 ? (rr() < 0.5 ? 'casa' : 'ospite') : null + + // ── la cronaca: i gol veri messi al loro minuto, e intorno la scena (dal seme della scena) ── + const eventi = cronaca({ golCasa, golOspite, tavolino, rand, formazioni, meteo }) + const risultato = tavolino ? { casa: tavolino === 'casa' ? 3 : 0, ospite: tavolino === 'ospite' ? 3 : 0, tavolino, causa: eventi.find((e) => e.tipo === 'sospesa').causa } : { casa: golCasa, ospite: golOspite, tavolino: null } + // il punteggio sul campo, prima della sospensione (per il tabellone durante la partita) + return { indice, inizio, fine: inizio + DURATA, casa, ospite, forze: { casa: casa.forza, ospite: ospite.forza }, quote, prob, attesi: { casa: lc, ospite: lo }, meteo, formazioni, maglie, portieri, ultime, risultato, eventi } +} + +// due maglie si somigliano se i colori principali sono vicini +const esa = (c) => [1, 3, 5].map((i) => parseInt(c.slice(i, i + 2), 16)) +const vicini = (a, b) => { const [r1, g1, b1] = esa(a), [r2, g2, b2] = esa(b); return Math.abs(r1 - r2) + Math.abs(g1 - g2) + Math.abs(b1 - b2) < 150 } +const simili = (a, b) => vicini(a[0], b[0]) || (a[1] && b[0] && vicini(a[1], b[0]) && vicini(a[0], b[1] ?? b[0])) +// il portiere di un colore che non c'è nelle due squadre +const PORTIERI = ['#1a8a3a', '#e8742a', '#f2c230', '#8e44ad', '#7fc4ea', '#e889b4'] +function portiere(nostri, loro) { return PORTIERI.find((c) => !nostri.some((n) => vicini(c, n)) && !loro.some((n) => vicini(c, n))) ?? '#8a8a8a' } + +/** Cinque in campo e due in panchina, con numeri e cognomi inventati; [0] è il portiere. */ +function formazione(rand) { + const usati = new Set(), numeri = [1] + while (numeri.length < 7) { const n = 2 + Math.floor(rand() * 22); if (!usati.has(n)) { usati.add(n); numeri.push(n) } } + return numeri.map((n, i) => ({ n, nome: cognome(rand), ruolo: i === 0 ? 'P' : i <= 2 ? 'D' : i === 3 ? 'C' : i === 4 ? 'A' : 'R' })) +} + +// Gli eventi della cronaca, ognuno con il secondo t in cui cade e quanto dura la sua messa in scena (dur). Tipi: +// inizio, intervallo, ripresa, fine, gol (squadra, chi, rigore?, punteggio), tiro (esito: fuori | palo | parata), +// rimessa, fallo, giallo, rosso, rigore (esito: parato | fuori: quelli segnati sono «gol» con rigore: true), +// infortunio (chi, entra), sospesa (causa). I grandi eventi stanno ad almeno 3,2 s l'uno dall'altro. +function cronaca({ golCasa, golOspite, tavolino, rand, formazioni, meteo }) { + const ev = [] + const minutoLibero = (occupati, lontano = 2) => { + for (let k = 0; k < 40; k++) { + const m = 2 + Math.floor(rand() * 90) // 2..91 + if (occupati.every((o) => Math.abs(o - m) >= lontano)) return m + } + return 2 + Math.floor(rand() * 90) + } + const chi = (squadra, { noPortiere = true } = {}) => { const f = formazioni[squadra]; const i = noPortiere ? 1 + Math.floor(rand() * 4) : Math.floor(rand() * 5); return { i, n: f[i].n, nome: f[i].nome } } + const grandi = [] // i minuti già presi dai grandi eventi + // i gol + const gol = [] + for (let i = 0; i < golCasa; i++) gol.push({ squadra: 'casa', m: minutoLibero(grandi, 3) }); + for (let i = 0; i < golOspite; i++) gol.push({ squadra: 'ospite', m: minutoLibero(grandi, 3) }) + for (const g of gol) grandi.push(g.m) + gol.sort((a, b) => a.m - b.m) + // il rigore: una partita su sette ne ha uno; se quella squadra ha segnato, uno dei suoi gol è su rigore, se no è sbagliato + let rigoreSbagliato = null + if (rand() < 0.14) { + const sq = rand() < 0.5 ? 'casa' : 'ospite' + const suoi = gol.filter((g) => g.squadra === sq) + if (suoi.length && rand() < 0.75) suoi[Math.floor(rand() * suoi.length)].rigore = true + else { const m = minutoLibero(grandi, 3); grandi.push(m); rigoreSbagliato = { squadra: sq, m, esito: rand() < 0.6 ? 'parato' : 'fuori' } } + } + // cartellini: gialli (in media 2,5, al più 4), un rosso una partita su quattordici + const gialli = Math.min(4, poisson(2.5, rand)) + const cartellini = [] + const ammoniti = { casa: new Set(), ospite: new Set() } + for (let i = 0; i < gialli; i++) { + const sq = rand() < 0.5 ? 'casa' : 'ospite', c = chi(sq), m = minutoLibero(grandi, 3) + grandi.push(m) + cartellini.push({ tipo: 'giallo', squadra: sq, m, chi: c }) + ammoniti[sq].add(c.i) + } + let rosso = null + if (rand() < 1 / 14) { + const sq = rand() < 0.5 ? 'casa' : 'ospite', c = chi(sq), m = 15 + Math.floor(rand() * 70) + grandi.push(m) + rosso = { tipo: 'rosso', squadra: sq, m, chi: c, doppio: ammoniti[sq].has(c.i) && cartellini.some((k) => k.squadra === sq && k.chi.i === c.i && k.m < m) } + } + // l'infortunio: una su otto; esce in barella ed entra la riserva + let infortunio = null + if (rand() < 0.125) { + const sq = rand() < 0.5 ? 'casa' : 'ospite', c = chi(sq), m = minutoLibero(grandi, 3) + grandi.push(m) + infortunio = { tipo: 'infortunio', squadra: sq, m, chi: c, entra: formazioni[sq][5 + Math.floor(rand() * 2)] } + } + // i quasi gol, le rimesse e i falli: la scena fra un grande evento e l'altro + const piccoli = [] + const nTiri = 4 + poisson(3, rand), nRimesse = 2 + Math.floor(rand() * 3), nFalli = 2 + Math.floor(rand() * 3) + const occupati = [...grandi] + for (let i = 0; i < nTiri; i++) { const m = minutoLibero(occupati, 2); occupati.push(m); const r = rand(); piccoli.push({ tipo: 'tiro', squadra: rand() < 0.5 ? 'casa' : 'ospite', m, esito: r < 0.45 ? 'fuori' : r < 0.6 ? 'palo' : 'parata' }) } + for (let i = 0; i < nRimesse; i++) { const m = minutoLibero(occupati, 2); occupati.push(m); piccoli.push({ tipo: 'rimessa', squadra: rand() < 0.5 ? 'casa' : 'ospite', m, lato: rand() < 0.5 ? 'alto' : 'basso' }) } + for (let i = 0; i < nFalli; i++) { const m = minutoLibero(occupati, 2); occupati.push(m); piccoli.push({ tipo: 'fallo', squadra: rand() < 0.5 ? 'casa' : 'ospite', m }) } + // con la pioggia qualcuno scivola (solo scena) + if (meteo.tipo === 'pioggia') for (let i = 0; i < 1 + Math.floor(rand() * 2); i++) { const m = minutoLibero(occupati, 2); occupati.push(m); piccoli.push({ tipo: 'scivola', squadra: rand() < 0.5 ? 'casa' : 'ospite', m, chi: chi(rand() < 0.5 ? 'casa' : 'ospite') }) } + + // tutti insieme, in secondi, poi la sospensione se c'è + let punteggio = { casa: 0, ospite: 0 } + const tutti = [] + for (const g of gol) tutti.push({ tipo: 'gol', squadra: g.squadra, m: g.m, rigore: !!g.rigore, chi: chi(g.squadra) }) + if (rigoreSbagliato) tutti.push({ tipo: 'rigore', ...rigoreSbagliato, chi: chi(rigoreSbagliato.squadra) }) + tutti.push(...cartellini) + if (rosso) tutti.push(rosso) + if (infortunio) tutti.push(infortunio) + tutti.push(...piccoli) + for (const e of tutti) e.t = secondoDelMinuto(e.m, 0.15 + rand() * 0.7) + tutti.sort((a, b) => a.t - b.t) + // i grandi eventi ad almeno 3,2 s l'uno dall'altro, i piccoli ad almeno 1,2 s da tutto (spostati in avanti); un piccolo + // evento che non ci sta più (la fine, l'intervallo) si lascia cadere, un grande si schiaccia all'indietro quanto basta + const grande = (e) => ['gol', 'rigore', 'giallo', 'rosso', 'infortunio'].includes(e.tipo) + const sistemati = [] + let ultimoGrande = -10, ultimo = 0.6 // (il primo evento dopo il fischio d'inizio) + for (const e of tutti) { + const minimo = grande(e) ? Math.max(ultimoGrande + 3.2, ultimo + 1.4) : ultimo + 1.2 + if (e.t < minimo) e.t = minimo + // mai a cavallo dell'intervallo + if (e.t > T.fine1 - 1.6 && e.t < T.via2 + 0.6) e.t = T.via2 + 0.6 + (e.t - (T.fine1 - 1.6)) * 0.2 + if (e.t > T.fine - 1.4) { + if (!grande(e)) continue + e.t = T.fine - 1.4 + // i grandi di prima arretrano, a catena, finché stanno a 3,2 s + for (let j = sistemati.length - 1, lim = e.t; j >= 0; j--) { const g = sistemati[j]; if (!grande(g)) continue; if (g.t > lim - 3.2) g.t = lim - 3.2; lim = g.t } + } + sistemati.push(e) + ultimo = e.t + if (grande(e)) ultimoGrande = e.t + } + tutti.length = 0 + tutti.push(...sistemati) + // (i piccoli che l'arretramento ha lasciato addosso a un grande si tolgono) + for (let j = tutti.length - 1; j >= 0; j--) { const e = tutti[j]; if (grande(e)) continue; if (tutti.some((g) => g !== e && grande(g) && Math.abs(g.t - e.t) < 1.1)) tutti.splice(j, 1) } + tutti.sort((a, b) => a.t - b.t) + for (const e of tutti) { e.m = minuto(e.t).min; e.minuto = minuto(e.t).scritta } + // il punteggio dopo ogni gol, i secondi gialli diventano l'espulsione già scritta + for (const e of tutti) if (e.tipo === 'gol') { punteggio = { ...punteggio, [e.squadra]: punteggio[e.squadra] + 1 }; e.punteggio = punteggio } + ev.push({ tipo: 'inizio', t: 0, dur: 1.2, minuto: "1'" }) + ev.push(...tutti) + ev.push({ tipo: 'intervallo', t: T.fine1, dur: T.via2 - T.fine1, minuto: '45\'' }, { tipo: 'ripresa', t: T.via2, dur: 1, minuto: '46\'' }) + if (tavolino) { + const ts = secondoDelMinuto(12 + Math.floor(rand() * 70), 0.5) + const causa = meteo.tipo === 'nebbia' ? CAUSA_NEBBIA : CAUSE[Math.floor(rand() * CAUSE.length)] + const prima = ev.filter((e) => e.t < ts - 1.5 || e.tipo === 'inizio') + prima.push({ tipo: 'sospesa', t: Math.max(ts, 4), dur: T.fine - Math.max(ts, 4), minuto: minuto(ts).scritta, causa, aChi: tavolino }) + prima.sort((a, b) => a.t - b.t) + for (const e of prima) e.dur ??= durata(e) + return prima + } + ev.push({ tipo: 'fine', t: T.fine, dur: 10, minuto: 'FINE' }) + ev.sort((a, b) => a.t - b.t) + for (const e of ev) e.dur ??= durata(e) + return ev +} +/** Quanto dura la messa in scena di un evento, in secondi. */ +export function durata(e) { + switch (e.tipo) { + case 'gol': return e.rigore ? 5.2 : 4.4 + case 'rigore': return 4.2 + case 'giallo': return 2.6 + case 'rosso': return 3.4 + case 'infortunio': return 3.6 + case 'tiro': return 1.1 + case 'rimessa': return 0.9 + case 'fallo': return 0.9 + case 'scivola': return 1 + default: return 1 + } +} + +/** Il punteggio sul campo a t secondi (prima dell'eventuale sospensione). */ +export function punteggioA(p, t) { + let c = 0, o = 0 + for (const e of p.eventi) if (e.tipo === 'gol' && e.t <= t) { if (e.squadra === 'casa') c++; else o++ } + return { casa: c, ospite: o } +} + +/** Una puntata vince? (`id` di PUNTATE, sul risultato ufficiale, tavolino compreso). */ +export function vince(id, risultato) { + const { casa, ospite } = risultato, tot = casa + ospite + if (id === '1') return casa > ospite + if (id === 'X') return casa === ospite + if (id === '2') return casa < ospite + const s = Number(id.slice(1)) + return id[0] === 'O' ? tot > s : tot < s +} +/** Il risultato come si scrive: «2–1», «3–0 a tavolino». */ +export const scriviRisultato = (r) => `${r.casa}–${r.ospite}${r.tavolino ? ' a tavolino' : ''}` diff --git a/albertopecchini.it/pzkko/games/virtual/schermo.js b/albertopecchini.it/pzkko/games/virtual/schermo.js new file mode 100644 index 00000000..e10f635a --- /dev/null +++ b/albertopecchini.it/pzkko/games/virtual/schermo.js @@ -0,0 +1,689 @@ +// Lo schermino di «Virtual»: un piccolo schermo a pixel, pulito come un tamagotchi, su cui si vede la partita del +// campionato inventato (partita.js). È un solo canvas: la tela interna è 192 × 120 «pixel grossi» moltiplicati per un +// fattore intero (misura()), così i pixel restano netti a qualunque misura e su qualunque schermo. Tutto quello che si +// vede è una funzione del tempo (il secondo di partita, o quanto manca al fischio d'inizio): niente stato che si accumula, +// quindi due schede mostrano la stessa cosa, chi arriva a metà vede il punto giusto e una scheda nascosta riprende da dove +// il tempo è arrivato. +// +// Dentro: il campo da calcio a cinque in erba con le righe e le due porte, i dieci omini con le maglie delle loro squadre +// (il portiere di un altro colore), gli spalti coi tifosi pixel nei colori delle due squadre, le bandiere, le panchine; +// la telecamera segue la palla con un po' di inerzia e si avvicina sulle azioni (campo largo, mezzo campo, primo piano); +// per i rigori l'inquadratura da dietro la porta; il replay del gol al rallentatore. Il meteo (sereno, nuvole, pioggia con +// le pozzanghere, nebbia, neve con la palla arancione, vento) e la luce (di giorno, al tramonto, di notte coi riflettori) +// vengono dalla partita e sono solo scena. Le scritte sono in un carattere a pixel 3 × 5 disegnato qui. +// +// const s = new Schermo(canvas) +// s.misura(larghezzaCss, altezzaCss) // il fattore intero e la misura della tela +// s.disegna(partita, { fase, t, manca, ridotto }) // un fotogramma: fase 'scommesse' | 'partita' | 'finita', t secondi +// s.evento // dopo disegna(): l'evento in scena (per i suoni e la voce), o null + +import { T, minuto, punteggioA } from './partita.js' + +export const LARG = 192, ALT = 120 +// il campo nel mondo: 160 × 80 unità (un'unità = un pixel con la telecamera larga), centrato in (80, 40); fuori gli spalti +const CAMPO = { x0: 0, y0: 0, x1: 160, y1: 80, cx: 80, cy: 40, porta: 14 } // porta: mezza larghezza della porta +const TRIB = 20 // la profondità degli spalti + +// ── il carattere a pixel 3 × 5 ────────────────────────────────────────────── +const GLIFI = { + A: '010101111101101', B: '110101110101110', C: '011100100100011', D: '110101101101110', E: '111100110100111', F: '111100110100100', G: '011100101101011', + H: '101101111101101', I: '111010010010111', J: '001001001101010', K: '101101110101101', L: '100100100100111', M: '101111111101101', N: '110101101101101', + O: '010101101101010', P: '110101110100100', Q: '010101101111011', R: '110101110101101', S: '011100010001110', T: '111010010010010', U: '101101101101011', + V: '101101101101010', W: '101101111111101', X: '101101010101101', Y: '101101010010010', Z: '111001010100111', + 0: '111101101101111', 1: '010110010010111', 2: '110001010100111', 3: '110001010001110', 4: '101101111001001', 5: '111100110001110', 6: '011100110101010', 7: '111001010010010', 8: '010101010101010', 9: '010101011001110', + ' ': '000000000000000', '!': '010010010000010', "'": '010010000000000', '-': '000000111000000', '–': '000000111000000', ':': '000010000010000', '.': '000000000000010', ',': '000000000010100', '+': '000010111010000', '?': '110001010000010', '/': '001001010100100', '(': '010100100100010', ')': '010001001001010', '°': '010101010000000', '%': '101001010100101', 'È': '111100110100111', 'À': '010101111101101', 'Ù': '101101101101011', 'É': '111100110100111', 'Ì': '111010010010111', 'Ò': '010101101101010', +} +/** Quanto è larga una scritta, in pixel grossi (3 per lettera più uno di spazio). */ +export const largTesto = (s) => s.length * 4 - 1 + +// ── piccoli rumori riproducibili (dal numero della partita e da un indice) ── +const h32 = (a, b) => { let h = (Math.imul(a ^ 0x9e3779b9, 0x85ebca6b) ^ Math.imul(b + 0x165667b1, 0xc2b2ae35)) >>> 0; h ^= h >>> 15; h = Math.imul(h, 0x27d4eb2f) >>> 0; h ^= h >>> 13; return (h >>> 0) / 4294967296 } +const lerp = (a, b, u) => a + (b - a) * u +const liscia = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)) +const limita = (v, a, b) => Math.max(a, Math.min(b, v)) + +export class Schermo { + constructor(canvas) { + this.c = canvas + this.ctx = canvas.getContext('2d', { alpha: false }) + this.k = 2 + this.cam = { x: CAMPO.cx, y: CAMPO.cy, z: 1 } // la telecamera di adesso (si muove con inerzia verso la meta) + this.ultimo = 0 + this.evento = null + this.scena = null + } + + /** Il fattore intero della tela dalla misura CSS del contenitore: la tela è LARG × ALT pixel grossi per k, e il CSS la + * allarga a tutto il contenitore (image-rendering: pixelated): con k scelto vicino ai pixel veri dello schermo i + * pixel grossi restano netti e tutti uguali a meno di un pixel del display. */ + misura(w, h) { + const dpr = Math.max(1, Math.min(3, devicePixelRatio || 1)) + const k = Math.max(1, Math.round(Math.min((w * dpr) / LARG, (h * dpr) / ALT))) + if (k !== this.k || this.c.width !== LARG * k) { + this.k = k + this.c.width = LARG * k; this.c.height = ALT * k + } + } + + // ── le primitive: tutto in pixel grossi, interi ── + px(x, y, col) { this.ctx.fillStyle = col; this.ctx.fillRect(Math.round(x) * this.k, Math.round(y) * this.k, this.k, this.k) } + rett(x, y, w, h, col) { const k = this.k, x0 = Math.round(x), y0 = Math.round(y), x1 = Math.round(x + w), y1 = Math.round(y + h); if (x1 <= x0 || y1 <= y0) return; this.ctx.fillStyle = col; this.ctx.fillRect(x0 * k, y0 * k, (x1 - x0) * k, (y1 - y0) * k) } + testo(s, x, y, col, { centro = false, ombra = null } = {}) { + s = String(s).toUpperCase() + if (centro) x -= Math.floor(largTesto(s) / 2) + if (ombra) this.testo(s, x + 1, y + 1, ombra) + for (const ch of s) { + const g = GLIFI[ch] ?? GLIFI['?'] + for (let i = 0; i < 15; i++) if (g[i] === '1') this.px(x + (i % 3), y + Math.floor(i / 3), col) + x += 4 + } + } + /** Una targhetta col testo: fondo scuro, testo chiaro, in basso o dove si dice. */ + targa(s, y, { fondo = '#161616', col = '#ffffff', x = LARG / 2 } = {}) { + const w = largTesto(s) + 6 + this.rett(x - Math.floor(w / 2), y - 2, w, 9, fondo) + this.testo(s, x, y, col, { centro: true }) + } + + // il mondo → lo schermo + sx(wx) { return LARG / 2 + (wx - this.cam.x) * this.cam.z } + sy(wy) { return ALT / 2 + (wy - this.cam.y) * this.cam.z } + rw(x, y, w, h, col) { const z = this.cam.z; this.rett(this.sx(x), this.sy(y), w * z, h * z, col) } + + // ── il fotogramma ── + /** + * Disegna la partita `p` com'è adesso. stato: { fase: 'scommesse' | 'partita' | 'finita', t: secondi di partita (anche + * negativi prima del fischio: −240…0), ridotto: movimento ridotto (telecamera ferma, solo scritte), dt: secondi dal + * fotogramma prima }. + */ + disegna(p, { fase, t, ridotto = false, dt = 1 / 60 }) { + this.p = p + this.t = t + this.rid = ridotto + const scena = fase === 'partita' ? this.scenaPartita(p, t) : fase === 'finita' ? this.scenaFinale(p) : this.scenaAttesa(p, t) + this.scena = scena + this.evento = scena.evento ?? null + // la telecamera: con inerzia verso la meta (ferma e larga col movimento ridotto) + const meta = ridotto ? { x: CAMPO.cx, y: CAMPO.cy, z: 1 } : scena.cam + const a = ridotto || scena.subito ? 1 : 1 - Math.exp(-dt * (scena.lesto ? 9 : 4.5)) + this.cam.x = lerp(this.cam.x, meta.x, a); this.cam.y = lerp(this.cam.y, meta.y, a); this.cam.z = lerp(this.cam.z, meta.z, a) + if (scena.scossa && !ridotto) { this.cam.x += (h32(p.indice, Math.floor(t * 30)) - 0.5) * scena.scossa; this.cam.y += (h32(p.indice + 7, Math.floor(t * 30)) - 0.5) * scena.scossa } + this.ctx.imageSmoothingEnabled = false + if (scena.dietroPorta && !ridotto) this.disegnaDietroPorta(p, scena) + else this.disegnaMondo(p, scena) + this.disegnaMeteoDavanti(p, scena) + for (const s of scena.scritte ?? []) this.targa(s.t, s.y, s) + if (scena.pannello) this.pannello(p, scena.pannello) + } + + // ── il mondo visto dall'alto ── + disegnaMondo(p, scena) { + const m = p.meteo, notte = m.notte, sera = m.sera + const L = this.luce(p) + // il fondo: il cemento degli spalti; di notte quasi buio + this.rett(0, 0, LARG, ALT, L.cemento) + this.spalti(p, scena, L) + this.panchine(p, scena, L) + // il prato a strisce + const z = this.cam.z + for (let i = 0; i < 8; i++) this.rw(CAMPO.x0 + i * 20, CAMPO.y0, 20, 80, i % 2 ? L.erba1 : L.erba2) + if (m.tipo === 'neve') for (let i = 0; i < 40; i++) { const r = h32(p.indice, 300 + i); this.rw(CAMPO.x0 + r * 156, CAMPO.y0 + h32(p.indice, 400 + i) * 77, 2 + r * 5, 1 + r * 2, L.neve) } + if (m.tipo === 'nuvole' || m.tipo === 'pioggia') this.ombreNuvole(p, L) + // le righe + const R = L.righe + this.rw(CAMPO.x0, CAMPO.y0, 160, 1, R); this.rw(CAMPO.x0, CAMPO.y1 - 1, 160, 1, R); this.rw(CAMPO.x0, CAMPO.y0, 1, 80, R); this.rw(CAMPO.x1 - 1, CAMPO.y0, 1, 80, R) + this.rw(CAMPO.cx, CAMPO.y0, 1, 80, R) + this.cerchio(CAMPO.cx, CAMPO.cy, 12, R) + for (const lato of [-1, 1]) { + const x = lato < 0 ? CAMPO.x0 : CAMPO.x1 + // l'area: un arco di 20 davanti alla porta + this.arco(x, CAMPO.cy, 20, lato, R) + this.rw(lato < 0 ? CAMPO.x0 + 24 : CAMPO.x1 - 25, CAMPO.cy, 1, 1, R) // il dischetto + } + if (m.tipo === 'pioggia') this.pozzanghere(p, L) + // le porte (viste dall'alto: la rete dietro la linea) + for (const lato of [-1, 1]) { + const x = lato < 0 ? CAMPO.x0 - 4 : CAMPO.x1 + const gonfia = scena.rete === lato ? 2 : 0 + this.rw(x - (lato > 0 ? 0 : gonfia), CAMPO.cy - CAMPO.porta, 4 + gonfia, CAMPO.porta * 2, L.rete) + this.rw(lato < 0 ? CAMPO.x0 - 1 : CAMPO.x1, CAMPO.cy - CAMPO.porta, 1, 1, '#ffffff'); this.rw(lato < 0 ? CAMPO.x0 - 1 : CAMPO.x1, CAMPO.cy + CAMPO.porta - 1, 1, 1, '#ffffff') + } + // gli omini, dal più in alto al più in basso (chi sta più giù copre) + const omini = [...scena.omini].sort((a, b) => a.y - b.y) + for (const o of omini) this.omino(o, L) + if (scena.barella) this.barella(scena.barella, L) + if (scena.arbitro) this.omino(scena.arbitro, L) + if (scena.cartellino) this.cartellino(scena.cartellino) + if (scena.palla) this.palla(scena.palla, L, p) + if (scena.palla2) this.palla(scena.palla2, L, p) + if (scena.nutrie) for (const n of scena.nutrie) this.nutria(n, L) + // i riflettori, di notte: quattro torri agli angoli e la luce che cade sul campo + if (notte || sera) this.riflettori(p, L, notte) + if (scena.buio) this.rett(0, 0, LARG, ALT, `rgba(4,6,14,${scena.buio})`) + } + + luce(p) { + const m = p.meteo + const notte = m.notte, sera = m.sera, grigio = m.tipo === 'nuvole' || m.tipo === 'pioggia' || m.tipo === 'nebbia' + if (notte) return { cemento: '#141724', erba1: '#2c7a3c', erba2: '#266c34', righe: '#e2eedb', rete: '#5b6473', neve: '#d9dfe8', tifosi: 0.55, pelle: '#c48f68', ombra: 'rgba(0,0,0,.35)', cielo: '#0c1020', gradoni: '#232738', neutro: '#3a3d48', pelle2: '#8a6a4a' } + if (sera) return { cemento: '#6a5850', erba1: '#4f9a3f', erba2: '#459038', righe: '#f4ecd8', rete: '#9a8e86', neve: '#f0e8e0', tifosi: 0.85, pelle: '#d9a37a', ombra: 'rgba(0,0,0,.25)', cielo: '#d87a3c', gradoni: '#5c4c46', neutro: '#6a6358', pelle2: '#a87a56' } + if (grigio) return { cemento: '#7c7f84', erba1: '#4e9a44', erba2: '#46903d', righe: '#eef2ec', rete: '#a9adb2', neve: '#eef1f4', tifosi: 0.9, pelle: '#d9a37a', ombra: 'rgba(0,0,0,.18)', cielo: '#9aa3ad', gradoni: '#6a6d72', neutro: '#8e9196', pelle2: '#a87a56' } + if (m.tipo === 'neve') return { cemento: '#9aa0a8', erba1: '#8fb58c', erba2: '#84ad81', righe: '#ffffff', rete: '#c2c7cf', neve: '#f7f9fb', tifosi: 0.9, pelle: '#d9a37a', ombra: 'rgba(0,0,0,.15)', cielo: '#c9d0d8', gradoni: '#8a9098', neutro: '#aab0b8', pelle2: '#a87a56' } + return { cemento: '#8b8f93', erba1: '#5bb24a', erba2: '#4fa842', righe: '#f6fbf3', rete: '#b9bfc5', neve: '#ffffff', tifosi: 1, pelle: '#d9a37a', ombra: 'rgba(0,0,0,.22)', cielo: '#7fc4ea', gradoni: '#767a7f', neutro: '#9a9ea3', pelle2: '#a87a56' } + } + + cerchio(cx, cy, r, col) { + const z = this.cam.z, n = Math.max(16, Math.floor(r * z * 1.2)) + for (let i = 0; i < n; i++) { const a = (i / n) * Math.PI * 2; this.rett(this.sx(cx + Math.cos(a) * r), this.sy(cy + Math.sin(a) * r), 1, 1, col) } + } + arco(x, cy, r, lato, col) { + const z = this.cam.z, n = Math.max(10, Math.floor(r * z)) + for (let i = 0; i <= n; i++) { const a = -Math.PI / 2 + (i / n) * Math.PI; this.rett(this.sx(x + Math.cos(a) * r * (lato < 0 ? 1 : -1)), this.sy(cy + Math.sin(a) * r), 1, 1, col) } + } + + // ── gli spalti: le file di tifosi coi colori delle due squadre, che si alzano al gol ── + spalti(p, scena, L) { + const { indice } = p, piena = scena.folla ?? 1, salto = scena.salto // salto: { squadra, u } i tifosi di quella squadra saltano + const colori = (sq) => p.maglie[sq].colori + // chi sta dove: la curva di casa in alto e a sinistra, gli ospiti in basso a destra (un quarto); in basso a sinistra + // misti, e in alto a destra ancora casa + const zone = [ + { x: CAMPO.x0 - 6, y: CAMPO.y0 - TRIB - 2, w: 172, h: TRIB, sq: 'casa', file: 5 }, // tribuna alta + { x: CAMPO.x0 - 6, y: CAMPO.y1 + 2, w: 92, h: TRIB, sq: 'misti', file: 5 }, // bassa, sinistra + { x: CAMPO.x0 + 88, y: CAMPO.y1 + 2, w: 78, h: TRIB, sq: 'ospite', file: 5 }, // bassa, destra: gli ospiti + { x: CAMPO.x0 - TRIB - 8, y: CAMPO.y0 - 2, w: TRIB, h: 84, sq: 'casa', file: 5, vert: true }, + { x: CAMPO.x1 + 8, y: CAMPO.y0 - 2, w: TRIB, h: 84, sq: 'casa', file: 5, vert: true }, + ] + for (const zo of zone) { + // i gradoni + const sx = this.sx(zo.x), sy = this.sy(zo.y), z = this.cam.z + if (sx > LARG || sy > ALT || sx + zo.w * z < 0 || sy + zo.h * z < 0) continue + this.rw(zo.x, zo.y, zo.w, zo.h, L.gradoni) + // le file: i tifosi a 2 × 2 con la testa sopra, ogni fila sfalsata di un pixel, qualche posto vuoto e qualche + // tifoso «neutro» (il cappotto grigio) così non è un tappeto a righe + const passo = 3 + const n1 = Math.floor((zo.vert ? zo.h : zo.w) / passo), n2 = Math.floor((zo.vert ? zo.w : zo.h) / 4) + for (let f = 0; f < n2; f++) for (let i = 0; i < n1; i++) { + const r = h32(indice + f * 977, i + 31 * zone.indexOf(zo)) + if (r > piena * 0.92) continue // il posto è vuoto (o non ancora preso) + const sq = zo.sq === 'misti' ? (r < 0.5 ? 'casa' : 'ospite') : zo.sq + const cc = colori(sq) + const r2 = (r * 13) % 1 + const col = r2 < 0.14 ? L.neutro : r2 < 0.65 ? cc[0] : cc[1] ?? cc[0] + const sf = f % 2 // la fila sfalsata + let x = zo.vert ? zo.x + f * 4 : zo.x + i * passo + sf, y = zo.vert ? zo.y + i * passo + sf : zo.y + f * 4 + 1 + // chi salta + if (salto && salto.squadra === sq && Math.sin(salto.u * 40 + r * 9) > 0.2) y -= 1 + // l'onda del coro: ogni tanto una fila si alza a onda + if (scena.onda != null && Math.abs((zo.vert ? y : x) - scena.onda) < 6) y -= 1 + this.rw(x, y, 2, 2, this.scuro(col, L.tifosi)) + this.rw(x, y - 1, 2, 1, (r * 29) % 1 < 0.5 ? L.pelle : L.pelle2) + } + } + // le bandiere degli sbandieratori: tre di casa e una ospite, che ondeggiano col vento + const vento = p.meteo.tipo === 'vento' ? 2.2 : 0.8 + const bandiere = [{ x: 20, y: CAMPO.y0 - TRIB + 2, sq: 'casa' }, { x: 120, y: CAMPO.y0 - TRIB + 2, sq: 'casa' }, { x: 130, y: CAMPO.y1 + TRIB - 2, sq: 'ospite' }, { x: 60, y: CAMPO.y1 + TRIB - 2, sq: 'casa' }] + for (const [i, b] of bandiere.entries()) { + if (h32(indice, 900 + i) > piena + 0.2) continue + const cc = colori(b.sq), ph = this.t * (3 + vento) + i + const dx = Math.round(Math.sin(ph) * vento) + this.rw(b.x, b.y - 5, 1, 6, '#e8e4dc') + this.rw(b.x + 1 + dx * 0.3, b.y - 5, 4, 2, cc[0]); this.rw(b.x + 1 + dx * 0.6, b.y - 3, 4, 2, cc[1] ?? cc[0]) + } + } + scuro(col, f) { + if (f >= 1) return col + const [r, g, b] = [1, 3, 5].map((i) => Math.round(parseInt(col.slice(i, i + 2), 16) * f)) + return `rgb(${r},${g},${b})` + } + + panchine(p, scena, L) { + // due panchine sotto la tribuna alta, ai lati del centro: la tettoia e le riserve sedute + for (const [i, sq] of ['casa', 'ospite'].entries()) { + const x = i === 0 ? CAMPO.cx - 34 : CAMPO.cx + 14 + this.rw(x, CAMPO.y0 - 8, 20, 5, '#3a3d44') + const seduti = scena.inPanchina?.[sq] ?? 2 + for (let j = 0; j < seduti; j++) this.omino({ x: x + 4 + j * 6, y: CAMPO.y0 - 5, sq, seduto: true, maglia: p.maglie[sq], fermo: true }, L) + } + } + + // ── un omino: 3 × 6 pixel grossi al mondo (testa, maglia a due righe, pantaloncini, gambe) ── + omino(o, L) { + const z = this.cam.z, X = this.sx(o.x), Y = this.sy(o.y) + const m = o.maglia, c1 = o.portiere ? o.portiere : m.colori[0], c2 = o.portiere ? o.portiere : m.colori[1] ?? m.colori[0] + const w = Math.max(1, Math.round(z * 3)), u = Math.max(1, Math.round(z)) // un pixel grosso del mondo + const x0 = Math.round(X - w / 2) + if (o.aTerra) { + // steso: lungo, la testa da un lato + const y0 = Math.round(Y - u) + this.rett(x0 - u, y0, u, u, L.pelle) + this.rett(x0, y0, w, u, c1); this.rett(x0 + w, y0, u, u, c2) + this.rett(x0 + w + u, y0, u * 2, u, '#2b2b2b') + return + } + const alto = o.seduto ? 4 : 6 + const y0 = Math.round(Y - alto * u) + this.rett(Math.round(X - u / 2), y0, u, u, L.pelle) // la testa + // la maglia: due righe, col disegno + const maglia = (yy) => { + if (o.portiere) { this.rett(x0, yy, w, u, c1); return } + const d = m.disegno + if (d === 'strisce') { for (let i = 0; i < w; i += u) this.rett(x0 + i, yy, u, u, Math.floor(i / u) % 2 ? c2 : c1) } + else if (d === 'meta') { this.rett(x0, yy, Math.ceil(w / 2), u, c1); this.rett(x0 + Math.ceil(w / 2), yy, Math.floor(w / 2), u, c2) } + else if (d === 'fascia') { this.rett(x0, yy, w, u, yy === y0 + u ? c1 : c2) } + else if (d === 'croce') { this.rett(x0, yy, w, u, c1); this.rett(x0 + Math.floor(w / 2 / u) * u, yy, u, u, c2); if (yy === y0 + 2 * u) this.rett(x0, yy, w, u, c2) } + else { this.rett(x0, yy, w, u, c1); if (yy === y0 + u && w >= 3 * u) this.rett(x0, yy, u, u, c2) } + } + maglia(y0 + u); maglia(y0 + 2 * u) + if (o.seduto) { this.rett(x0, y0 + 3 * u, w, u, '#2b2b2b'); return } + this.rett(x0, y0 + 3 * u, w, u, o.portiere ? '#2b2b2b' : m.colori[1] === '#ffffff' && m.colori[0] !== '#ffffff' ? '#ffffff' : '#2b2b2b') // i pantaloncini + // le gambe: due pixel che si alternano correndo + const passo = o.fermo ? 0 : Math.floor(this.t * 10 + (o.i ?? 0)) % 2 + this.rett(x0 + (passo ? u : 0), y0 + 4 * u, u, u, L.pelle); this.rett(x0 + w - u - (passo ? u : 0), y0 + 4 * u, u, u, L.pelle) + this.rett(x0 + (passo ? 0 : u) + 0, y0 + 5 * u, u, u, '#161616'); this.rett(x0 + w - u - (passo ? 0 : u), y0 + 5 * u, u, u, '#161616') + if (o.numero != null && z >= 2.4) this.testo(String(o.numero), Math.round(X) - (o.numero >= 10 ? 3 : 1), y0 - 7, '#ffffff', { ombra: '#161616' }) + } + palla(b, L, p) { + const z = this.cam.z, s = Math.max(1, Math.round(z * 0.9)) + const col = p.meteo.tipo === 'neve' ? '#ff7a1a' : '#ffffff' + const alto = b.h ?? 0 // in aria: l'ombra resta sotto + if (alto > 0) this.rett(this.sx(b.x) - s / 2, this.sy(b.y) - s / 2, s, s, L.ombra) + this.rett(this.sx(b.x) - s / 2, this.sy(b.y - alto) - s / 2, s, s, col) + if (s >= 3) this.rett(this.sx(b.x) - s / 2 + 1, this.sy(b.y - alto) - s / 2 + 1, 1, 1, '#161616') + } + barella(b, L) { + // due barellieri in giallo che portano via l'infortunato steso + const z = this.cam.z, u = Math.max(1, Math.round(z)) + const X = this.sx(b.x), Y = this.sy(b.y) + this.rett(X - 4 * u, Y - u, 8 * u, u, '#d0cfc8') + this.rett(X - 3 * u, Y - 2 * u, 6 * u, u, b.maglia.colori[0]) + this.rett(X - 4 * u, Y - 2 * u, u, u, L.pelle) + for (const dx of [-5, 5]) { this.rett(X + dx * u - u / 2, Y - 5 * u, u, u, L.pelle); this.rett(X + dx * u - u, Y - 4 * u, 2 * u, 2 * u, '#f2c230'); this.rett(X + dx * u - u, Y - 2 * u, u, 2 * u, '#161616'); this.rett(X + dx * u, Y - 2 * u, u, 2 * u, '#161616') } + } + cartellino(c) { + // il primo piano dell'arbitro che alza il cartellino: la mano e il rettangolo + this.rett(c.x - 1, c.y - 8, 2, 4, '#161616') + this.rett(c.x - 3, c.y - 16, 6, 9, c.rosso ? '#e0202a' : '#ffd400') + this.rett(c.x - 2, c.y - 15, 4, 7, c.rosso ? '#f04a52' : '#ffe566') + } + nutria(n, L) { + // la nutria: una salsiccia marrone con la coda, che corre sul prato + const z = this.cam.z, u = Math.max(1, Math.round(z)) + const X = this.sx(n.x), Y = this.sy(n.y) + this.rett(X - 2 * u, Y - u, 4 * u, u, '#6b4a2b'); this.rett(X + 2 * u * n.dir, Y - u, u, u, '#4a3220'); this.rett(X - 2 * u * n.dir, Y - u, 2 * u, u * 0.5, '#4a3220') + } + + pozzanghere(p, L) { + for (let i = 0; i < 6; i++) { const r = h32(p.indice, 500 + i); this.rw(CAMPO.x0 + 8 + r * 140, CAMPO.y0 + 6 + h32(p.indice, 600 + i) * 66, 5 + r * 6, 2 + r * 2, L.cielo === '#0c1020' ? '#3a4a6a' : '#9db6c8') } + } + ombreNuvole(p, L) { + // tre ombre di nuvola che scorrono piano sul prato + const t = this.t + 300 + for (let i = 0; i < 3; i++) { const x = ((t * (2 + i) + i * 70) % 240) - 40, y = 10 + i * 25; this.rw(x, y, 36, 14, 'rgba(0,30,0,.14)') } + } + riflettori(p, L, notte) { + // le torri e il bagliore sul campo + const torri = [[CAMPO.x0 - 10, CAMPO.y0 - 12], [CAMPO.x1 + 10, CAMPO.y0 - 12], [CAMPO.x0 - 10, CAMPO.y1 + 12], [CAMPO.x1 + 10, CAMPO.y1 + 12]] + for (const [x, y] of torri) { this.rw(x - 1, y - 1, 3, 3, '#fff6c8'); this.rw(x, y, 1, 1, '#ffffff') } + if (notte) { + // la luce: più chiara al centro del campo, buia fuori (la vignetta della notte) + this.ctx.save() + const k = this.k, g = this.ctx.createRadialGradient(this.sx(CAMPO.cx) * k, this.sy(CAMPO.cy) * k, 30 * k * this.cam.z, this.sx(CAMPO.cx) * k, this.sy(CAMPO.cy) * k, 100 * k * this.cam.z) + g.addColorStop(0, 'rgba(255,245,200,.14)'); g.addColorStop(0.45, 'rgba(0,0,0,0)'); g.addColorStop(1, 'rgba(0,0,16,.7)') + this.ctx.fillStyle = g; this.ctx.fillRect(0, 0, LARG * k, ALT * k) + this.ctx.restore() + } + } + + // ── il meteo davanti a tutto: pioggia, neve, nebbia, vento ── + disegnaMeteoDavanti(p, scena) { + const m = p.meteo, t = this.t + 1000, vento = m.tipo === 'vento' ? 1.6 : 0.3 + if (m.tipo === 'pioggia') { + for (let i = 0; i < 46; i++) { const r = h32(p.indice, 700 + i), x = (r * LARG + t * 60 * vento * 0.4) % LARG, y = (h32(p.indice, 800 + i) * ALT + t * 140 * (0.7 + r * 0.6)) % ALT; this.rett(x, y, 1, 3, 'rgba(210,225,240,.55)') } + } else if (m.tipo === 'neve') { + for (let i = 0; i < 40; i++) { const r = h32(p.indice, 700 + i), x = (r * LARG + Math.sin(t * 1.3 + i) * 6 + t * 10 * vento) % LARG, y = (h32(p.indice, 800 + i) * ALT + t * (12 + r * 14)) % ALT; this.rett((x + LARG) % LARG, y, 1, 1, '#ffffff') } + } else if (m.tipo === 'vento') { + // foglie e carte che volano + for (let i = 0; i < 7; i++) { const r = h32(p.indice, 700 + i), x = (r * LARG + t * (40 + r * 40)) % LARG, y = (h32(p.indice, 800 + i) * ALT + Math.sin(t * 3 + i) * 5 + ALT) % ALT; this.rett(x, y, 2, 1, i % 2 ? '#c9a94f' : '#e8e4dc') } + } + if (m.tipo === 'nebbia' || scena.nebbia) { + // la nebbia a blocchi: più fitta lontano dal centro dello schermo, e che respira piano + const forza = scena.nebbia ?? (0.45 + 0.35 * m.forza) + const B = 8 + for (let y = 0; y < ALT; y += B) for (let x = 0; x < LARG; x += B) { + const d = Math.hypot(x + B / 2 - LARG / 2, (y + B / 2 - ALT / 2) * 1.4) / 100 + const a = limita(forza * (0.45 + d) + 0.05 * Math.sin(t * 0.7 + x * 0.05 + y * 0.09), 0, 0.92) + this.rett(x, y, B, B, `rgba(214,218,222,${a.toFixed(2)})`) + } + } + } + + // ── la vista da dietro la porta, per i rigori ── + disegnaDietroPorta(p, s) { + const L = this.luce(p), u = s.u // 0..1 dentro il rigore + this.rett(0, 0, LARG, ALT, L.cielo) + if (p.meteo.notte) for (let i = 0; i < 12; i++) this.px(h32(p.indice, 1000 + i) * LARG, h32(p.indice, 1100 + i) * 30, '#aab') + // gli spalti di fronte + this.rett(0, 28, LARG, 22, L.cemento) + const cc = p.maglie[s.squadra === 'casa' ? 'ospite' : 'casa'].colori + for (let f = 0; f < 5; f++) for (let i = 0; i < 48; i++) { const r = h32(p.indice + f, i); this.rett(i * 4 + 1, 30 + f * 4 + (r > 0.5 ? 1 : 0), 1, 2, this.scuro(r < 0.6 ? cc[0] : cc[1] ?? cc[0], L.tifosi)); this.rett(i * 4 + 1, 29 + f * 4 + (r > 0.5 ? 1 : 0), 1, 1, L.pelle) } + // il prato in prospettiva + this.rett(0, 50, LARG, ALT - 50, L.erba1) + for (let y = 50; y < ALT; y += 10) this.rett(0, y, LARG, 5, L.erba2) + this.rett(0, 50, LARG, 1, L.righe) + // la porta: i pali e la rete (visti da dietro) + const gx = 36, gy = 22, gw = 120, gh = 44 + for (let y = gy; y < gy + gh; y += 3) this.rett(gx, y, gw, 1, 'rgba(255,255,255,.35)') + for (let x = gx; x < gx + gw; x += 3) this.rett(x, gy, 1, gh, 'rgba(255,255,255,.35)') + this.rett(gx - 2, gy - 2, gw + 4, 2, '#ffffff'); this.rett(gx - 2, gy, 2, gh + 2, '#ffffff'); this.rett(gx + gw, gy, 2, gh + 2, '#ffffff') + // il portiere: grande, al centro, che si tuffa da un lato dopo il tiro + const sq = s.squadra === 'casa' ? 'ospite' : 'casa' + const tuffo = u > 0.62 ? limita((u - 0.62) / 0.12, 0, 1) : 0 + const gkX = LARG / 2 + s.tuffo * tuffo * 34, gkY = 66 + tuffo * 6 + const pc = p.portieri[sq] + this.rett(gkX - 3, gkY - 22, 6, 6, L.pelle); this.rett(gkX - 7 - tuffo * 6, gkY - 16, 14 + tuffo * 12, 9, pc); this.rett(gkX - 6, gkY - 7, 12, 4, '#2b2b2b'); this.rett(gkX - 5, gkY - 3, 4, 3, L.pelle); this.rett(gkX + 1, gkY - 3, 4, 3, L.pelle) + if (!tuffo) { this.rett(gkX - 13, gkY - 15, 6, 3, pc); this.rett(gkX + 7, gkY - 15, 6, 3, pc) } + // il tiratore di spalle, in fondo, e la palla sul dischetto che poi vola verso di noi (cresce) o in porta + const corsa = limita((u - 0.3) / 0.3, 0, 1) + const tx = LARG / 2 - 20 + corsa * 18, ty = ALT - 8 - corsa * 4 + if (u < 0.6) { + // il tiratore di spalle: testa, maglia coi colori, pantaloncini, gambe (grande: è vicino a noi) + const mg = p.maglie[s.squadra].colori + this.rett(tx - 3, ty - 24, 6, 6, L.pelle); this.rett(tx - 7, ty - 18, 14, 10, mg[0]); this.rett(tx - 7, ty - 14, 14, 2, mg[1] ?? mg[0]); this.rett(tx - 6, ty - 8, 12, 4, '#2b2b2b') + const passo = corsa > 0 ? Math.floor(this.t * 12) % 2 : 0 + this.rett(tx - 5, ty - 4 - passo * 2, 4, 4 + passo * 2, L.pelle); this.rett(tx + 1, ty - 4, 4, 4, L.pelle) + } + let bx = LARG / 2, by = ALT - 14, bs = 4 + if (u >= 0.6) { + const v = limita((u - 0.6) / 0.14, 0, 1) + const destX = s.esito === 'fuori' ? LARG / 2 + s.tuffo * -1 * 76 : LARG / 2 + s.tuffo * -1 * 30 // il portiere va dal lato sbagliato, tranne se para + const paraX = LARG / 2 + s.tuffo * 32 + bx = lerp(LARG / 2, s.esito === 'parato' ? paraX : destX, v); by = lerp(ALT - 14, s.esito === 'fuori' ? gy - 8 : gy + 14, v); bs = lerp(4, 2, v) + if (s.esito === 'gol' && v >= 1) { this.rett(gx + (s.tuffo < 0 ? gw - 30 : 6), gy + 6, 24, 20, 'rgba(255,255,255,.25)') } + } + this.rett(bx - bs / 2, by - bs / 2, bs, bs, p.meteo.tipo === 'neve' ? '#ff7a1a' : '#ffffff') + if (bs >= 3) this.rett(bx - bs / 2 + 1, by - bs / 2 + 1, 1, 1, '#161616') + } + + // ── il pannello della presentazione (prima della partita) ── + pannello(p, q) { + // un cartello scuro con i due stemmi a pixel (le forme semplificate), i nomi, la forza e le ultime cinque + const y0 = 14 + this.rett(14, y0, LARG - 28, 66, 'rgba(22,22,22,.86)') + this.rett(14, y0, LARG - 28, 1, '#ffd400') + const col = (sq, i) => p.maglie[sq].colori[i] ?? p.maglie[sq].colori[0] + for (const [i, sq] of ['casa', 'ospite'].entries()) { + const x = i === 0 ? 24 : LARG - 24 - 18 + // lo stemmino: la forma dello stemma col fondo e la sigla + this.stemmino(p[sq], x, y0 + 8) + this.testo(p[sq].sigla, x + 9, y0 + 30, '#ffffff', { centro: true }) + // la forza come barra + this.rett(x - 2, y0 + 38, 22, 3, '#3a3a3a'); this.rett(x - 2, y0 + 38, Math.round((22 * p.forze[sq]) / 100), 3, i === 0 ? col('casa', 0) : col('ospite', 0)) + this.testo(`${p.forze[sq]}`, x + 9, y0 + 43, '#b8b8b8', { centro: true }) + // le ultime cinque: V N P (inventate: non contano) + for (let j = 0; j < 5; j++) { const u = p.ultime[sq][j]; this.rett(x - 2 + j * 5, y0 + 52, 4, 4, u === 'V' ? '#1a8a3a' : u === 'N' ? '#8a8a8a' : '#b3121f'); this.testo(u, x - 2 + j * 5 + 1, y0 + 52, '#ffffff') } + } + this.testo('VS', LARG / 2, y0 + 18, '#ffd400', { centro: true }) + if (q.riga) this.testo(q.riga, LARG / 2, y0 + 30, '#ffffff', { centro: true }) + if (q.riga2) this.testo(q.riga2, LARG / 2, y0 + 40, '#b8b8b8', { centro: true }) + if (q.riga3) this.testo(q.riga3, LARG / 2, y0 + 52, '#b8b8b8', { centro: true }) + } + stemmino(sq, x, y) { + const { fondo, tinta, forma } = sq.stemma + const bordo = fondo === '#ffffff' ? tinta : '#161616' + if (forma === 'tondo' || forma === 'ovale') { this.rett(x + 3, y, 12, 18, fondo); this.rett(x, y + 3, 18, 12, fondo); this.rett(x + 3, y, 12, 1, bordo); this.rett(x + 3, y + 17, 12, 1, bordo); this.rett(x, y + 3, 1, 12, bordo); this.rett(x + 17, y + 3, 1, 12, bordo) } + else if (forma === 'rombo') { for (let i = 0; i < 9; i++) { this.rett(x + 9 - i, y + i, 2 * i, 1, fondo); this.rett(x + 9 - i, y + 17 - i, 2 * i, 1, fondo) } } + else { this.rett(x, y, 18, 11, fondo); for (let i = 0; i < 7; i++) this.rett(x + i, y + 11 + i, 18 - 2 * i, 1, fondo); this.rett(x, y, 18, 1, bordo) } + this.rett(x + 7, y + 5, 4, 6, tinta); this.rett(x + 5, y + 7, 8, 2, tinta) + } + + // ════════════════════════════════════════════════════════════════════════════ + // LE SCENE: da (partita, tempo) a cosa c'è in campo. Tutto deterministico. + // ════════════════════════════════════════════════════════════════════════════ + + /** Da che parte attacca una squadra a t secondi (si cambia campo nella ripresa): +1 verso destra. */ + verso(squadra, t) { const destra = squadra === 'casa'; return (destra !== t >= T.via2) ? 1 : -1 } + portaDi(squadra, t) { return this.verso(squadra, t) > 0 ? CAMPO.x1 : CAMPO.x0 } // la porta che la squadra attacca + + /** La palla del gioco «normale»: tappe ogni 1,3 s pescate dal seme, raccordate con morbidezza. */ + pallaBase(p, t) { + const passo = 1.3, k = Math.floor(t / passo), u = liscia((t - k * passo) / passo) + const tappa = (j) => ({ x: 14 + h32(p.indice * 3 + 1, j) * 132, y: 8 + h32(p.indice * 3 + 2, j) * 64 }) + const a = tappa(k), b = tappa(k + 1) + return { x: lerp(a.x, b.x, u), y: lerp(a.y, b.y, u) } + } + /** I dieci omini attorno alla palla: il portiere sulla sua linea, i quattro in formazione che seguono la palla. */ + omini(p, t, palla, { fuori = {}, sostituiti = {}, corrono = true } = {}) { + const out = [] + for (const sq of ['casa', 'ospite']) { + const v = this.verso(sq, t), f = p.formazioni[sq], maglia = p.maglie[sq] + const px = v > 0 ? CAMPO.x0 + 5 : CAMPO.x1 - 6 // la propria porta + out.push({ x: px, y: limita(CAMPO.cy + (palla.y - CAMPO.cy) * 0.4, CAMPO.cy - 10, CAMPO.cy + 10), sq, maglia, portiere: p.portieri[sq], numero: f[0].n, i: 0, fermo: !corrono }) + const base = [[-22, -18], [-22, 18], [-6, -6], [8, 8]] // rispetto al centro della squadra, verso la porta avversaria + for (let j = 1; j <= 4; j++) { + if (fuori[sq]?.has?.(j)) continue + const [bx, by] = base[j - 1] + const cx = lerp(CAMPO.cx - v * 14, palla.x, 0.55), cy = lerp(CAMPO.cy, palla.y, 0.5) + const jitter = Math.sin(t * 2.1 + j * 1.7 + (sq === 'casa' ? 0 : 2)) * 2 + const chi = sostituiti[sq]?.[j] ?? f[j] + out.push({ x: limita(cx + v * bx + jitter, CAMPO.x0 + 3, CAMPO.x1 - 4), y: limita(cy + by + jitter * 0.5, CAMPO.y0 + 4, CAMPO.y1 - 2), sq, maglia, numero: chi.n, i: j + (sq === 'casa' ? 0 : 5), fermo: !corrono }) + } + } + return out + } + // chi è fuori a t secondi (espulso) e chi è entrato al posto di chi + statoSquadre(p, t) { + const fuori = { casa: new Set(), ospite: new Set() }, sostituiti = { casa: {}, ospite: {} }, inPanchina = { casa: 2, ospite: 2 } + for (const e of p.eventi) { + if (e.t + (e.dur ?? 1) > t) continue + if (e.tipo === 'rosso') fuori[e.squadra].add(e.chi.i) + if (e.tipo === 'infortunio') { sostituiti[e.squadra][e.chi.i] = e.entra; inPanchina[e.squadra] = 1 } + } + return { fuori, sostituiti, inPanchina } + } + cartelliniA(p, t) { + const g = { casa: 0, ospite: 0 }, r = { casa: 0, ospite: 0 } + for (const e of p.eventi) if (e.t <= t) { if (e.tipo === 'giallo') g[e.squadra]++; if (e.tipo === 'rosso') r[e.squadra]++ } + return { gialli: g, rossi: r } + } + + /** La scena a t secondi di partita. */ + scenaPartita(p, t) { + const S = this.statoSquadre(p, t) + // l'evento in corso: quello il cui [t, t + dur] contiene t + const e = p.eventi.find((x) => t >= x.t && t < x.t + (x.dur ?? 1) && x.tipo !== 'inizio') ?? (t < 1.2 ? p.eventi[0] : null) + const base = this.pallaBase(p, t) + const scena = { cam: { x: lerp(CAMPO.cx, base.x, 0.6), y: lerp(CAMPO.cy, base.y, 0.5), z: 1.35 }, palla: base, omini: [], scritte: [], evento: e ?? null, inPanchina: S.inPanchina, folla: 1 } + const minu = minuto(t) + // l'onda del coro ogni tanto + if (Math.floor(t / 7) % 3 === 0) scena.onda = ((t % 7) / 7) * 220 - 20 + if (!e) { + // fra un evento e l'altro: si gioca. Vicino al prossimo grande evento la palla si porta dove servirà + const prossimo = p.eventi.find((x) => x.t > t && x.t - t < 1.1 && ['gol', 'tiro', 'rigore'].includes(x.tipo)) + if (prossimo) { const dest = this.puntoDiAttacco(p, prossimo); const u = liscia(1 - (prossimo.t - t) / 1.1); scena.palla = { x: lerp(base.x, dest.x, u), y: lerp(base.y, dest.y, u) }; scena.cam = { x: lerp(CAMPO.cx, dest.x, 0.7), y: lerp(CAMPO.cy, dest.y, 0.5), z: 1.6 } } + scena.omini = this.omini(p, t, scena.palla, S) + return scena + } + const u = limita((t - e.t) / (e.dur ?? 1), 0, 1) + const v = e.squadra ? this.verso(e.squadra, t) : 1 + const porta = e.squadra ? this.portaDi(e.squadra, t) : CAMPO.x1 + const att = e.squadra ? this.puntoDiAttacco(p, e) : base + switch (e.tipo) { + case 'inizio': { + scena.palla = { x: CAMPO.cx, y: CAMPO.cy } + scena.omini = this.omini(p, t, scena.palla, { ...S, corrono: u > 0.7 }) + scena.cam = { x: CAMPO.cx, y: CAMPO.cy, z: 1 } + scena.scritte.push({ t: 'FISCHIO D\'INIZIO', y: 104 }) + break + } + case 'gol': { + // 0–0,3 il tiro, 0,3–0,55 la rete che si gonfia e GOL, 0,55–1 l'esultanza e il replay al rallentatore + const inPorta = { x: porta + v * 2, y: CAMPO.cy + (h32(p.indice, e.t * 100) - 0.5) * 20 } + const replay = u > 0.58 + const uu = replay ? limita((u - 0.58) / 0.42, 0, 1) * 0.55 : u // il replay rifà il tiro a metà velocità + const tiro = limita(uu / 0.3, 0, 1) + scena.palla = { x: lerp(att.x, inPorta.x, tiro), y: lerp(att.y, inPorta.y, tiro), h: Math.sin(tiro * Math.PI) * 3 } + scena.omini = this.omini(p, t, replay ? att : { x: att.x, y: att.y }, S) + // chi ha segnato corre verso la palla e poi esulta (salta) + const chi = scena.omini.find((o) => o.sq === e.squadra && o.i % 5 === e.chi.i) + if (chi) { chi.x = lerp(att.x - v * 6, att.x - v * 3, tiro); chi.y = att.y + 1; if (!replay && u > 0.4) { chi.y -= Math.abs(Math.sin(u * 30)) * 2; chi.fermo = false } } + if (!replay && u > 0.3 && u < 0.5) scena.rete = v + if (replay && uu > 0.3 && uu < 0.5) scena.rete = v + scena.cam = replay ? { x: lerp(att.x, inPorta.x, 0.6), y: CAMPO.cy, z: 2.6 } : u < 0.3 ? { x: lerp(att.x, porta, 0.5), y: att.y, z: 2.1 } : { x: att.x, y: att.y, z: 2.4 } + if (u > 0.28 && u < 0.58) { scena.salto = { squadra: e.squadra, u }; if (Math.floor(u * 12) % 2) scena.scritte.push({ t: 'GOL!', y: 30, fondo: '#b3121f' }); scena.scossa = u < 0.4 ? 1.5 : 0 } + if (u >= 0.5 && u < 0.58) scena.scritte.push({ t: `${e.chi.nome} ${e.minuto}`, y: 104 }) + if (replay) { scena.scritte.push({ t: 'REPLAY', y: 8, fondo: '#ffd400', col: '#161616' }); scena.lesto = true } + if (e.rigore && u < 0.3) scena.scritte.push({ t: 'RIGORE', y: 104 }) + break + } + case 'rigore': { + // da dietro la porta: 0–0,3 la palla sul dischetto, 0,3–0,6 la rincorsa, 0,6–0,75 il tiro, poi l'esito + scena.dietroPorta = true; scena.subito = true; scena.u = u; scena.squadra = e.squadra; scena.esito = e.esito; scena.tuffo = h32(p.indice, e.t * 10) < 0.5 ? -1 : 1 + scena.scritte.push({ t: u < 0.75 ? 'RIGORE' : e.esito === 'parato' ? 'PARATO!' : 'FUORI!', y: 8, fondo: u < 0.75 ? '#161616' : '#1446a0' }) + if (u > 0.75) scena.salto = { squadra: e.squadra === 'casa' ? 'ospite' : 'casa', u } + break + } + case 'tiro': { + const fin = e.esito === 'fuori' ? { x: porta + v * 7, y: CAMPO.cy + (h32(p.indice, e.t * 100) < 0.5 ? -CAMPO.porta - 7 : CAMPO.porta + 7) } : e.esito === 'palo' ? { x: porta, y: CAMPO.cy + (h32(p.indice, e.t * 100) < 0.5 ? -CAMPO.porta : CAMPO.porta - 1) } : { x: porta - v * 3, y: CAMPO.cy + (h32(p.indice, e.t * 100) - 0.5) * 12 } + const tiro = limita(u / 0.55, 0, 1) + scena.palla = { x: lerp(att.x, fin.x, tiro), y: lerp(att.y, fin.y, tiro), h: Math.sin(tiro * Math.PI) * 2 } + if (e.esito === 'palo' && u > 0.55) { const r = limita((u - 0.55) / 0.45, 0, 1); scena.palla = { x: fin.x - v * r * 14, y: fin.y + (fin.y > CAMPO.cy ? -1 : 1) * r * 6 } } + scena.omini = this.omini(p, t, att, S) + if (e.esito === 'parata' && u > 0.4) { const gk = scena.omini.find((o) => o.sq !== e.squadra && o.i === 0); if (gk) { gk.y = fin.y; gk.aTerra = u > 0.6 } } + scena.cam = { x: lerp(att.x, porta, 0.5), y: lerp(att.y, fin.y, 0.5), z: 2 } + if (u > 0.5) scena.scritte.push({ t: e.esito === 'fuori' ? 'FUORI' : e.esito === 'palo' ? 'PALO!' : 'PARATA', y: 104, fondo: e.esito === 'palo' ? '#1446a0' : '#161616' }) + break + } + case 'rimessa': { + const y = e.lato === 'alto' ? CAMPO.y0 - 1 : CAMPO.y1 + const x = limita(base.x, 20, 140) + scena.palla = u < 0.5 ? { x, y } : { x: x + v * (u - 0.5) * 30, y: lerp(y, CAMPO.cy + (e.lato === 'alto' ? -10 : 10), (u - 0.5) * 2), h: Math.sin((u - 0.5) * 2 * Math.PI) * 4 } + scena.omini = this.omini(p, t, { x, y: CAMPO.cy + (e.lato === 'alto' ? -14 : 14) }, S) + const chi = scena.omini.find((o) => o.sq === e.squadra && o.i % 5 === 1); if (chi) { chi.x = x - v * 1; chi.y = y + (e.lato === 'alto' ? 1 : 2); chi.fermo = u < 0.5 } + scena.cam = { x, y: lerp(CAMPO.cy, y, 0.6), z: 1.7 } + if (u < 0.5) scena.scritte.push({ t: 'RIMESSA', y: 104 }) + break + } + case 'fallo': + case 'scivola': { + scena.palla = base + scena.omini = this.omini(p, t, base, S) + const a = scena.omini.find((o) => o.sq === e.squadra && o.i % 5 === 2), b = scena.omini.find((o) => o.sq !== e.squadra && o.i % 5 === 3) + if (a) { a.x = base.x - 2; a.y = base.y + 1; a.aTerra = e.tipo === 'scivola' || u > 0.2 } + if (b && e.tipo === 'fallo') { b.x = base.x + 2; b.y = base.y + 1; b.fermo = true } + scena.cam = { x: base.x, y: base.y, z: 2 } + scena.scritte.push({ t: e.tipo === 'fallo' ? 'FALLO' : 'SCIVOLA!', y: 104 }) + break + } + case 'giallo': + case 'rosso': { + const rosso = e.tipo === 'rosso' + scena.palla = base + scena.omini = this.omini(p, t, base, S) + const chi = scena.omini.find((o) => o.sq === e.squadra && o.i % 5 === e.chi.i) + if (chi) { chi.fermo = true; chi.x = base.x + 3; chi.y = base.y + 2; if (rosso && u > 0.5) { const w = (u - 0.5) * 2; chi.x = lerp(base.x + 3, CAMPO.cx, w); chi.y = lerp(base.y + 2, CAMPO.y0 - 3, w); chi.fermo = false } } + scena.arbitro = { x: base.x - 3, y: base.y + 2, maglia: { colori: ['#161616', '#161616'], disegno: 'pieno' }, fermo: true } + scena.cam = { x: base.x, y: base.y, z: 3 } + if (u > 0.15 && u < 0.7) scena.cartellino = { x: this.sx(base.x - 3) + 4, y: this.sy(base.y + 2) - 10, rosso } + scena.scritte.push({ t: rosso ? (e.doppio ? 'DOPPIO GIALLO: ROSSO' : 'ROSSO!') : 'GIALLO', y: 8, fondo: rosso ? '#b3121f' : '#ffd400', col: rosso ? '#ffffff' : '#161616' }) + if (u > 0.3) scena.scritte.push({ t: `${e.chi.n} ${e.chi.nome}`, y: 104 }) + if (rosso && u > 0.6) scena.scritte.push({ t: `${p[e.squadra].sigla} IN QUATTRO`, y: 94 }) + break + } + case 'infortunio': { + scena.palla = base + scena.omini = this.omini(p, t, base, S) + const chi = scena.omini.find((o) => o.sq === e.squadra && o.i % 5 === e.chi.i) + if (chi) { chi.aTerra = true; chi.x = base.x + 2; chi.y = base.y + 2; if (u > 0.45) { const w = liscia((u - 0.45) / 0.5); scena.barella = { x: lerp(base.x + 2, CAMPO.cx + (e.squadra === 'casa' ? -20 : 28), w), y: lerp(base.y + 2, CAMPO.y0 - 2, w), maglia: p.maglie[e.squadra] }; chi.x = -100 } } + if (u > 0.7) { const w = (u - 0.7) / 0.3; scena.omini.push({ x: lerp(CAMPO.cx + (e.squadra === 'casa' ? -20 : 28), base.x, w), y: lerp(CAMPO.y0 - 2, base.y, w), sq: e.squadra, maglia: p.maglie[e.squadra], numero: e.entra.n, i: 9 }) } + scena.inPanchina = { ...S.inPanchina, [e.squadra]: u > 0.7 ? 1 : S.inPanchina[e.squadra] } + scena.cam = { x: base.x, y: base.y, z: u < 0.45 ? 2.6 : 1.6 } + scena.scritte.push({ t: u < 0.45 ? 'INFORTUNIO' : u < 0.7 ? 'BARELLA' : `ENTRA ${e.entra.n} ${e.entra.nome}`, y: 104 }) + break + } + case 'intervallo': { + const pt = punteggioA(p, t) + scena.palla = { x: CAMPO.cx, y: CAMPO.cy } + scena.omini = this.omini(p, t, scena.palla, S).map((o) => ({ ...o, y: lerp(o.y, CAMPO.y0 - 4, liscia(u * 1.4)), x: lerp(o.x, CAMPO.cx + (o.sq === 'casa' ? -24 : 24), liscia(u * 1.4)) })) + scena.cam = { x: CAMPO.cx, y: CAMPO.cy, z: 1 } + scena.scritte.push({ t: 'INTERVALLO', y: 8 }, { t: `${p.casa.sigla} ${pt.casa} - ${pt.ospite} ${p.ospite.sigla}`, y: 104 }) + break + } + case 'ripresa': { + scena.palla = { x: CAMPO.cx, y: CAMPO.cy } + scena.omini = this.omini(p, t, scena.palla, { ...S, corrono: u > 0.6 }) + scena.cam = { x: CAMPO.cx, y: CAMPO.cy, z: 1 } + scena.scritte.push({ t: 'SI RIPRENDE', y: 104 }) + break + } + case 'sospesa': { + scena.palla = base + scena.omini = this.omini(p, t, base, { ...S, corrono: false }) + scena.cam = { x: CAMPO.cx, y: CAMPO.cy, z: 1 } + const causa = e.causa + if (causa.startsWith('nebbia')) scena.nebbia = limita(0.5 + u * 1.5, 0, 0.95) + else if (causa.startsWith('blackout')) scena.buio = u < 0.1 ? u * 8 : 0.82 + else if (causa.startsWith('invasione')) scena.nutrie = Array.from({ length: 7 }, (_, i) => ({ x: ((h32(p.indice, 1300 + i) * 200 + t * (14 + i * 3)) % 200) - 20, y: 6 + h32(p.indice, 1400 + i) * 68, dir: 1 })) + const r = p.risultato + scena.scritte.push({ t: 'PARTITA SOSPESA', y: 8, fondo: '#b3121f' }) + if (u > 0.08) scena.scritte.push({ t: causa.toUpperCase().slice(0, 44), y: 94, col: '#ffd400' }) + if (u > 0.2) scena.scritte.push({ t: `${r.casa}-${r.ospite} A TAVOLINO`, y: 104 }) + break + } + case 'fine': { + const r = p.risultato + scena.palla = base + scena.omini = this.omini(p, t, base, { ...S, corrono: false }) + scena.cam = { x: CAMPO.cx, y: CAMPO.cy, z: 1 } + if (r.casa !== r.ospite) scena.salto = { squadra: r.casa > r.ospite ? 'casa' : 'ospite', u } + scena.scritte.push({ t: 'FINALE', y: 8 }, { t: `${p.casa.sigla} ${r.casa} - ${r.ospite} ${p.ospite.sigla}`, y: 104 }) + break + } + default: { + scena.omini = this.omini(p, t, base, S) + } + } + return scena + } + puntoDiAttacco(p, e) { + const v = this.verso(e.squadra, e.t), porta = this.portaDi(e.squadra, e.t) + return { x: porta - v * (22 + h32(p.indice, e.t * 100 + 1) * 10), y: CAMPO.cy + (h32(p.indice, e.t * 100 + 2) - 0.5) * 40 } + } + + /** Prima della partita (t negativo: secondi al fischio): il riscaldamento, i tifosi che entrano, la presentazione. */ + scenaAttesa(p, t) { + const manca = -t // secondi al fischio, 0..240 + const folla = limita(1 - (manca - 20) / 200, 0.15, 1) + const scena = { cam: { x: CAMPO.cx, y: CAMPO.cy, z: 1 }, omini: [], scritte: [], evento: null, folla, inPanchina: { casa: 2, ospite: 2 } } + // le due squadre si scaldano nella loro metà: corsette e un pallone a testa che gira + const palle = [] + for (const [i, sq] of ['casa', 'ospite'].entries()) { + const v = i === 0 ? 1 : -1, f = p.formazioni[sq], maglia = p.maglie[sq] + const cx = CAMPO.cx - v * 40 + palle.push({ x: cx + Math.cos(this.t * 1.1 + i) * 14, y: CAMPO.cy + Math.sin(this.t * 1.7 + i) * 10 }) + for (let j = 0; j < 5; j++) { + const a = this.t * 0.7 + j * 1.26 + i + scena.omini.push({ x: cx + Math.cos(a) * 16, y: CAMPO.cy + Math.sin(a) * 12, sq, maglia, portiere: j === 0 ? p.portieri[sq] : null, numero: f[j].n, i: j + i * 5 }) + } + } + scena.palla = palle[0]; scena.palla2 = palle[1] + // la presentazione: a rotazione, ogni 12 secondi un cartello diverso (le squadre, il meteo, «quote vere, risultati a caso») + const tt = manca, giro = Math.floor(tt / 14) % 3, dentro = tt % 14 + if (dentro < 7) { + const m = p.meteo + const meteoTxt = { sereno: 'SERENO', nuvole: 'NUVOLOSO', pioggia: 'PIOGGIA', nebbia: 'NEBBIA', neve: 'NEVE', vento: 'VENTO FORTE' }[m.tipo] + if (giro === 0) scena.pannello = { riga: `${p.casa.citta.toUpperCase().slice(0, 12)}`, riga2: `${meteoTxt} ${m.temp}°`, riga3: m.notte ? 'IN NOTTURNA' : m.sera ? 'AL TRAMONTO' : 'DI GIORNO' } + else if (giro === 1) scena.pannello = { riga: 'LE QUOTE COPIANO', riga2: 'LA SERIE A VERA', riga3: 'I RISULTATI NO' } + else scena.pannello = { riga: `SERIE ${p.casa.serie}${p.casa.serie !== p.ospite.serie ? ' CONTRO ' + p.ospite.serie : ''}`, riga2: 'FORZA E ULTIME 5', riga3: '(INVENTATE)' } + } + const mm = Math.floor(manca / 60), ss = Math.floor(manca % 60) + scena.scritte.push({ t: manca > 0 ? `FISCHIO TRA ${mm}:${String(ss).padStart(2, '0')}` : 'SI COMINCIA', y: 104 }) + return scena + } + scenaFinale(p) { + const r = p.risultato + const scena = { cam: { x: CAMPO.cx, y: CAMPO.cy, z: 1 }, omini: [], scritte: [{ t: 'FINALE', y: 8 }, { t: `${p.casa.sigla} ${r.casa} - ${r.ospite} ${p.ospite.sigla}${r.tavolino ? ' TAV.' : ''}`, y: 104 }], evento: null, folla: 0.6, palla: { x: CAMPO.cx, y: CAMPO.cy }, inPanchina: { casa: 2, ospite: 2 } } + scena.omini = this.omini(p, T.fine, scena.palla, { corrono: false }) + return scena + } +} diff --git a/albertopecchini.it/pzkko/games/virtual/squadre.js b/albertopecchini.it/pzkko/games/virtual/squadre.js new file mode 100644 index 00000000..48a3b01d --- /dev/null +++ b/albertopecchini.it/pzkko/games/virtual/squadre.js @@ -0,0 +1,128 @@ +// Le squadre di «Virtual», il campionato inventato di pzkko.it: tutte quelle di Serie A e Serie B 2026/27 coi nomi +// storpiati ma riconoscibili (stile videogioco senza licenza), la sigla, la città, i colori della maglia di casa e di +// trasferta (che si vedono addosso agli omini del campo), uno stemma originale disegnato qui in SVG (gli stessi colori +// sociali e lo stesso «spirito» del vero: una zebra, un biscione, un giglio; mai il logo vero) e una forza da 1 a 100 +// presa dalla realtà. La forza decide SOLO le quote (partita.js): i risultati no. +// +// Niente nomi veri di giocatori: i cognomi li inventa cognome(rand), con sillabe italiane a caso. +// +// Il clima della città (per il meteo della partita, partita.js): 'nord' la pianura padana e il Veneto (nebbia e nevicate +// d'inverno), 'alpi' Bolzano, 'liguria' la costa ligure e toscana del nord (piove tanto, mai nebbia), 'centro' Toscana, +// Marche e Lazio, 'sud' il meridione e le isole (sereno e caldo). + +export const SERIE_A = 'A', SERIE_B = 'B' + +// colori: maglia casa [primo, secondo], disegno della maglia ('strisce' verticali, 'meta' due metà, 'pieno' tinta unita +// con dettagli del secondo colore, 'croce', 'fascia' orizzontale, 'quarti'); la trasferta [primo, secondo] sempre 'pieno' +const S = (id, nome, sigla, citta, serie, forza, clima, casa, disegno, fuori, stemma) => ({ id, nome, sigla, citta, serie, forza, clima, casa, disegno, fuori, stemma }) + +export const SQUADRE = [ + // ── Serie A 2026/27 ── + S('atalanta', 'Atalanto', 'ATL', 'Bergamo', SERIE_A, 84, 'nord', ['#1b2a6b', '#161616'], 'strisce', ['#ffffff', '#1b2a6b'], { forma: 'scudo', simbolo: 'dea', fondo: '#1b2a6b', tinta: '#ffffff' }), + S('bologna', 'Bolognese', 'BOL', 'Bologna', SERIE_A, 77, 'nord', ['#b3121f', '#1b2a6b'], 'strisce', ['#ffffff', '#b3121f'], { forma: 'scudo', simbolo: 'croce', fondo: '#1b2a6b', tinta: '#b3121f' }), + S('cagliari', 'Cagliaritas', 'CAG', 'Cagliari', SERIE_A, 56, 'sud', ['#b3121f', '#1b2a6b'], 'meta', ['#ffffff', '#b3121f'], { forma: 'tondo', simbolo: 'mori', fondo: '#ffffff', tinta: '#161616' }), + S('como', 'Comodo', 'COM', 'Como', SERIE_A, 72, 'nord', ['#1e4fb5', '#ffffff'], 'pieno', ['#ffffff', '#1e4fb5'], { forma: 'ovale', simbolo: 'onde', fondo: '#1e4fb5', tinta: '#ffffff' }), + S('fiorentina', 'Fiorentoina', 'FIO', 'Firenze', SERIE_A, 76, 'centro', ['#5a2d8e', '#ffffff'], 'pieno', ['#ffffff', '#5a2d8e'], { forma: 'rombo', simbolo: 'giglio', fondo: '#ffffff', tinta: '#b3121f' }), + S('frosinone', 'Frosinoni', 'FRO', 'Frosinone', SERIE_A, 45, 'centro', ['#f2c230', '#1e4fb5'], 'pieno', ['#1e4fb5', '#f2c230'], { forma: 'scudo', simbolo: 'leone', fondo: '#f2c230', tinta: '#1e4fb5' }), + S('genoa', 'Genova Grifo', 'GEN', 'Genova', SERIE_A, 62, 'liguria', ['#b3121f', '#1b2a6b'], 'meta', ['#ffffff', '#1b2a6b'], { forma: 'scudo', simbolo: 'grifone', fondo: '#b3121f', tinta: '#ffffff' }), + S('inter', 'Inder', 'IND', 'Milano', SERIE_A, 92, 'nord', ['#161616', '#1e4fb5'], 'strisce', ['#ffffff', '#1e4fb5'], { forma: 'tondo', simbolo: 'biscione', fondo: '#161616', tinta: '#1e4fb5' }), + S('juventus', 'Juvenclub', 'JUV', 'Torino', SERIE_A, 85, 'nord', ['#ffffff', '#161616'], 'strisce', ['#161616', '#ffffff'], { forma: 'scudo', simbolo: 'zebra', fondo: '#ffffff', tinta: '#161616' }), + S('lazio', 'Lazzio', 'LAZ', 'Roma', SERIE_A, 78, 'centro', ['#7fc4ea', '#ffffff'], 'pieno', ['#ffffff', '#7fc4ea'], { forma: 'tondo', simbolo: 'aquila', fondo: '#7fc4ea', tinta: '#ffffff' }), + S('lecce', 'Leccese', 'LEC', 'Lecce', SERIE_A, 52, 'sud', ['#f2c230', '#b3121f'], 'strisce', ['#ffffff', '#b3121f'], { forma: 'scudo', simbolo: 'lupo', fondo: '#f2c230', tinta: '#b3121f' }), + S('milan', 'Milano Rossonera', 'MIL', 'Milano', SERIE_A, 86, 'nord', ['#b3121f', '#161616'], 'strisce', ['#ffffff', '#b3121f'], { forma: 'ovale', simbolo: 'tridente', fondo: '#161616', tinta: '#b3121f' }), + S('monza', 'Monzese', 'MON', 'Monza', SERIE_A, 50, 'nord', ['#b3121f', '#ffffff'], 'pieno', ['#ffffff', '#b3121f'], { forma: 'scudo', simbolo: 'corona', fondo: '#b3121f', tinta: '#f2c230' }), + S('napoli', 'Napule', 'NAP', 'Napoli', SERIE_A, 90, 'sud', ['#2e86d9', '#ffffff'], 'pieno', ['#ffffff', '#2e86d9'], { forma: 'tondo', simbolo: 'ciuccio', fondo: '#2e86d9', tinta: '#ffffff' }), + S('parma', 'Parmigiano', 'PAR', 'Parma', SERIE_A, 58, 'nord', ['#ffffff', '#f2c230'], 'croce', ['#1b2a6b', '#f2c230'], { forma: 'scudo', simbolo: 'croce', fondo: '#f2c230', tinta: '#1b2a6b' }), + S('roma', 'Romma', 'ROM', 'Roma', SERIE_A, 82, 'centro', ['#8e1a2b', '#f2c230'], 'pieno', ['#ffffff', '#8e1a2b'], { forma: 'scudo', simbolo: 'lupo', fondo: '#8e1a2b', tinta: '#f2c230' }), + S('sassuolo', 'Sassolino', 'SAS', 'Sassuolo', SERIE_A, 55, 'nord', ['#161616', '#1a8a3a'], 'strisce', ['#ffffff', '#1a8a3a'], { forma: 'ovale', simbolo: 'foglia', fondo: '#1a8a3a', tinta: '#161616' }), + S('torino', 'Toro Granata', 'TOR', 'Torino', SERIE_A, 66, 'nord', ['#8e1a2b', '#ffffff'], 'pieno', ['#ffffff', '#8e1a2b'], { forma: 'scudo', simbolo: 'toro', fondo: '#8e1a2b', tinta: '#ffffff' }), + S('udinese', 'Udinesi', 'UDI', 'Udine', SERIE_A, 64, 'nord', ['#ffffff', '#161616'], 'strisce', ['#161616', '#ffffff'], { forma: 'scudo', simbolo: 'castello', fondo: '#161616', tinta: '#ffffff' }), + S('venezia', 'Venexia', 'VEN', 'Venezia', SERIE_A, 48, 'nord', ['#161616', '#e8742a'], 'pieno', ['#e8742a', '#1a8a3a'], { forma: 'scudo', simbolo: 'leone', fondo: '#161616', tinta: '#e8742a' }), + // ── Serie B 2026/27 ── + S('arezzo', 'Aretini', 'ARE', 'Arezzo', SERIE_B, 24, 'centro', ['#8e1a2b', '#ffffff'], 'pieno', ['#ffffff', '#8e1a2b'], { forma: 'scudo', simbolo: 'cavallo', fondo: '#8e1a2b', tinta: '#f2c230' }), + S('avellino', 'Avellinesi', 'AVE', 'Avellino', SERIE_B, 30, 'sud', ['#1a8a3a', '#ffffff'], 'pieno', ['#ffffff', '#1a8a3a'], { forma: 'scudo', simbolo: 'lupo', fondo: '#1a8a3a', tinta: '#ffffff' }), + S('benevento', 'Beneventani', 'BEN', 'Benevento', SERIE_B, 31, 'sud', ['#f2c230', '#b3121f'], 'strisce', ['#ffffff', '#b3121f'], { forma: 'tondo', simbolo: 'strega', fondo: '#f2c230', tinta: '#161616' }), + S('carrarese', 'Carraresi', 'CAR', 'Carrara', SERIE_B, 27, 'liguria', ['#2e86d9', '#f2c230'], 'pieno', ['#f2c230', '#2e86d9'], { forma: 'scudo', simbolo: 'montagna', fondo: '#2e86d9', tinta: '#ffffff' }), + S('catanzaro', 'Catanzarese', 'CTZ', 'Catanzaro', SERIE_B, 34, 'sud', ['#f2c230', '#b3121f'], 'strisce', ['#ffffff', '#b3121f'], { forma: 'scudo', simbolo: 'aquila', fondo: '#b3121f', tinta: '#f2c230' }), + S('cesena', 'Cesenati', 'CES', 'Cesena', SERIE_B, 35, 'nord', ['#ffffff', '#161616'], 'strisce', ['#161616', '#ffffff'], { forma: 'tondo', simbolo: 'cavalluccio', fondo: '#ffffff', tinta: '#161616' }), + S('cremonese', 'Cremonata', 'CRE', 'Cremona', SERIE_B, 42, 'nord', ['#8a8a8a', '#b3121f'], 'strisce', ['#ffffff', '#b3121f'], { forma: 'scudo', simbolo: 'violino', fondo: '#8a8a8a', tinta: '#b3121f' }), + S('empoli', 'Empolesi', 'EMP', 'Empoli', SERIE_B, 43, 'centro', ['#2e86d9', '#ffffff'], 'pieno', ['#ffffff', '#2e86d9'], { forma: 'scudo', simbolo: 'castello', fondo: '#2e86d9', tinta: '#ffffff' }), + S('verona', 'Verona Scaligera', 'VER', 'Verona', SERIE_B, 44, 'nord', ['#f2c230', '#1b2a6b'], 'strisce', ['#ffffff', '#1b2a6b'], { forma: 'scudo', simbolo: 'scala', fondo: '#1b2a6b', tinta: '#f2c230' }), + S('juvestabia', 'Stabia Vespe', 'STB', 'Castellammare', SERIE_B, 30, 'sud', ['#f2c230', '#1b2a6b'], 'fascia', ['#ffffff', '#1b2a6b'], { forma: 'tondo', simbolo: 'vespa', fondo: '#f2c230', tinta: '#161616' }), + S('mantova', 'Mantua', 'MAN', 'Mantova', SERIE_B, 29, 'nord', ['#ffffff', '#b3121f'], 'meta', ['#b3121f', '#ffffff'], { forma: 'scudo', simbolo: 'alloro', fondo: '#ffffff', tinta: '#b3121f' }), + S('modena', 'Modenesi', 'MOD', 'Modena', SERIE_B, 36, 'nord', ['#f2c230', '#1b2a6b'], 'pieno', ['#1b2a6b', '#f2c230'], { forma: 'scudo', simbolo: 'torre', fondo: '#f2c230', tinta: '#1b2a6b' }), + S('padova', 'Padovana', 'PAD', 'Padova', SERIE_B, 32, 'nord', ['#ffffff', '#b3121f'], 'croce', ['#b3121f', '#ffffff'], { forma: 'scudo', simbolo: 'croce', fondo: '#ffffff', tinta: '#b3121f' }), + S('palermo', 'Palermitano', 'PAL', 'Palermo', SERIE_B, 40, 'sud', ['#e889b4', '#161616'], 'meta', ['#161616', '#e889b4'], { forma: 'scudo', simbolo: 'aquila', fondo: '#e889b4', tinta: '#161616' }), + S('pisa', 'Pisa Torre', 'PIS', 'Pisa', SERIE_B, 39, 'liguria', ['#161616', '#2e86d9'], 'strisce', ['#ffffff', '#2e86d9'], { forma: 'scudo', simbolo: 'torrependente', fondo: '#2e86d9', tinta: '#ffffff' }), + S('sampdoria', 'Sampdoriana', 'SAM', 'Genova', SERIE_B, 40, 'liguria', ['#1e4fb5', '#ffffff'], 'fascia', ['#ffffff', '#1e4fb5'], { forma: 'scudo', simbolo: 'ancora', fondo: '#1e4fb5', tinta: '#ffffff' }), + S('sudtirol', 'Sudtirolo', 'SUD', 'Bolzano', SERIE_B, 28, 'alpi', ['#ffffff', '#b3121f'], 'pieno', ['#b3121f', '#ffffff'], { forma: 'scudo', simbolo: 'montagna', fondo: '#ffffff', tinta: '#b3121f' }), + S('vicenza', 'Vicentini', 'VIC', 'Vicenza', SERIE_B, 27, 'nord', ['#b3121f', '#ffffff'], 'strisce', ['#ffffff', '#b3121f'], { forma: 'scudo', simbolo: 'leone', fondo: '#b3121f', tinta: '#ffffff' }), + S('entella', 'Entella Virtuosa', 'ENT', 'Chiavari', SERIE_B, 25, 'liguria', ['#ffffff', '#7fc4ea'], 'strisce', ['#7fc4ea', '#ffffff'], { forma: 'ovale', simbolo: 'onde', fondo: '#7fc4ea', tinta: '#ffffff' }), + S('ascoli', 'Ascolano', 'ASC', 'Ascoli Piceno', SERIE_B, 22, 'centro', ['#ffffff', '#161616'], 'strisce', ['#161616', '#ffffff'], { forma: 'scudo', simbolo: 'picchio', fondo: '#161616', tinta: '#ffffff' }), +] +export const PER_ID = new Map(SQUADRE.map((s) => [s.id, s])) + +// ── i cognomi inventati ────────────────────────────────────────────────────── +const INIZI = ['Ber', 'Gra', 'Mon', 'Pel', 'Ros', 'Fer', 'Cas', 'Bal', 'Lom', 'Ven', 'Tor', 'Mar', 'Pic', 'Bru', 'Spa', 'Zan', 'Gal', 'Fab', 'Riz', 'Col', 'Ser', 'Pas', 'Tre', 'Buf', 'Can', 'Del', 'Mor', 'Pal', 'Sal', 'Vit'] +const MEZZI = ['to', 'na', 'ri', 'lo', 'ga', 'mi', 'ne', 'ca', 'de', 'ti', 'bo', 'sa', 'vi', 'gu', 'ra', ''] +const FINI = ['lazzi', 'nchetti', 'tolini', 'rdello', 'sconi', 'ttini', 'gnoli', 'relli', 'zzoni', 'vanti', 'ssini', 'ldi', 'nari', 'retti', 'cchi', 'sotti', 'melli', 'ndri', 'ttoni', 'gli'] +/** Un cognome inventato (rand = una funzione che dà un numero fra 0 e 1). */ +export function cognome(rand) { + const i = INIZI[Math.floor(rand() * INIZI.length)], m = MEZZI[Math.floor(rand() * MEZZI.length)], f = FINI[Math.floor(rand() * FINI.length)] + return i + m + f +} + +// ── gli stemmi ─────────────────────────────────────────────────────────────── +// Ogni stemma è una forma (scudo, tondo, ovale, rombo) col fondo del colore sociale, un bordo, il simbolo disegnato con +// forme semplici (in un quadrato 40 × 40) e sotto la sigla. Disegnati qui, non copiati: una zebra stilizzata, un biscione +// che è una esse, un giglio di tre petali… Lo stesso SVG si stampa sul tabellone in HTML e, piccolo, sullo schermino. +const FORME = { + scudo: 'M50 6 L90 16 L88 56 Q86 84 50 96 Q14 84 12 56 L10 16 Z', + tondo: 'M50 6 A44 44 0 1 1 49.9 6 Z', + ovale: 'M50 4 Q92 4 92 50 Q92 96 50 96 Q8 96 8 50 Q8 4 50 4 Z', + rombo: 'M50 4 L94 50 L50 96 L6 50 Z', +} +// i simboli, in un quadrato 40 × 40, come pezzi SVG (riempiti con currentColor = la tinta; «a» = il colore di accento) +const SIMBOLI = { + // la zebra: la testa di profilo con le strisce + zebra: '', + // il biscione: una esse col capo e la lingua + biscione: '', + giglio: '', + lupo: '', + aquila: '', + tridente: '', + toro: '', + grifone: '', + ciuccio: '', + dea: '', + mori: '', + onde: '', + leone: '', + corona: '', + croce: '', + foglia: '', + castello: '', + torre: '', + torrependente: '', + cavallo: '', + strega: '', + montagna: '', + cavalluccio: '', + violino: '', + scala: '', + vespa: '', + alloro: '', + ancora: '', + picchio: '', +} + +/** Lo stemma di una squadra come SVG (stringa). `misura` in px; senza, scala col contenitore. */ +export function stemmaSVG(sq, misura = null) { + const { forma, simbolo, fondo, tinta } = sq.stemma + const chiaro = fondo === '#ffffff' + const bordo = chiaro ? tinta : '#161616' + const accento = tinta === '#ffffff' ? fondo : fondo === '#ffffff' ? '#ffffff' : sq.casa.includes(tinta) ? (sq.casa.find((c) => c !== tinta) ?? '#ffffff') : fondo + const sz = misura ? ` width="${misura}" height="${misura}"` : '' + return `` +} diff --git a/albertopecchini.it/pzkko/games/virtual/virtual.css b/albertopecchini.it/pzkko/games/virtual/virtual.css new file mode 100644 index 00000000..711f8c17 --- /dev/null +++ b/albertopecchini.it/pzkko/games/virtual/virtual.css @@ -0,0 +1,164 @@ +/* La finestra di «Virtual» di pzkko.it: lo stesso foglio col nastro della roulette e degli altri giochi + (art/roulette/carta.webp), 800 × 600 sul computer, a tutto schermo sul telefono. Dentro: lo schermino a pixel + (canvas, pixel netti) con sotto il tabellone, le quote stampate sul foglio come un tavolo, la schedina, le fiches + fotografate. Caratteri e colori quelli del muro. */ + +.vi{position:fixed;inset:0;margin:auto;width:800px;height:600px;max-width:none;max-height:none;padding:0;border:0;background:none;overflow:visible;color:#161616; + font:400 14px/1.3 "Special Elite","Courier New",monospace;--rosso:#b3121f;--nero:#161616;--evid:#ffd400;--marker:"Permanent Marker","Comic Sans MS",cursive;--fa:"Anton",Impact,sans-serif;--verde:#0b5d2b;--blu:#1446a0} +.vi::backdrop{background:rgba(12,10,8,.62);backdrop-filter:blur(2px)} +.vi[open]{animation:vi-attacca .32s cubic-bezier(.22,1,.36,1)} +.vi.chiude{animation:vi-stacca .22s ease-in forwards} +@keyframes vi-attacca{from{opacity:0;transform:translateY(18px) rotate(-1deg) scale(.97)}} +@keyframes vi-stacca{to{opacity:0;transform:translateY(10px) rotate(.8deg) scale(.98)}} +.vi::before{content:"";position:absolute;left:-8.66%;right:-9.04%;top:-12.49%;bottom:-12.6%;z-index:-1;background:url(/art/roulette/carta.webp) 0 0/100% 100% no-repeat;filter:drop-shadow(0 4px 6px rgba(0,0,0,.35)) drop-shadow(0 24px 30px rgba(0,0,0,.4));pointer-events:none} +.vi *{box-sizing:border-box} +.vi button{font:inherit;color:inherit} +.vi :focus-visible{outline:3px solid var(--evid);outline-offset:2px} +.vi [hidden]{display:none !important} + +.vi-dentro{position:absolute;inset:0;display:grid;grid-template-columns:400px 1fr;grid-template-rows:50px 1fr 78px;column-gap:14px;padding:10px 20px 8px 14px} + +/* la testata: il titolo come quello dei volantini, la voce a pennarello (la telecronaca), la chiusura */ +.vi-testa{grid-column:1/3;display:flex;align-items:center;gap:14px;min-width:0;padding-left:8px} +.vi-testa h2{margin:0;font:400 44px/1 var(--fa);letter-spacing:.005em;flex:none} +.vi-voce{margin:0;flex:1;min-width:0;font:400 18px/1.1 var(--marker);color:var(--rosso);transform:rotate(-1.5deg);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.vi-chiudi{flex:none;width:44px;height:44px;padding:6px;border:0;border-radius:50%;background:none;cursor:pointer;color:#161616;transform:rotate(-6deg);transition:transform .2s cubic-bezier(.22,1,.36,1)} +.vi-chiudi svg{width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:4.2;stroke-linecap:round} +@media (hover:hover){.vi-chiudi:hover{transform:rotate(-6deg) scale(1.12)}} +.vi-regole-tel{display:none} +.vi-piccolo{flex:none;padding:2px 4px;border:0;border-bottom:2px solid currentColor;background:none;cursor:pointer;font:400 15px/1.1 var(--marker) !important;color:var(--blu) !important} +.vi-piccolo:disabled{opacity:.35;cursor:not-allowed} + +/* la diretta: lo schermino e il tabellone */ +.vi-diretta{grid-row:2;display:flex;flex-direction:column;gap:6px;min-height:0;justify-content:center} +/* lo schermino: la cornice di plastica grigia di un tamagotchi, il vetro leggermente scuro, la tela coi pixel netti + centrata (la misura la decide schermo.js: un multiplo intero di 192 × 120) */ +.vi-schermo{position:relative;flex:none;width:100%;aspect-ratio:192/120;display:grid;place-items:center;padding:8px;border-radius:14px;background:#d8d4cc;box-shadow:inset 0 2px 0 rgba(255,255,255,.7),inset 0 -2px 0 rgba(0,0,0,.18),0 2px 3px rgba(0,0,0,.25);overflow:hidden} +.vi-schermo::before{content:"";position:absolute;inset:8px;border-radius:6px;background:#1c2a1e;box-shadow:inset 0 0 0 2px #3a3d3a,inset 0 0 18px rgba(0,0,0,.5)} +.vi-schermo canvas{position:relative;display:block;width:100%;height:100%;image-rendering:pixelated;image-rendering:crisp-edges;border-radius:3px;box-shadow:0 0 0 1px rgba(0,0,0,.4)} +/* il vetro: un riflesso in diagonale, sopra tutto */ +.vi-schermo::after{content:"";position:absolute;inset:8px;border-radius:6px;pointer-events:none;background:linear-gradient(118deg,rgba(255,255,255,.1),rgba(255,255,255,.02) 40%,transparent 42%)} +.vi-tabellone{flex:none;display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:center;gap:8px;padding:6px 8px;border:1.8px solid var(--nero);border-radius:6px;background:rgba(255,255,255,.15)} +.vi-sq{display:grid;grid-template-columns:40px 1fr;grid-template-rows:auto auto;column-gap:7px;align-items:center;min-width:0} +.vi-sq.ospite{grid-template-columns:1fr 40px;text-align:right} +.vi-sq.ospite .vi-st{grid-column:2;grid-row:1/3} +.vi-sq.ospite .vi-nome,.vi-sq.ospite .vi-citta,.vi-sq.ospite .vi-cart{grid-column:1} +.vi-st{grid-row:1/3;width:40px;height:40px} +.vi-st svg{width:100%;height:100%;display:block;filter:drop-shadow(0 1px 1px rgba(0,0,0,.3))} +.vi-nome{font:400 15px/1 var(--fa);letter-spacing:.01em;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;text-wrap:balance} +.vi-citta{font-size:10px;line-height:1.1;color:#5a544c;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0} +.vi-cart{display:flex;gap:2px;min-height:8px;margin-top:2px} +.vi-sq.ospite .vi-cart{justify-content:flex-end} +.vi-cart i{display:block;width:6px;height:8px;border-radius:1px;background:var(--evid);box-shadow:0 0 0 1px rgba(0,0,0,.4)} +.vi-cart i.r{background:#e0202a} +.vi-centro{display:flex;flex-direction:column;align-items:center;gap:1px;min-width:92px;flex:none} +.vi-punti{font:400 30px/1 var(--fa);white-space:nowrap;padding:1px 8px 2px;background:var(--nero);color:#fff;border-radius:3px} +.vi-punti.salta{animation:vi-salta .5s cubic-bezier(.2,1.6,.4,1)} +@keyframes vi-salta{30%{transform:scale(1.25) rotate(-3deg)}} +.vi-minuto{font:400 13px/1 var(--marker);color:var(--rosso);white-space:nowrap} +.vi-conto{font-size:10.5px;line-height:1.1;color:#5a544c;white-space:nowrap} +.vi.diretta .vi-conto{color:var(--rosso);font-weight:700} +.vi-meteo{flex:none;margin:0;font-size:10.5px;line-height:1.2;color:#5a544c;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + +/* il tavolo delle quote, stampato sul foglio: una griglia di caselle col nome e la quota; sopra ci volano le fiches */ +.vi-tavolo{grid-column:2;grid-row:2;display:flex;flex-direction:column;gap:8px;min-height:0;min-width:0} +.vi-quote{display:flex;flex-direction:column;gap:5px;flex:none} +.vi-esiti{display:grid;grid-template-columns:repeat(3,1fr);gap:5px} +.vi-ou{display:grid;grid-template-columns:repeat(4,1fr);gap:4px} +.vi-ou-titolo{margin:2px 0 -2px;font:400 12px/1 var(--marker);color:#3a3632;transform:rotate(-1deg)} +.vi-quota{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;min-height:44px;padding:4px 2px 5px;border:1.8px solid var(--nero);border-radius:5px;background:none;cursor:pointer;transition:transform .15s cubic-bezier(.22,1,.36,1),background .15s} +.vi-quota.esito{min-height:64px} +.vi-quota.esito .vi-q-nome{font:400 24px/1 var(--fa)} +.vi-quota.esito .vi-q-val{font-size:20px} +.vi-quota .vi-q-nome{font:400 10.5px/1 "Special Elite","Courier New",monospace;text-transform:lowercase;color:#3a3632;white-space:nowrap} +.vi-quota .vi-q-val{font:400 17px/1 var(--fa);letter-spacing:.02em} +.vi-quota.fav::after{content:"";position:absolute;inset:3px;border-radius:4px;background:rgba(255,212,0,.35);z-index:-1} +@media (hover:hover){.vi-quota:not(:disabled):hover{background:rgba(255,212,0,.25);transform:translateY(-2px)}} +.vi-quota:disabled{cursor:not-allowed;opacity:.65} +.vi.chiuso .vi-quota .vi-q-val{color:#5a544c} +.vi-q-pila{position:absolute;right:3px;bottom:2px;display:flex;align-items:flex-end;pointer-events:none} +.vi-q-pila img{position:absolute;right:0;bottom:0;width:22px;height:22px;max-width:none;filter:drop-shadow(0 1px 1px rgba(0,0,0,.35))} +.vi-q-pila b{position:relative;z-index:1;margin:0 0 -3px 20px;padding:0 3px;font:400 10px/1.3 var(--fa);background:var(--nero);color:#fff;border-radius:2px} +.vi-volo{position:fixed;left:0;top:0;width:26px;height:26px;margin:-13px 0 0 -13px;z-index:5;pointer-events:none;will-change:transform;filter:drop-shadow(0 2px 2px rgba(0,0,0,.3))} + +/* la schedina: le righe scritte sul foglio */ +.vi-schedina{flex:1;min-height:0;margin:0;padding:0 2px;list-style:none;overflow-y:auto;overscroll-behavior:contain;font-size:11.5px;line-height:1.25} +.vi-schedina li{display:grid;grid-template-columns:1fr auto auto;column-gap:6px;padding:3px 0;border-bottom:1px dashed rgba(22,22,22,.35)} +.vi-schedina li.vuota{display:block;color:#5a544c;font:400 14px/1.2 var(--marker);transform:rotate(-1deg);border:0;padding:6px 0} +.vi-schedina .vi-s-partita{grid-column:1/4;font-size:10px;color:#5a544c} +.vi-schedina .vi-s-partita i{font-style:normal;color:var(--nero)} +.vi-schedina .vi-s-cosa small{color:#5a544c} +.vi-schedina .vi-s-punt{color:#3a3632} +.vi-schedina .vi-s-punt::before{content:"$pzk "} +.vi-schedina b{font:400 13px/1 var(--marker)} +.vi-schedina b.vinta{color:var(--verde)} +.vi-schedina b.persa{color:var(--rosso);text-decoration:line-through} +.vi-schedina b.attesa{color:var(--blu)} +.vi-schedina b.corso{color:var(--rosso)} +.vi-schedina li.vinta{background:rgba(11,93,43,.08)} +.vi-schedina li.aperta .vi-s-cosa{font-weight:700} + +/* il banco: il saldo, le fiches da scegliere, i tasti */ +.vi-banco{grid-column:1/3;grid-row:3;display:flex;align-items:center;gap:16px;padding:0 4px 0 12px;min-width:0} +.vi-saldo{flex:none;margin:0;line-height:1} +.vi-saldo small{display:block;font:400 12px/1 var(--marker);color:#3a3632} +.vi-saldo b{display:block;font:400 26px/1.05 var(--fa);font-weight:400} +.vi-saldo .puntato{display:block;margin-top:3px;font-size:11px;color:#5a544c} +.vi-saldo .pzk-ig{display:inline-block;margin-top:4px;padding-bottom:1px;border-bottom:2px solid currentColor;font:400 12px/1.15 var(--marker);color:var(--blu);text-decoration:none;white-space:nowrap} +.vi-saldo .pzk-ig[hidden]{display:none} +.vi-saldo .pzk-ig b{display:inline;font:inherit;color:var(--rosso)} +.vi-saldo .pzk-eur{display:block;margin-top:3px;font:400 12px/1.15 var(--marker);color:var(--verde);text-decoration:none;white-space:nowrap} +.vi-saldo .pzk-eur[hidden]{display:none} +.vi-saldo .pzk-eur b{font:inherit;color:var(--verde);border-bottom:2px solid currentColor} +.vi-scegli{display:flex;gap:2px;flex:none} +.vi-scegli button{position:relative;width:42px;height:46px;padding:0;border:0;background:none;cursor:pointer;transition:transform .18s cubic-bezier(.22,1,.36,1)} +.vi-scegli img{width:38px;height:38px;max-width:none;filter:drop-shadow(0 1px 1px rgba(0,0,0,.35)) drop-shadow(0 3px 4px rgba(0,0,0,.18))} +.vi-scegli button[aria-pressed="true"]{transform:translateY(-7px)} +.vi-scegli button[aria-pressed="true"] img{filter:drop-shadow(0 2px 1px rgba(0,0,0,.3)) drop-shadow(0 9px 8px rgba(0,0,0,.28))} +.vi-scegli button:disabled{opacity:.35;cursor:not-allowed} +.vi-tasti{display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center;justify-content:flex-end;flex:1;min-width:0} +.vi-stato{font:400 15px/1.1 var(--marker);color:var(--verde);transform:rotate(-1deg)} +.vi.chiuso .vi-stato{color:var(--rosso)} + +/* le regole: un foglietto sopra il tavolo */ +.vi-regole{position:absolute;inset:60px 20px 20px;z-index:3;padding:16px 22px;border:1.8px solid var(--nero);border-radius:6px;background:#f3eee2;box-shadow:0 8px 24px rgba(0,0,0,.35);overflow-y:auto;font-size:14px;line-height:1.4} +.vi-regole h3{margin:0 0 8px;font:400 30px/1 var(--fa)} +.vi-regole p{margin:0 0 8px} +.vi-regole .vi-battuta{padding:6px 10px;background:rgba(255,212,0,.35);border-radius:4px} +.vi-regole .vi-regole-chiudi{margin-top:4px;font-size:18px !important} + +@media (prefers-reduced-motion:reduce){ + .vi[open],.vi.chiude{animation:none} + .vi-punti.salta{animation:none} + .vi-quota{transition:none} +} + +/* il telefono e le finestre basse: il foglio a tutto schermo, tutto in colonna, lo schermino largo */ +@media (max-width:820px),(max-height:640px){ + .vi{width:100vw;height:100svh;max-height:100svh} + .vi::before{inset:0;background:url(/art/roulette/carta.webp) 50% 50%/240% 220% no-repeat;filter:none} + .vi-dentro{overflow-y:auto;overscroll-behavior:contain;grid-template-columns:1fr;grid-template-rows:44px auto auto auto;padding:6px 12px calc(10px + env(safe-area-inset-bottom));row-gap:8px} + .vi-testa{grid-column:1;padding-left:2px;gap:8px} + .vi-testa h2{font-size:32px} + .vi-voce{font-size:15px} + .vi-testa .vi-regole-tasto{display:none} + .vi-diretta{grid-row:2} + .vi-tabellone{gap:6px;padding:5px 6px} + .vi-st{width:34px;height:34px} + .vi-sq{grid-template-columns:34px 1fr} + .vi-sq.ospite{grid-template-columns:1fr 34px} + .vi-nome{font-size:14px} + .vi-punti{font-size:26px} + .vi-meteo{white-space:normal} + .vi-tavolo{grid-column:1;grid-row:3} + .vi-quota.esito{min-height:60px} + .vi-quota{min-height:50px} + .vi-schedina{flex:none;max-height:240px} + .vi-banco{grid-column:1;grid-row:4;padding:0;flex-wrap:wrap;gap:6px 10px} + .vi-scegli button{width:40px} + .vi-scegli img{width:36px;height:36px} + .vi-tasti{flex:1 1 100%;justify-content:space-between;gap:6px 12px} + .vi-regole-tel{display:inline-block} + .vi-stato{flex:1 1 100%;text-align:center;order:3} + .vi-regole{inset:50px 10px 10px;padding:12px 14px} +} diff --git a/albertopecchini.it/pzkko/games/virtual/virtual.js b/albertopecchini.it/pzkko/games/virtual/virtual.js new file mode 100644 index 00000000..008eef70 --- /dev/null +++ b/albertopecchini.it/pzkko/games/virtual/virtual.js @@ -0,0 +1,445 @@ +// «Virtual», le scommesse sul calcio inventato di pzkko.it, in una finestra sopra il muro come gli altri giochi: il foglio +// grande col nastro, lo schermino a pixel in stile tamagotchi (schermo.js) con la partita, il tabellone (stemmi, nomi, +// punteggio, minuto, cartellini), le quote stampate sul foglio come un tavolo, le fiches fotografate (le stesse degli +// altri giochi) e la schedina. Il campionato è infinito e uguale per tutti: ogni cinque minuti una partita (quattro di +// scommesse, uno di diretta), tutto deciso dall'orario (partita.js). Le quote copiano la forza vera delle squadre, i +// risultati sono a caso: è la battuta del gioco. Soldi finti: i $pzk di ../pzk.js. +// +// Le scommesse fatte restano nella schedina (localStorage, pzkko-virtual) e si pagano al fischio finale anche se la +// finestra era chiusa: riaprendo si trova l'esito. I $pzk puntati stanno sul «tavolo» di pzk.js finché la partita non è +// pagata, come negli altri giochi (così se la pagina si chiude a metà tornano nel saldo). +// +// index.html la apre con import('/games/virtual/virtual.js').then((m) => m.apri()). + +import * as pzk from '../pzk.js' +import { PUNTATE, PER_PUNTATA, fase, partita, vince, minuto, punteggioA, scriviRisultato, GOL_MEDI } from './partita.js' +import { stemmaSVG } from './squadre.js' +import { Schermo } from './schermo.js' + +const QUI = new URL('.', import.meta.url).href +const ART = '/art/' +const GIOCO = 'virtual' +const CHIAVE = 'pzkko-virtual' +const RIDOTTO = matchMedia('(prefers-reduced-motion: reduce)') +const fmt = pzk.fmt +const fmtQuota = (q) => q.toLocaleString('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) +const suona = (nome, d = {}) => window.pzkkoSuono?.evento(nome, d) +const esc = (s) => String(s).replace(/&/g, '&').replace(/ Number.isInteger(b.indice) && PER_PUNTATA.has(b.id) && b.puntata > 0 && b.quota >= 1).slice(-40) + if (s && Array.isArray(s.ultime)) stato.ultime = s.ultime + } catch {} +} +function salva() { try { localStorage.setItem(CHIAVE, JSON.stringify({ schedina: stato.schedina, ultime: stato.ultime })) } catch {} } +const diAdesso = () => stato.schedina.filter((b) => b.indice === corrente.indice) +const sulTavolo = () => diAdesso().reduce((a, b) => a + b.puntata, 0) + +// ── la finestra ────────────────────────────────────────────────────────────── +let dlg, el = {}, schermo = null, raf = 0, ultimoT = 0 +let corrente = fase() // dove siamo nel ciclo +let detto = false // la voce ha appena detto l'esito: aggiorna() non la copre +let P = null // la partita in scena + +export async function apri() { + if (!dlg) await costruisci() + if (!dlg.open) { + dlg.classList.remove('chiude') + dlg.showModal() + el.regole.hidden = true + } + misura() + aggiorna(true) + sveglia() +} + +async function costruisci() { + carica() + await new Promise((ok) => { + const l = document.createElement('link') + l.rel = 'stylesheet'; l.href = QUI + 'virtual.css' + l.onload = ok; l.onerror = ok + document.head.appendChild(l) + }) + dlg = document.createElement('dialog') + dlg.className = 'vi' + dlg.setAttribute('aria-labelledby', 'vi-titolo') + dlg.innerHTML = ` +
+
+

virtual

+

+ + +
+
+
+
+
+
0 – 0
+
+
+

+
+
+

gol in tutto

+
    +
    +
    +

    hai

    +
    +
    + + + + +
    +
    + +
    ` + document.body.appendChild(dlg) + const q = (s) => dlg.querySelector(s) + el = { + voce: q('.vi-voce'), chiudi: q('.vi-chiudi'), schermoBox: q('.vi-schermo'), canvas: q('canvas'), tab: q('.vi-tabellone'), punti: q('.vi-punti'), minuto: q('.vi-minuto'), conto: q('.vi-conto'), meteo: q('.vi-meteo'), + casa: q('.vi-sq.casa'), ospite: q('.vi-sq.ospite'), quote: q('.vi-quote'), schedina: q('.vi-schedina'), saldo: q('.vi-saldo b'), puntato: q('.vi-saldo .puntato'), scegli: q('.vi-scegli'), + togli: q('.vi-togli'), rifai: q('.vi-rifai'), statoT: q('.vi-stato'), regole: q('.vi-regole'), regoleTasto: q('.vi-regole-tasto'), + } + // chi mi segue su instagram riceve 1.000 $pzk (una volta, ../pzk.js) + q('.vi-saldo').appendChild(pzk.linkInstagram({ fatto: () => voce(`grazie del follow: +${fmt(pzk.PREMIO_IG)} $pzk`) })) + // col conto, gli EUR€ parcheggiati nel wallet (../pzk.js, wallet/) + q('.vi-saldo').appendChild(pzk.rigaEuro()) + for (const v of pzk.FICHES) { + const b = document.createElement('button') + b.type = 'button' + b.dataset.v = v + b.setAttribute('aria-label', `fiche da ${v}`) + b.innerHTML = `` + b.addEventListener('click', () => { stato.fiche = v; aggiornaBanco() }) + el.scegli.appendChild(b) + } + // le quote: 1 X 2 su una riga; sotto gli over in fila e gli under in fila (per soglia) + const ordine = [...PUNTATE.filter((p) => p.id.length === 1), ...PUNTATE.filter((p) => p.id[0] === 'O'), ...PUNTATE.filter((p) => p.id[0] === 'U')] + for (const p of ordine) { + const b = document.createElement('button') + b.type = 'button' + b.className = `vi-quota ${p.id[0] === 'O' ? 'over' : p.id[0] === 'U' ? 'under' : 'esito'}` + b.dataset.id = p.id + b.title = p.dice + b.innerHTML = `${esc(p.nome)}–` + b.addEventListener('click', () => punta(p.id)) + el.quote.querySelector(p.id.length === 1 ? '.vi-esiti' : '.vi-ou').appendChild(b) + } + el.chiudi.addEventListener('click', chiudi) + dlg.addEventListener('cancel', (e) => { e.preventDefault(); chiudi() }) + dlg.addEventListener('click', (e) => { if (e.target === dlg) chiudi() }) + el.togli.addEventListener('click', togli) + el.rifai.addEventListener('click', rifai) + const regole = (si) => { el.regole.hidden = !si; for (const b of dlg.querySelectorAll('.vi-regole-tasto')) b.setAttribute('aria-expanded', String(si)); if (si) el.regole.scrollTop = 0 } + for (const b of dlg.querySelectorAll('.vi-regole-tasto')) b.addEventListener('click', () => regole(el.regole.hidden)) + q('.vi-regole-chiudi').addEventListener('click', () => regole(false)) + new ResizeObserver(() => { if (dlg.open) { misura(); disegna(true) } }).observe(el.schermoBox) + pzk.ascolta(() => aggiornaBanco()) // il saldo cambia anche giocando agli altri giochi + document.addEventListener('visibilitychange', () => { if (!document.hidden && dlg.open) { aggiorna(true); sveglia() } }) + // un'altra scheda ha puntato o pagato: la schedina si rilegge + addEventListener('storage', (e) => { if (e.key === CHIAVE) { carica(); riconcilia(); if (dlg.open) { quote(); schedina(); aggiornaBanco() } } }) + schermo = new Schermo(el.canvas) + // le puntate ancora aperte tornano sul tavolo di pzk.js (una ricarica le aveva rimesse nel saldo), poi le partite finite + // mentre la finestra era chiusa si pagano subito + rimettiSulTavolo(stato.schedina.filter((b) => !b.pagata).reduce((a, b) => a + b.puntata, 0)) + pagaScadute() +} +/** Fa in modo che sul tavolo di pzk.js ci siano almeno `quanto` $pzk (quelli delle puntate aperte), prendendoli dal saldo. */ +function rimettiSulTavolo(quanto) { + const manca = Math.round((quanto - pzk.sulTavolo(GIOCO)) * 2) / 2 + if (manca <= 0) return + if (!pzk.punta(GIOCO, manca)) pzk.punta(GIOCO, Math.min(manca, pzk.saldo())) +} +/** Il tavolo deve valere quanto le puntate ancora aperte: se un'altra scheda le ha pagate (o tolte), il di più torna nel saldo. */ +function riconcilia() { + const aperte = stato.schedina.filter((b) => !b.pagata).reduce((a, b) => a + b.puntata, 0) + const troppo = Math.round((pzk.sulTavolo(GIOCO) - aperte) * 2) / 2 + if (troppo > 0) pzk.ritira(GIOCO, troppo) + else if (troppo < 0) rimettiSulTavolo(aperte) +} + +function chiudi() { + if (!dlg?.open || dlg.classList.contains('chiude')) return + const fine = () => { dlg.close(); dlg.classList.remove('chiude'); if (location.hash === '#virtual') history.replaceState(null, '', location.pathname + location.search) } + if (RIDOTTO.matches) return fine() + dlg.classList.add('chiude') + setTimeout(fine, 220) +} +function voce(t) { if (el.voce) el.voce.textContent = t } +function misura() { if (schermo && el.schermoBox.clientWidth) schermo.misura(el.schermoBox.clientWidth, el.schermoBox.clientHeight) } + +// ── il ciclo: ogni fotogramma si guarda dove siamo nel ciclo e si disegna di conseguenza ── +function sveglia() { if (!raf && dlg?.open) { ultimoT = performance.now(); raf = requestAnimationFrame(ciclo) } } +function ciclo(now) { + raf = 0 + if (!dlg.open || document.hidden) return + const f = fase() + // è cambiata la fase o la partita: si rifanno il tabellone, le quote, i pagamenti + if (f.indice !== corrente.indice || f.fase !== corrente.fase) aggiorna() + else corrente = f + disegna(false, (now - ultimoT) / 1000) + ultimoT = now + // sui tempi lunghi (le scommesse, senza movimento ridotto) si va comunque a pieno ritmo: il riscaldamento si muove; + // col movimento ridotto bastano due fotogrammi al secondo + if (RIDOTTO.matches) setTimeout(sveglia, 500) + else raf = requestAnimationFrame(ciclo) +} + +/** La fase è cambiata (o la finestra si è aperta): la partita di adesso, le quote, i pagamenti, il tabellone. */ +function aggiorna(apertura = false) { + const prima = corrente + corrente = fase() + P = partita(corrente.indice) + if (!apertura && prima.indice !== corrente.indice) { + // la partita prima è finita: si paga (le scommesse di quella partita) + paga(prima.indice) + } else pagaScadute() + if (corrente.fase === 'partita' && (apertura || prima.fase !== 'partita')) { if (!apertura) suona('virtual-fischio'); voce(`in diretta: ${P.casa.nome} – ${P.ospite.nome}`) } + else if (corrente.fase === 'scommesse' && !detto) voce(sulTavolo() ? `hai ${fmt(sulTavolo())} $pzk sulla partita: puoi ancora puntare` : `${P.casa.nome} – ${P.ospite.nome}: scegli una fiche e tocca una quota`) + detto = false + tabellone() + quote() + schedina() + aggiornaBanco() +} + +/** Le scommesse delle partite già finite che non sono state pagate (la finestra era chiusa): si pagano ora. */ +function pagaScadute() { + const adesso = fase().indice + const indici = new Set(stato.schedina.filter((b) => !b.pagata && b.indice < adesso).map((b) => b.indice)) + for (const i of indici) paga(i, true) +} +function paga(indice, silenzio = false) { + // (un'altra scheda può aver già pagato: si rilegge quello che c'è scritto nel browser) + carica() + const mie = stato.schedina.filter((b) => b.indice === indice && !b.pagata) + if (!mie.length) { riconcilia(); return } + const r = partita(indice).risultato + let giocato = 0, torna = 0 + for (const b of mie) { + b.esito = vince(b.id, r) ? 'vinta' : 'persa' + b.pagata = true + b.risultato = scriviRisultato(r) + giocato += b.puntata + if (b.esito === 'vinta') torna += Math.round(b.puntata * b.quota * 2) / 2 + } + // il tavolo di pzk.js potrebbe non avere queste puntate: ricaricando la pagina pzk.js rimette nel saldo quello che era + // sui tavoli (vedi ../pzk.js), e all'apertura rimettiamo sul tavolo le puntate ancora aperte (rimettiSulTavolo); se non + // bastavano i $pzk (spesi altrove) la puntata si regola per quanto c'è davvero + rimettiSulTavolo(giocato) + const g = Math.min(giocato, pzk.sulTavolo(GIOCO)) + pzk.regola(GIOCO, g, giocato ? (torna * g) / giocato : 0) + // le puntate di questa partita restano come «ultime», per «rifai» + stato.ultime = mie.map((b) => ({ id: b.id, puntata: b.puntata })) + salva() + if (!silenzio) { + const p = partita(indice) + voce(torna > 0 ? `${p.casa.sigla}–${p.ospite.sigla} ${scriviRisultato(r)}: hai vinto ${fmt(torna)} $pzk` : `${p.casa.sigla}–${p.ospite.sigla} ${scriviRisultato(r)}: niente, stavolta`) + detto = true + if (torna > 0) suona('virtual-vinta') + } +} + +// ── il tabellone ───────────────────────────────────────────────────────────── +function tabellone() { + for (const sq of ['casa', 'ospite']) { + const e = el[sq], s = P[sq] + e.querySelector('.vi-st').innerHTML = stemmaSVG(s) + e.querySelector('.vi-nome').textContent = s.nome + e.querySelector('.vi-citta').textContent = `${s.citta} · ${P.forze[sq]}` + } + const m = P.meteo + const meteoTxt = { sereno: 'sereno', nuvole: 'nuvoloso', pioggia: 'pioggia', nebbia: 'nebbia', neve: 'neve', vento: 'vento forte' }[m.tipo] + el.meteo.textContent = `a ${P.casa.citta}: ${meteoTxt}, ${m.temp}°${m.notte ? ', in notturna coi riflettori' : m.sera ? ', al tramonto' : ''} · ultime 5 (inventate): ${P.casa.sigla} ${P.ultime.casa.join('')} · ${P.ospite.sigla} ${P.ultime.ospite.join('')}` + dlg.classList.toggle('diretta', corrente.fase === 'partita') +} +let ultimoMin = '', ultimiPunti = '' +function tabelloneVivo(t) { + if (corrente.fase === 'partita') { + const pt = punteggioA(P, Math.max(0, t)) + const sospesa = P.eventi.find((e) => e.tipo === 'sospesa' && e.t <= t) + const punti = sospesa ? `${P.risultato.casa} – ${P.risultato.ospite}` : `${pt.casa} – ${pt.ospite}` + const mn = sospesa ? 'SOSP.' : minuto(t).scritta + if (punti !== ultimiPunti) { el.punti.textContent = punti; ultimiPunti = punti; if (!RIDOTTO.matches) { el.punti.classList.remove('salta'); void el.punti.offsetWidth; el.punti.classList.add('salta') } } + if (mn !== ultimoMin) { el.minuto.textContent = mn === 'INT' ? 'intervallo' : mn === 'FINE' ? 'finale' : mn; ultimoMin = mn } + const manca = Math.ceil(corrente.manca / 1000) + el.conto.textContent = `IN DIRETTA · finisce tra ${manca}s` + // i cartellini + const c = schermo.cartelliniA(P, t) + for (const sq of ['casa', 'ospite']) el[sq].querySelector('.vi-cart').innerHTML = ''.repeat(c.gialli[sq]) + ''.repeat(c.rossi[sq]) + } else { + const s = Math.ceil(corrente.manca / 1000), mm = Math.floor(s / 60), ss = s % 60 + const txt = `si chiude tra ${mm}:${String(ss).padStart(2, '0')}` + if (el.conto.textContent !== txt) el.conto.textContent = txt + if (ultimiPunti !== '–') { el.punti.textContent = '–'; ultimiPunti = '–'; el.minuto.textContent = 'scommesse aperte'; ultimoMin = ''; for (const sq of ['casa', 'ospite']) el[sq].querySelector('.vi-cart').innerHTML = '' } + } +} + +// ── le quote ───────────────────────────────────────────────────────────────── +function quote() { + const aperte = corrente.fase === 'scommesse' + for (const b of el.quote.querySelectorAll('.vi-quota')) { + const id = b.dataset.id + b.querySelector('.vi-q-val').textContent = fmtQuota(P.quote[id]) + b.disabled = !aperte + const mie = diAdesso().filter((x) => x.id === id).reduce((a, x) => a + x.puntata, 0) + pila(b.querySelector('.vi-q-pila'), mie) + } + // la favorita evidenziata a pennarello + const fav = P.quote[1] < P.quote[2] ? '1' : '2' + for (const b of el.quote.querySelectorAll('.vi-quota')) b.classList.toggle('fav', b.dataset.id === fav) +} +function pila(cont, importo) { + cont.innerHTML = '' + if (!importo) return + const f = pzk.inFiches(importo).slice(0, 8) + f.forEach((v, i) => { const img = document.createElement('img'); img.src = `${ART}fiches/f${v}.webp`; img.alt = ''; img.style.transform = `translateY(${-i * 2.4}px) rotate(${((v * 37 + i * 101) % 23) - 11}deg)`; cont.appendChild(img) }) + const n = document.createElement('b'); n.textContent = fmt(importo); cont.appendChild(n) +} + +function punta(id) { + if (corrente.fase !== 'scommesse') { voce('la partita è in corso: si punta sulla prossima'); return } + const v = stato.fiche + if (pzk.aSecco()) { + const dati = pzk.ricarica() + aggiornaBanco() + voce(dati ? `altri ${fmt(pzk.INIZIALE)} $pzk finti: punta` : pzk.perche()) + if (!dati) return + } + if (!pzk.punta(GIOCO, v)) { voce(pzk.saldo() >= 1 ? `non ti bastano: hai ${fmt(pzk.saldo())} $pzk` : 'hai finito i $pzk'); scuoti(el.saldo); return } + const q = P.quote[id] + // la stessa puntata sulla stessa quota si somma + const gia = stato.schedina.find((b) => b.indice === corrente.indice && b.id === id && !b.pagata) + if (gia) gia.puntata += v + else stato.schedina.push({ indice: corrente.indice, id, quota: q, puntata: v, esito: null, pagata: false, casa: P.casa.sigla, ospite: P.ospite.sigla }) + stato.schedina = stato.schedina.slice(-40) + salva() + const b = el.quote.querySelector(`[data-id="${id}"]`) + vola(v, centro(el.scegli.querySelector(`[data-v="${v}"] img`) ?? el.saldo), centro(b.querySelector('.vi-q-pila')), { alto: 30 }).then(() => { pila(b.querySelector('.vi-q-pila'), diAdesso().filter((x) => x.id === id).reduce((a, x) => a + x.puntata, 0)) }) + suona('virtual-fiche') + schedina() + aggiornaBanco() + const pu = PER_PUNTATA.get(id) + voce(`${pu.nome} a ${fmtQuota(q)}: se va, ${fmt(Math.round(v * q * 2) / 2)} $pzk`) +} +// «togli»: l'ultima fiche posata su questa partita torna nel saldo +function togli() { + if (corrente.fase !== 'scommesse') return + const mie = diAdesso().filter((b) => !b.pagata) + const b = mie[mie.length - 1] + if (!b) return + // si toglie l'ultima fiche della sua pila + const f = pzk.inFiches(b.puntata) + const v = f[f.length - 1] + b.puntata -= v + if (b.puntata <= 0) stato.schedina.splice(stato.schedina.indexOf(b), 1) + pzk.ritira(GIOCO, v) + salva() + const q = el.quote.querySelector(`[data-id="${b.id}"] .vi-q-pila`) + vola(v, centro(q), centro(el.scegli.querySelector(`[data-v="${v}"] img`) ?? el.saldo), { durata: 300, alto: 24 }) + quote(); schedina(); aggiornaBanco() +} +// «rifai»: le puntate dell'ultima partita sulla partita di adesso (alle quote di adesso) +async function rifai() { + if (corrente.fase !== 'scommesse' || !stato.ultime?.length || sulTavolo()) return + const tot = stato.ultime.reduce((a, b) => a + b.puntata, 0) + if (tot > pzk.saldo()) { voce(`per rifare servono ${fmt(tot)} $pzk`); scuoti(el.saldo); return } + const prima = stato.fiche + for (const u of stato.ultime) for (const v of pzk.inFiches(u.puntata)) { stato.fiche = v; punta(u.id); await new Promise((r) => setTimeout(r, RIDOTTO.matches ? 0 : 70)) } + stato.fiche = prima + aggiornaBanco() +} + +// ── la schedina ────────────────────────────────────────────────────────────── +function schedina() { + const mie = [...stato.schedina].reverse().slice(0, 12) + if (!mie.length) { el.schedina.innerHTML = '
  1. la schedina è vuota: tocca una quota
  2. '; return } + el.schedina.innerHTML = mie.map((b) => { + const pu = PER_PUNTATA.get(b.id), diQuesta = b.indice === corrente.indice + const vinc = Math.round(b.puntata * b.quota * 2) / 2 + const esito = b.pagata ? (b.esito === 'vinta' ? `+${fmt(vinc)}` : `persa`) : diQuesta && corrente.fase === 'partita' ? 'in corso' : `→ ${fmt(vinc)}` + return `
  3. ${esc(b.casa)}–${esc(b.ospite)}${b.risultato ? ` ${esc(b.risultato)}` : ''}${esc(pu.nome)} @ ${fmtQuota(b.quota)}${fmt(b.puntata)}${esito}
  4. ` + }).join('') +} + +// ── il banco ───────────────────────────────────────────────────────────────── +function aggiornaBanco() { + if (!el.saldo) return + el.saldo.textContent = `$pzk ${fmt(pzk.saldo())}` + const p = pzk.sulTavolo(GIOCO) + el.puntato.textContent = p ? `sulla partita: ${fmt(p)}` : '' + const aperte = corrente.fase === 'scommesse' + for (const b of el.scegli.children) { b.setAttribute('aria-pressed', String(Number(b.dataset.v) === stato.fiche)); b.disabled = !aperte } + el.togli.disabled = !aperte || !diAdesso().length + el.rifai.disabled = !aperte || !stato.ultime?.length || !!sulTavolo() + el.statoT.textContent = aperte ? 'scommesse aperte' : 'scommesse chiuse: si gioca' + dlg.classList.toggle('chiuso', !aperte) +} +function scuoti(e) { if (!RIDOTTO.matches) e.animate([{ transform: 'translateX(0)' }, { transform: 'translateX(-5px)' }, { transform: 'translateX(5px)' }, { transform: 'translateX(-3px)' }, { transform: 'translateX(0)' }], { duration: 300 }) } +const centro = (e) => { const r = e.getBoundingClientRect(); return { x: r.left + r.width / 2, y: r.top + r.height / 2 } } +/** Una fiche che vola da a a b (coordinate della finestra), con l'arco e l'ombra, come negli altri giochi. */ +function vola(v, a, b, { durata = 380, alto = 40, svanisce = false } = {}) { + if (RIDOTTO.matches) return Promise.resolve() + const img = document.createElement('img') + img.className = 'vi-volo' + img.src = `${ART}fiches/f${v}.webp` + img.alt = '' + dlg.appendChild(img) + const mx = (a.x + b.x) / 2, my = Math.min(a.y, b.y) - alto + const anim = img.animate([ + { transform: `translate(${a.x}px, ${a.y}px) scale(1)`, opacity: 1 }, + { offset: 0.5, transform: `translate(${mx}px, ${my}px) scale(1.3)`, opacity: 1 }, + { transform: `translate(${b.x}px, ${b.y}px) scale(1)`, opacity: svanisce ? 0 : 1 }, + ], { duration: durata, easing: 'cubic-bezier(.3,.7,.4,1)', fill: 'forwards' }) + return anim.finished.then(() => img.remove(), () => img.remove()) +} + +// ── il disegno dello schermino e del tabellone, a ogni fotogramma ─────────── +let ultimoEvento = null +function disegna(forza = false, dt = 1 / 60) { + if (!schermo || !P) return + const f = corrente + const now = Date.now() + let t, fs + if (f.fase === 'partita') { t = (now - P.inizio) / 1000; fs = 'partita' } + else { t = -(f.manca / 1000); fs = 'scommesse' } + schermo.disegna(P, { fase: fs, t, ridotto: RIDOTTO.matches, dt }) + tabelloneVivo(t) + // i suoni degli eventi (una volta per evento), e la voce + const e = schermo.evento + if (e !== ultimoEvento) { + ultimoEvento = e + if (e && f.fase === 'partita') { + if (e.tipo === 'gol') { suona('virtual-gol'); voce(`GOL! ${e.chi.nome} (${P[e.squadra].nome}) al ${e.minuto}`) } + else if (e.tipo === 'tiro' && e.esito === 'palo') { suona('virtual-palo'); voce(`palo di ${P[e.squadra].sigla} al ${e.minuto}!`) } + else if (e.tipo === 'rigore') voce(`rigore per ${P[e.squadra].nome}: ${e.esito}`) + else if (e.tipo === 'rosso') voce(`espulso ${e.chi.nome}: ${P[e.squadra].nome} in quattro`) + else if (e.tipo === 'giallo') voce(`ammonito ${e.chi.nome} (${P[e.squadra].sigla})`) + else if (e.tipo === 'infortunio') voce(`${e.chi.nome} esce in barella, entra ${e.entra.nome}`) + else if (e.tipo === 'sospesa') voce(`partita sospesa: ${e.causa}. ${scriviRisultato(P.risultato)}`) + else if (e.tipo === 'intervallo') { suona('virtual-fischio'); voce('intervallo') } + else if (e.tipo === 'fine') { suona('virtual-fischio'); voce(`finale: ${scriviRisultato(P.risultato)}`) } + } + } +} diff --git a/albertopecchini.it/tools/pzkko/virtual/prova.mjs b/albertopecchini.it/tools/pzkko/virtual/prova.mjs new file mode 100644 index 00000000..515f855d --- /dev/null +++ b/albertopecchini.it/tools/pzkko/virtual/prova.mjs @@ -0,0 +1,139 @@ +#!/usr/bin/env node +// Le prove di «Virtual» (pzkko/games/virtual/partita.js): costruisce N partite di fila dal calendario (indici +// consecutivi, come quelle che il sito farà vedere) e controlla, senza browser: +// - che la stessa partita chiesta due volte sia identica (il determinismo: due schede vedono la stessa cosa); +// - che le quote seguano la forza: la favorita (forza più alta) ha la quota più bassa, sempre; la correlazione fra la +// differenza di forza e la quota dell'1 è forte e negativa; 1 X 2 sommano a ~106% in probabilità implicita e +// over/under della stessa soglia pure; nessuna quota sotto 1,05; +// - che i risultati NON dipendano dalla forza: la favorita vince quanto la sfavorita (percentuali e un chi quadro +// fra «chi è più forte» e l'esito 1/X/2), la correlazione fra differenza di forza e differenza di gol è ~0, i gol a +// squadra fanno in media GOL_MEDI con la varianza di una Poisson, la quota non predice niente; +// - che la cronaca sia sensata: i gol della cronaca sono quelli del risultato, gli eventi stanno in ordine e dentro i 60 +// secondi, i grandi eventi ad almeno 3 s l'uno dall'altro, mai a cavallo dell'intervallo; il rosso lascia la squadra in +// quattro; il tavolino dà 3–0 e paga come tale; +// - che le puntate paghino giuste: per ogni partita, ogni puntata vincente è coerente col risultato (un conto fatto in un +// altro modo), e il ritorno al giocatore di chi punta sempre la stessa cosa sta attorno a 100% − margine. +// +// node tools/pzkko/virtual/prova.mjs [partite, 2000] [--da=indice] + +import { partita, vince, PUNTATE, GOL_MEDI, MARGINE, QUOTA_MINIMA, SOGLIE, T, indiceAdesso } from '../../../pzkko/games/virtual/partita.js' + +const arg = (n, d) => { const a = process.argv.find((x) => x.startsWith(`--${n}=`)); return a ? a.slice(n.length + 3) : d } +const N = Number(process.argv.slice(2).find((a) => /^\d+$/.test(a)) ?? 2000) +const DA = Number(arg('da', indiceAdesso())) +const errori = [] +const err = (s) => { if (errori.length < 30) errori.push(s) } +const pct = (x) => `${(100 * x).toFixed(1)}%` +const corr = (xs, ys) => { + const n = xs.length, mx = xs.reduce((a, b) => a + b, 0) / n, my = ys.reduce((a, b) => a + b, 0) / n + let sxy = 0, sxx = 0, syy = 0 + for (let i = 0; i < n; i++) { sxy += (xs[i] - mx) * (ys[i] - my); sxx += (xs[i] - mx) ** 2; syy += (ys[i] - my) ** 2 } + return sxy / Math.sqrt(sxx * syy) +} +// p di un chi quadro con gdl gradi (serie, basta per gdl piccoli) +function pChi(x, gdl) { + const gamma = (s, z) => { let sum = 1 / s, term = 1 / s; for (let k = 1; k < 200; k++) { term *= z / (s + k); sum += term } return sum * Math.exp(-z + s * Math.log(z)) } + const G = (s) => (s === 0.5 ? Math.sqrt(Math.PI) : s === 1 ? 1 : (s - 1) * G(s - 1)) + return 1 - gamma(gdl / 2, x / 2) / G(gdl / 2) +} + +const esiti = { fav: { 1: 0, X: 0, 2: 0 }, sfav: { 1: 0, X: 0, 2: 0 } } // 1 = vince la favorita, X, 2 = vince la sfavorita +const perForza = [] // [diff forza, diff gol] +const perQuota = [] // [quota 1, casa vince] +let golTot = 0, golQ = 0, nGol = 0, tavolini = 0, rossi = 0, rigori = 0, infortuni = 0, incroci = 0 +const ritorno = Object.fromEntries(PUNTATE.map((p) => [p.id, { puntato: 0, tornato: 0 }])) +const casaVince = { si: 0, tot: 0 } +const meteo = {} +let sommaMedia = 0 + +for (let k = 0; k < N; k++) { + const i = DA + k + const p = partita(i), p2 = partita(i) + if (JSON.stringify(p) !== JSON.stringify(p2)) err(`${i}: due costruzioni diverse`) + if (p.casa === p.ospite) err(`${i}: la squadra gioca contro se stessa`) + if (p.casa.serie !== p.ospite.serie) incroci++ + meteo[p.meteo.tipo] = (meteo[p.meteo.tipo] ?? 0) + 1 + // le quote + const q = p.quote + for (const [id, v] of Object.entries(q)) if (!(v >= QUOTA_MINIMA) || Math.abs(Math.round(v * 100) - v * 100) > 1e-6) err(`${i}: quota ${id} = ${v}`) + const s1x2 = 1 / q[1] + 1 / q.X + 1 / q[2] + sommaMedia += s1x2 + if (Math.abs(s1x2 - (1 + MARGINE)) > 0.02 && q[1] > QUOTA_MINIMA && q[2] > QUOTA_MINIMA) err(`${i}: 1 X 2 sommano a ${pct(s1x2)}`) + for (const s of SOGLIE) { const so = 1 / q[`O${s}`] + 1 / q[`U${s}`]; if (Math.abs(so - (1 + MARGINE)) > 0.03 && q[`O${s}`] > QUOTA_MINIMA && q[`U${s}`] > QUOTA_MINIMA) err(`${i}: over/under ${s} sommano a ${pct(so)}`) } + const favCasa = p.forze.casa + 4 >= p.forze.ospite + if (p.forze.casa - p.forze.ospite > 8 && q[1] >= q[2]) err(`${i}: ${p.casa.nome} (${p.forze.casa}) in casa quotata ${q[1]} contro ${p.ospite.nome} (${p.forze.ospite}) a ${q[2]}`) + if (p.forze.ospite - p.forze.casa > 12 && q[2] >= q[1]) err(`${i}: ${p.ospite.nome} (${p.forze.ospite}) fuori quotata ${q[2]} contro ${p.casa.nome} (${p.forze.casa}) a ${q[1]}`) + // il risultato + const r = p.risultato + const diffForza = p.forze.casa - p.forze.ospite + if (r.tavolino) { tavolini++; if (!((r.casa === 3 && r.ospite === 0) || (r.casa === 0 && r.ospite === 3))) err(`${i}: tavolino ${r.casa}-${r.ospite}`) } + else { + perForza.push([diffForza, r.casa - r.ospite]) + perQuota.push([q[1], r.casa > r.ospite ? 1 : 0]) + golTot += r.casa + r.ospite; golQ += r.casa ** 2 + r.ospite ** 2; nGol += 2 + const chiVince = r.casa > r.ospite ? 'casa' : r.casa < r.ospite ? 'ospite' : 'X' + const e = chiVince === 'X' ? 'X' : (chiVince === 'casa') === favCasa ? '1' : '2' + esiti[Math.abs(diffForza) >= 15 ? 'fav' : 'sfav'][e]++ + casaVince.tot++; if (chiVince === 'casa') casaVince.si++ + } + // la cronaca + const ev = p.eventi + let c = 0, o = 0, prima = -1, ultimoGrande = -10, inCampo = { casa: 5, ospite: 5 } + for (const e of ev) { + if (!(e.t >= 0 && e.t <= T.fine)) err(`${i}: evento ${e.tipo} a ${e.t}`) + if (e.t < prima) err(`${i}: eventi in disordine (${e.tipo} a ${e.t} dopo ${prima})`) + prima = e.t + if (e.tipo === 'gol') { if (e.squadra === 'casa') c++; else o++; if (e.rigore) rigori++ } + if (e.tipo === 'rigore') rigori++ + if (e.tipo === 'rosso') { rossi++; inCampo[e.squadra]-- } + if (e.tipo === 'infortunio') infortuni++ + if (['gol', 'rigore', 'giallo', 'rosso', 'infortunio'].includes(e.tipo)) { + if (e.t - ultimoGrande < 3.1) err(`${i}: ${e.tipo} a ${e.t.toFixed(1)} troppo vicino al grande evento prima (${ultimoGrande.toFixed(1)})`) + if (e.t > T.fine1 - 1.5 && e.t < T.via2 + 0.5) err(`${i}: ${e.tipo} a cavallo dell'intervallo (${e.t.toFixed(1)})`) + ultimoGrande = e.t + } + } + if (!r.tavolino && (c !== r.casa || o !== r.ospite)) err(`${i}: la cronaca dice ${c}-${o}, il risultato ${r.casa}-${r.ospite}`) + if (r.tavolino && !ev.some((e) => e.tipo === 'sospesa')) err(`${i}: tavolino senza sospensione`) + if (!r.tavolino && ev[ev.length - 1].tipo !== 'fine') err(`${i}: non finisce col fischio finale`) + if (inCampo.casa < 4 || inCampo.ospite < 4) err(`${i}: una squadra sotto i quattro`) + // le puntate: ogni id contro un conto fatto al contrario, e il ritorno di chi punta sempre 1 $pzk su ognuna + const tot = r.casa + r.ospite + const giuste = new Set([r.casa > r.ospite ? '1' : r.casa < r.ospite ? '2' : 'X', ...SOGLIE.map((s) => (tot > s ? `O${s}` : `U${s}`))]) + for (const pu of PUNTATE) { + const v = vince(pu.id, r) + if (v !== giuste.has(pu.id)) err(`${i}: la puntata ${pu.id} ${v ? 'vince' : 'perde'} su ${r.casa}-${r.ospite}`) + ritorno[pu.id].puntato += 1 + if (v) ritorno[pu.id].tornato += q[pu.id] + } +} + +console.log(`\nVirtual: ${N} partite dall'indice ${DA} (${new Date(DA * 300000).toISOString()})`) +console.log(`incroci A–B ${pct(incroci / N)} · tavolini ${tavolini} (${pct(tavolini / N)}) · rossi ${rossi} · rigori ${rigori} · infortuni ${infortuni}`) +console.log(`meteo: ${Object.entries(meteo).map(([k, v]) => `${k} ${pct(v / N)}`).join(', ')}`) +console.log(`\nLE QUOTE SEGUONO LA FORZA`) +console.log(` 1 X 2 in probabilità implicita: ${pct(sommaMedia / N)} in media (atteso ${pct(1 + MARGINE)})`) +const df = perForza.map((x) => x[0]), q1 = perQuota.map((x) => x[0]) +console.log(` correlazione fra (forza casa − forza ospite) e quota dell'1: ${corr(df, q1).toFixed(3)} (forte e negativa: più forte la casa, più bassa la quota)`) +console.log(`\nI RISULTATI NON DIPENDONO DALLA FORZA`) +const dg = perForza.map((x) => x[1]) +console.log(` correlazione fra (forza casa − forza ospite) e (gol casa − gol ospite): ${corr(df, dg).toFixed(3)} (atteso ~0)`) +console.log(` correlazione fra la quota dell'1 e «la casa vince»: ${corr(q1, perQuota.map((x) => x[1])).toFixed(3)} (atteso ~0: la quota non predice niente)`) +const tf = esiti.fav[1] + esiti.fav.X + esiti.fav[2], ts = esiti.sfav[1] + esiti.sfav.X + esiti.sfav[2] +console.log(` partite con una favorita netta (≥ 15 punti di forza), ${tf}: vince la favorita ${pct(esiti.fav[1] / tf)}, pareggio ${pct(esiti.fav.X / tf)}, vince la sfavorita ${pct(esiti.fav[2] / tf)}`) +console.log(` partite equilibrate, ${ts}: vince la «favorita» ${pct(esiti.sfav[1] / ts)}, pareggio ${pct(esiti.sfav.X / ts)}, vince l'altra ${pct(esiti.sfav[2] / ts)}`) +// chi quadro: favorita netta vs equilibrate, sugli esiti +{ + const righe = [esiti.fav, esiti.sfav], tot = tf + ts + let chi = 0 + for (const e of ['1', 'X', '2']) { const col = esiti.fav[e] + esiti.sfav[e]; for (const r of righe) { const n = r[1] + r.X + r[2], att = (n * col) / tot; if (att > 0) chi += (r[e] - att) ** 2 / att } } + console.log(` chi quadro «favorita netta o no» × esito: ${chi.toFixed(2)} su 2 gradi, p = ${pChi(chi, 2).toFixed(2)} (se dipendesse dalla forza sarebbe ~0)`) +} +console.log(` la casa vince ${pct(casaVince.si / casaVince.tot)} (nessun fattore campo nei risultati: quanto gli ospiti, 36,1% per due Poisson di media 1,35)`) +const media = golTot / nGol, varianza = golQ / nGol - media ** 2 +console.log(` gol a squadra: media ${media.toFixed(3)} (atteso ${GOL_MEDI}), varianza ${varianza.toFixed(3)} (Poisson: uguale alla media)`) +console.log(`\nLE PUNTATE PAGANO GIUSTE`) +for (const pu of PUNTATE) { const r = ritorno[pu.id]; console.log(` ${pu.nome.padEnd(10)} ritorno ${pct(r.tornato / r.puntato)}`) } +console.log(` (chi punta a caso sulle quote «vere» con risultati a caso: la favorita paga poco e vince come l'altra, quindi sull'1 e sul 2 il ritorno dipende da chi si sceglie; over/under ~${pct(1 / (1 + MARGINE))})`) +console.log(errori.length ? `\n${errori.length} ERRORI:\n ${errori.join('\n ')}` : '\nnessun errore') +process.exit(errori.length ? 1 : 0) From 94ffb997d1becaa944b360eb081212924a80b7e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 14:02:34 +0200 Subject: [PATCH 03/11] feat(pzkko): il volantino di virtual sul muro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Il foglio arancio in fondo alla quinta colonna (sul telefono alla prima), col titolo in Bungee e il pallone a pixel disegnato in SVG (art/virtual/pallone.svg, niente immagini generate); #virtual lo apre subito e il codice si precarica passando sul foglio. La parete cresce di poco (3355 e 5691 unità), la 404 e la storia rifatte, le foto del volantino per la scena 3D (solo le sue: le altre non sono state rifatte). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015VZM7WrmYAstfLRGvRUqFy --- albertopecchini.it/pzkko/404.html | 11 +- .../pzkko/art/scena/vol-lo/virtual.webp | Bin 0 -> 3136 bytes .../pzkko/art/scena/vol-m-lo/virtual.webp | Bin 0 -> 3982 bytes albertopecchini.it/pzkko/art/scena/vol-m.json | 10 +- .../pzkko/art/scena/vol-m/virtual.webp | Bin 0 -> 32038 bytes albertopecchini.it/pzkko/art/scena/vol.json | 10 +- .../pzkko/art/scena/vol/virtual.webp | Bin 0 -> 39346 bytes .../pzkko/art/virtual/pallone.svg | 36 ++++++ albertopecchini.it/pzkko/games/pzk.js | 2 +- albertopecchini.it/pzkko/index.html | 120 ++++++++++-------- albertopecchini.it/pzkko/storia/index.html | 7 +- .../tools/pzkko/disposizione.js | 2 + 12 files changed, 134 insertions(+), 64 deletions(-) create mode 100644 albertopecchini.it/pzkko/art/scena/vol-lo/virtual.webp create mode 100644 albertopecchini.it/pzkko/art/scena/vol-m-lo/virtual.webp create mode 100644 albertopecchini.it/pzkko/art/scena/vol-m/virtual.webp create mode 100644 albertopecchini.it/pzkko/art/scena/vol/virtual.webp create mode 100644 albertopecchini.it/pzkko/art/virtual/pallone.svg diff --git a/albertopecchini.it/pzkko/404.html b/albertopecchini.it/pzkko/404.html index ee82b6c1..279cc53b 100644 --- a/albertopecchini.it/pzkko/404.html +++ b/albertopecchini.it/pzkko/404.html @@ -156,7 +156,7 @@ .muro::after{content:"";position:absolute;inset:0;z-index:1;pointer-events:none;mix-blend-mode:screen;background:url(/art/muro.webp) 0 0/2200px auto repeat;filter:grayscale(1) contrast(3.2) brightness(1.25);opacity:calc(.75 * var(--auto-l)); -webkit-mask-image:radial-gradient(34% 100% at calc(var(--auto-x) * 100%) 50%,#000,transparent 70%); mask-image:radial-gradient(34% 100% at calc(var(--auto-x) * 100%) 50%,#000,transparent 70%)} -.parete{position:relative;height:calc(3252 * var(--u))} +.parete{position:relative;height:calc(3320 * var(--u))} .gruppo{display:contents} /* ogni cosa sul muro è posizionata in 1280esimi (e in 390esimi sul telefono) */ @@ -295,7 +295,7 @@ /* ── il telefono: il muro visto da vicino, due volantini per riga ─────────── */ @media (max-width:899px){ :root{--u:calc(min(100cqw, 600px) / 390)} - .parete{height:calc(5654 * var(--u));max-width:600px;margin:0 auto} + .parete{height:calc(5684 * var(--u));max-width:600px;margin:0 auto} .it{--px:var(--mx,var(--x));--py:var(--my,var(--y));--pw:var(--mw,var(--w));--pr:var(--mr,var(--r,0deg))} .spray{font-size:calc(40 * var(--u))} .vol h3{font-size:max(calc(22px * var(--ts,1)),calc(16cqw * var(--ts,1)))} @@ -831,15 +831,16 @@

    questa pagina non c'è.

    setTimeout(tic, veloce > 1 ? 250 : 1000) document.addEventListener('visibilitychange', () => { if (!document.hidden) aggiorna() }) - // I giochi: la roulette, il blackjack, testa o croce, le mine e il dado, ognuno in una finestra sopra il muro - // (games//). Il codice e il 3D si caricano solo quando si apre (o quando - // il puntatore passa sul volantino); #roulette, #blackjack, #moneta, #mine o #dado nell'indirizzo li apre subito. + // I giochi: la roulette, il blackjack, testa o croce, le mine, il dado e virtual, ognuno in una finestra sopra il muro + // (games//). Il codice e il 3D si caricano solo quando si apre (o quando il puntatore passa sul volantino); + // #roulette, #blackjack, #moneta, #mine, #dado o #virtual nell'indirizzo li apre subito. const GIOCHI = { roulette: ['roulette.js', 'ruota.js', 'regole.js', 'lib/three.js'].map((f) => `/games/roulette/${f}`), blackjack: ['blackjack.js', 'regole.js', 'scena.js', 'carte.js', '../pzk.js', '../roulette/lib/three.js'].map((f) => new URL(f, `${location.origin}/games/blackjack/`).pathname), moneta: ['moneta.js', 'lancio.js', 'scena.js', '../pzk.js', '../roulette/lib/three.js'].map((f) => new URL(f, `${location.origin}/games/moneta/`).pathname), mine: ['mine.js', 'regole.js', 'scena.js', 'tessere.js', '../pzk.js', '../roulette/lib/three.js'].map((f) => new URL(f, `${location.origin}/games/mine/`).pathname), dado: ['dado.js', 'regole.js', 'campana.js', '../pzk.js', '../roulette/lib/three.js', '../roulette/lib/rapier.mjs'].map((f) => new URL(f, `${location.origin}/games/dado/`).pathname), + virtual: ['virtual.js', 'partita.js', 'squadre.js', 'schermo.js', '../pzk.js'].map((f) => new URL(f, `${location.origin}/games/virtual/`).pathname), } const apriGioco = (nome) => import(`/games/${nome}/${nome}.js`).then((m) => m.apri()) for (const gioco of document.querySelectorAll('a[data-gioco]')) { diff --git a/albertopecchini.it/pzkko/art/scena/vol-lo/virtual.webp b/albertopecchini.it/pzkko/art/scena/vol-lo/virtual.webp new file mode 100644 index 0000000000000000000000000000000000000000..4fbf6d0f6d7f956432ab5ed7f82f3d96c83da97a GIT binary patch literal 3136 zcmV-G48QYINk&FE3;+OEMM6+kP&il$0000G0001g005Z)06|PpNNxfE01Aw+5=}g;Uw6vp>B_gByXj0xd?1quH zPD|eEaohJj&!8;JWBWvm&ZzW?TNb5xT(Cz>Pl6@UD$Ct?3grc zQyKtf*-RE}lkFF^J43#fNOxYd@s!lcHp6h^JSKfeRq)jaYlfFSBTGK1zls8^^^ZrLIYFQ0#Ps-=8`gCZQ z6Om@MJWlz%6VW7@@;^V|IiUSb`iea%pDF6oRp(XRU#|oDzU71GhQ9AB?-95MJ_PKZ zZ?X>h+-tl8t@sSIKWDrGTBOz{w5rf)V1+@(4X82yz|!(DWhVO1$y2=Fh^;k1 zw`jvg9&D{aI)pLyVr;EJ;zS7ZUS=zFav)@B!8#Y`inEn)Lb-G;V;BuqP&go@2><{v zEC8JWDu4i*06wu!pG&7CKcT4A-1G1j31%~aOVdkYg@78sdP>9qF@=`b*QDRB<}K~W z=(;cA`}co{{aF6J@e`XjqhB$8aDVH+WWCJ)iS*#=QS<=mcKiU#B>Z5`kXjoGj!j57W>?IzP-w zulOS1oks>iiVkISf2V7zuiAgqO35+IUoZLH%Xgei?fs_(nwgj)R`96PM0(}pH9JiP zJA09c;0CB>dt7QUd-W|(T#9{#S*2JyCz|rCH?Shnge#bS^$ZPhS#A{yV);gMgBtl} zJ~^i(U9P(aV-io+)CFQ|O9I7YXOgv=Zq;g#RZbrT#Aw=IWxN}$yL;&rcSs=U0IA7@ zvu>~0RECsFZWb}UE~`Im-UKWyT7C>m7j8SzT=-%fx24w9QbKzd@31c0H-cqR*+NI_ zkA7N3s1AiZ2;R&Hb8~cJl2Z9^h9KWbGB5uGZo^j%uCmD8QxEySXZRMuF(iGDy3^VI zLy@vXJ6|?Q@D4kh!ES?EXwBPZY0&Y)FIjwAaN5VtgnX3=eoRYzMWo&IV4?`OCSzz1g_aL{Iq8JC-{ zGdWpXYW*Q}!uG_*WHkNn%BkCkE*jO&TqvZGSL0eF&ir-sQaPFL*QpbZ!E1%oVO4Qs zY!Pgwd&#$s^9<>>1KFPaV@~+LqOpbH{;8ugAeD0ZD^z?m!LR$92En3H}I?=b-r=juJ5&5!NQ&D-trhL8@pl)47cc!1dsjmTfb_x&ynoS z*woOysXm6>EELHfPn{Rt39z7(;pg;4lS3OWxPX8brRUrP`ew85fjhO zD-Pr*(Zh|&fh;-xg~t&GcVm}OPP`b!Um-^`JiA=LbLq@>m2gCNBLP8{%L-S@T~_uk zM#YES%!wW(>2}vr$XUQ*dJgDOWM{KXH8!1dtwQ$DJOV16Q@Dsynb{>UXw~hpd~HnZ z@%(`f6AxT5+JxC8e*-F%v^?7$R<2cU+W!==cM)c31OTcpJtF-_@RUG=NqKkTF^^On z)H!ZZhn8%=CV@5mwp3qcq&^v6|M$`zFGz<-x`k>#P`0~_Zl)qveHZ9Fl}iI8?E(F? z6ScA{{s%IobmC|BR6NGK&|fyqw9_y|q1v;PcC=59fQb0{ax3@HE_1!c_3j-y614+sP#SS5#yG; z@w|%)qM5R!xt9a`>&0q@@Dj2qM#8%oIXw}hh|jyGbS_3kd2N39m1Lfkb0c( z5pvb3iJUehJ>HI18yENs3nY!hL9TdiJXtgC{C#!$i?X*fAa^&j3L^(iLV|6me=4iL z`F;vx z@fDBw55e8RUe9rsoYxNDzKIzAXMrG-^EgSeGHShOKs(Okk;OO$xdj?X@_1vm1t^}r zswgA-Pasv&#M6yrY{M*E>?}#!K08ItvLZHJcBGw7=V~tS&kJe&&*xB7ug#(%07Qn1k|W-E{h$|&vu8>r!(I{DGmR@ zsxNB__Pk|UV2qCg!~jSlnjEyyLsZsvN6M--3VlB6@JX1{dcK!{2p8c{Kc44$E6xI> a4RFlG+mnV{=Ic{{ud<-p(MSBc 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 z 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

    SBHAYl-x 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/albertopecchini.it/pzkko/games/pzk.js b/albertopecchini.it/pzkko/games/pzk.js index 489185e9..a1dde669 100644 --- a/albertopecchini.it/pzkko/games/pzk.js +++ b/albertopecchini.it/pzkko/games/pzk.js @@ -1,4 +1,4 @@ -// I $pzk di pzkko.it: soldi finti, gli stessi per tutti i giochi (roulette, blackjack, mine, dado, moneta e quelli che +// I $pzk di pzkko.it: soldi finti, gli stessi per tutti i giochi (roulette, blackjack, mine, dado, moneta, virtual e quelli che // verranno) e per il wallet in alto a sinistra (wallet/wallet.js). Non valgono niente e non si comprano. // // Due modi di stare al mondo (dal 22.10 notte): diff --git a/albertopecchini.it/pzkko/index.html b/albertopecchini.it/pzkko/index.html index a62dcfd5..51e04bad 100644 --- a/albertopecchini.it/pzkko/index.html +++ b/albertopecchini.it/pzkko/index.html @@ -167,7 +167,7 @@ .muro::after{content:"";position:absolute;inset:0;z-index:1;pointer-events:none;mix-blend-mode:screen;background:url(/art/muro.webp) 0 0/2200px auto repeat;filter:grayscale(1) contrast(3.2) brightness(1.25);opacity:calc(.75 * var(--auto-l)); -webkit-mask-image:radial-gradient(34% 100% at calc(var(--auto-x) * 100%) 50%,#000,transparent 70%); mask-image:radial-gradient(34% 100% at calc(var(--auto-x) * 100%) 50%,#000,transparent 70%)} -.parete{position:relative;height:calc(3252 * var(--u))} +.parete{position:relative;height:calc(3320 * var(--u))} .gruppo{display:contents} /* ogni cosa sul muro è posizionata in 1280esimi (e in 390esimi sul telefono) */ @@ -306,7 +306,7 @@ /* ── il telefono: il muro visto da vicino, due volantini per riga ─────────── */ @media (max-width:899px){ :root{--u:calc(min(100cqw, 600px) / 390)} - .parete{height:calc(5654 * var(--u));max-width:600px;margin:0 auto} + .parete{height:calc(5684 * var(--u));max-width:600px;margin:0 auto} .it{--px:var(--mx,var(--x));--py:var(--my,var(--y));--pw:var(--mw,var(--w));--pr:var(--mr,var(--r,0deg))} .spray{font-size:calc(40 * var(--u))} .vol h3{font-size:max(calc(22px * var(--ts,1)),calc(16cqw * var(--ts,1)))} @@ -419,36 +419,36 @@

    - - - + + + - + - - + + - - - - - + + + + + - + - + - - + + - + - - + + @@ -463,24 +463,24 @@

    - - - + + + - - - - - + + + + + - - - - + + + + - - + +

    siti

    @@ -558,7 +558,7 @@

    da dove viene questo muro?

    -
    +
    la piaggio ciao grigia sull'erba, al tramonto @@ -575,7 +575,7 @@

    +

    petizione popolare

    @@ -602,10 +602,10 @@

    pzkko.it è un bel sito?

    - + - - + +
    @@ -647,7 +647,7 @@

    + -
    +
    @@ -686,7 +686,7 @@

    +
    @@ -701,10 +701,10 @@

    -

    games

    +

    games

    -
    + -
    +
    +18
    @@ -739,7 +739,7 @@

    mine

    -
    +
    +18
    @@ -751,7 +751,7 @@

    dado

    -
    +
    +18
    @@ -761,10 +761,23 @@

    moneta

    gioca

    + + +
    + + +18 +
    + +

    virtual

    +

    il campionato inventato: quote vere, risultati a caso. ogni 5 minuti una partita sullo schermino. soldi finti.

    +

    gioca

    +
    +
    -

    tools

    +

    tools

    + + + + + + + + + + + + + + + + + + + + +` +await sharp(Buffer.from(svg)).webp({ quality: 92, alphaQuality: 90 }).toFile(OUT) +console.log('scritto', OUT) From e45fb6dd747b92d63e267b0aa5ba3c50d0012690 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 16:52:44 +0200 Subject: [PATCH 11/11] docs(pzkko): l'app virtual del cercapersone e la schedina in comune nel README Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015VZM7WrmYAstfLRGvRUqFy --- albertopecchini.it/pzkko/README.md | 32 +++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/albertopecchini.it/pzkko/README.md b/albertopecchini.it/pzkko/README.md index f0611e39..8f6503c7 100644 --- a/albertopecchini.it/pzkko/README.md +++ b/albertopecchini.it/pzkko/README.md @@ -98,8 +98,11 @@ serio ma è un progetto Vercel a parte. quote dalla forza vera, il risultato a caso, il meteo, la cronaca minuto per minuto, tutto dal seme dell'orario), `squadre.js` (le quaranta squadre di A e B coi nomi storpiati, i colori, gli stemmi disegnati in SVG, i cognomi inventati), `schermo.js` (lo schermino a pixel: il campo, gli - omini, i tifosi, la telecamera, il meteo, le scene degli eventi). L'illustrazione del volantino è - `art/virtual/pallone.svg`, disegnata a mano. Le prove: `../tools/pzkko/virtual/prova.mjs`. + omini, i tifosi, la telecamera, il meteo, le scene degli eventi), `schedina.js` (la schedina in + comune fra la finestra e l'app del cercapersone: le puntate nel browser, il tavolo dei $pzk, il + pagamento, l'unione fra schede). L'illustrazione del volantino è `art/virtual/pallone.svg`, + disegnata a mano. Le prove: `../tools/pzkko/virtual/prova.mjs`; `../tools/pzkko/virtual/icona.mjs` + scrive la piastrella dell'app (`art/chat-app2/app-virtual.webp`). - `games/pzk.js` — i $pzk, gli stessi per tutti i giochi e per il wallet: la sessione (1.000, non si salvano) o il conto (vedi «Il wallet e la borsa», sotto). - `wallet/` — il wallet dei $pzk (22.10 notte), il portafoglio di pelle alla Fallout in alto a sinistra: @@ -1535,9 +1538,14 @@ sola alla volta, uguale per chiunque sia sul sito in quel momento; si gioca coi tifosi entrano, il meteo c'è già, e a rotazione i cartelli della presentazione (stemmi, forza, le ultime cinque «uscite», inventate; la città e il meteo; «le quote copiano la Serie A vera, i risultati no»). -- **Il tabellone** sotto lo schermino: gli stemmi SVG, i nomi, città · serie · forza, il - punteggio che salta al gol, il minuto, i cartellini, il conto alla rovescia; sotto, una riga col - meteo e le ultime cinque. «come si gioca» apre le regole in un foglietto. +- **Il tabellone** sotto lo schermino: gli stemmi SVG, i nomi, città · forza, il punteggio che + salta al gol, il minuto, i cartellini, il conto alla rovescia; sotto, una riga col meteo e le + ultime cinque. «come si gioca» apre le regole in un foglietto. +- **La schedina in comune** (`schedina.js`, 23.10 sera): le puntate, il tavolo dei $pzk, il pagamento + e l'unione fra schede stanno in un modulo solo, usato dalla finestra grande e dall'app del + cercapersone (`telefono/app/virtual.js`, vedi «Il cercapersone»): una copia per pagina, le due + interfacce si avvisano a vicenda (`ascolta`) e con le altre schede via `storage`. Chi apre per + primo (`avvia`) rimette sul tavolo le puntate aperte e paga le partite finite nel frattempo. - **I suoni** (`suono/voci.js`, `suono/suono.js`): il fischietto, il boato del gol, l'«uuuh» del palo, la fiche posata, la vincita a 8 bit; passano dal motore della strada, quindi spenti se chi guarda ha spento il suono. Nella regia, scheda «suono», si provano. @@ -1929,7 +1937,17 @@ Sul cellulare lo schermo prende tutta la finestra e i tasti diventano una barra col conto alla rovescia a mezzanotte), reparti, ricerca, ordine, preferiti, la scheda del prodotto, il carrello, la cassa con lo scontrino. **Sfide**: le tre sfide di oggi. **Quiz**: i quiz retributivi. **Quest**: le side quest. **Giocatori**: il database (classifiche col podio, notizie, cerca, il tuo profilo coi rivali, i numeri della banca). **Zaino**: quello - che hai, coi bonus in corso. **Foto**: le foto vere dei quiz. **Banca**: apre il wallet. + che hai, coi bonus in corso. **Foto**: le foto vere dei quiz. **Virtual** (23.10 sera, Alberto: «il gioco dovrebbe essere + anche dentro al cercapersone»; `telefono/app/virtual.js`): le scommesse sul calcio inventato, la **stessa partita** del + gioco grande sullo stesso schermino a pixel (`games/virtual/partita.js` e `schermo.js`), col tabellone, il conto alla + rovescia, le quote 1 X 2 e over/under, le fiches e la schedina; scommesse e pagamenti passano dalla **stessa schedina** + della finestra grande (`games/virtual/schedina.js`, `pzkko-virtual`, lo stesso tavolo dei $pzk), quindi quello che si + punta qui si vede là e viceversa; «gioco grande» apre la finestra sopra il muro; sullo schermo largo lo schermino sta a + sinistra e il banco a destra, sul cellulare tutto in colonna. Il pallino: le vincite non ancora viste, o le scommesse + aperte sulla partita che chiude entro un minuto. In chat `virtual` (anche `scommesse`, `calcio`) la apre. La piastrella + (`art/chat-app2/app-virtual.webp`) per ora è disegnata a mano in SVG da `tools/pzkko/virtual/icona.mjs` (il pallone + davanti alla porta col fischietto, sul verde): da rigenerare con gpt-image-2 come le altre, `node tools/pzkko/immagini.mjs + genera chat-app2` (il prompt è nel foglio `chat-app2` di `tools/pzkko/fogli-chat.js`). **Banca**: apre il wallet. **La salute della lumachina** (23.10 sera; `chat/lumaca.mjs` il motore, `chat/lumaca-dati.mjs` le 300 giornate, `chat/lumaca-avvisi.js` le notifiche, la scheda in `telefono/app/giocatori.js`): in Giocatori > **io** c'è la lumachina @@ -1968,7 +1986,7 @@ scorrerle. Niente emoji di sistema: solo le nostre icone (`chat/icone.js`, `art/ - *negozio*: `negozio [reparto]` (offerte, cibo, attrezzi, fortuna, roba, pzk: ogni pezzo in una riga col tasto «compra»), `compra` (da solo apre la vetrina), `vendi`, `usa`, `info`, `zaino`, `apri`, `gratta`. Un nome di oggetto scritto da solo propone compra/usa/info. -- *app*: `telefono`, `shop`, `sfide [riscuoti]`, `missioni`, `quest [scelta]`. +- *app*: `telefono`, `shop`, `virtual`, `sfide [riscuoti]`, `missioni`, `quest [scelta]`. - *giocatori* (il database): `classifica [metrica]`, `cerca `, `giocatore [nome|pzk123456]`, `notizie`, `rivale`, `banca`.