140 lines
5.4 KiB
TypeScript
140 lines
5.4 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 Spiel in zwei Tabellenzeilen ein.
|
|
// Standard: nur abgeschlossene Spiele (FINISHED). Mit includeLive=true werden
|
|
// auch laufende Spiele (LIVE/IN_PLAY/PAUSED) mit ihrem Zwischenstand gezählt.
|
|
function applyMatch(rows: Map<string, StandingRow>, m: Match, includeLive = false) {
|
|
const live = m.status === "LIVE" || m.status === "IN_PLAY" || m.status === "PAUSED";
|
|
const counts = m.status === "FINISHED" || (includeLive && live);
|
|
if (!counts) 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.
|
|
// includeLive=false (Standard): nur abgeschlossene Spiele (offizielle Tabelle,
|
|
// Basis für die fix-Berechnung). includeLive=true: laufende Spiele werden mit
|
|
// Zwischenstand eingerechnet (Live-Tabelle).
|
|
export function computeGroupTables(
|
|
teams: Team[], matches: Match[], includeLive = false,
|
|
): 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, includeLive);
|
|
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;
|
|
} |