Corregge la classifica interna di Squadra a parità di punteggio

I giocatori a pari valore condividono ora la stessa posizione (dense rank) invece di
essere numerati in sequenza, e il sottotitolo di ogni riga segue il criterio scelto
invece di mostrare sempre le presenze consecutive. Per Palloni mostra le volte
consecutive in cui il giocatore li ha portati (nuovo Giocatore.seriePalloni).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 10:55:06 +02:00
co-authored by Claude Sonnet 5
parent 8dfd81ebe4
commit 15f058f760
8 changed files with 214 additions and 7 deletions
+3
View File
@@ -41,6 +41,8 @@ export type Giocatore = {
votiPagella: number;
/** Quante volte ha portato i palloni. */
palloni: number;
/** Volte consecutive (fino a oggi) in cui ha portato i palloni. */
seriePalloni: number;
/** Partite di campionato con almeno 3 cacche dichiarate. */
cacche: number;
/** Media di cacche dichiarate per partita. */
@@ -111,6 +113,7 @@ export const giocatori: Giocatore[] = rosaCSI
infortuni: 0,
ritardi: 0,
palloni: 0,
seriePalloni: 0,
cacche: 0,
cacchePartita: 0,
}))
+18
View File
@@ -1,6 +1,7 @@
import { formatData } from "./crapp-data";
import type { Evento } from "./eventi";
import { dataOggi } from "./scout-live";
import { aggiornaSerie } from "./serie";
export type Turno = { evento_id: string; giocatore_id: string; aggiornato_da: string | null };
@@ -81,6 +82,23 @@ export function conteggioTurni(
return out;
}
/**
* Volte consecutive in cui il giocatore ha portato i palloni, contando solo eventi già
* passati (stesso criterio `e.data < oggi` di `conteggioTurni`): un evento passato senza
* turno confermato non spezza la serie di nessuno, perché non dice ancora chi ha portato
* i palloni quella volta.
*/
export function serieConsecutivaPalloni(
giocatoreId: string,
turni: Record<string, string>,
eventi: Evento[],
oggi: string = dataOggi(),
): number {
return eventiPalloni(eventi)
.filter((e) => e.data < oggi && turni[e.id])
.reduce((serie, e) => aggiornaSerie(serie, turni[e.id] === giocatoreId), 0);
}
export function eventiDelGiorno(eventi: Evento[], isoData: string): Evento[] {
return eventiPalloni(eventi).filter((e) => e.data === isoData);
}
+51 -1
View File
@@ -4,7 +4,7 @@ import { nomeCompleto, useGiocatoriSquadra } from "./giocatori-squadra";
import { mvpVintiPerGiocatore, useVotiMvp } from "./mvp-voti";
import { mediePagelle, usePagelle } from "./pagelle";
import { statisticheCacche, useCacche } from "./cacche";
import { conteggioTurni } from "./palloni-core";
import { conteggioTurni, serieConsecutivaPalloni } from "./palloni-core";
import { useTurniPalloni } from "./palloni";
import { useInfortuniERitardi } from "./infortuni";
import { useGiocatoreId } from "./user-store";
@@ -96,6 +96,7 @@ export function useRosa(): Giocatore[] {
mediaVoto: medie[g.id]?.media ?? 0,
votiPagella: medie[g.id]?.voti ?? 0,
palloni: palloni[g.id] ?? 0,
seriePalloni: serieConsecutivaPalloni(g.id, turniSalvati, eventi),
cacche: statCacche[g.id]?.giornateTop ?? 0,
cacchePartita: statCacche[g.id]?.media ?? 0,
infortuni: infortuni[g.id] ?? 0,
@@ -115,6 +116,55 @@ export function useRosa(): Giocatore[] {
]);
}
/** Criteri di ordinamento della classifica interna di Squadra. */
export type CriterioClassifica = "presenze" | "mediaVoto" | "mvp" | "palloni" | "cacchePartita";
/**
* Dettaglio mostrato sotto il nome nella classifica interna, coerente col criterio
* selezionato: mostrare sempre le "presenze consecutive" aveva senso solo per Presenze,
* per gli altri criteri era un dato fuorviante perché scollegato dal valore in classifica.
*/
export function dettaglioClassifica(
g: {
streak: number;
votiPagella: number;
totaliEventi: number;
cacche: number;
seriePalloni: number;
},
criterio: CriterioClassifica,
): string {
switch (criterio) {
case "mediaVoto":
return `${g.votiPagella} voti pagella`;
case "mvp":
return `${g.totaliEventi} partite giocate`;
case "cacchePartita":
return `${g.cacche} giornate top`;
case "palloni":
return `${g.seriePalloni} volte consecutive`;
default:
return `${g.streak} presenze consecutive`;
}
}
/**
* Posizione in classifica ("dense rank"): a parità di valore i giocatori condividono la
* stessa posizione e il numero successivo non salta (1, 1, 2 — non 1, 1, 3). `valori` deve
* essere già ordinato in modo decrescente, coerente con l'ordine visualizzato.
*/
export function classificaRank(valori: number[]): number[] {
const rank: number[] = [];
for (let i = 0; i < valori.length; i++) {
if (i > 0 && valori[i] === valori[i - 1]) {
rank.push(rank[i - 1] ?? 1);
} else {
rank.push((rank[i - 1] ?? 0) + 1);
}
}
return rank;
}
/** Il giocatore selezionato sul dispositivo, con le statistiche complete. */
export function useIo(): Giocatore | null {
const id = useGiocatoreId();
+13 -6
View File
@@ -7,7 +7,13 @@ import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
import { Avatar } from "@/components/crapp/Avatar";
import { formatData } from "@/lib/crapp-data";
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
import { useRosa, useObiettivi } from "@/lib/rosa";
import {
useRosa,
useObiettivi,
classificaRank,
dettaglioClassifica,
type CriterioClassifica,
} from "@/lib/rosa";
import { usePresenzeUltimoMeseTutti } from "@/lib/presenze-mese";
import { totaliSquadra, useScoutMatches } from "@/lib/scout-store";
import { useCsi } from "@/lib/csi";
@@ -70,7 +76,7 @@ const criteri = [
{ id: "cacchePartita", label: "Cacche" },
] as const;
type Criterio = (typeof criteri)[number]["id"];
type Criterio = CriterioClassifica;
function valore(
g: { presenze: number; mediaVoto: number; mvp: number; palloni: number; cacchePartita: number },
@@ -95,6 +101,7 @@ function Squadra() {
)
: 0;
const ordinati = [...rosa].sort((a, b) => valore(b, criterio) - valore(a, criterio));
const rank = classificaRank(ordinati.map((g) => valore(g, criterio)));
const max = ordinati[0] ? valore(ordinati[0], criterio) || 1 : 1;
const { data: csi } = useCsi();
const matchGiocati = csi ? partiteGiocate(csi.partite).length : scoutMatches.length;
@@ -288,17 +295,17 @@ function Squadra() {
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3">
<div className="flex min-w-0 items-center gap-3">
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-secondary font-display text-lg">
{i + 1}
{rank[i]}
</span>
<div className="min-w-0">
<p className="truncate text-sm font-bold">
{g.nome}
{i === 0 ? (
{rank[i] === 1 ? (
<Crown className="ml-1 inline h-3.5 w-3.5 text-warning" />
) : null}
</p>
<p className="truncate text-xs text-muted-foreground">
#{g.numero} · {g.ruolo} · {g.streak} presenze consecutive
#{g.numero} · {g.ruolo} · {dettaglioClassifica(g, criterio)}
</p>
</div>
</div>
@@ -317,7 +324,7 @@ function Squadra() {
</div>
</div>
),
[mediaPresenze, matchGiocati, pagelle, team, filtroAperto, criterio, ordinati, max],
[mediaPresenze, matchGiocati, pagelle, team, filtroAperto, criterio, ordinati, rank, max],
);
const contenutoObiettivi = useMemo(