import { GroupId, GroupTable, Match, Team, ThirdPlaceRow } from "@/lib/types"; import { R32, LATER_ROUNDS, R32Match, BracketSlot, ThirdAssignment, slotLabel, } from "@/lib/bracket"; import { placeIsSecure } from "@/lib/secure-places"; import { thirdSlotIsSecure } from "@/lib/third-place-security"; // Ein aufgelöster Teilnehmer einer K.o.-Begegnung. export interface ResolvedSide { teamId: string | null; // null = noch Platzhalter label: string; // Anzeige (Teamname oder "Sieger E" etc.) code: string; score: number | null; isWinner: boolean; provisional: boolean; // true = nur auf Basis aktueller Tabelle, noch nicht fix tooltip: string | null; // Hover-Erklärung (Herkunft + ob fix) } export interface ResolvedTie { matchNumber: number; stage: string; home: ResolvedSide; away: ResolvedSide; status: string; prob?: { home: number; draw: number | null; away: number } | null; } // Findet das tatsächliche Spiel (aus dem Feed) zu einer FIFA-Match-Nummer. function feedMatch(matches: Match[], num: number): Match | undefined { return matches.find((m) => m.matchNumber === num && m.group == null); } // Bestimmt den Sieger eines abgeschlossenen Spiels (Feed) als Team-ID. function winnerOf(m: Match | undefined): string | null { if (!m || m.status !== "FINISHED") return null; if (m.homeScore == null || m.awayScore == null) return null; if (m.homeScore > m.awayScore) return m.homeTeamId; if (m.awayScore > m.homeScore) return m.awayTeamId; return null; // Unentschieden -> Elfmeter; Feed liefert i.d.R. Sieger separat } function loserOf(m: Match | undefined): string | null { if (!m || m.status !== "FINISHED") return null; if (m.homeScore == null || m.awayScore == null) return null; if (m.homeScore > m.awayScore) return m.awayTeamId; if (m.awayScore > m.homeScore) return m.homeTeamId; return null; } // Löst einen R32-Slot (W/R/3) zu einer Team-ID auf, sofern bereits bekannt. // provisional = true, solange die zugrunde liegende Gruppe/Zuordnung nicht fix ist. function resolveR32Slot( slot: BracketSlot, winnerGroup: GroupId | undefined, tables: GroupTable[], assignment: ThirdAssignment | null, thirds: ThirdPlaceRow[], matches: Match[], teams: Team[], annexResolved: boolean, ): { teamId: string | null; provisional: boolean; tooltip: string | null } { const table = (g: GroupId) => tables.find((t) => t.group === g); if (slot.type === "W") { const t = table(slot.group!); const teamId = t?.rows.find((r) => r.rank === 1)?.teamId ?? null; // Sieger fix, sobald Platz 1 rechnerisch gesichert ist (auch vor Gruppenende). const provisional = !placeIsSecure(slot.group!, 1, teams, matches); const prefix = provisional ? "aktuell " : ""; return { teamId, provisional, tooltip: `${prefix}1. Gruppe ${slot.group}` }; } if (slot.type === "R") { const t = table(slot.group!); const teamId = t?.rows.find((r) => r.rank === 2)?.teamId ?? null; // Zweiter fix, sobald Platz 2 rechnerisch gesichert ist. const provisional = !placeIsSecure(slot.group!, 2, teams, matches); const prefix = provisional ? "aktuell " : ""; return { teamId, provisional, tooltip: `${prefix}2. Gruppe ${slot.group}` }; } // 3. Platz: braucht Annex-C-Zuordnung + den Gruppensieger dieses Spiels if (slot.type === "3" && assignment && winnerGroup) { const thirdGroup = assignment[winnerGroup]; if (thirdGroup) { const row = thirds.find((r) => r.group === thirdGroup && r.qualifies); // Fix nur, wenn ALLE drei Bedingungen erfüllt sind: // 1. Annex C aufgelöst (8 Dritte zuweisbar) // 2. Der Slot ist in allen noch möglichen Konstellationen stabil // (Enumeration aller Annex-C-Kombinationen) // 3. Das konkrete Team ist bekannt (Gruppe fertig) const fix = annexResolved && thirdSlotIsSecure(winnerGroup, matches, teams) && row?.qualifies === true; const provisional = !fix; const prefix = provisional ? "aktuell " : ""; return { teamId: row?.teamId ?? null, provisional, tooltip: `${prefix}3. der Gruppe ${thirdGroup}`, }; } } if (slot.type === "3") { const pool = slot.thirdPool?.join("") ?? "?"; return { teamId: null, provisional: true, tooltip: `aktuell 3. Gruppe ${pool}` }; } return { teamId: null, provisional: true, tooltip: null }; } function sideFrom( teamId: string | null, fallbackLabel: string, teams: Team[], feed: Match | undefined, which: "home" | "away", provisional: boolean, tooltip: string | null, ): ResolvedSide { const team = teamId ? teams.find((t) => t.id === teamId) : undefined; const score = feed ? which === "home" ? feed.homeScore : feed.awayScore : null; const w = winnerOf(feed); return { teamId, label: team?.localisedName ?? team?.name ?? fallbackLabel, code: team?.code ?? "", score, isWinner: teamId != null && w === teamId, provisional: teamId != null && provisional, tooltip, }; } // Baut die komplette aufgelöste Bracket-Struktur. // resolveWinners: wenn true, werden die Gewinner-TeamIDs aus den aufgelösten // R32-Slots abgeleitet (basierend auf den Spiel-Scores), statt aus den Team-IDs // des Feeds. Nötig für die Simulation, wo KO-Matches Scores aber keine Team-IDs // im Feed haben. export function resolveBracket( matches: Match[], teams: Team[], tables: GroupTable[], thirds: ThirdPlaceRow[], assignment: ThirdAssignment | null, annexResolved: boolean, resolveWinners = false, ): { r32: ResolvedTie[]; later: Record } { // Map: Match-Nummer -> Sieger-Team-ID (für Propagation in Folgerunden) const winners = new Map(); const losers = new Map(); // Map: Match-Nummer -> ist das Spiel beendet (Sieger fix)? const decided = new Map(); const r32: ResolvedTie[] = R32.map((rm: R32Match) => { const feed = feedMatch(matches, rm.matchNumber); const homeGroup = rm.home.type === "W" ? rm.home.group : undefined; const awayGroup = rm.away.type === "W" ? rm.away.group : undefined; // Für 3.-Platz-Slots: Gruppensieger ist die andere Seite (immer W) const winnerGroup = (homeGroup ?? awayGroup) as GroupId | undefined; const h = resolveR32Slot(rm.home, winnerGroup, tables, assignment, thirds, matches, teams, annexResolved); const a = resolveR32Slot(rm.away, winnerGroup, tables, assignment, thirds, matches, teams, annexResolved); const home = sideFrom(h.teamId, slotLabel(rm.home, assignment, winnerGroup), teams, feed, "home", h.provisional, h.tooltip); const away = sideFrom(a.teamId, slotLabel(rm.away, assignment, winnerGroup), teams, feed, "away", a.provisional, a.tooltip); const feedWinner = winnerOf(feed); if (resolveWinners && !feedWinner && feed && h.teamId && a.teamId && feed.homeScore != null && feed.awayScore != null) { // Simulation: Gewinner aus Scores und aufgelösten Team-IDs ableiten if (feed.homeScore > feed.awayScore) { winners.set(rm.matchNumber, h.teamId); losers.set(rm.matchNumber, loserOf(feed) ?? a.teamId); } else if (feed.awayScore > feed.homeScore) { winners.set(rm.matchNumber, a.teamId); losers.set(rm.matchNumber, loserOf(feed) ?? h.teamId); } else { // Unentschieden → Heim gewinnt winners.set(rm.matchNumber, h.teamId); losers.set(rm.matchNumber, loserOf(feed) ?? a.teamId); } } else { winners.set(rm.matchNumber, feedWinner); losers.set(rm.matchNumber, loserOf(feed)); } decided.set(rm.matchNumber, feed?.status === "FINISHED"); return { matchNumber: rm.matchNumber, stage: "R32", home, away, status: feed?.status ?? "SCHEDULED", prob: feed?.prob, }; }); const later: Record = {}; for (const km of LATER_ROUNDS) { const feed = feedMatch(matches, km.matchNumber); const src = km.losers ? losers : winners; const homeId = src.get(km.fromHome) ?? null; const awayId = src.get(km.fromAway) ?? null; // Ein Folgeteam ist fix, wenn das speisende Spiel beendet ist. const homeProv = !(decided.get(km.fromHome) ?? false); const awayProv = !(decided.get(km.fromAway) ?? false); const homeLabel = `${km.losers ? "Verlierer" : "Sieger"} ${km.fromHome}`; const awayLabel = `${km.losers ? "Verlierer" : "Sieger"} ${km.fromAway}`; const homeTtip = `${km.losers ? "Verlierer" : "Sieger"} aus Spiel ${km.fromHome}`; const awayTtip = `${km.losers ? "Verlierer" : "Sieger"} aus Spiel ${km.fromAway}`; const home = sideFrom(homeId, homeLabel, teams, feed, "home", homeProv, homeTtip); const away = sideFrom(awayId, awayLabel, teams, feed, "away", awayProv, awayTtip); const feedWinner = winnerOf(feed); if (resolveWinners && !feedWinner && feed && homeId && awayId && feed.homeScore != null && feed.awayScore != null) { if (feed.homeScore > feed.awayScore) { winners.set(km.matchNumber, homeId); losers.set(km.matchNumber, loserOf(feed) ?? awayId); } else if (feed.awayScore > feed.homeScore) { winners.set(km.matchNumber, awayId); losers.set(km.matchNumber, loserOf(feed) ?? homeId); } else { winners.set(km.matchNumber, homeId); losers.set(km.matchNumber, loserOf(feed) ?? awayId); } } else { winners.set(km.matchNumber, feedWinner); losers.set(km.matchNumber, loserOf(feed)); } decided.set(km.matchNumber, feed?.status === "FINISHED"); later[km.matchNumber] = { matchNumber: km.matchNumber, stage: km.stage, home, away, status: feed?.status ?? "SCHEDULED", prob: feed?.prob, }; } return { r32, later }; }