Applica una soglia minima di campione a Media voto e MVP in home (DD-028)
Rimuove l'hint statico "+2 questo mese" dalla StatTile Presenze. La StatTile Media voto usa ora la stessa soglia minima di voti del badge Pagellone (VOTI_MINIMI_PAGELLA), tramite la nuova funzione pura testabile mediaVotoColpoDOcchio(). L'MVP di partita richiede un quorum minimo di 2 voti totali (VOTI_MINIMI_MVP) oltre al margine netto già richiesto: un solo voto non assegna più la vittoria, con effetto anche sui conteggi già mostrati. Aggiorna test unitari e di integrazione, e documenta la decisione in DESIGN_DECISIONS.md, pagelle.md, mvp.md e CHANGELOG.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,12 @@ Prima versione, pre-release.
|
|||||||
di Statistiche; la tab «Classifica» è stata rimossa dalla barra delle sottosezioni.
|
di Statistiche; la tab «Classifica» è stata rimossa dalla barra delle sottosezioni.
|
||||||
- Tolto l'hint statico "+2 questo mese" dalla StatTile Presenze in home (sezione «Colpo
|
- Tolto l'hint statico "+2 questo mese" dalla StatTile Presenze in home (sezione «Colpo
|
||||||
d'occhio»): mostrava un testo fisso, non un dato calcolato.
|
d'occhio»): mostrava un testo fisso, non un dato calcolato.
|
||||||
|
- La StatTile Media voto in home applica ora la stessa soglia minima di voti del badge
|
||||||
|
Pagellone (`VOTI_MINIMI_PAGELLA`): sotto soglia mostra `—` invece di una media poco
|
||||||
|
significativa (DD-028).
|
||||||
|
- L'MVP di una partita richiede ora un quorum minimo di 2 voti totali (`VOTI_MINIMI_MVP`)
|
||||||
|
oltre al margine netto già richiesto: un solo voto non assegna più la vittoria (DD-028).
|
||||||
|
Alcuni conteggi `mvp` già mostrati possono scendere per effetto della nuova regola.
|
||||||
|
|
||||||
### Sicurezza
|
### Sicurezza
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ Serve a rispondere a domande del tipo:
|
|||||||
| [DD-025](#dd-025--il-promemoria-palloni-lo-manda-ladmin-per-un-evento) | Promemoria palloni manuale |
|
| [DD-025](#dd-025--il-promemoria-palloni-lo-manda-ladmin-per-un-evento) | Promemoria palloni manuale |
|
||||||
| [DD-026](#dd-026--il-testo-della-notifica-viaggia-dentro-la-push) | Payload push cifrato |
|
| [DD-026](#dd-026--il-testo-della-notifica-viaggia-dentro-la-push) | Payload push cifrato |
|
||||||
| [DD-027](#dd-027--chi-vota-deve-essere-convocato-non-solo-autenticato-come-sé-stesso) | Voto limitato ai convocati |
|
| [DD-027](#dd-027--chi-vota-deve-essere-convocato-non-solo-autenticato-come-sé-stesso) | Voto limitato ai convocati |
|
||||||
|
| [DD-028](#dd-028--soglia-minima-di-campione-per-media-voto-e-mvp-in-home) | Soglia minima Media voto e MVP |
|
||||||
|
|
||||||
**In valutazione**
|
**In valutazione**
|
||||||
|
|
||||||
@@ -1024,3 +1025,54 @@ dall'audit che ha originato questa decisione.
|
|||||||
**Riesame**
|
**Riesame**
|
||||||
Se un giorno servisse bloccare anche il voto di un convocato-ma-assente a livello database,
|
Se un giorno servisse bloccare anche il voto di un convocato-ma-assente a livello database,
|
||||||
non solo in UI.
|
non solo in UI.
|
||||||
|
|
||||||
|
### DD-028 — Soglia minima di campione per Media voto e MVP in home
|
||||||
|
|
||||||
|
**Data:** 9 settembre 2026
|
||||||
|
**Stato:** Accettata
|
||||||
|
|
||||||
|
**Contesto**
|
||||||
|
Un audit della sezione «Colpo d'occhio» in home (`index.tsx`, StatTile Presenze/Media
|
||||||
|
voto/MVP) ha trovato che due delle tre statistiche non avevano nessun minimo campionario:
|
||||||
|
`mediePagelle()` calcola una media aritmetica pura, così un giocatore con un solo voto da 10
|
||||||
|
mostrava "10" in home, più alto di un titolare con 40 voti e media 7.2 — lo stesso problema
|
||||||
|
che il badge Pagellone già risolve con `VOTI_MINIMI_PAGELLA` (badge.md), ma applicato solo al
|
||||||
|
badge, non alla StatTile home. Allo stesso modo `mvpVintiPerGiocatore()`/`vincitoriMvp()`
|
||||||
|
assegnavano un MVP di partita anche con un solo voto totale: bastava che un solo giocatore
|
||||||
|
votasse perché il votato "vincesse" nettamente, senza nessun quorum di partecipazione.
|
||||||
|
|
||||||
|
**Decisione**
|
||||||
|
|
||||||
|
- **Media voto** in home usa la stessa soglia del badge Pagellone: sotto `VOTI_MINIMI_PAGELLA`
|
||||||
|
(5) voti ricevuti, la StatTile mostra `—` invece della media, tramite la funzione pura
|
||||||
|
`mediaVotoColpoDOcchio()` (`pagelle.ts`), estratta dalla route per restare testabile (DD-020).
|
||||||
|
La funzione `mediePagelle()` non cambia: il filtro resta lato chiamante, come già faceva il
|
||||||
|
badge.
|
||||||
|
- **MVP**: `conteggioPartita`'s aggregazione, tramite `vincitoriMvp()` e
|
||||||
|
`mvpVintiPerGiocatore()`, richiede ora un quorum minimo di voti totali sulla partita
|
||||||
|
(`VOTI_MINIMI_MVP = 2`, `mvp-voti.ts`) prima di assegnare un vincitore, oltre alla regola già
|
||||||
|
esistente del margine netto tra primo e secondo. Un solo voto non basta più a incoronare
|
||||||
|
nessuno, nemmeno in assenza di concorrenza.
|
||||||
|
|
||||||
|
**Alternative scartate**
|
||||||
|
|
||||||
|
- Alzare la soglia MVP oltre 2 (es. metà dei convocati) → serve conoscere i convocati
|
||||||
|
dell'evento dentro una funzione che oggi lavora solo sui voti; complessità non giustificata
|
||||||
|
per il gap trovato in audit.
|
||||||
|
- Lasciare l'MVP senza quorum e limitarsi al fix della Media voto → il problema di fondo
|
||||||
|
(un numero esiguo di voti che decide una statistica mostrata come solida) resterebbe aperto
|
||||||
|
per l'MVP.
|
||||||
|
|
||||||
|
**Conseguenze**
|
||||||
|
|
||||||
|
- Alcuni MVP di partita già assegnati con un solo voto totale non contano più nel conteggio
|
||||||
|
`mvp` del giocatore: è una modifica retroattiva al dato mostrato, non solo al calcolo futuro,
|
||||||
|
perché `mvpVintiPerGiocatore()` deriva sempre il conteggio dai voti grezzi, senza storico
|
||||||
|
persistito a parte.
|
||||||
|
- `mediePagelle()` resta invariata: chi la chiama altrove (profilo, squadra) senza applicare la
|
||||||
|
soglia continua a mostrare la media grezza — non tocca questa decisione, resta il limite già
|
||||||
|
noto in [pagelle.md](modules/pagelle.md).
|
||||||
|
|
||||||
|
**Riesame**
|
||||||
|
Se la squadra segnala che il quorum di 2 voti per l'MVP è troppo permissivo o troppo severo, o
|
||||||
|
se si vuole applicare la stessa soglia di Media voto anche alle StatTile di profilo e squadra.
|
||||||
|
|||||||
+5
-1
@@ -37,6 +37,9 @@ restano nel database ma non vengono più letti da nessuna schermata).
|
|||||||
direttamente su PostgREST, come già faceva `pagelle_no_autovoto` per le pagelle.
|
direttamente su PostgREST, come già faceva `pagelle_no_autovoto` per le pagelle.
|
||||||
- `conteggioPartita()`/`vincitoriMvp()` richiedono un margine netto: in caso di parità,
|
- `conteggioPartita()`/`vincitoriMvp()` richiedono un margine netto: in caso di parità,
|
||||||
nessun vincitore viene assegnato per quella partita finché non arrivano altri voti.
|
nessun vincitore viene assegnato per quella partita finché non arrivano altri voti.
|
||||||
|
- `vincitoriMvp()`/`mvpVintiPerGiocatore()` richiedono anche un quorum minimo di voti totali
|
||||||
|
sulla partita (`VOTI_MINIMI_MVP = 2`, `mvp-voti.ts`, DD-028): un solo voto non basta a
|
||||||
|
incoronare nessuno, nemmeno senza concorrenza.
|
||||||
- `mvpVintiPerGiocatore()` conta una vittoria per ogni partita "vinta" con margine netto; il
|
- `mvpVintiPerGiocatore()` conta una vittoria per ogni partita "vinta" con margine netto; il
|
||||||
risultato alimenta il campo `mvp` del `Giocatore` in `useRosa()`, mostrato come StatTile
|
risultato alimenta il campo `mvp` del `Giocatore` in `useRosa()`, mostrato come StatTile
|
||||||
nel profilo e in home.
|
nel profilo e in home.
|
||||||
@@ -57,7 +60,8 @@ restano nel database ma non vengono più letti da nessuna schermata).
|
|||||||
(non solo convocati: `presente`/`ritardo` in `usePresenzeEvento`, un controllo più stretto
|
(non solo convocati: `presente`/`ritardo` in `usePresenzeEvento`, un controllo più stretto
|
||||||
della sola convocazione) a quella partita, e le due ore d'attesa dall'inizio evento
|
della sola convocazione) a quella partita, e le due ore d'attesa dall'inizio evento
|
||||||
(`votoMvpAperto()`) — un amministratore, o chiunque scriva su PostgREST, passa comunque.
|
(`votoMvpAperto()`) — un amministratore, o chiunque scriva su PostgREST, passa comunque.
|
||||||
- In caso di parità, nessun MVP viene assegnato per quella partita.
|
- In caso di parità, o sotto il quorum minimo di voti, nessun MVP viene assegnato per quella
|
||||||
|
partita.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+10
-5
@@ -36,8 +36,12 @@ UI), `UNIQUE (match_id, votante_id, votato_id)`.
|
|||||||
- `useRosa()` inietta questa media storica nel campo `mediaVoto` di ogni giocatore, insieme al
|
- `useRosa()` inietta questa media storica nel campo `mediaVoto` di ogni giocatore, insieme al
|
||||||
numero di voti ricevuti (`votiPagella`) — usato dal badge Pagellone (vedi
|
numero di voti ricevuti (`votiPagella`) — usato dal badge Pagellone (vedi
|
||||||
[badge.md](badge.md)) per richiedere un minimo di voti prima che la media conti, e mostrato
|
[badge.md](badge.md)) per richiedere un minimo di voti prima che la media conti, e mostrato
|
||||||
come StatTile nel profilo e in home (sezione «Colpo d'occhio», `index.tsx`): senza voti
|
come StatTile nel profilo e in home (sezione «Colpo d'occhio», `index.tsx`).
|
||||||
ricevuti la media vale `0` e la StatTile mostra `—` invece del numero.
|
- La StatTile **home** applica la stessa soglia del badge Pagellone (DD-028) tramite la
|
||||||
|
funzione pura `mediaVotoColpoDOcchio()`: sotto `VOTI_MINIMI_PAGELLA` voti ricevuti mostra
|
||||||
|
`—` invece della media, non solo quando i voti sono zero. È stata estratta come funzione
|
||||||
|
testabile (coerente con DD-020) invece di restare una condizione inline nella route. Le
|
||||||
|
StatTile di **profilo** e **squadra** non applicano questa soglia (vedi "Limiti noti").
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -57,9 +61,10 @@ UI), `UNIQUE (match_id, votante_id, votato_id)`.
|
|||||||
- **L'anonimato è solo applicativo, non tecnico**: la riga salvata contiene sia `votante_id`
|
- **L'anonimato è solo applicativo, non tecnico**: la riga salvata contiene sia `votante_id`
|
||||||
sia `votato_id`, leggibili da chiunque sia autenticato (policy SELECT aperta). La UI non
|
sia `votato_id`, leggibili da chiunque sia autenticato (policy SELECT aperta). La UI non
|
||||||
mostra mai il votante, ma il dato non è né aggregato né mascherato lato server.
|
mostra mai il votante, ma il dato non è né aggregato né mascherato lato server.
|
||||||
- La media mostrata nel profilo non richiede un numero minimo di voti: con un solo voto
|
- La media mostrata nel **profilo** e in **squadra** non richiede un numero minimo di voti:
|
||||||
ricevuto, la media coincide con quel voto. Il badge Pagellone (`badge.md`) applica invece un
|
con un solo voto ricevuto, la media coincide con quel voto. Il badge Pagellone (`badge.md`)
|
||||||
minimo di voti prima di considerarla — la StatTile del profilo no.
|
e la StatTile **home** (DD-028) applicano invece la stessa soglia minima prima di
|
||||||
|
considerarla — profilo e squadra no.
|
||||||
- Le due regole di M13 (convocazione, `pagelle_chiuse`) valgono solo per la policy "Ognuno
|
- Le due regole di M13 (convocazione, `pagelle_chiuse`) valgono solo per la policy "Ognuno
|
||||||
gestisce i propri voti pagella": un amministratore può ancora correggere un voto fuori
|
gestisce i propri voti pagella": un amministratore può ancora correggere un voto fuori
|
||||||
convocazione o dopo la chiusura, di proposito (deve poter sistemare un errore).
|
convocazione o dopo la chiusura, di proposito (deve poter sistemare un errore).
|
||||||
|
|||||||
+21
-4
@@ -60,6 +60,9 @@ export function useVotaMvp() {
|
|||||||
|
|
||||||
export type ConteggioMvp = { id: string; nome: string; voti: number };
|
export type ConteggioMvp = { id: string; nome: string; voti: number };
|
||||||
|
|
||||||
|
/** Voti minimi in una partita perché l'MVP possa essere assegnato (un solo voto non decide). */
|
||||||
|
export const VOTI_MINIMI_MVP = 2;
|
||||||
|
|
||||||
/** Conteggio voti di una partita, dal più votato. */
|
/** Conteggio voti di una partita, dal più votato. */
|
||||||
export function conteggioPartita(voti: VotoMvp[], matchId: string): ConteggioMvp[] {
|
export function conteggioPartita(voti: VotoMvp[], matchId: string): ConteggioMvp[] {
|
||||||
const map = new Map<string, ConteggioMvp>();
|
const map = new Map<string, ConteggioMvp>();
|
||||||
@@ -83,8 +86,13 @@ export function vincitoriMvp(voti: VotoMvp[]): Record<string, string> {
|
|||||||
const out: Record<string, string> = {};
|
const out: Record<string, string> = {};
|
||||||
for (const [matchId] of perMatch) {
|
for (const [matchId] of perMatch) {
|
||||||
const top = conteggioPartita(voti, matchId);
|
const top = conteggioPartita(voti, matchId);
|
||||||
// In caso di parità nessun MVP assegnato finché il voto non si sblocca.
|
const totaleVoti = top.reduce((s, c) => s + c.voti, 0);
|
||||||
if (top.length > 0 && (top.length === 1 || top[0]!.voti > top[1]!.voti)) {
|
// In caso di parità, o sotto il quorum minimo, nessun MVP assegnato.
|
||||||
|
if (
|
||||||
|
totaleVoti >= VOTI_MINIMI_MVP &&
|
||||||
|
top.length > 0 &&
|
||||||
|
(top.length === 1 || top[0]!.voti > top[1]!.voti)
|
||||||
|
) {
|
||||||
out[matchId] = top[0]!.nome;
|
out[matchId] = top[0]!.nome;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,13 +103,22 @@ export function mioVoto(voti: VotoMvp[], matchId: string, votanteId: string) {
|
|||||||
return voti.find((v) => v.match_id === matchId && v.votante_id === votanteId) ?? null;
|
return voti.find((v) => v.match_id === matchId && v.votante_id === votanteId) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** MVP vinti per giocatore, contando una vittoria per partita votata. */
|
/**
|
||||||
|
* MVP vinti per giocatore, contando una vittoria per partita votata (una partita in pareggio
|
||||||
|
* al vertice, o sotto il quorum minimo di voti, non assegna vittorie a nessuno). Senza voti
|
||||||
|
* restituisce una mappa vuota.
|
||||||
|
*/
|
||||||
export function mvpVintiPerGiocatore(voti: VotoMvp[]): Record<string, number> {
|
export function mvpVintiPerGiocatore(voti: VotoMvp[]): Record<string, number> {
|
||||||
const out: Record<string, number> = {};
|
const out: Record<string, number> = {};
|
||||||
const matchIds = new Set(voti.map((v) => v.match_id));
|
const matchIds = new Set(voti.map((v) => v.match_id));
|
||||||
for (const matchId of matchIds) {
|
for (const matchId of matchIds) {
|
||||||
const top = conteggioPartita(voti, matchId);
|
const top = conteggioPartita(voti, matchId);
|
||||||
if (top.length > 0 && (top.length === 1 || top[0]!.voti > top[1]!.voti)) {
|
const totaleVoti = top.reduce((s, c) => s + c.voti, 0);
|
||||||
|
if (
|
||||||
|
totaleVoti >= VOTI_MINIMI_MVP &&
|
||||||
|
top.length > 0 &&
|
||||||
|
(top.length === 1 || top[0]!.voti > top[1]!.voti)
|
||||||
|
) {
|
||||||
const id = top[0]!.id;
|
const id = top[0]!.id;
|
||||||
out[id] = (out[id] ?? 0) + 1;
|
out[id] = (out[id] ?? 0) + 1;
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-1
@@ -1,5 +1,6 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { supabase } from "@/integrations/supabase/client";
|
import { supabase } from "@/integrations/supabase/client";
|
||||||
|
import { VOTI_MINIMI_PAGELLA } from "./badges";
|
||||||
|
|
||||||
/** Voto anonimo da 1 a 10 dato a un compagno per una partita. */
|
/** Voto anonimo da 1 a 10 dato a un compagno per una partita. */
|
||||||
export type VotoPagella = {
|
export type VotoPagella = {
|
||||||
@@ -62,7 +63,8 @@ function arrotonda(n: number) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Media storica di ciascun giocatore su tutti i voti mai ricevuti (l'app non ha un concetto
|
* Media storica di ciascun giocatore su tutti i voti mai ricevuti (l'app non ha un concetto
|
||||||
* di stagione/reset): giocatoreId -> media e numero di voti.
|
* di stagione/reset): giocatoreId -> media e numero di voti. Chi non ha ancora ricevuto voti
|
||||||
|
* non compare nella mappa (nessuna divisione per zero): sta al chiamante gestire il fallback.
|
||||||
*/
|
*/
|
||||||
export function mediePagelle(voti: VotoPagella[]): Record<string, MediaPagella> {
|
export function mediePagelle(voti: VotoPagella[]): Record<string, MediaPagella> {
|
||||||
const somma: Record<string, { tot: number; n: number }> = {};
|
const somma: Record<string, { tot: number; n: number }> = {};
|
||||||
@@ -93,6 +95,15 @@ export function mieiVoti(voti: VotoPagella[], matchId: string, votanteId: string
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Media voto da mostrare nella StatTile home (sezione «Colpo d'occhio», DD-028): sotto
|
||||||
|
* `VOTI_MINIMI_PAGELLA` voti ricevuti, `—` invece della media grezza — stessa soglia del
|
||||||
|
* badge Pagellone, non applicata invece dalle StatTile di profilo e squadra.
|
||||||
|
*/
|
||||||
|
export function mediaVotoColpoDOcchio(g: { mediaVoto: number; votiPagella: number }): number | "—" {
|
||||||
|
return g.votiPagella >= VOTI_MINIMI_PAGELLA ? g.mediaVoto : "—";
|
||||||
|
}
|
||||||
|
|
||||||
/** Media pagelle di tutta la squadra su tutte le partite. */
|
/** Media pagelle di tutta la squadra su tutte le partite. */
|
||||||
export function mediaSquadra(voti: VotoPagella[]) {
|
export function mediaSquadra(voti: VotoPagella[]) {
|
||||||
if (voti.length === 0) return 0;
|
if (voti.length === 0) return 0;
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ function eventiContanoPresenze(eventi: Evento[], giocatoreId?: string, oggi = da
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Presenze effettive (presente o in ritardo) su eventi CrAPP. */
|
/** Presenze effettive (presente o in ritardo) su eventi CrAPP. Senza eventi rilevanti restituisce 0. */
|
||||||
export function contaPresenzeGiocatore(
|
export function contaPresenzeGiocatore(
|
||||||
giocatoreId: string,
|
giocatoreId: string,
|
||||||
eventi: Evento[],
|
eventi: Evento[],
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { useCsi } from "@/lib/csi";
|
|||||||
import { isNostraSquadra, matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
|
import { isNostraSquadra, matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
|
||||||
import { useScoutMatches } from "@/lib/scout-store";
|
import { useScoutMatches } from "@/lib/scout-store";
|
||||||
import { useVotiMvp, vincitoriMvp } from "@/lib/mvp-voti";
|
import { useVotiMvp, vincitoriMvp } from "@/lib/mvp-voti";
|
||||||
|
import { mediaVotoColpoDOcchio } from "@/lib/pagelle";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
head: () => ({
|
head: () => ({
|
||||||
@@ -238,8 +239,8 @@ function Index() {
|
|||||||
|
|
||||||
<Section titolo="Colpo d'occhio" indice={6}>
|
<Section titolo="Colpo d'occhio" indice={6}>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
<StatTile valore={giocatore.presenze} label="Presenze" hint="+2 questo mese" />
|
<StatTile valore={giocatore.presenze} label="Presenze" />
|
||||||
<StatTile valore={giocatore.mediaVoto || "—"} label="Media voto" />
|
<StatTile valore={mediaVotoColpoDOcchio(giocatore)} label="Media voto" />
|
||||||
<StatTile valore={giocatore.mvp} label="MVP" />
|
<StatTile valore={giocatore.mvp} label="MVP" />
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -127,13 +127,20 @@ if (!locale) {
|
|||||||
const badgeVg2 = statoBadge(mvpDef, giocatoreAzzerato(vinti["vg2"] ?? 0));
|
const badgeVg2 = statoBadge(mvpDef, giocatoreAzzerato(vinti["vg2"] ?? 0));
|
||||||
assert.equal(badgeVg2.grado, null, "zero vittorie nette: nessun badge sbloccato");
|
assert.equal(badgeVg2.grado, null, "zero vittorie nette: nessun badge sbloccato");
|
||||||
|
|
||||||
// Una terza vittoria netta porta vg1 da bronzo ad argento (soglia 3).
|
// Una terza vittoria netta porta vg1 da bronzo ad argento (soglia 3). Due voti,
|
||||||
|
// non uno solo: sotto il quorum minimo (VOTI_MINIMI_MVP) nessuna vittoria conterebbe.
|
||||||
await upsert({
|
await upsert({
|
||||||
match_id: `${PREFISSO}-m4`,
|
match_id: `${PREFISSO}-m4`,
|
||||||
votante_id: "va",
|
votante_id: "va",
|
||||||
votato_id: "vg1",
|
votato_id: "vg1",
|
||||||
votato_nome: "Uno",
|
votato_nome: "Uno",
|
||||||
});
|
});
|
||||||
|
await upsert({
|
||||||
|
match_id: `${PREFISSO}-m4`,
|
||||||
|
votante_id: "vb",
|
||||||
|
votato_id: "vg1",
|
||||||
|
votato_nome: "Uno",
|
||||||
|
});
|
||||||
const vintiDopo = mvpVintiPerGiocatore(await leggiVoti());
|
const vintiDopo = mvpVintiPerGiocatore(await leggiVoti());
|
||||||
assert.equal(vintiDopo["vg1"], 3);
|
assert.equal(vintiDopo["vg1"], 3);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
|
|||||||
@@ -89,20 +89,33 @@ if (!locale) {
|
|||||||
await prova(
|
await prova(
|
||||||
"sotto la soglia minima di voti pagella, 2 MVP e media alta non bastano",
|
"sotto la soglia minima di voti pagella, 2 MVP e media alta non bastano",
|
||||||
async () => {
|
async () => {
|
||||||
// "tb1" vince nettamente m1 e m2 (2 MVP), e riceve un solo voto pagella da 9 (media
|
// "tb1" vince nettamente m1 e m2 (2 MVP, due voti a testa per rispettare il quorum
|
||||||
// alta ma su un campione troppo piccolo): il segreto deve restare bloccato.
|
// minimo), e riceve un solo voto pagella da 9 (media alta ma su un campione troppo
|
||||||
|
// piccolo): il segreto deve restare bloccato.
|
||||||
await votaMvp({
|
await votaMvp({
|
||||||
match_id: `${PREFISSO}-m1`,
|
match_id: `${PREFISSO}-m1`,
|
||||||
votante_id: "va",
|
votante_id: "va",
|
||||||
votato_id: "tb1",
|
votato_id: "tb1",
|
||||||
votato_nome: "Uno",
|
votato_nome: "Uno",
|
||||||
});
|
});
|
||||||
|
await votaMvp({
|
||||||
|
match_id: `${PREFISSO}-m1`,
|
||||||
|
votante_id: "vf",
|
||||||
|
votato_id: "tb1",
|
||||||
|
votato_nome: "Uno",
|
||||||
|
});
|
||||||
await votaMvp({
|
await votaMvp({
|
||||||
match_id: `${PREFISSO}-m2`,
|
match_id: `${PREFISSO}-m2`,
|
||||||
votante_id: "va",
|
votante_id: "va",
|
||||||
votato_id: "tb1",
|
votato_id: "tb1",
|
||||||
votato_nome: "Uno",
|
votato_nome: "Uno",
|
||||||
});
|
});
|
||||||
|
await votaMvp({
|
||||||
|
match_id: `${PREFISSO}-m2`,
|
||||||
|
votante_id: "vf",
|
||||||
|
votato_id: "tb1",
|
||||||
|
votato_nome: "Uno",
|
||||||
|
});
|
||||||
await votaPagella({
|
await votaPagella({
|
||||||
match_id: `${PREFISSO}-m1`,
|
match_id: `${PREFISSO}-m1`,
|
||||||
votante_id: "va",
|
votante_id: "va",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
mvpVintiPerGiocatore,
|
mvpVintiPerGiocatore,
|
||||||
vincitoriMvp,
|
vincitoriMvp,
|
||||||
votoMvpAperto,
|
votoMvpAperto,
|
||||||
|
VOTI_MINIMI_MVP,
|
||||||
type VotoMvp,
|
type VotoMvp,
|
||||||
} from "@/lib/mvp-voti";
|
} from "@/lib/mvp-voti";
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ const partita = [
|
|||||||
v("m1", "g3", "g2", "Bruno"),
|
v("m1", "g3", "g2", "Bruno"),
|
||||||
v("m1", "g4", "g5", "Anna"),
|
v("m1", "g4", "g5", "Anna"),
|
||||||
v("m2", "g1", "g5", "Anna"),
|
v("m2", "g1", "g5", "Anna"),
|
||||||
|
v("m2", "g3", "g5", "Anna"),
|
||||||
];
|
];
|
||||||
|
|
||||||
assert.deepEqual(conteggioPartita(partita, "m1"), [
|
assert.deepEqual(conteggioPartita(partita, "m1"), [
|
||||||
@@ -46,10 +48,18 @@ assert.deepEqual(
|
|||||||
assert.deepEqual(vincitoriMvp(partita), { m1: "Bruno", m2: "Anna" });
|
assert.deepEqual(vincitoriMvp(partita), { m1: "Bruno", m2: "Anna" });
|
||||||
assert.deepEqual(vincitoriMvp(pari), {}, "due voti pari: MVP non assegnato");
|
assert.deepEqual(vincitoriMvp(pari), {}, "due voti pari: MVP non assegnato");
|
||||||
assert.deepEqual(vincitoriMvp([]), {});
|
assert.deepEqual(vincitoriMvp([]), {});
|
||||||
|
|
||||||
|
// --- vincitoriMvp: quorum minimo di voti -------------------------------------
|
||||||
|
assert.equal(VOTI_MINIMI_MVP, 2, "un solo voto non deve mai bastare da solo");
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
vincitoriMvp([v("m4", "g1", "g2", "Solo")]),
|
vincitoriMvp([v("m4", "g1", "g2", "Solo")]),
|
||||||
|
{},
|
||||||
|
"un solo votante, senza concorrenza: sotto quorum, nessun MVP",
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
vincitoriMvp([v("m4", "g1", "g2", "Solo"), v("m4", "g3", "g2", "Solo")]),
|
||||||
{ m4: "Solo" },
|
{ m4: "Solo" },
|
||||||
"un solo votante basta se non c'è concorrenza",
|
"due voti allo stesso candidato: quorum raggiunto",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Tre candidati: i primi due pari in testa, un terzo staccato. Deve restare senza MVP,
|
// Tre candidati: i primi due pari in testa, un terzo staccato. Deve restare senza MVP,
|
||||||
@@ -83,10 +93,15 @@ assert.deepEqual(
|
|||||||
"la parità non assegna",
|
"la parità non assegna",
|
||||||
);
|
);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
mvpVintiPerGiocatore([...partita, v("m5", "g1", "g2", "Bruno")]),
|
mvpVintiPerGiocatore([...partita, v("m5", "g1", "g2", "Bruno"), v("m5", "g3", "g2", "Bruno")]),
|
||||||
{ g2: 2, g5: 1 },
|
{ g2: 2, g5: 1 },
|
||||||
"i titoli si sommano su partite diverse",
|
"i titoli si sommano su partite diverse",
|
||||||
);
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
mvpVintiPerGiocatore([...partita, v("m5", "g1", "g2", "Bruno")]),
|
||||||
|
{ g2: 1, g5: 1 },
|
||||||
|
"m5 ha un solo voto: sotto quorum, non conta",
|
||||||
|
);
|
||||||
assert.deepEqual(mvpVintiPerGiocatore([]), {});
|
assert.deepEqual(mvpVintiPerGiocatore([]), {});
|
||||||
|
|
||||||
// --- votoMvpAperto: due ore dopo il fischio d'inizio -------------------------
|
// --- votoMvpAperto: due ore dopo il fischio d'inizio -------------------------
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import {
|
import {
|
||||||
mediaSquadra,
|
mediaSquadra,
|
||||||
|
mediaVotoColpoDOcchio,
|
||||||
mediePagelle,
|
mediePagelle,
|
||||||
mieiVoti,
|
mieiVoti,
|
||||||
pagellePartita,
|
pagellePartita,
|
||||||
type VotoPagella,
|
type VotoPagella,
|
||||||
} from "@/lib/pagelle";
|
} from "@/lib/pagelle";
|
||||||
|
import { VOTI_MINIMI_PAGELLA } from "@/lib/badges";
|
||||||
|
|
||||||
const voto = (
|
const voto = (
|
||||||
match_id: string,
|
match_id: string,
|
||||||
@@ -61,4 +63,17 @@ assert.deepEqual(mieiVoti(voti, "m1", "g99"), {}, "chi non ha votato non ha voti
|
|||||||
assert.equal(mediaSquadra(voti), 6.8, "(8+7+6+6)/4 = 6.75 → 6.8");
|
assert.equal(mediaSquadra(voti), 6.8, "(8+7+6+6)/4 = 6.75 → 6.8");
|
||||||
assert.equal(mediaSquadra([]), 0, "nessun voto: media zero, non NaN");
|
assert.equal(mediaSquadra([]), 0, "nessun voto: media zero, non NaN");
|
||||||
|
|
||||||
|
// --- mediaVotoColpoDOcchio: soglia minima per la StatTile home (DD-028) ------
|
||||||
|
assert.equal(
|
||||||
|
mediaVotoColpoDOcchio({ mediaVoto: 10, votiPagella: VOTI_MINIMI_PAGELLA - 1 }),
|
||||||
|
"—",
|
||||||
|
"sotto soglia: nascosta anche con media altissima",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
mediaVotoColpoDOcchio({ mediaVoto: 7.5, votiPagella: VOTI_MINIMI_PAGELLA }),
|
||||||
|
7.5,
|
||||||
|
"esattamente in soglia: la media conta",
|
||||||
|
);
|
||||||
|
assert.equal(mediaVotoColpoDOcchio({ mediaVoto: 0, votiPagella: 0 }), "—", "nessun voto ricevuto");
|
||||||
|
|
||||||
console.log("pagelle: ok");
|
console.log("pagelle: ok");
|
||||||
|
|||||||
Reference in New Issue
Block a user