import { GroupId, GROUP_IDS, Match, Team } from "./types"; import { computeGroupTables } from "./standings"; import { resolveAnnexC } 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; } function openMatches(group: GroupId, matches: Match[]): Match[] { return matches.filter( (m) => m.group === group && m.status !== "FINISHED" && m.homeTeamId != null && m.awayTeamId != null, ); } 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) { 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); 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 }; } function buildThirdStates(matches: Match[], teams: Team[]): Map { const states = new Map(); for (const g of GROUP_IDS) { const gm = matches.filter((m) => m.group === g); const allDone = gm.length > 0 && gm.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 }); } } return states; } function classifyGroups(states: Map): { lockedIn: Set; lockedOut: Set; contested: Set; } { 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; } 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++; } const alwaysTop8 = countCouldBeBetter <= 7; 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++; } const neverTop8 = countDefinitelyBetter >= 8; if (alwaysTop8) lockedIn.add(g); else if (neverTop8) lockedOut.add(g); else contested.add(g); } return { lockedIn, lockedOut, contested }; } // --- Public API --- // Liefert die Team-IDs der Dritten, die mathematisch sicher unter den Top 8 // der gruppenübergreifenden Dritten-Tabelle sind. Prüft für jedes Team aus // einer FERTIGEN Gruppe (played === 3), ob es in ALLEN noch möglichen // Restspiel-Konstellationen von maximal 7 anderen Dritten überholt werden kann. export function securelyQualifiedThirdTeams(matches: Match[], teams: Team[]): Set { console.log("[3RD-DISPLAY] ThirdPlace ✓-haekchen = securelyQualifiedThirdTeams (team-level, zaehlt GD)", "| ThirdPlace GRUENER name = CSS .team-complete (played===3, KEIN Sicherheitsindikator)", "| Bracket fix = thirdSlotIsSecure -> classifyGroups -> lockedIn (group-level, NUR Punkte, KEIN GD)"); console.log("[3RD-SOURCE] securelyQualifiedThirdTeams aufgerufen (teams)"); const states = buildThirdStates(matches, teams); const secureTeams = new Set(); const { lockedIn } = classifyGroups(states); for (const g of GROUP_IDS) { const st = states.get(g)!; if (!st.finished || !st.third || st.possibleTeamIds.size !== 1) continue; const my = st.third; let couldBeBetter = 0; for (const og of GROUP_IDS) { if (og === g) continue; const os = states.get(og)!; if (os.finished && os.third) { if (os.third.points > my.points) couldBeBetter++; else if (os.third.points === my.points && os.third.goalDiff > my.goalDiff) couldBeBetter++; else if (os.third.points === my.points && os.third.goalDiff === my.goalDiff && os.third.goalsFor > my.goalsFor) couldBeBetter++; } else { if (os.maxPoints > my.points) couldBeBetter++; else if (os.maxPoints === my.points && !os.finished) couldBeBetter++; } } const team = teams.find(t => t.id === my.teamId); const isSecure = couldBeBetter <= 7; if (isSecure) secureTeams.add(my.teamId); // Diagnose-Log für Iran und Grenzfälle const targetTeams = new Set(["IRN", "KOR", "ALG", "CRO"]); // Team-Codes if (team && targetTeams.has(team.code)) { console.log("[3RD-TEAM-SECURE]", "team:", team.name, "| group:", g, "| punkte:", my.points, "| gd:", my.goalDiff, "| couldBeBetter:", couldBeBetter, "| gruppe lockedIn:", lockedIn.has(g), "| als sicher markiert:", isSecure); } } const iranTeam = teams.find(t => t.code === "IRN"); if (iranTeam) { console.log("[3RD-IRAN-DISPLAY]", "secureTeams enthaelt Iran:", secureTeams.has(iranTeam.id), "| secureGroups (lockedIn) enthaelt G:", lockedIn.has("G"), "| haekchen gezeigt:", secureTeams.has(iranTeam.id) ? "ja" : "nein", "| name gruen (CSS team-complete, played===3):", "immer bei finished gruppe", "| im baum fix:", lockedIn.has("G") ? "ja (thirdSlotIsSecure via classifyGroups/POINTS)" : "nein"); } return secureTeams; } // Liefert die Gruppen, deren Dritter mathematisch sicher unter den Top 8 ist. // (Gruppen-Ebene — für Fix-Markierung im Baum, nicht für Team-Häkchen.) export function securelyQualifiedThirdGroups(matches: Match[], teams: Team[]): GroupId[] { console.log("[3RD-SOURCE] securelyQualifiedThirdGroups aufgerufen (groups)"); const states = buildThirdStates(matches, teams); const { lockedIn, lockedOut, contested } = classifyGroups(states); console.log("[3RD-CLASSIFY] lockedIn:", [...lockedIn].join(","), "| lockedOut:", [...lockedOut].join(","), "| contested:", [...contested].join(",")); return [...lockedIn]; } // Prüft, ob ein bestimmter Dritten-Slot (Gegner des Siegers von `winnerGroup`) // in ALLEN noch möglichen Konstellationen identisch bleibt. export function thirdSlotIsSecure(winnerGroup: GroupId, matches: Match[], teams: Team[]): boolean { console.log("[3RD-SOURCE] thirdSlotIsSecure aufgerufen (slot-fix via classifyGroups)"); const states = buildThirdStates(matches, teams); const { lockedIn, contested } = classifyGroups(states); if (lockedIn.size > 8) return false; if (lockedIn.size === 8) { const assignment = resolveAnnexC([...lockedIn]); if (!assignment) return false; const ag = assignment[winnerGroup]; if (!ag) return false; const ss = states.get(ag); if (!ss) return false; const result = ss.finished && ss.third != null && ss.possibleTeamIds.size === 1; return result; } if (lockedIn.size + contested.size < 8) return false; const contestedArr = [...contested]; const need = 8 - lockedIn.size; const combos = combinations(contestedArr.length, need); if (combos > 200) return false; let stableGroup: GroupId | null = null; for (const indices of enumerateCombinations(contestedArr.length, need)) { const qGroups: GroupId[] = [...lockedIn, ...indices.map((i) => contestedArr[i])]; if (qGroups.length !== 8) continue; const assignment = resolveAnnexC(qGroups); if (!assignment) continue; const ag = assignment[winnerGroup]; if (!ag) return false; if (stableGroup === null) stableGroup = ag; else if (stableGroup !== ag) return false; } if (stableGroup === null) return false; const ss = states.get(stableGroup); if (!ss) return false; const result = ss.finished && ss.third != null && ss.possibleTeamIds.size === 1; return result; } // --- Combinatorics helpers --- function combinations(n: number, k: number): number { if (k < 0 || k > n) return 0; if (k === 0 || k === n) return 1; let r = 1; for (let i = 1; i <= k; i++) r = r * (n - i + 1) / i; return Math.round(r); } function* enumerateCombinations(n: number, k: number): Generator { if (k === 0) { yield []; return; } if (k > n) return; const idx = Array.from({ length: k }, (_, i) => i); while (true) { yield [...idx]; let i = k - 1; while (i >= 0 && idx[i] === n - k + i) i--; if (i < 0) break; idx[i]++; for (let j = i + 1; j < k; j++) idx[j] = idx[j - 1] + 1; } }