Nessun bug trovato: serieConsecutiva() gestisce correttamente buco (azzera) vs infortunio (congela senza azzerare), lo stesso filtro sui convocati della funzione pura protegge anche questo badge senza bisogno dell'estensione RLS di M13. Confermato che il limite noto "risposto_il non ricostruibile prima di m9" riguarda solo serie-conferme e il segreto s-mai-forfait, non questo badge: serieConsecutiva() guarda solo lo stato della risposta, non il suo istante. Aggiunti test unit sulle soglie (badges.test.ts) e un nuovo end-to-end (serie-allenamenti-badge.test.ts) che scrive allenamenti e risposte reali sul database locale e verifica l'intera pipeline fetchPresenze()/daRiga() -> serieConsecutiva() -> statoBadge() attraverso le tre soglie, incluso il caso "un buco azzera" e, separatamente, "un infortunio congela invece di azzerare". docs/modules/badge.md aggiornato con la pipeline, la copertura test e la descrizione corretta (l'infortunio non spezza la serie). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
229 lines
8.4 KiB
TypeScript
229 lines
8.4 KiB
TypeScript
/** Check dei badge e dei gradi: `bun src/lib/badges.test.ts`. */
|
|
import assert from "node:assert/strict";
|
|
import { giocatori, type Giocatore } from "@/lib/crapp-data";
|
|
import {
|
|
badgeDefs,
|
|
badgeGiocatore,
|
|
badgeSegreti,
|
|
badgeSegretiSbloccati,
|
|
collezioneBadge,
|
|
descrizioneSoglie,
|
|
gradiOrdine,
|
|
gradoRaggiunto,
|
|
mancanoPer,
|
|
microcopyBadge,
|
|
prossimoTraguardo,
|
|
segretiNascosti,
|
|
statoBadge,
|
|
tuttiBadge,
|
|
} from "@/lib/badges";
|
|
|
|
/** Giocatore azzerato: ogni test dichiara solo i valori che gli servono. */
|
|
function g(valori: Partial<Giocatore> = {}): Giocatore {
|
|
return {
|
|
...giocatori[0]!,
|
|
presenze: 0,
|
|
mvp: 0,
|
|
mediaVoto: 0,
|
|
votiPagella: 0,
|
|
palloni: 0,
|
|
cacche: 0,
|
|
cacchePartita: 0,
|
|
infortuni: 0,
|
|
ritardi: 0,
|
|
streak: 0,
|
|
serieAllenamenti: 0,
|
|
seriePartite: 0,
|
|
serieConferme: 0,
|
|
...valori,
|
|
};
|
|
}
|
|
|
|
const mvpDef = badgeDefs.find((b) => b.id === "mvp")!;
|
|
|
|
// --- gradoRaggiunto: soglie 1 / 3 / 5 ----------------------------------------
|
|
assert.equal(gradoRaggiunto(mvpDef, 0), null, "sotto la prima soglia nessun grado");
|
|
assert.equal(gradoRaggiunto(mvpDef, 1), "bronzo", "la soglia è inclusiva");
|
|
assert.equal(gradoRaggiunto(mvpDef, 2), "bronzo");
|
|
assert.equal(gradoRaggiunto(mvpDef, 3), "argento");
|
|
assert.equal(gradoRaggiunto(mvpDef, 99), "oro", "oltre l'oro resta oro");
|
|
assert.deepEqual(gradiOrdine, ["bronzo", "argento", "oro"]);
|
|
|
|
// --- statoBadge: progresso verso la soglia successiva ------------------------
|
|
const aMeta = statoBadge(mvpDef, g({ mvp: 2 }));
|
|
assert.deepEqual(
|
|
[aMeta.grado, aMeta.prossimo, aMeta.prossimaSoglia, aMeta.progresso],
|
|
["bronzo", "argento", 3, 67],
|
|
);
|
|
const alMassimo = statoBadge(mvpDef, g({ mvp: 5 }));
|
|
assert.deepEqual(
|
|
[alMassimo.grado, alMassimo.prossimo, alMassimo.prossimaSoglia],
|
|
["oro", null, null],
|
|
);
|
|
assert.equal(alMassimo.progresso, 100, "raggiunto l'oro il progresso è pieno");
|
|
|
|
// La media voto usa soglie decimali: non deve arrotondare per eccesso.
|
|
const pagella = badgeDefs.find((b) => b.id === "pagella")!;
|
|
assert.equal(gradoRaggiunto(pagella, 6.4), null);
|
|
assert.equal(gradoRaggiunto(pagella, 6.5), "bronzo");
|
|
|
|
// Sotto i voti minimi il badge resta bloccato anche con una media altissima: un voto solo
|
|
// non deve poter sbloccare/sfilare Pagellone.
|
|
assert.equal(
|
|
statoBadge(pagella, g({ mediaVoto: 10, votiPagella: 1 })).grado,
|
|
null,
|
|
"1 voto su 10: non basta, sotto la soglia minima",
|
|
);
|
|
assert.equal(
|
|
statoBadge(pagella, g({ mediaVoto: 10, votiPagella: 4 })).grado,
|
|
null,
|
|
"4 voti: ancora sotto la soglia minima (5)",
|
|
);
|
|
assert.equal(
|
|
statoBadge(pagella, g({ mediaVoto: 10, votiPagella: 5 })).grado,
|
|
"oro",
|
|
"5 voti: la soglia minima è raggiunta, la media conta",
|
|
);
|
|
assert.equal(
|
|
statoBadge(pagella, g({ mediaVoto: 6.5, votiPagella: 5 })).grado,
|
|
"bronzo",
|
|
"sopra la soglia minima, valgono le normali soglie di grado",
|
|
);
|
|
|
|
// --- palloni: soglie 3 / 6 / 10 -----------------------------------------------
|
|
const palloniDef = badgeDefs.find((b) => b.id === "palloni")!;
|
|
assert.equal(gradoRaggiunto(palloniDef, 2), null, "sotto la prima soglia nessun grado");
|
|
assert.equal(gradoRaggiunto(palloniDef, 3), "bronzo", "la soglia è inclusiva");
|
|
assert.equal(gradoRaggiunto(palloniDef, 5), "bronzo");
|
|
assert.equal(gradoRaggiunto(palloniDef, 6), "argento");
|
|
assert.equal(gradoRaggiunto(palloniDef, 9), "argento");
|
|
assert.equal(gradoRaggiunto(palloniDef, 10), "oro");
|
|
assert.equal(gradoRaggiunto(palloniDef, 99), "oro", "oltre l'oro resta oro");
|
|
assert.equal(
|
|
statoBadge(palloniDef, g({ palloni: 5 })).progresso,
|
|
83,
|
|
"5 turni su 6 per l'argento: 83%",
|
|
);
|
|
|
|
// --- presenze: soglie 5 / 15 / 30 ---------------------------------------------
|
|
const presenzeDef = badgeDefs.find((b) => b.id === "presenze")!;
|
|
assert.equal(gradoRaggiunto(presenzeDef, 4), null, "sotto la prima soglia nessun grado");
|
|
assert.equal(gradoRaggiunto(presenzeDef, 5), "bronzo", "la soglia è inclusiva");
|
|
assert.equal(gradoRaggiunto(presenzeDef, 14), "bronzo");
|
|
assert.equal(gradoRaggiunto(presenzeDef, 15), "argento");
|
|
assert.equal(gradoRaggiunto(presenzeDef, 29), "argento");
|
|
assert.equal(gradoRaggiunto(presenzeDef, 30), "oro");
|
|
assert.equal(gradoRaggiunto(presenzeDef, 999), "oro", "oltre l'oro resta oro");
|
|
|
|
// --- serie-allenamenti: soglie 3 / 6 / 10 -------------------------------------
|
|
const serieAllenamentiDef = badgeDefs.find((b) => b.id === "serie-allenamenti")!;
|
|
assert.equal(gradoRaggiunto(serieAllenamentiDef, 2), null, "sotto la prima soglia nessun grado");
|
|
assert.equal(gradoRaggiunto(serieAllenamentiDef, 3), "bronzo", "la soglia è inclusiva");
|
|
assert.equal(gradoRaggiunto(serieAllenamentiDef, 5), "bronzo");
|
|
assert.equal(gradoRaggiunto(serieAllenamentiDef, 6), "argento");
|
|
assert.equal(gradoRaggiunto(serieAllenamentiDef, 9), "argento");
|
|
assert.equal(gradoRaggiunto(serieAllenamentiDef, 10), "oro");
|
|
assert.equal(gradoRaggiunto(serieAllenamentiDef, 50), "oro", "oltre l'oro resta oro");
|
|
|
|
// --- badgeGiocatore ----------------------------------------------------------
|
|
assert.equal(badgeGiocatore(g()).length, badgeDefs.length, "i badge normali sono sempre tutti");
|
|
assert.ok(
|
|
badgeGiocatore(g()).every((b) => b.grado === null),
|
|
"un giocatore a zero non ha gradi",
|
|
);
|
|
|
|
// --- badge segreti: invisibili finché non si sbloccano ------------------------
|
|
const nessunSegreto = g({ mvp: 2, mediaVoto: 7.9 });
|
|
assert.equal(badgeSegretiSbloccati(nessunSegreto).length, 0, "serve media 8, non 7.9");
|
|
assert.equal(segretiNascosti(nessunSegreto), badgeSegreti.length);
|
|
|
|
const tiebreak = badgeSegretiSbloccati(g({ mvp: 2, mediaVoto: 8 }));
|
|
assert.deepEqual(
|
|
tiebreak.map((b) => b.def.id),
|
|
["s-tiebreak"],
|
|
"sblocca solo il segreto il cui requisito è soddisfatto",
|
|
);
|
|
|
|
assert.deepEqual(
|
|
badgeSegretiSbloccati(g({ infortuni: 3 })).map((b) => b.def.id),
|
|
["s-infermeria"],
|
|
);
|
|
assert.equal(badgeSegretiSbloccati(g({ infortuni: 2 })).length, 0, "2 infortuni non bastano");
|
|
assert.deepEqual(
|
|
badgeSegretiSbloccati(g({ ritardi: 5 })).map((b) => b.def.id),
|
|
["s-ritardi"],
|
|
);
|
|
assert.deepEqual(
|
|
badgeSegretiSbloccati(g({ cacche: 3 })).map((b) => b.def.id),
|
|
["s-cacche"],
|
|
);
|
|
assert.deepEqual(
|
|
badgeSegretiSbloccati(g({ serieConferme: 10, presenze: 15 })).map((b) => b.def.id),
|
|
["s-mai-forfait"],
|
|
);
|
|
assert.equal(
|
|
badgeSegretiSbloccati(g({ serieConferme: 10, presenze: 14 })).length,
|
|
0,
|
|
"servono entrambe le condizioni",
|
|
);
|
|
|
|
// --- collezioneBadge ---------------------------------------------------------
|
|
const vuota = collezioneBadge(g());
|
|
assert.equal(vuota.ottenuti, 0);
|
|
assert.equal(vuota.totali, tuttiBadge.length);
|
|
assert.equal(vuota.sbloccati.length, 0);
|
|
assert.equal(vuota.inProgresso.length, badgeDefs.length);
|
|
assert.equal(vuota.nascosti, badgeSegreti.length);
|
|
|
|
const piena = collezioneBadge(
|
|
g({ mvp: 5, mediaVoto: 8, votiPagella: 5, presenze: 30, infortuni: 3 }),
|
|
);
|
|
assert.equal(
|
|
piena.ottenuti,
|
|
piena.sbloccati.length + piena.segreti.length,
|
|
"gli ottenuti sommano normali e segreti",
|
|
);
|
|
assert.equal(piena.nascosti, badgeSegreti.length - piena.segreti.length);
|
|
assert.ok(piena.ottenuti > 0 && piena.ottenuti < piena.totali);
|
|
|
|
// --- prossimoTraguardo: il badge in proporzione più vicino -------------------
|
|
const vicino = prossimoTraguardo(g({ mvp: 2, presenze: 1 }));
|
|
assert.equal(vicino?.def.id, "mvp", "67% batte 20%");
|
|
assert.equal(
|
|
prossimoTraguardo(
|
|
g({
|
|
mvp: 5,
|
|
mediaVoto: 10,
|
|
votiPagella: 5,
|
|
palloni: 10,
|
|
presenze: 30,
|
|
serieAllenamenti: 10,
|
|
serieConferme: 15,
|
|
}),
|
|
),
|
|
null,
|
|
"tutto al massimo: nessun traguardo residuo",
|
|
);
|
|
|
|
// --- microcopy ---------------------------------------------------------------
|
|
assert.equal(mancanoPer(statoBadge(mvpDef, g({ mvp: 2 }))), "Ti mancano 1 MVP per il argento");
|
|
assert.equal(mancanoPer(alMassimo), "Livello massimo raggiunto");
|
|
assert.equal(microcopyBadge(alMassimo), "Hai fatto tutto: badge d'oro in bacheca.");
|
|
assert.equal(microcopyBadge(aMeta), "Sei in piena corsa, continua così.");
|
|
assert.equal(microcopyBadge(statoBadge(mvpDef, g())), "Ogni partita conta: si parte da qui.");
|
|
assert.equal(descrizioneSoglie(mvpDef), "1/3/5 MVP");
|
|
|
|
// --- invarianti sulle definizioni --------------------------------------------
|
|
for (const def of tuttiBadge) {
|
|
assert.ok(def.soglie.bronzo <= def.soglie.argento, `${def.id}: soglie crescenti`);
|
|
assert.ok(def.soglie.argento <= def.soglie.oro, `${def.id}: soglie crescenti`);
|
|
assert.ok(def.nome && def.descrizione && def.unita, `${def.id}: testi presenti`);
|
|
}
|
|
assert.equal(
|
|
new Set(tuttiBadge.map((b) => b.id)).size,
|
|
tuttiBadge.length,
|
|
"gli id dei badge sono unici",
|
|
);
|
|
|
|
console.log("badges: ok");
|