KO Bracket fixed
This commit is contained in:
75
lib/feeds.ts
75
lib/feeds.ts
@@ -561,8 +561,25 @@ export function attachKOOdds(matches: Match[], teams: Team[], odds: ParsedOdds[]
|
||||
const FIFA_BASE = "https://api.fifa.com/api/v3";
|
||||
const FIFA_SEASON = "285023";
|
||||
|
||||
// FIFA-Code → App-Code (nur Abweichungen; sonst identisch)
|
||||
const FIFA_CODE_OVERRIDE: Record<string, string> = {
|
||||
CRO: "HRV", // Kroatien
|
||||
POR: "PRT", // Portugal
|
||||
SUI: "CHE", // Schweiz
|
||||
};
|
||||
function fifaCodeToAppCode(fifaCode: string): string {
|
||||
return FIFA_CODE_OVERRIDE[fifaCode] ?? fifaCode;
|
||||
}
|
||||
|
||||
interface FifaTeamBlock {
|
||||
IdTeam: string; // FIFA-Team-ID
|
||||
Abbreviation: string; // FIFA-Code (z.B. PAR, CRO)
|
||||
}
|
||||
|
||||
interface FifaMatch {
|
||||
MatchNumber: number;
|
||||
Home: FifaTeamBlock | null;
|
||||
Away: FifaTeamBlock | null;
|
||||
HomeTeamScore: number | null;
|
||||
AwayTeamScore: number | null;
|
||||
HomeTeamPenaltyScore: number | null;
|
||||
@@ -570,6 +587,7 @@ interface FifaMatch {
|
||||
MatchStatus: number; // 0=finished, 1=scheduled, else=live
|
||||
MatchTime: string | null; // z.B. "132'"
|
||||
ResultType: number | null; // 1=regular, 2=penalties
|
||||
Winner?: string | null; // Team-ID des Siegers
|
||||
}
|
||||
|
||||
interface FifaScores {
|
||||
@@ -580,10 +598,14 @@ interface FifaScores {
|
||||
resultType: number | null;
|
||||
status: MatchStatus;
|
||||
matchTime: string | null;
|
||||
winnerTeamId: string | null;
|
||||
}
|
||||
|
||||
// Holt Live-Scores von der FIFA-API. Gibt Map MatchNumber → Scores zurück.
|
||||
export async function fetchFifaScores(): Promise<Map<number, FifaScores>> {
|
||||
// Holt Live-Scores von der FIFA-API. Gibt Map MatchNumber → Scores + FIFA-ID→App-Code-Map zurück.
|
||||
export async function fetchFifaScores(): Promise<{
|
||||
scores: Map<number, FifaScores>;
|
||||
fifaIdToAppCode: Map<string, string>;
|
||||
}> {
|
||||
return cached("fifa:scores", 45_000, async () => {
|
||||
const url = `${FIFA_BASE}/calendar/matches?language=de&count=500&idSeason=${FIFA_SEASON}`;
|
||||
const res = await fetch(url, {
|
||||
@@ -593,15 +615,24 @@ export async function fetchFifaScores(): Promise<Map<number, FifaScores>> {
|
||||
});
|
||||
if (!res.ok) throw new Error(`fifa ${res.status}`);
|
||||
const data = (await res.json()) as { Results: FifaMatch[] };
|
||||
const map = new Map<number, FifaScores>();
|
||||
const scores = new Map<number, FifaScores>();
|
||||
const fifaIdToAppCode = new Map<string, string>();
|
||||
|
||||
for (const fm of data.Results ?? []) {
|
||||
// Team-Mapping sammeln
|
||||
for (const tb of [fm.Home, fm.Away]) {
|
||||
if (tb && !fifaIdToAppCode.has(tb.IdTeam)) {
|
||||
fifaIdToAppCode.set(tb.IdTeam, fifaCodeToAppCode(tb.Abbreviation));
|
||||
}
|
||||
}
|
||||
|
||||
let status: MatchStatus;
|
||||
switch (fm.MatchStatus) {
|
||||
case 0: status = "FINISHED"; break;
|
||||
case 1: status = "SCHEDULED"; break;
|
||||
default: status = "LIVE"; break; // 2,3,... = läuft
|
||||
default: status = "LIVE"; break;
|
||||
}
|
||||
map.set(fm.MatchNumber, {
|
||||
scores.set(fm.MatchNumber, {
|
||||
homeScore: fm.HomeTeamScore,
|
||||
awayScore: fm.AwayTeamScore,
|
||||
homePenalty: fm.HomeTeamPenaltyScore,
|
||||
@@ -609,15 +640,25 @@ export async function fetchFifaScores(): Promise<Map<number, FifaScores>> {
|
||||
resultType: fm.ResultType,
|
||||
status,
|
||||
matchTime: fm.MatchTime,
|
||||
winnerTeamId: fm.Winner ?? null,
|
||||
});
|
||||
}
|
||||
return map;
|
||||
return { scores, fifaIdToAppCode };
|
||||
});
|
||||
}
|
||||
|
||||
// Wendet FIFA-Live-Scores auf Matches an (MatchNumber als exakter Schlüssel).
|
||||
export function applyFifaScores(matches: Match[], fifaMap: Map<number, FifaScores>): Match[] {
|
||||
export function applyFifaScores(
|
||||
matches: Match[], teams: Team[],
|
||||
fifaData: { scores: Map<number, FifaScores>; fifaIdToAppCode: Map<string, string> },
|
||||
): Match[] {
|
||||
const { scores: fifaMap, fifaIdToAppCode } = fifaData;
|
||||
if (fifaMap.size === 0) return matches;
|
||||
// Baue App-Code → App-Team-ID Map
|
||||
const appCodeToId = new Map<string, string>();
|
||||
for (const t of teams) {
|
||||
if (t.code) appCodeToId.set(t.code.toLowerCase(), t.id);
|
||||
}
|
||||
let applied = 0;
|
||||
const result = matches.map((m) => {
|
||||
const fs = fifaMap.get(m.matchNumber);
|
||||
@@ -627,6 +668,26 @@ export function applyFifaScores(matches: Match[], fifaMap: Map<number, FifaScore
|
||||
if (fs.awayScore != null) r.awayScore = fs.awayScore;
|
||||
if (fs.homePenalty != null) r.homePenalty = fs.homePenalty;
|
||||
if (fs.awayPenalty != null) r.awayPenalty = fs.awayPenalty;
|
||||
// FIFA-ID → App-Team-ID auflösen
|
||||
if (fs.winnerTeamId) {
|
||||
const appCode = fifaIdToAppCode.get(fs.winnerTeamId);
|
||||
if (appCode) {
|
||||
const appTeamId = appCodeToId.get(appCode.toLowerCase());
|
||||
if (appTeamId) {
|
||||
r.winnerTeamId = appTeamId;
|
||||
} else {
|
||||
console.warn("[fifa] Winner-Team-Code nicht in App-Teams:", appCode, "| matchNumber:", m.matchNumber);
|
||||
}
|
||||
} else {
|
||||
console.warn("[fifa] FIFA-Winner-ID nicht in Team-Map:", fs.winnerTeamId, "| matchNumber:", m.matchNumber);
|
||||
}
|
||||
}
|
||||
console.log("[FIFA-WINNER]", "matchNumber:", m.matchNumber,
|
||||
"| fifa.Winner:", fs.winnerTeamId,
|
||||
"| status:", r.status,
|
||||
"| homeScore:", r.homeScore, "| awayScore:", r.awayScore,
|
||||
"| homePenalty:", r.homePenalty, "| awayPenalty:", r.awayPenalty,
|
||||
"| -> m.winnerTeamId gesetzt:", r.winnerTeamId);
|
||||
if (fs.matchTime) {
|
||||
const min = parseInt(fs.matchTime, 10);
|
||||
if (!isNaN(min)) r.minute = min;
|
||||
|
||||
@@ -24,6 +24,8 @@ export interface ResolvedTie {
|
||||
away: ResolvedSide;
|
||||
status: string;
|
||||
prob?: { home: number; draw: number | null; away: number } | null;
|
||||
homePenalty?: number | null;
|
||||
awayPenalty?: number | null;
|
||||
}
|
||||
|
||||
// Findet das tatsächliche Spiel (aus dem Feed) zu einer FIFA-Match-Nummer.
|
||||
@@ -34,13 +36,29 @@ function feedMatch(matches: Match[], num: number): Match | undefined {
|
||||
// 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.winnerTeamId) return m.winnerTeamId;
|
||||
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
|
||||
return null;
|
||||
}
|
||||
let _winnerDbg = true; // Nur einmal loggen
|
||||
function winnerDbg(m: Match | undefined, result: string | null, ctx: string): void {
|
||||
if (!_winnerDbg || !m || m.matchNumber !== 74) return;
|
||||
_winnerDbg = false;
|
||||
console.log("[WINNER-OF]", ctx,
|
||||
"| matchNumber:", m.matchNumber,
|
||||
"| winnerTeamId:", m.winnerTeamId,
|
||||
"| homeScore:", m.homeScore, "| awayScore:", m.awayScore,
|
||||
"| status:", m.status,
|
||||
"| ergebnis (zurückgegebener sieger):", result);
|
||||
}
|
||||
|
||||
function loserOf(m: Match | undefined): string | null {
|
||||
if (!m || m.status !== "FINISHED") return null;
|
||||
if (m.winnerTeamId) {
|
||||
return m.homeTeamId === m.winnerTeamId ? m.awayTeamId : m.homeTeamId;
|
||||
}
|
||||
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;
|
||||
@@ -159,6 +177,7 @@ export function resolveBracket(
|
||||
const away = sideFrom(a.teamId, slotLabel(rm.away, assignment, winnerGroup), teams, feed, "away", a.provisional, a.tooltip);
|
||||
|
||||
const feedWinner = winnerOf(feed);
|
||||
winnerDbg(feed, feedWinner, "resolveR32Slot");
|
||||
// Sim-Modus: Gewinner NUR aus Scores + Slot-Team-IDs ableiten.
|
||||
// feedWinner (aus feed.homeTeamId/awayTeamId) wird IGNORIERT,
|
||||
// weil matchNumber-Zuweisung (assignNumbersAndVenues) von der
|
||||
@@ -178,9 +197,16 @@ export function resolveBracket(
|
||||
losers.set(rm.matchNumber, loserOf(feed));
|
||||
}
|
||||
decided.set(rm.matchNumber, feed?.status === "FINISHED");
|
||||
if (rm.matchNumber === 74) {
|
||||
console.log("[PROPAGATE]", "von matchNumber:", rm.matchNumber,
|
||||
"| feedWinner:", feedWinner,
|
||||
"| -> in winners map unter key:", rm.matchNumber,
|
||||
"| ziel z.B. slot 89 fromHome:", 74);
|
||||
}
|
||||
return {
|
||||
matchNumber: rm.matchNumber, stage: "R32",
|
||||
home, away, status: feed?.status ?? "SCHEDULED", prob: feed?.prob,
|
||||
homePenalty: feed?.homePenalty, awayPenalty: feed?.awayPenalty,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -190,6 +216,12 @@ export function resolveBracket(
|
||||
const src = km.losers ? losers : winners;
|
||||
const homeId = src.get(km.fromHome) ?? null;
|
||||
const awayId = src.get(km.fromAway) ?? null;
|
||||
if (km.matchNumber === 89) {
|
||||
console.log("[PROPAGATE]", "ziel slot:", km.matchNumber,
|
||||
"| fromHome:", km.fromHome, "| fromAway:", km.fromAway,
|
||||
"| homeId (aus winners):", homeId,
|
||||
"| awayId (aus winners):", awayId);
|
||||
}
|
||||
|
||||
// Ein Folgeteam ist fix, wenn das speisende Spiel beendet ist.
|
||||
const homeProv = !(decided.get(km.fromHome) ?? false);
|
||||
@@ -217,6 +249,7 @@ export function resolveBracket(
|
||||
later[km.matchNumber] = {
|
||||
matchNumber: km.matchNumber, stage: km.stage,
|
||||
home, away, status: feed?.status ?? "SCHEDULED", prob: feed?.prob,
|
||||
homePenalty: feed?.homePenalty, awayPenalty: feed?.awayPenalty,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface Match {
|
||||
awayScore: number | null;
|
||||
homePenalty?: number | null; // Elfmeterschießen (FIFA-API)
|
||||
awayPenalty?: number | null;
|
||||
winnerTeamId?: string | null; // FIFA-Winner (auch bei Elfmeterschießen)
|
||||
// Wahrscheinlichkeiten 0..1 (Polymarket), falls vorhanden
|
||||
prob?: { home: number; draw: number; away: number } | null;
|
||||
venue?: string | null; // Austragungsort
|
||||
|
||||
Reference in New Issue
Block a user