import { GroupId, Match, Team } from "./types"; import { computeGroupTables } from "./standings"; // Bestimmt, welche Tabellenplätze einer Gruppe bereits MATHEMATISCH feststehen – // auch wenn noch Spiele ausstehen. Berücksichtigt ALLE FIFA-Tiebreaker, weil die // Tabelle über computeGroupTables() berechnet wird (Punkte, Direktvergleich, // Tordifferenz, Tore). // // Methode: vollständige Worst-Case-Enumeration der Restspiele. Ein Platz p ist // für ein Team sicher, wenn es in JEDEM möglichen Ausgang aller Restspiele auf // Platz p oder besser bleibt. Wir enumerieren pro Restspiel mehrere // repräsentative Ergebnisse, die alle relevanten Tiebreaker-Effekte abdecken: // - Heimsieg knapp (1:0) und hoch (5:0) // - Auswärtssieg knapp (0:1) und hoch (0:5) // - Unentschieden (0:0 und 2:2) // Diese Menge deckt Punkt- UND Tordifferenz-Szenarien ausreichend ab, um echte // Sicherheit korrekt zu erkennen (inkl. Fälle wie "Direktvergleich bereits // gewonnen -> uneinholbar"). // Mögliche Ergebnis-Varianten je Restspiel (homeGoals, awayGoals). const OUTCOME_VARIANTS: Array<[number, number]> = [ [1, 0], [5, 0], // Heimsieg knapp / hoch [0, 1], [0, 5], // Auswärtssieg knapp / hoch [0, 0], [2, 2], // Unentschieden ]; function openMatches(group: GroupId, matches: Match[]): Match[] { return matches.filter( (m) => m.group === group && m.status !== "FINISHED" && m.homeTeamId != null && m.awayTeamId != null, ); } // Liefert für eine Gruppe Map: Platz(1-basiert) -> teamId, aber nur für Plätze, // die in ALLEN Szenarien stabil von demselben Team gehalten werden. export function securePlaces( group: GroupId, teams: Team[], matches: Match[], ): Map { const open = openMatches(group, matches); // Begrenzung: bei sehr vielen offenen Spielen wird die Enumeration groß. // In 4er-Gruppen sind es maximal 6 offene Spiele (Turnierstart) -> 6^? zu viel. // Wir enumerieren nur, wenn die Kombinationszahl handhabbar ist; sonst gilt // konservativ "nichts sicher" (am Turnierstart ohnehin korrekt). const combos = Math.pow(OUTCOME_VARIANTS.length, open.length); if (open.length === 0) { // Alles gespielt: aktuelle Tabelle ist final. const table = computeGroupTables(teams, matches).find((t) => t.group === group); const res = new Map(); table?.rows.forEach((r) => res.set(r.rank, r.teamId)); return res; } if (combos > 100_000) { return new Map(); // zu früh im Turnier -> nichts gesichert } // Für jeden Platz das Set der Teams sammeln, die diesen Platz über ALLE // Szenarien einnehmen können. Ist das Set einelementig, ist der Platz sicher. const placeTeams: Array> = [new Set(), new Set(), new Set(), new Set()]; const total = combos; for (let combo = 0; combo < total; combo++) { // Szenario zusammenbauen: jedes offene Spiel bekommt eine Variante. let c = combo; const simulated: Match[] = open.map((m) => { const variantIdx = c % OUTCOME_VARIANTS.length; c = Math.floor(c / OUTCOME_VARIANTS.length); const [hg, ag] = OUTCOME_VARIANTS[variantIdx]; return { ...m, status: "FINISHED", homeScore: hg, awayScore: ag }; }); // Gespielte + simulierte Spiele kombinieren. const played = matches.filter((m) => !(m.group === group && m.status !== "FINISHED")); const all = [...played, ...simulated]; const table = computeGroupTables(teams, all).find((t) => t.group === group); if (!table) continue; table.rows.forEach((r) => { placeTeams[r.rank - 1]?.add(r.teamId); }); } const result = new Map(); for (let i = 0; i < 4; i++) { if (placeTeams[i].size === 1) { result.set(i + 1, [...placeTeams[i]][0]); } } return result; } export function placeIsSecure( group: GroupId, place: number, teams: Team[], matches: Match[], ): boolean { return securePlaces(group, teams, matches).has(place); }