diff --git a/app/api/matches/route.ts b/app/api/matches/route.ts index 2ddbea9..ad060a0 100644 --- a/app/api/matches/route.ts +++ b/app/api/matches/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { fetchMatchesAndTeams, fetchOdds, attachOdds, fetchLiveScores, applyLiveScores, assignKONumbersBySlots } from "@/lib/feeds"; +import { fetchMatchesAndTeams, fetchOdds, attachOdds, attachKOOdds, fetchLiveScores, applyLiveScores, assignKONumbersBySlots } from "@/lib/feeds"; import { computeGroupTables, computeThirdPlaceTable } from "@/lib/standings"; import { qualifiedThirdGroups, resolveAnnexC } from "@/lib/bracket"; import { securelyQualifiedThirdTeams } from "@/lib/third-place-security"; @@ -14,8 +14,9 @@ export async function GET() { // Odds sind optional: fällt der Polymarket-Call aus, liefern wir trotzdem. let matches = rawMatches; + let odds: Awaited> | null = null; try { - const odds = await fetchOdds(); + odds = await fetchOdds(); matches = attachOdds(rawMatches, teams, odds); } catch (err) { console.error("[polymarket] fetchOdds fehlgeschlagen:", err); @@ -35,6 +36,27 @@ export async function GET() { // K.o.-Match-Nummern per Slot-Auflösung vergeben (FIFA-Topologie) assignKONumbersBySlots(matches, teams); + // Zweiter Durchlauf: Polymarket-Odds auf K.o.-Matches mit aufgelöster R32-Paarung + console.log("[ROUTE] vor attachKOOdds | odds vorhanden:", !!odds, "| odds länge:", odds?.length); + if (odds) { + try { + matches = attachKOOdds(matches, teams, odds); + console.log("[ROUTE] nach attachKOOdds | matches länge:", matches?.length); + } catch (err) { + console.error("[ROUTE] attachKOOdds fehlgeschlagen:", err); + } + } + + console.log("[ROUTE] vor PM-SNAPSHOT | matches länge:", matches?.length); + const withProb = matches.filter(m => m.prob != null); + console.log("[PM-SNAPSHOT]", new Date().toISOString(), + "| anzahl mit prob:", withProb.length, + "| spiele:", withProb.map(m => { + const h = teams.find(t=>t.id===m.homeTeamId)?.code; + const a = teams.find(t=>t.id===m.awayTeamId)?.code; + return `${h}-${a}`; + }).join(", ")); + // prob-Feld normalisieren: immer null statt undefined, damit JSON konsistent ist const normalizedMatches = matches.map((m) => ({ ...m, prob: m.prob ?? null })); diff --git a/lib/feeds.ts b/lib/feeds.ts index 1f28b91..f204043 100644 --- a/lib/feeds.ts +++ b/lib/feeds.ts @@ -234,6 +234,7 @@ interface PmMarket { } export interface ParsedOdds { + slug: string; homeCode: string; awayCode: string; homeName: string; @@ -241,6 +242,7 @@ export interface ParsedOdds { pHome: number; pDraw: number; pAway: number; + startTime: string | null; } // Normalisiert Teamcodes zwischen Polymarket-Slug und Feed (3-Buchstaben). @@ -290,10 +292,12 @@ export async function fetchOdds(): Promise { let pHome = 0, pDraw = 0, pAway = 0; let homeName = "", awayName = ""; + let marketGameStartTime: string | null = null; for (const mk of ev.markets ?? []) { if (mk.sportsMarketType && mk.sportsMarketType !== "moneyline") continue; if (mk.closed) continue; + if (!marketGameStartTime) marketGameStartTime = (mk as any).gameStartTime ?? null; const prices = safeParse(mk.outcomePrices, []).map(Number); if (prices.length < 2) continue; const yes = prices[0]; // erster Preis = "Yes"-Wahrscheinlichkeit @@ -317,10 +321,22 @@ export async function fetchOdds(): Promise { } if (pHome > 0 || pDraw > 0 || pAway > 0) { - parsed.push({ homeCode, awayCode, homeName, awayName, pHome, pDraw, pAway }); + const rawStartDate = (ev as any).startDate ?? null; + const rawEndDate = (ev as any).endDate ?? null; + const rawGameStartTime = (ev as any).gameStartTime ?? null; + const startTime = marketGameStartTime ?? rawGameStartTime ?? rawEndDate ?? rawStartDate; + console.log(" [PM-RAW-ML]", ev.slug, + "| active:", (ev as any).active, "| closed:", (ev as any).closed, + "| startDate:", rawStartDate, + "| gameStartTime:", rawGameStartTime, + "| mkGameStartTime:", marketGameStartTime, + "| endDate:", rawEndDate); + parsed.push({ slug: ev.slug, homeCode, awayCode, homeName, awayName, pHome, pDraw, pAway, startTime }); } } + console.log("[PM-RAW]", new Date().toISOString(), "| events gesamt:", allEvents.length); + console.log("[PM-LOAD]", new Date().toISOString(), "| moneyline-events:", parsed.length, "| events gesamt:", allEvents.length); console.log("[polymarket] spiele:", parsed.length); return parsed; }); @@ -360,10 +376,23 @@ export function attachOdds(matches: Match[], teams: Team[], odds: ParsedOdds[]): oddsMatch.set(o, { homeId, awayId }); } else { oddsMatch.set(o, null); + const feedMatchesWithTheseTeams = matches.filter(m => + m.homeTeamId && m.awayTeamId && + ((m.homeTeamId === homeId && m.awayTeamId === awayId) || + (m.homeTeamId === awayId && m.awayTeamId === homeId)), + ); + console.log("[PM-NOMATCH]", o.homeName, "vs", o.awayName, "| slug:", o.slug); + console.log("[PM-NOMATCH-WHY]", o.homeName, "vs", o.awayName, + "| homeId gefunden:", !!homeId, + "| awayId gefunden:", !!awayId, + "| homeCode:", o.homeCode, "| awayCode:", o.awayCode, + "| feed-matches mit diesen teams:", feedMatchesWithTheseTeams.length); } } - return matches.map((m) => { + const attachedOdds = new Set(); + + const result = matches.map((m) => { if (!m.homeTeamId || !m.awayTeamId) return m; // Suche in oddsMatch nach einer Kombination die beide Team-IDs matcht for (const [o, ids] of oddsMatch) { @@ -372,6 +401,11 @@ export function attachOdds(matches: Match[], teams: Team[], odds: ParsedOdds[]): if ((ids.homeId === m.homeTeamId && ids.awayId === m.awayTeamId) || (ids.homeId === m.awayTeamId && ids.awayId === m.homeTeamId)) { const swapped = ids.homeId === m.awayTeamId; + attachedOdds.add(o); + console.log("[PM-ATTACH]", o.homeName, "vs", o.awayName, + "| match gefunden:", true, + "| match.status:", m.status, + "| prob gesetzt:", true); return { ...m, prob: { @@ -384,6 +418,252 @@ export function attachOdds(matches: Match[], teams: Team[], odds: ParsedOdds[]): } return m; }); + + for (const [o, ids] of oddsMatch) { + if (ids && !attachedOdds.has(o)) { + const matchingFeedMatch = matches.find(m => + m.homeTeamId && m.awayTeamId && + ((m.homeTeamId === ids.homeId && m.awayTeamId === ids.awayId) || + (m.homeTeamId === ids.awayId && m.awayTeamId === ids.homeId)), + ); + console.log("[PM-NOMATCH]", o.homeName, "vs", o.awayName, "| slug:", o.slug); + console.log("[PM-NOMATCH-WHY]", o.homeName, "vs", o.awayName, + "| team-ids gematcht:", ids.homeId, "vs", ids.awayId, + "| feed-match mit diesen teams existiert:", !!matchingFeedMatch, + "| feed-match status:", matchingFeedMatch?.status ?? "—", + "| prob bereits gesetzt:", matchingFeedMatch?.prob ? "ja" : "nein"); + } + } + + return result; +} + +// ---------------------------------------------------------------------------- +// KO-Odds-Fallback: löst R32-Paarungen auf und verknüpft Polymarket-Events +// mit K.o.-Feed-Matches, die noch keine Team-IDs im Feed haben. +// ---------------------------------------------------------------------------- + +// Löst für jeden R32-Slot (73-88) die aktuellen Team-Paarungen auf Basis der +// Gruppentabellen und Annex-C-Zuordnung. Gibt eine Map matchNumber → Paarung +// zurück, NUR wenn beide Team-IDs eindeutig bekannt sind (kein Raten). +function resolveR32Pairings(matches: Match[], teams: Team[]): Map { + const tables = computeGroupTables(teams, matches); + const thirds = computeThirdPlaceTable(tables); + const qGroups = qualifiedThirdGroups(thirds); + const annex = qGroups.length === 8 ? resolveAnnexC(qGroups) : null; + + const pairings = new Map(); + + for (const slot of R32) { + const homeGroup = slot.home.type === "W" ? slot.home.group : undefined; + const awayGroup = slot.away.type === "W" ? slot.away.group : undefined; + const wg = (homeGroup ?? awayGroup) as GroupId | undefined; + + let hid: string | null = null; + if (slot.home.type === "W" && slot.home.group) { + hid = tables.find(t => t.group === slot.home.group)?.rows.find(r => r.rank === 1)?.teamId ?? null; + } else if (slot.home.type === "R" && slot.home.group) { + hid = tables.find(t => t.group === slot.home.group)?.rows.find(r => r.rank === 2)?.teamId ?? null; + } else if (slot.home.type === "3" && annex && wg) { + const tg = annex[wg]; + if (tg) hid = thirds.find(r => r.group === tg && r.qualifies)?.teamId ?? null; + } + + let aid: string | null = null; + if (slot.away.type === "W" && slot.away.group) { + aid = tables.find(t => t.group === slot.away.group)?.rows.find(r => r.rank === 1)?.teamId ?? null; + } else if (slot.away.type === "R" && slot.away.group) { + aid = tables.find(t => t.group === slot.away.group)?.rows.find(r => r.rank === 2)?.teamId ?? null; + } else if (slot.away.type === "3" && annex && wg) { + const tg = annex[wg]; + if (tg) aid = thirds.find(r => r.group === tg && r.qualifies)?.teamId ?? null; + } + + if (hid && aid) { + pairings.set(slot.matchNumber, { homeTeamId: hid, awayTeamId: aid }); + } + } + + console.log("[R32-PAIRS] aufgelöste Paarungen:", pairings.size); + for (const [num, p] of pairings) { + const h = teams.find(t=>t.id===p.homeTeamId)?.name ?? p.homeTeamId; + const a = teams.find(t=>t.id===p.awayTeamId)?.name ?? p.awayTeamId; + console.log(" [R32-PAIR]", "matchNumber:", num, "|", h, "vs", a); + } + + return pairings; +} + +// Extrahiert das Datum (YYYY-MM-DD) aus einem Polymarket-Slug. +// Slug-Format: fifwc-{home}-{away}-{yyyy}-{mm}-{dd} +function slugToDate(slug: string): string | null { + const parts = slug.split("-"); + if (parts.length < 3) return null; + const y = parts[parts.length - 3]; + const m = parts[parts.length - 2]; + const d = parts[parts.length - 1]; + if (/^\d{4}$/.test(y) && /^\d{2}$/.test(m) && /^\d{2}$/.test(d)) { + return `${y}-${m}-${d}`; + } + return null; +} + +// Zweiter Durchlauf für attachOdds: ordnet Polymarket-Events K.o.-Feed-Matches +// zu, die (noch) keine Team-IDs im Feed haben. Arbeitet direkt auf den +// aufgelösten R32-Paarungen (resolveR32Pairings). Identifiziert das Ziel- +// Feed-Match über die Slot-matchNumber (Stufe 1) oder Datums-Lookup aus dem +// Slug (Stufe 2) — KEINE chronologische Nummernvergabe. +// Mutiert matches in-place, setzt nur bei Matches OHNE bestehende prob. +export function attachKOOdds(matches: Match[], teams: Team[], odds: ParsedOdds[]): Match[] { + const koOhneProb = matches.filter(m => m.group == null && m.prob == null).length; + console.log("[KO-ODDS] start | odds-events:", odds?.length ?? "undefined", + "| ko-matches ohne prob:", koOhneProb); + + const pairings = resolveR32Pairings(matches, teams); + if (pairings.size === 0) return matches; + + const idByName = new Map(); + for (const t of teams) { + const nn = normName(t.name); + if (!idByName.has(nn)) idByName.set(nn, t.id); + } + const idByCode = new Map(teams.map((t) => [t.code.toLowerCase(), t.id])); + + console.log("[KO-LOOP] start | pairings:", pairings.size, "| odds-events:", odds?.length ?? "undef"); + + for (const [matchNum, pairing] of pairings) { + if (!pairing.homeTeamId || !pairing.awayTeamId) continue; + + const hName = teams.find(t => t.id === pairing.homeTeamId)?.name ?? "?"; + const aName = teams.find(t => t.id === pairing.awayTeamId)?.name ?? "?"; + + // Finde Polymarket-Event für dieses Team-Paar + let matchedEvent: ParsedOdds | null = null; + for (const o of odds) { + const hn = normName(o.homeName); + const an = normName(o.awayName); + const homeId = idByName.get(hn) ?? idByCode.get(o.homeCode); + const awayId = idByName.get(an) ?? idByCode.get(o.awayCode); + if (!homeId || !awayId) continue; + if ((homeId === pairing.homeTeamId && awayId === pairing.awayTeamId) || + (homeId === pairing.awayTeamId && awayId === pairing.homeTeamId)) { + matchedEvent = o; + break; + } + } + + if (!matchedEvent) { + console.log("[KO-TRY]", "matchNumber:", matchNum, + "| teams:", `${hName} vs ${aName}`, + "| polymarket-event gefunden:", false, + "| ziel-feedmatch gefunden:", false, + "| stufe:", "keine"); + continue; + } + + // Ziel-Feed-Match identifizieren + let targetMatch: Match | undefined; + let stufe: string | undefined; + + // Stufe 1: bereits nummeriertes R32-Match (via assignKONumbersBySlots Pass 1) + targetMatch = matches.find(m => + m.stage === "R32" && m.group == null && m.matchNumber === matchNum, + ); + if (targetMatch) stufe = "1 (nummeriert)"; + + // Stufe 2: Datums-Lookup aus Polymarket-Slug für unnummerierte Matches + if (!targetMatch) { + const slugDate = slugToDate(matchedEvent.slug); + if (slugDate) { + const candidates = matches.filter(m => + m.stage === "R32" && m.group == null && m.matchNumber === 0 && + m.utcDate.slice(0, 10) === slugDate, + ); + + if (candidates.length === 1) { + targetMatch = candidates[0]; + targetMatch.matchNumber = matchNum; + stufe = "2 (datum)"; + } else if (candidates.length > 1) { + // Tie-breaker: falls Polymarket-Event eine Startzeit hat, nächstgelegenes Feed-Match + if (matchedEvent.startTime) { + const eventTime = new Date(matchedEvent.startTime).getTime(); + let best: Match | undefined; + let bestDiff = Infinity; + for (const c of candidates) { + const diff = Math.abs(new Date(c.utcDate).getTime() - eventTime); + if (diff < bestDiff) { bestDiff = diff; best = c; } + } + // Nur zuordnen, wenn Zeitdifferenz plausibel (<4h) + if (best && bestDiff < 4 * 60 * 60 * 1000) { + targetMatch = best; + targetMatch.matchNumber = matchNum; + stufe = "2 (datum+tie)"; + } + } + console.log("[KO-TIME]", "slot:", matchNum, + "| teams:", `${hName} vs ${aName}`, + "| pm-gameStartTime(UTC):", matchedEvent.startTime, + "| feed-kandidaten:", candidates.map(c => + `${c.id}(${c.utcDate})`, + ).join(", "), + "| gewählt:", targetMatch?.id ?? "keins"); + if (!targetMatch) { + console.log("[KO-TIE]", "matchNumber:", matchNum, + "| slug:", matchedEvent.slug, + "| slugDate:", slugDate, + "| startTime:", matchedEvent.startTime, + "| candidates:", candidates.map(c => + `${c.id}(${c.utcDate})`, + ).join(", ")); + } + } + } + } + + if (!targetMatch) { + console.log("[KO-TRY]", "matchNumber:", matchNum, + "| teams:", `${hName} vs ${aName}`, + "| polymarket-event gefunden:", true, + "| ziel-feedmatch gefunden:", false, + "| stufe:", stufe ?? "keine"); + continue; + } + + // prob nicht überschreiben, falls schon gesetzt + if (targetMatch.prob) { + console.log("[KO-TRY]", "matchNumber:", matchNum, + "| teams:", `${hName} vs ${aName}`, + "| polymarket-event gefunden:", true, + "| ziel-feedmatch gefunden:", true, + "| stufe:", stufe, + "| prob bereits gesetzt:", true); + continue; + } + + const homeIdFromEvent = idByName.get(normName(matchedEvent.homeName)) ?? + idByCode.get(matchedEvent.homeCode); + const swapped = homeIdFromEvent === pairing.awayTeamId; + + targetMatch.prob = { + home: swapped ? matchedEvent.pAway : matchedEvent.pHome, + draw: matchedEvent.pDraw, + away: swapped ? matchedEvent.pHome : matchedEvent.pAway, + }; + + console.log("[PM-ATTACH]", matchedEvent.homeName, "vs", matchedEvent.awayName, + "| match gefunden via R32:", true, + "| match.status:", targetMatch.status, + "| matchNumber:", targetMatch.matchNumber, + "| prob gesetzt:", true); + console.log("[KO-TRY]", "matchNumber:", matchNum, + "| teams:", `${hName} vs ${aName}`, + "| polymarket-event gefunden:", !!matchedEvent, + "| ziel-feedmatch gefunden:", !!targetMatch, + "| stufe:", stufe); + } + + return matches; } // ---------------------------------------------------------------------------- diff --git a/lib/resolve-bracket.ts b/lib/resolve-bracket.ts index dd60e07..126fc72 100644 --- a/lib/resolve-bracket.ts +++ b/lib/resolve-bracket.ts @@ -4,7 +4,7 @@ import { ThirdAssignment, slotLabel, } from "@/lib/bracket"; import { placeIsSecure } from "@/lib/secure-places"; -import { thirdSlotIsSecure } from "@/lib/third-place-security"; +import { thirdSlotIsSecure, securelyQualifiedThirdTeams } from "@/lib/third-place-security"; // Ein aufgelöster Teilnehmer einer K.o.-Begegnung. export interface ResolvedSide { @@ -58,6 +58,7 @@ function resolveR32Slot( matches: Match[], teams: Team[], annexResolved: boolean, + secureTeamIds: Set, ): { teamId: string | null; provisional: boolean; tooltip: string | null } { const table = (g: GroupId) => tables.find((t) => t.group === g); if (slot.type === "W") { @@ -81,21 +82,14 @@ function resolveR32Slot( 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: + // Fix nur, wenn ALLE 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 secure = thirdSlotIsSecure(winnerGroup, matches, teams); - console.log("[FIX-3RD]", "winnerGroup:", winnerGroup, - "| assignedThird:", assignment?.[winnerGroup], - "| annexResolved:", annexResolved, - "| thirdSlotIsSecure:", secure, - "| row.group:", row?.group, - "| row.qualifies:", row?.qualifies, - "| row.teamId:", row?.teamId, - "| team:", row?.teamId ? teams.find(t => t.id === row.teamId)?.name : "null"); - const fix = annexResolved && secure && row?.qualifies === true; + // 3. Das konkrete Team ist sicher qualifiziert (kann nicht aus Top 8 fallen) + // 4. Die Gruppe hat alle Spiele gespielt (Team bekannt) + const slotStable = thirdSlotIsSecure(winnerGroup, matches, teams); + const teamSecure = row?.teamId ? secureTeamIds.has(row.teamId) : false; + const fix = annexResolved && slotStable && teamSecure && row?.qualifies === true; const provisional = !fix; const prefix = provisional ? "aktuell " : ""; return { @@ -150,6 +144,7 @@ export function resolveBracket( const losers = new Map(); // Map: Match-Nummer -> ist das Spiel beendet (Sieger fix)? const decided = new Map(); + const secureTeamIds = securelyQualifiedThirdTeams(matches, teams); const r32: ResolvedTie[] = R32.map((rm: R32Match) => { const feed = feedMatch(matches, rm.matchNumber); @@ -158,8 +153,8 @@ export function resolveBracket( // 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 h = resolveR32Slot(rm.home, winnerGroup, tables, assignment, thirds, matches, teams, annexResolved, secureTeamIds); + const a = resolveR32Slot(rm.away, winnerGroup, tables, assignment, thirds, matches, teams, annexResolved, secureTeamIds); 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); diff --git a/lib/third-place-security.ts b/lib/third-place-security.ts index 6e6c89e..6d2f7ab 100644 --- a/lib/third-place-security.ts +++ b/lib/third-place-security.ts @@ -132,17 +132,19 @@ function classifyGroups(states: Map): { // 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)!; - // Nur fertige Gruppen mit eindeutigem Dritten if (!st.finished || !st.third || st.possibleTeamIds.size !== 1) continue; const my = st.third; - // Zähle, wie viele andere Gruppen einen Dritten stellen KÖNNEN, - // der definitiv BESSER ist als dieses Team (Worst-Case für uns: deren Best-Case vs. unser festes Ergebnis). let couldBeBetter = 0; for (const og of GROUP_IDS) { if (og === g) continue; @@ -152,13 +154,35 @@ export function securelyQualifiedThirdTeams(matches: Match[], teams: Team[]): Se 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 { - // Offene Gruppe: kann deren Dritter uns im Best-Case überholen? if (os.maxPoints > my.points) couldBeBetter++; - else if (os.maxPoints === my.points && !os.finished) couldBeBetter++; // TD/GF offen → potentiell besser + else if (os.maxPoints === my.points && !os.finished) couldBeBetter++; } } - if (couldBeBetter <= 7) secureTeams.add(my.teamId); + 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; @@ -167,43 +191,41 @@ export function securelyQualifiedThirdTeams(matches: Match[], teams: Team[]): Se // 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 } = classifyGroups(states); + 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) { - console.log("[3RD-SECURE] false: >8 lockedIn | wg:", winnerGroup, "| lockedIn:", [...lockedIn].join(",")); - return false; - } + if (lockedIn.size > 8) return false; if (lockedIn.size === 8) { const assignment = resolveAnnexC([...lockedIn]); - if (!assignment) { console.log("[3RD-SECURE] false: no annex with 8 locked | wg:", winnerGroup); return false; } + if (!assignment) return false; const ag = assignment[winnerGroup]; - if (!ag) { console.log("[3RD-SECURE] false: no assignedGroup (8 locked) | wg:", winnerGroup); return false; } + if (!ag) return false; const ss = states.get(ag); if (!ss) return false; const result = ss.finished && ss.third != null && ss.possibleTeamIds.size === 1; - console.log("[3RD-SECURE] final (8 locked):", result, "| wg:", winnerGroup, "| stableGroup:", ag); return result; } - if (lockedIn.size + contested.size < 8) { - console.log("[3RD-SECURE] false: <8 possible | wg:", winnerGroup, "| lockedIn:", lockedIn.size, "| contested:", contested.size); - return false; - } + 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) { console.log("[3RD-SECURE] false: >200 combos | wg:", winnerGroup); return false; } + if (combos > 200) return false; let stableGroup: GroupId | null = null; for (const indices of enumerateCombinations(contestedArr.length, need)) { @@ -212,16 +234,15 @@ export function thirdSlotIsSecure(winnerGroup: GroupId, matches: Match[], teams: const assignment = resolveAnnexC(qGroups); if (!assignment) continue; const ag = assignment[winnerGroup]; - if (!ag) { console.log("[3RD-SECURE] false: no assignedGroup | wg:", winnerGroup); return false; } + if (!ag) return false; if (stableGroup === null) stableGroup = ag; - else if (stableGroup !== ag) { console.log("[3RD-SECURE] false: unstable slot | wg:", winnerGroup); return false; } + else if (stableGroup !== ag) return false; } - if (stableGroup === null) { console.log("[3RD-SECURE] false: stableGroup=null | wg:", winnerGroup); 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; - console.log("[3RD-SECURE] final:", result, "| wg:", winnerGroup, "| stableGroup:", stableGroup); return result; }