Files
wm-projekt/lib/standings.ts
2026-06-22 15:32:32 -05:00

132 lines
4.8 KiB
TypeScript

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;
}