init
This commit is contained in:
131
lib/standings.ts
Normal file
131
lib/standings.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
GROUP_IDS, GroupId, Match, Team,
|
||||
StandingRow, GroupTable, ThirdPlaceRow,
|
||||
} from "./types";
|
||||
|
||||
// Berechnet eine leere Tabellenzeile.
|
||||
function emptyRow(teamId: string): StandingRow {
|
||||
return {
|
||||
teamId, played: 0, won: 0, drawn: 0, lost: 0,
|
||||
goalsFor: 0, goalsAgainst: 0, goalDiff: 0, points: 0, rank: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Trägt ein abgeschlossenes Spiel in zwei Tabellenzeilen ein.
|
||||
function applyMatch(rows: Map<string, StandingRow>, m: Match) {
|
||||
if (m.status !== "FINISHED") return;
|
||||
if (m.homeTeamId == null || m.awayTeamId == null) return;
|
||||
if (m.homeScore == null || m.awayScore == null) return;
|
||||
const h = rows.get(m.homeTeamId);
|
||||
const a = rows.get(m.awayTeamId);
|
||||
if (!h || !a) return;
|
||||
h.played++; a.played++;
|
||||
h.goalsFor += m.homeScore; h.goalsAgainst += m.awayScore;
|
||||
a.goalsFor += m.awayScore; a.goalsAgainst += m.homeScore;
|
||||
if (m.homeScore > m.awayScore) { h.won++; a.lost++; h.points += 3; }
|
||||
else if (m.homeScore < m.awayScore) { a.won++; h.lost++; a.points += 3; }
|
||||
else { h.drawn++; a.drawn++; h.points += 1; a.points += 1; }
|
||||
}
|
||||
|
||||
function finalizeRow(r: StandingRow) {
|
||||
r.goalDiff = r.goalsFor - r.goalsAgainst;
|
||||
}
|
||||
|
||||
// Vergleicht zwei Teams nach FIFA-Kriterien innerhalb derselben Gruppe.
|
||||
// Reihenfolge: Punkte, dann Direktvergleich (Punkte/TD/Tore in den Spielen
|
||||
// der punktgleichen Teams), dann Gesamt-TD, Gesamt-Tore. Fair-Play/Ranking
|
||||
// werden hier nicht abgebildet (kein Feed dafür) -> bleibt bei Gleichstand stabil.
|
||||
function compareInGroup(
|
||||
a: StandingRow, b: StandingRow,
|
||||
matches: Match[], tiedIds: Set<string>,
|
||||
): number {
|
||||
if (b.points !== a.points) return b.points - a.points;
|
||||
|
||||
// Direktvergleich nur unter den punktgleichen Teams
|
||||
if (tiedIds.size >= 2 && tiedIds.has(a.teamId) && tiedIds.has(b.teamId)) {
|
||||
const mini = miniTable(matches, tiedIds);
|
||||
const ma = mini.get(a.teamId);
|
||||
const mb = mini.get(b.teamId);
|
||||
if (ma && mb) {
|
||||
if (mb.points !== ma.points) return mb.points - ma.points;
|
||||
const mdA = ma.goalsFor - ma.goalsAgainst;
|
||||
const mdB = mb.goalsFor - mb.goalsAgainst;
|
||||
if (mdB !== mdA) return mdB - mdA;
|
||||
if (mb.goalsFor !== ma.goalsFor) return mb.goalsFor - ma.goalsFor;
|
||||
}
|
||||
}
|
||||
|
||||
if (b.goalDiff !== a.goalDiff) return b.goalDiff - a.goalDiff;
|
||||
if (b.goalsFor !== a.goalsFor) return b.goalsFor - a.goalsFor;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Baut eine Minitabelle aus den Spielen, in denen NUR die tiedIds gegeneinander spielten.
|
||||
function miniTable(matches: Match[], tiedIds: Set<string>): Map<string, StandingRow> {
|
||||
const rows = new Map<string, StandingRow>();
|
||||
tiedIds.forEach((id) => rows.set(id, emptyRow(id)));
|
||||
for (const m of matches) {
|
||||
if (m.status !== "FINISHED") continue;
|
||||
if (m.homeTeamId == null || m.awayTeamId == null) continue;
|
||||
if (!tiedIds.has(m.homeTeamId) || !tiedIds.has(m.awayTeamId)) continue;
|
||||
applyMatch(rows, m);
|
||||
}
|
||||
rows.forEach(finalizeRow);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Berechnet alle 12 Gruppentabellen aus Teams + Spielen.
|
||||
export function computeGroupTables(teams: Team[], matches: Match[]): GroupTable[] {
|
||||
const tables: GroupTable[] = [];
|
||||
for (const g of GROUP_IDS) {
|
||||
const groupTeams = teams.filter((t) => t.group === g);
|
||||
const rows = new Map<string, StandingRow>();
|
||||
groupTeams.forEach((t) => rows.set(t.id, emptyRow(t.id)));
|
||||
const groupMatches = matches.filter((m) => m.group === g);
|
||||
for (const m of groupMatches) applyMatch(rows, m);
|
||||
rows.forEach(finalizeRow);
|
||||
|
||||
const arr = [...rows.values()];
|
||||
// Gruppen punktgleicher Teams für den Direktvergleich bestimmen
|
||||
const byPoints = new Map<number, string[]>();
|
||||
arr.forEach((r) => {
|
||||
const list = byPoints.get(r.points) ?? [];
|
||||
list.push(r.teamId);
|
||||
byPoints.set(r.points, list);
|
||||
});
|
||||
|
||||
arr.sort((a, b) => {
|
||||
const tied = byPoints.get(a.points) ?? [];
|
||||
const tiedIds = new Set(a.points === b.points && tied.length >= 2 ? tied : []);
|
||||
return compareInGroup(a, b, groupMatches, tiedIds);
|
||||
});
|
||||
arr.forEach((r, i) => (r.rank = i + 1));
|
||||
tables.push({ group: g, rows: arr });
|
||||
}
|
||||
return tables;
|
||||
}
|
||||
|
||||
// Rankt die 12 Gruppendritten gruppenübergreifend (ohne Direktvergleich).
|
||||
// Kriterien: Punkte, TD, Tore. Beste 8 -> qualifies = true.
|
||||
export function computeThirdPlaceTable(tables: GroupTable[]): ThirdPlaceRow[] {
|
||||
const thirds: ThirdPlaceRow[] = tables
|
||||
.map((t) => {
|
||||
const r = t.rows.find((x) => x.rank === 3);
|
||||
if (!r) return null;
|
||||
return { ...r, group: t.group, qualifies: false, overallRank: 0 };
|
||||
})
|
||||
.filter((x): x is ThirdPlaceRow => x !== null);
|
||||
|
||||
thirds.sort((a, b) => {
|
||||
if (b.points !== a.points) return b.points - a.points;
|
||||
if (b.goalDiff !== a.goalDiff) return b.goalDiff - a.goalDiff;
|
||||
if (b.goalsFor !== a.goalsFor) return b.goalsFor - a.goalsFor;
|
||||
return a.group.localeCompare(b.group); // stabil
|
||||
});
|
||||
|
||||
thirds.forEach((r, i) => {
|
||||
r.overallRank = i + 1;
|
||||
r.qualifies = i < 8;
|
||||
});
|
||||
return thirds;
|
||||
}
|
||||
Reference in New Issue
Block a user