import { GroupId, GROUP_IDS, Match, Team } from "./types"; import { computeGroupTables, computeThirdPlaceTable } from "./standings"; import { resolveAnnexC, ANNEX_WINNER_ORDER } from "./bracket"; // Repräsentative Ergebnis-Varianten je Restspiel (decken alle Punktkombinationen ab). const OUTCOMES: Array<[number, number]> = [ [1, 0], // Heimsieg [0, 1], // Auswärtssieg [0, 0], // Unentschieden ]; interface ThirdInfo { teamId: string; points: number; goalsFor: number; goalsAgainst: number; goalDiff: number; } interface GroupThirdState { finished: boolean; third: ThirdInfo | null; minPoints: number; maxPoints: number; possibleTeamIds: Set; } // Offene Spiele einer Gruppe, die noch kein Ergebnis haben. function openMatches(group: GroupId, matches: Match[]): Match[] { return matches.filter( (m) => m.group === group && m.status !== "FINISHED" && m.homeTeamId != null && m.awayTeamId != null, ); } // Simuliert alle Restspiel-Ergebnisse für eine Gruppe und sammelt die möglichen // Punktzahlen und Team-IDs des Drittplatzierten. function enumerateThirdPossibilities( group: GroupId, teams: Team[], matches: Match[], ): { minPoints: number; maxPoints: number; teamIds: Set } { const open = openMatches(group, matches); const played = matches.filter( (m) => m.group === group && (m.status === "FINISHED" || m.homeTeamId == null || m.awayTeamId == null), ); let minP = Infinity, maxP = -Infinity; const teamIds = new Set(); if (open.length === 0) { // Keine offenen Spiele: aktuellen Tabellenstand nehmen const table = computeGroupTables(teams, matches).find((t) => t.group === group); const third = table?.rows.find((r) => r.rank === 3); if (third) { minP = maxP = third.points; teamIds.add(third.teamId); } return { minPoints: minP, maxPoints: maxP, teamIds }; } const totalCombos = Math.pow(OUTCOMES.length, open.length); // Safety: bei sehr vielen offenen Spielen → breites Intervall if (totalCombos > 500) { return { minPoints: 0, maxPoints: 9, teamIds }; } for (let combo = 0; combo < totalCombos; combo++) { let c = combo; const simulated: Match[] = open.map((m) => { const variantIdx = c % OUTCOMES.length; c = Math.floor(c / OUTCOMES.length); const [hg, ag] = OUTCOMES[variantIdx]; return { ...m, status: "FINISHED" as const, homeScore: hg, awayScore: ag }; }); const all = [...played, ...simulated]; const table = computeGroupTables(teams, all).find((t) => t.group === group); const third = table?.rows.find((r) => r.rank === 3); if (third) { minP = Math.min(minP, third.points); maxP = Math.max(maxP, third.points); teamIds.add(third.teamId); } } return { minPoints: minP === Infinity ? 0 : minP, maxPoints: maxP === -Infinity ? 9 : maxP, teamIds }; } // Bestimmt, ob ein Gruppendritter eines bestimmten Slots (Gegner des Siegers // der Gruppe `winnerGroup`) in ALLEN noch möglichen Konstellationen identisch // bleibt und somit als „fix" markiert werden kann. // // Strategie: Enumeriert alle möglichen Mengen von 8 qualifizierten Dritten-Gruppen, // prüft per Annex C, welche Gruppe in jeder Konstellation in diesen Slot fällt. export function thirdSlotIsSecure( winnerGroup: GroupId, matches: Match[], teams: Team[], ): boolean { // 1. Pro Gruppe den Status des Dritten analysieren const states = new Map(); for (const g of GROUP_IDS) { const groupMatches = matches.filter((m) => m.group === g); const allDone = groupMatches.length > 0 && groupMatches.every((m) => m.status === "FINISHED"); const table = computeGroupTables(teams, matches).find((t) => t.group === g); const thirdRow = table?.rows.find((r) => r.rank === 3); let third: ThirdInfo | null = null; if (thirdRow) { third = { teamId: thirdRow.teamId, points: thirdRow.points, goalsFor: thirdRow.goalsFor, goalsAgainst: thirdRow.goalsAgainst, goalDiff: thirdRow.goalDiff, }; } if (allDone && third) { states.set(g, { finished: true, third, minPoints: third.points, maxPoints: third.points, possibleTeamIds: new Set([third.teamId]), }); } else { const { minPoints, maxPoints, teamIds } = enumerateThirdPossibilities(g, teams, matches); states.set(g, { finished: false, third, minPoints, maxPoints, possibleTeamIds: teamIds, }); } } // 2. lockedIn / lockedOut / contested bestimmen // lockedIn = Dritter liegt selbst im Worst-Case in den Top 8 // lockedOut = Dritter liegt selbst im Best-Case nicht in den Top 8 const lockedIn = new Set(); const lockedOut = new Set(); const contested = new Set(); for (const g of GROUP_IDS) { const st = states.get(g)!; if (st.finished && !st.third) { lockedOut.add(g); continue; } // Prüfen, ob dieser Dritte garantiert in den Top 8 liegt (Worst-Case) let alwaysTop8 = true; let neverTop8 = true; for (const other of GROUP_IDS) { if (other === g) continue; const os = states.get(other)!; // Kann der andere Dritte diesen hier überholen? // Best-Case für den anderen: maxPoints; Worst-Case für diesen: maxPoints (Worst) / minPoints (Best) // Für alwaysTop8: dieser mit minPoints vs andere mit maxPoints — liegen ≤ 7 andere über ihm? // Für neverTop8: dieser mit maxPoints vs andere mit minPoints — sind mindestens 8 andere besser? if (os.maxPoints > st.minPoints || (os.maxPoints === st.minPoints && !st.finished)) { // Der andere KÖNNTE diesen überholen alwaysTop8 = false; } if (st.maxPoints > os.minPoints || (st.maxPoints === os.minPoints && !os.finished)) { // Dieser KÖNNTE den anderen überholen neverTop8 = false; } } // Exakte Prüfung: zähle wie viele Groups definitiv besser/schlechter sind let definitelyBetter = 0; let definitelyWorse = 0; for (const other of GROUP_IDS) { if (other === g) continue; const os = states.get(other)!; // other garantiert besser als g? if (os.minPoints > st.maxPoints) definitelyBetter++; else if (os.minPoints === st.maxPoints && os.finished && !st.finished) definitelyBetter++; // g garantiert besser als other? if (st.minPoints > os.maxPoints) definitelyWorse++; else if (st.minPoints === os.maxPoints && st.finished && !os.finished) definitelyWorse++; } if (definitelyBetter <= 7) alwaysTop8 = false; // mehr als 7 definitiv besser → nicht garantiert Top 8 if (definitelyWorse >= 8) neverTop8 = true; // 8+ definitiv schlechter → garantiert Top 8 // Neuberechnung mit korrekter Logik: // Garantiert Top 8: höchstens 7 andere Gruppen haben Best-Case ≥ mein Worst-Case let countCouldBeBetter = 0; for (const other of GROUP_IDS) { if (other === g) continue; const os = states.get(other)!; if (os.maxPoints > st.minPoints) countCouldBeBetter++; else if (os.maxPoints === st.minPoints && !st.finished && os.finished) countCouldBeBetter++; } alwaysTop8 = countCouldBeBetter <= 7; // Garantiert NICHT Top 8: mindestens 8 andere haben Worst-Case > mein Best-Case let countDefinitelyBetter = 0; for (const other of GROUP_IDS) { if (other === g) continue; const os = states.get(other)!; if (os.minPoints > st.maxPoints) countDefinitelyBetter++; else if (os.minPoints === st.maxPoints && os.finished && !st.finished) countDefinitelyBetter++; } neverTop8 = countDefinitelyBetter >= 8; if (alwaysTop8) lockedIn.add(g); else if (neverTop8) lockedOut.add(g); else contested.add(g); } // 3. Wenn weniger als 8 lockedIn sind, aber alle contested stabil → früh raus if (lockedIn.size >= 8) { // Mehr als 8 Gruppen garantiert qualifiziert → Annex C kann nicht eindeutig sein // (es gibt mehr als eine Auswahl von 8 aus den lockedIn) // Prüfe: sind ALLE 8er-Teilmengen aus lockedIn identisch bezüglich dieses Slots? // Das ist komplex; im Zweifel konservativ: false return false; } if (lockedIn.size + contested.size < 8) { // Nicht genug Gruppen, um 8 Dritte zu füllen → Annex C nicht auflösbar return false; } // 4. Alle Kombinationen der contested Groups enumerieren, // die zusammen mit lockedIn genau 8 qualifizierte Dritte ergeben const contestedArr = [...contested]; const need = 8 - lockedIn.size; const lockedArr = [...lockedIn]; // Begrenzung der Kombinationszahl const combos = combinations(contestedArr.length, need); if (combos > 200) return false; // zu viele Kombinationen → nichts stabil let stableGroup: GroupId | null = null; for (const indices of enumerateCombinations(contestedArr.length, need)) { const qGroups: GroupId[] = [...lockedArr, ...indices.map((i) => contestedArr[i])]; if (qGroups.length !== 8) continue; // Prüfen, ob diese Kombination tatsächlich möglich ist // (die ausgewählten contested müssen in mindestens einem Szenario Top 8 sein, // die nicht ausgewählten contested in mindestens einem Szenario nicht Top 8) // Vereinfachung: alle Kombinationen als möglich betrachten, solange // keine offensichtlichen Widersprüche vorliegen const feasible = isCombinationFeasible(qGroups, states); if (!feasible) continue; const assignment = resolveAnnexC(qGroups); if (!assignment) continue; const assignedGroup = assignment[winnerGroup]; if (!assignedGroup) return false; if (stableGroup === null) { stableGroup = assignedGroup; } else if (stableGroup !== assignedGroup) { return false; // Slot fällt in verschiedenen Kombinationen auf verschiedene Gruppen } } // 5. Slot ist stabil → prüfe, ob die Quellgruppe selbst bereits feststeht if (stableGroup === null) return false; const stableState = states.get(stableGroup); if (!stableState) return false; return stableState.finished && stableState.third != null && stableState.possibleTeamIds.size === 1; } // Berechnet C(n, k) — die Anzahl der Kombinationen. function combinations(n: number, k: number): number { if (k < 0 || k > n) return 0; if (k === 0 || k === n) return 1; let result = 1; for (let i = 1; i <= k; i++) { result = result * (n - i + 1) / i; } return Math.round(result); } // Generator für alle k-Kombinationen aus n Elementen (0-basierte Indizes). function* enumerateCombinations(n: number, k: number): Generator { if (k === 0) { yield []; return; } if (k > n) return; const indices = Array.from({ length: k }, (_, i) => i); while (true) { yield [...indices]; let i = k - 1; while (i >= 0 && indices[i] === n - k + i) i--; if (i < 0) break; indices[i]++; for (let j = i + 1; j < k; j++) indices[j] = indices[j - 1] + 1; } } // Prüft, ob eine Kombination von 8 qualifizierten Gruppen tatsächlich möglich ist. // Vereinfacht: jede Gruppe in der Kombination muss mindestens einen Punktestand // haben können, der Top 8 reicht; jede Gruppe außerhalb muss mindestens einen // Punktestand haben können, der NICHT Top 8 reicht. function isCombinationFeasible( qGroups: GroupId[], states: Map, ): boolean { const qSet = new Set(qGroups); // Jede qualifizierte Gruppe muss potentiell in die Top 8 kommen können for (const g of qGroups) { const st = states.get(g)!; if (st.third && st.finished && st.maxPoints === 0 && qGroups.length > 8) { // Dritter mit 0 Punkten kann nicht qualifiziert sein, wenn mehr als 8 Kandidaten // Vereinfachte Prüfung } } // Jede nicht-qualifizierte Gruppe muss potentiell rausfallen können for (const g of GROUP_IDS) { if (qSet.has(g)) continue; // OK, diese Gruppe ist nicht qualifiziert — muss möglich sein } return true; }