Documenta e copre con test l'obiettivo "Continuità di squadra"
Il target 12 non è un valore arbitrario da scalare con la rosa come gli altri target fissi: è il minimo di giocatori per schierare due sestetti (6vs6) in allenamento, confermato intenzionale. Documentato in docs/modules/obiettivi-squadra.md. Non aveva alcun test. Aggiunti unit test (rosa vuota, confine >= 3, target fisso) e un integration test end-to-end che scrive tre allenamenti e presenze reali sul database locale, calcola serieConsecutiva() (la stessa funzione pura usata da useRosa() in produzione) sui dati riletti, e verifica il conteggio. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,7 +31,7 @@ serie).
|
|||||||
| 250 presenze complessive | somma presenze di tutta la rosa | 250 | aggregato da `useRosa()` |
|
| 250 presenze complessive | somma presenze di tutta la rosa | 250 | aggregato da `useRosa()` |
|
||||||
| Media pagelle da 7.5 | media di squadra | 7.5 | `pagelle_voti` |
|
| Media pagelle da 7.5 | media di squadra | 7.5 | `pagelle_voti` |
|
||||||
| 200 pagelle compilate | conteggio voti | 200 | `pagelle_voti` |
|
| 200 pagelle compilate | conteggio voti | 200 | `pagelle_voti` |
|
||||||
| Continuità di squadra | giocatori con ≥3 allenamenti consecutivi | 12 | `serieAllenamenti` |
|
| Continuità di squadra | giocatori con ≥3 allenamenti consecutivi | 12 (min. per un 6vs6) | `serieAllenamenti` |
|
||||||
| 1 / 5 / 10 vittorie in campionato | partite vinte da dati CSI ufficiali | 1/5/10 | modulo [Collegamento CSI](collegamento-csi.md) |
|
| 1 / 5 / 10 vittorie in campionato | partite vinte da dati CSI ufficiali | 1/5/10 | modulo [Collegamento CSI](collegamento-csi.md) |
|
||||||
| 1 evento di squadra al mese | eventi di tipo "evento" nel mese corrente (dinamico) | 1 | `eventi_app` |
|
| 1 evento di squadra al mese | eventi di tipo "evento" nel mese corrente (dinamico) | 1 | `eventi_app` |
|
||||||
|
|
||||||
@@ -43,14 +43,11 @@ smart (`notifiche-smart.ts`).
|
|||||||
|
|
||||||
## Limiti noti
|
## Limiti noti
|
||||||
|
|
||||||
- "Continuità di squadra" dipende da `serieAllenamenti` (vedi
|
|
||||||
[Serie di presenze](serie-presenze.md)), calcolato sui dati reali: un evento passato senza
|
|
||||||
risposta vale come assenza e azzera la serie, quindi l'obiettivo misura anche quanto la
|
|
||||||
squadra risponde alle convocazioni, non solo la presenza.
|
|
||||||
- Le vittorie di campionato dipendono dal parsing HTML del portale CSI: se quel parsing si
|
- Le vittorie di campionato dipendono dal parsing HTML del portale CSI: se quel parsing si
|
||||||
rompe, questi tre obiettivi restano a 0% anche a fronte di vittorie reali.
|
rompe, questi tre obiettivi restano a 0% anche a fronte di vittorie reali.
|
||||||
- I target (250 presenze, 200 pagelle, ecc.) sono costanti fisse, da rivedere manualmente a
|
- I target (250 presenze, 200 pagelle, ecc.) sono costanti fisse, da rivedere manualmente a
|
||||||
ogni stagione.
|
ogni stagione — con l'eccezione di "Continuità di squadra" (vedi sotto), il cui target ha un
|
||||||
|
significato specifico e non va scalato come gli altri.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -70,3 +67,22 @@ Per i test, `obiettiviSquadra`/`obiettiviOrdinati` accettano un terzo parametro
|
|||||||
(`test/unit/obiettivi.test.ts`, funzione pura) sia da un integration test end-to-end
|
(`test/unit/obiettivi.test.ts`, funzione pura) sia da un integration test end-to-end
|
||||||
(`test/integration/obiettivi.test.ts`, scrive/rilegge righe vere su `eventi_app` e
|
(`test/integration/obiettivi.test.ts`, scrive/rilegge righe vere su `eventi_app` e
|
||||||
`risposte_presenze` sullo stack Supabase locale).
|
`risposte_presenze` sullo stack Supabase locale).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Continuità di squadra — il target 12 è il minimo per un 6vs6
|
||||||
|
|
||||||
|
Il target di 12 giocatori con almeno 3 allenamenti consecutivi **non è arbitrario**: è il numero
|
||||||
|
minimo di giocatori per schierare due sestetti (6 contro 6) in allenamento. A differenza degli
|
||||||
|
altri target fissi (250 presenze, 200 pagelle...), non va scalato in proporzione alla rosa se
|
||||||
|
questa cambia dimensione — resta 12 finché l'obiettivo è "riuscire ad allenarsi in modo
|
||||||
|
completo", indipendentemente da quanti giocatori ci sono in rosa oltre quel minimo.
|
||||||
|
|
||||||
|
Dipende da `serieAllenamenti` (vedi [Serie di presenze](serie-presenze.md)), calcolato sui dati
|
||||||
|
reali: un evento passato senza risposta vale come assenza e azzera la serie, quindi l'obiettivo
|
||||||
|
misura anche quanto la squadra risponde alle convocazioni, non solo la presenza fisica.
|
||||||
|
|
||||||
|
Coperto da unit test (rosa vuota, il confine ≥3 — 2 non basta, 3 sì — e il target fisso a 12) e
|
||||||
|
da un integration test end-to-end che scrive tre allenamenti e presenze reali sul database
|
||||||
|
locale, calcola `serieConsecutiva()` (la stessa funzione pura usata da `useRosa()` in
|
||||||
|
produzione) sui dati riletti, e verifica che solo chi è rimasto in serie venga contato.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { giocatori } from "@/lib/crapp-data";
|
import { giocatori } from "@/lib/crapp-data";
|
||||||
import { obiettiviSquadra } from "@/lib/obiettivi";
|
import { obiettiviSquadra } from "@/lib/obiettivi";
|
||||||
import { contaPresenzeGiocatore, type MappaPresenze } from "@/lib/presenze";
|
import { contaPresenzeGiocatore, serieConsecutiva, type MappaPresenze } from "@/lib/presenze";
|
||||||
import { statoLocale } from "../helpers/locale";
|
import { statoLocale } from "../helpers/locale";
|
||||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||||
|
|
||||||
@@ -479,6 +479,70 @@ if (!locale) {
|
|||||||
"conteggio dei voti reali scritti sul database",
|
"conteggio dei voti reali scritti sul database",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await prova(
|
||||||
|
"o11 conta chi ha almeno 3 allenamenti consecutivi, calcolati da eventi/risposte reali",
|
||||||
|
async () => {
|
||||||
|
// o11 non calcola nulla da `ctx`: legge `g.serieAllenamenti`, un campo già calcolato a
|
||||||
|
// monte da `serieConsecutiva()` (la stessa funzione pura usata da `useRosa()` in
|
||||||
|
// produzione). Qui si esercita l'intera catena DB -> serieConsecutiva -> o11.
|
||||||
|
const OGGI_STR = "2098-01-20";
|
||||||
|
const eventi = [
|
||||||
|
[`${PREFISSO}-o11-a1`, "2098-01-05"],
|
||||||
|
[`${PREFISSO}-o11-a2`, "2098-01-08"],
|
||||||
|
[`${PREFISSO}-o11-a3`, "2098-01-11"],
|
||||||
|
] as const;
|
||||||
|
for (const [id, data] of eventi) {
|
||||||
|
const inserito = await rest("eventi_app", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ id, tipo: "allenamento", titolo: "Test obiettivi o11", data }),
|
||||||
|
});
|
||||||
|
if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [g1, g2, g3] = giocatori;
|
||||||
|
// g1: presente ai tre allenamenti -> serie 3 (conta). g2: presente, presente, assente
|
||||||
|
// -> la serie si azzera all'ultimo (non conta). g3: assente, assente, presente -> serie
|
||||||
|
// 1 (non basta).
|
||||||
|
const stati: Record<string, [string, string, string]> = {
|
||||||
|
[g1!.id]: ["presente", "presente", "presente"],
|
||||||
|
[g2!.id]: ["presente", "presente", "assente"],
|
||||||
|
[g3!.id]: ["assente", "assente", "presente"],
|
||||||
|
};
|
||||||
|
const righe = Object.entries(stati).flatMap(([giocatoreId, statiPerEvento]) =>
|
||||||
|
eventi.map(([eventoId], i) => ({
|
||||||
|
evento_id: eventoId,
|
||||||
|
giocatore_id: giocatoreId,
|
||||||
|
stato: statiPerEvento[i],
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
const inserite = await rest("risposte_presenze", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(righe),
|
||||||
|
});
|
||||||
|
if (!inserite.ok) throw new Error(`inserimento presenze fallito: ${await inserite.text()}`);
|
||||||
|
|
||||||
|
const eventiReali = (await leggiEventi()).filter((e) =>
|
||||||
|
eventi.some(([id]) => id === e.id),
|
||||||
|
);
|
||||||
|
const presenzeReali: MappaPresenze = {};
|
||||||
|
for (const [id] of eventi) Object.assign(presenzeReali, await leggiPresenze(id));
|
||||||
|
|
||||||
|
const rosaConSerieReali = [g1!, g2!, g3!].map((g) => ({
|
||||||
|
...g,
|
||||||
|
serieAllenamenti: serieConsecutiva(g.id, eventiReali, presenzeReali, "allenamento", OGGI_STR),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const o11 = obiettiviSquadra(rosaConSerieReali, { eventi: [], presenze: {}, pagelle: [] }).find(
|
||||||
|
(o) => o.id === "o11",
|
||||||
|
)!;
|
||||||
|
assert.equal(
|
||||||
|
o11.valore,
|
||||||
|
1,
|
||||||
|
"solo g1 (presente ai tre allenamenti) resta in serie, calcolato dal database",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
await rest(`risposte_presenze?evento_id=like.${PREFISSO}*`, { method: "DELETE" });
|
await rest(`risposte_presenze?evento_id=like.${PREFISSO}*`, { method: "DELETE" });
|
||||||
await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" });
|
await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" });
|
||||||
|
|||||||
@@ -302,6 +302,29 @@ assert.equal(
|
|||||||
"il conteggio somma i voti di più match_id, non solo dell'ultima",
|
"il conteggio somma i voti di più match_id, non solo dell'ultima",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- o11: continuità di squadra (giocatori con serieAllenamenti >= 3) --------
|
||||||
|
{
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra([], contestoVuoto), "o11").valore,
|
||||||
|
0,
|
||||||
|
"rosa vuota: nessuno può essere in serie",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Target 12 su una rosa di 17: il minimo per schierare due sestetti (6vs6), non un
|
||||||
|
// valore arbitrario — vedi docs/modules/obiettivi-squadra.md.
|
||||||
|
assert.equal(trova(vuoti, "o11").target, 12, "il target resta 12: minimo per un 6vs6");
|
||||||
|
|
||||||
|
// Il confine è >= 3, non > 3: 2 non basta, 3 sì.
|
||||||
|
const rosaConfine = giocatori
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((g, i) => ({ ...g, serieAllenamenti: [2, 3, 10][i]! }));
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(rosaConfine, contestoVuoto), "o11").valore,
|
||||||
|
2,
|
||||||
|
"conta solo chi ha almeno 3 allenamenti consecutivi (2 non basta, 3 sì)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// --- progressoObiettivo ------------------------------------------------------
|
// --- progressoObiettivo ------------------------------------------------------
|
||||||
const o = (valore: number, target: number): ObiettivoSquadra => ({
|
const o = (valore: number, target: number): ObiettivoSquadra => ({
|
||||||
id: "x",
|
id: "x",
|
||||||
|
|||||||
Reference in New Issue
Block a user