From 6bb6f9be7a738ca675e294cfd19243d28eddc3b4 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Fri, 26 Jun 2026 18:34:58 -0500 Subject: [PATCH] fixes --- app/api/matches/route.ts | 5 ++- lib/feeds.ts | 79 +++++++++++++++++++++++++++++++++------- lib/resolve-bracket.ts | 44 ++++++++++------------ 3 files changed, 89 insertions(+), 39 deletions(-) diff --git a/app/api/matches/route.ts b/app/api/matches/route.ts index 8f40dd0..ed93337 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 } from "@/lib/feeds"; +import { fetchMatchesAndTeams, fetchOdds, attachOdds, fetchLiveScores, applyLiveScores, assignKONumbersBySlots } from "@/lib/feeds"; import { computeGroupTables, computeThirdPlaceTable } from "@/lib/standings"; import { qualifiedThirdGroups, resolveAnnexC } from "@/lib/bracket"; @@ -31,6 +31,9 @@ export async function GET() { const groupTables = computeGroupTables(teams, matches); const groupTablesLive = computeGroupTables(teams, matches, true); + // K.o.-Match-Nummern per Slot-Auflösung vergeben (FIFA-Topologie) + assignKONumbersBySlots(matches, teams); + // 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 ade506b..1f28b91 100644 --- a/lib/feeds.ts +++ b/lib/feeds.ts @@ -1,6 +1,8 @@ import { GroupId, Match, MatchStatus, Team } from "./types"; import { venueFor } from "./venues"; import { localisedTeamName } from "./team-mappings"; +import { R32, LATER_ROUNDS, resolveAnnexC, qualifiedThirdGroups } from "./bracket"; +import { computeGroupTables, computeThirdPlaceTable } from "./standings"; // ---------------------------------------------------------------------------- // Caching: einfacher In-Memory-Cache mit TTL. Verhindert, dass jede Browser- @@ -88,26 +90,74 @@ const STAGE_ORDER: Record = { }; // Setzt Spielnummern und Stadien. -// - K.o.-Spiele: chronologisch ab 73 (Anstöße dort eindeutig) -> für Bracket nötig. -// - Venue: Gruppenspiele über die Teampaarung, K.o.-Spiele über die Spielnummer. +// K.o.-Spiele: Nummerierung über Slot-Auflösung (assignKONumbersBySlots), +// NICHT mehr chronologisch — die FIFA-Nummern folgen der Bracket-Topologie. +// Venue: Gruppenspiele über die Teampaarung, K.o.-Spiele über die Spielnummer. function assignNumbersAndVenues(matches: Match[], teams: Team[]): void { - // K.o.-Spiele eindeutig durchnummerieren (73..104). - const ko = matches - .filter((m) => m.group == null) - .sort((a, b) => { - const sa = STAGE_ORDER[a.stage], sb = STAGE_ORDER[b.stage]; - if (sa !== sb) return sa - sb; - const t = +new Date(a.utcDate) - +new Date(b.utcDate); - return t !== 0 ? t : Number(a.id) - Number(b.id); - }); - ko.forEach((m, i) => { m.matchNumber = 73 + i; }); - // Stadien setzen (Gruppenphase per Paarung, K.o. per Nummer). for (const m of matches) { m.venue = venueFor(m, teams); } } +// Vergibt K.o.-Match-Nummern über Slot-Auflösung (FIFA-Topologie) +// statt chronologisch. Für R32-Matches mit Team-IDs wird der Slot gesucht, +// dessen aufgelöste Teams dem Feed-Paar entsprechen. +export function assignKONumbersBySlots(matches: Match[], teams: Team[]): void { + const tables = computeGroupTables(teams, matches); + const thirds = computeThirdPlaceTable(tables); + const qGroups = qualifiedThirdGroups(thirds); + const annex = qGroups.length === 8 ? resolveAnnexC(qGroups) : null; + + const byPair = new Map(); + for (const m of matches) { + if (m.group != null || !m.homeTeamId || !m.awayTeamId) continue; + byPair.set(`${m.homeTeamId}::${m.awayTeamId}`, m); + byPair.set(`${m.awayTeamId}::${m.homeTeamId}`, m); + } + + 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) { + const fm = byPair.get(`${hid}::${aid}`); + if (fm) fm.matchNumber = slot.matchNumber; + } + } + + // R16–Finale: per Stage + Datum den LATER_ROUNDS-Slots zuordnen + for (const stage of ["R16", "QF", "SF", "3RD", "FINAL"] as const) { + const sm = matches.filter(m => m.stage === stage && m.group == null && m.matchNumber === 0) + .sort((a, b) => +new Date(a.utcDate) - +new Date(b.utcDate)); + const ls = LATER_ROUNDS.filter(k => k.stage === (stage === "3RD" ? "3RD" : stage)); + for (let i = 0; i < sm.length && i < ls.length; i++) { + sm[i].matchNumber = ls[i].matchNumber; + } + } +} + // Holt alle Spiele + leitet die Teamliste daraus ab (spart einen Extra-Call). export async function fetchMatchesAndTeams(): Promise<{ matches: Match[]; teams: Team[] }> { return cached("fd:matches", 60_000, async () => { @@ -154,6 +204,7 @@ export async function fetchMatchesAndTeams(): Promise<{ matches: Match[]; teams: }); const teams = [...teamMap.values()]; + assignNumbersAndVenues(matches, teams); return { matches, teams }; @@ -227,9 +278,9 @@ export async function fetchOdds(): Promise { allEvents.push(...page); if (page.length < 100) break; } - //console.log("[polymarket] events fetched:", allEvents.length); const parsed: ParsedOdds[] = []; + for (const ev of allEvents) { // Slug: fifwc-{home}-{away}-{yyyy}-{mm}-{dd} const slugParts = ev.slug.replace("fifwc-", "").split("-"); diff --git a/lib/resolve-bracket.ts b/lib/resolve-bracket.ts index 3ff3816..67d50b3 100644 --- a/lib/resolve-bracket.ts +++ b/lib/resolve-bracket.ts @@ -157,20 +157,20 @@ export function resolveBracket( 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 + // Sim-Modus: Gewinner NUR aus Scores + Slot-Team-IDs ableiten. + // feedWinner (aus feed.homeTeamId/awayTeamId) wird IGNORIERT, + // weil matchNumber-Zuweisung (assignNumbersAndVenues) von der + // FIFA-Nummerierung abweichen kann → feed gehört evtl. zum falschen Spiel. + if (resolveWinners && 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); - } + // Simulation: Gewinner aus Scores und aufgelösten Team-IDs ableiten. + // Nutze NICHT feed.homeTeamId/awayTeamId (sind null für KO-Matches), + // sondern die aufgelösten h.teamId / a.teamId aus resolveR32Slot. + const homeWins = feed.homeScore > feed.awayScore; + const awayWins = feed.awayScore > feed.homeScore; + const winnerId = homeWins ? h.teamId : awayWins ? a.teamId : h.teamId; + winners.set(rm.matchNumber, winnerId); + losers.set(rm.matchNumber, loserOf(feed) ?? (homeWins ? a.teamId : awayWins ? h.teamId : a.teamId)); } else { winners.set(rm.matchNumber, feedWinner); losers.set(rm.matchNumber, loserOf(feed)); @@ -188,6 +188,7 @@ export function resolveBracket( 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); @@ -199,18 +200,13 @@ export function resolveBracket( const away = sideFrom(awayId, awayLabel, teams, feed, "away", awayProv, awayTtip); const feedWinner = winnerOf(feed); - if (resolveWinners && !feedWinner && feed && homeId && awayId + if (resolveWinners && 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); - } + const homeWins = feed.homeScore > feed.awayScore; + const awayWins = feed.awayScore > feed.homeScore; + const winnerId = homeWins ? homeId : awayWins ? awayId : homeId; + winners.set(km.matchNumber, winnerId); + losers.set(km.matchNumber, loserOf(feed) ?? (homeWins ? awayId : awayWins ? homeId : awayId)); } else { winners.set(km.matchNumber, feedWinner); losers.set(km.matchNumber, loserOf(feed));