Compare commits
2 Commits
edaca815a7
...
d982ff1068
| Author | SHA1 | Date | |
|---|---|---|---|
| d982ff1068 | |||
| 6811fc8161 |
@@ -1,5 +1,5 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { fetchMatchesAndTeams, fetchOdds, attachOdds, attachKOOdds, fetchLiveScores, applyLiveScores, assignKONumbersBySlots, fetchKOLiveData, attachKOLiveData } from "@/lib/feeds";
|
import { fetchMatchesAndTeams, fetchOdds, attachOdds, attachKOOdds, fetchFifaScores, applyFifaScores, assignKONumbersBySlots } from "@/lib/feeds";
|
||||||
import { computeGroupTables, computeThirdPlaceTable } from "@/lib/standings";
|
import { computeGroupTables, computeThirdPlaceTable } from "@/lib/standings";
|
||||||
import { qualifiedThirdGroups, resolveAnnexC } from "@/lib/bracket";
|
import { qualifiedThirdGroups, resolveAnnexC } from "@/lib/bracket";
|
||||||
import { securelyQualifiedThirdTeams } from "@/lib/third-place-security";
|
import { securelyQualifiedThirdTeams } from "@/lib/third-place-security";
|
||||||
@@ -22,31 +22,23 @@ export async function GET() {
|
|||||||
console.error("[polymarket] fetchOdds fehlgeschlagen:", err);
|
console.error("[polymarket] fetchOdds fehlgeschlagen:", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Live-Scores von worldcup26.ir (additiv, Fallback auf football-data)
|
// FIFA-Live-Scores (additiv, Fallback auf football-data)
|
||||||
try {
|
|
||||||
const liveScores = await fetchLiveScores();
|
|
||||||
matches = applyLiveScores(matches, teams, liveScores);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("[worldcup26] fetchLiveScores fehlgeschlagen:", err instanceof Error ? err.message : err);
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupTables = computeGroupTables(teams, matches);
|
|
||||||
const groupTablesLive = computeGroupTables(teams, matches, true);
|
|
||||||
|
|
||||||
assignKONumbersBySlots(matches, teams);
|
assignKONumbersBySlots(matches, teams);
|
||||||
|
|
||||||
if (odds) {
|
if (odds) {
|
||||||
matches = attachKOOdds(matches, teams, odds);
|
matches = attachKOOdds(matches, teams, odds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// KO-Live-Daten (Tore, Minute) von worldcup26
|
|
||||||
try {
|
try {
|
||||||
const koGames = await fetchKOLiveData();
|
const fifaData = await fetchFifaScores();
|
||||||
matches = attachKOLiveData(matches, teams, koGames);
|
matches = applyFifaScores(matches, teams, fifaData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[worldcup26] KO-Live-Daten fehlgeschlagen:", err instanceof Error ? err.message : err);
|
console.warn("[fifa] Scores fehlgeschlagen:", err instanceof Error ? err.message : err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const groupTables = computeGroupTables(teams, matches);
|
||||||
|
const groupTablesLive = computeGroupTables(teams, matches, true);
|
||||||
|
|
||||||
const normalizedMatches = matches.map((m) => ({ ...m, prob: m.prob ?? null }));
|
const normalizedMatches = matches.map((m) => ({ ...m, prob: m.prob ?? null }));
|
||||||
|
|
||||||
const thirdTable = computeThirdPlaceTable(groupTablesLive);
|
const thirdTable = computeThirdPlaceTable(groupTablesLive);
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ function Tie({
|
|||||||
</div>
|
</div>
|
||||||
<Side s={tie.home} prob={tie.prob?.home} teams={teams} onShowTip={onShowTip} onHideTip={onHideTip} />
|
<Side s={tie.home} prob={tie.prob?.home} teams={teams} onShowTip={onShowTip} onHideTip={onHideTip} />
|
||||||
<Side s={tie.away} prob={tie.prob?.away} teams={teams} onShowTip={onShowTip} onHideTip={onHideTip} />
|
<Side s={tie.away} prob={tie.prob?.away} teams={teams} onShowTip={onShowTip} onHideTip={onHideTip} />
|
||||||
|
{tie.homePenalty != null && tie.awayPenalty != null && (
|
||||||
|
<div className="tie-meta" style={{ padding: "2px 10px 4px", textAlign: "center" }}>
|
||||||
|
({tie.homePenalty}:{tie.awayPenalty} i.E.)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ function fmtTime(iso: string): string {
|
|||||||
|
|
||||||
function scoreDisplay(m: Match): string {
|
function scoreDisplay(m: Match): string {
|
||||||
if (m.status === "SCHEDULED" || (m.homeScore == null && m.awayScore == null)) return "– : –";
|
if (m.status === "SCHEDULED" || (m.homeScore == null && m.awayScore == null)) return "– : –";
|
||||||
return `${m.homeScore ?? 0} : ${m.awayScore ?? 0}`;
|
const base = `${m.homeScore ?? 0} : ${m.awayScore ?? 0}`;
|
||||||
|
if (m.homePenalty != null && m.awayPenalty != null) {
|
||||||
|
return `${base} (${m.homePenalty}:${m.awayPenalty} i.E.)`;
|
||||||
|
}
|
||||||
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusLabel(m: Match): string {
|
function statusLabel(m: Match): string {
|
||||||
|
|||||||
307
lib/feeds.ts
307
lib/feeds.ts
@@ -1,4 +1,4 @@
|
|||||||
import { GroupId, GoalEvent, Match, MatchStatus, Team } from "./types";
|
import { GroupId, Match, MatchStatus, Team } from "./types";
|
||||||
import { venueFor } from "./venues";
|
import { venueFor } from "./venues";
|
||||||
import { localisedTeamName } from "./team-mappings";
|
import { localisedTeamName } from "./team-mappings";
|
||||||
import { R32, LATER_ROUNDS, resolveAnnexC, qualifiedThirdGroups } from "./bracket";
|
import { R32, LATER_ROUNDS, resolveAnnexC, qualifiedThirdGroups } from "./bracket";
|
||||||
@@ -556,221 +556,146 @@ export function attachKOOdds(matches: Match[], teams: Team[], odds: ParsedOdds[]
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// worldcup26.ir: schnelle Live-Scores (additiv, Fallback auf football-data)
|
// FIFA-API: Live-Scores, Elfmeterschießen, Spielminute
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
const WC26_BASE = "https://worldcup26.ir";
|
const FIFA_BASE = "https://api.fifa.com/api/v3";
|
||||||
|
const FIFA_SEASON = "285023";
|
||||||
|
|
||||||
interface Wc26Game {
|
// FIFA-Code → App-Code (nur Abweichungen; sonst identisch)
|
||||||
home_team_name_en?: string;
|
const FIFA_CODE_OVERRIDE: Record<string, string> = {
|
||||||
away_team_name_en?: string;
|
CRO: "HRV", // Kroatien
|
||||||
group?: string;
|
POR: "PRT", // Portugal
|
||||||
matchday?: string;
|
SUI: "CHE", // Schweiz
|
||||||
home_score?: string;
|
};
|
||||||
away_score?: string;
|
function fifaCodeToAppCode(fifaCode: string): string {
|
||||||
time_elapsed?: string;
|
return FIFA_CODE_OVERRIDE[fifaCode] ?? fifaCode;
|
||||||
home_scorers?: string;
|
|
||||||
away_scorers?: string;
|
|
||||||
current_minute?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LiveScore {
|
interface FifaTeamBlock {
|
||||||
homeName: string;
|
IdTeam: string; // FIFA-Team-ID
|
||||||
awayName: string;
|
Abbreviation: string; // FIFA-Code (z.B. PAR, CRO)
|
||||||
group: string;
|
}
|
||||||
matchday: string;
|
|
||||||
|
interface FifaMatch {
|
||||||
|
MatchNumber: number;
|
||||||
|
Home: FifaTeamBlock | null;
|
||||||
|
Away: FifaTeamBlock | null;
|
||||||
|
HomeTeamScore: number | null;
|
||||||
|
AwayTeamScore: number | null;
|
||||||
|
HomeTeamPenaltyScore: number | null;
|
||||||
|
AwayTeamPenaltyScore: number | null;
|
||||||
|
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 {
|
||||||
homeScore: number | null;
|
homeScore: number | null;
|
||||||
awayScore: number | null;
|
awayScore: number | null;
|
||||||
status: string; // "IN_PLAY" | "FINISHED"
|
homePenalty: number | null;
|
||||||
|
awayPenalty: number | null;
|
||||||
|
resultType: number | null;
|
||||||
|
status: MatchStatus;
|
||||||
|
matchTime: string | null;
|
||||||
|
winnerTeamId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ruft alle Spiele von worldcup26.ir ab.
|
// Holt Live-Scores von der FIFA-API. Gibt Map MatchNumber → Scores + FIFA-ID→App-Code-Map zurück.
|
||||||
export async function fetchLiveScores(): Promise<LiveScore[]> {
|
export async function fetchFifaScores(): Promise<{
|
||||||
const res = await fetch(`${WC26_BASE}/get/games`, {
|
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, {
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
headers: { "User-Agent": "wm2026-board/1.0" },
|
headers: { "User-Agent": "wm2026-board/1.0" },
|
||||||
signal: AbortSignal.timeout(5000),
|
signal: AbortSignal.timeout(5000),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`worldcup26 ${res.status}`);
|
if (!res.ok) throw new Error(`fifa ${res.status}`);
|
||||||
const data = (await res.json()) as { games: Wc26Game[] };
|
const data = (await res.json()) as { Results: FifaMatch[] };
|
||||||
const games = data.games ?? [];
|
const scores = new Map<number, FifaScores>();
|
||||||
|
const fifaIdToAppCode = new Map<string, string>();
|
||||||
|
|
||||||
const scores: LiveScore[] = [];
|
for (const fm of data.Results ?? []) {
|
||||||
for (const g of games) {
|
// Team-Mapping sammeln
|
||||||
if (!g.home_team_name_en || !g.away_team_name_en) continue;
|
for (const tb of [fm.Home, fm.Away]) {
|
||||||
const hScore = parseScore(g.home_score);
|
if (tb && !fifaIdToAppCode.has(tb.IdTeam)) {
|
||||||
const aScore = parseScore(g.away_score);
|
fifaIdToAppCode.set(tb.IdTeam, fifaCodeToAppCode(tb.Abbreviation));
|
||||||
let status = "";
|
|
||||||
switch (g.time_elapsed) {
|
|
||||||
case "live": status = "IN_PLAY"; break;
|
|
||||||
case "finished": status = "FINISHED"; break;
|
|
||||||
default: continue; // notstarted → überspringen
|
|
||||||
}
|
}
|
||||||
// Nur anwenden, wenn mindestens ein Score vorhanden ist
|
}
|
||||||
if (hScore == null && aScore == null) continue;
|
|
||||||
scores.push({
|
let status: MatchStatus;
|
||||||
homeName: g.home_team_name_en,
|
switch (fm.MatchStatus) {
|
||||||
awayName: g.away_team_name_en,
|
case 0: status = "FINISHED"; break;
|
||||||
group: g.group ?? "",
|
case 1: status = "SCHEDULED"; break;
|
||||||
matchday: String(g.matchday ?? ""),
|
default: status = "LIVE"; break;
|
||||||
homeScore: hScore,
|
}
|
||||||
awayScore: aScore,
|
scores.set(fm.MatchNumber, {
|
||||||
|
homeScore: fm.HomeTeamScore,
|
||||||
|
awayScore: fm.AwayTeamScore,
|
||||||
|
homePenalty: fm.HomeTeamPenaltyScore,
|
||||||
|
awayPenalty: fm.AwayTeamPenaltyScore,
|
||||||
|
resultType: fm.ResultType,
|
||||||
status,
|
status,
|
||||||
|
matchTime: fm.MatchTime,
|
||||||
|
winnerTeamId: fm.Winner ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log("[worldcup26] spiele:", scores.length);
|
return { scores, fifaIdToAppCode };
|
||||||
return scores;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseScore(s: string | undefined): number | null {
|
// Wendet FIFA-Live-Scores auf Matches an (MatchNumber als exakter Schlüssel).
|
||||||
if (!s || s === "null") return null;
|
export function applyFifaScores(
|
||||||
const n = Number(s);
|
matches: Match[], teams: Team[],
|
||||||
return Number.isFinite(n) ? n : null;
|
fifaData: { scores: Map<number, FifaScores>; fifaIdToAppCode: Map<string, string> },
|
||||||
}
|
|
||||||
|
|
||||||
// Wendet worldcup26-Live-Scores auf football-data-Matches an.
|
|
||||||
// Matching über normalisierte Teamnamen + group + matchday.
|
|
||||||
export function applyLiveScores(
|
|
||||||
matches: Match[], teams: Team[], liveScores: LiveScore[],
|
|
||||||
): Match[] {
|
): Match[] {
|
||||||
// Build lookup: normName(home)::normName(away)::group::matchday → match
|
const { scores: fifaMap, fifaIdToAppCode } = fifaData;
|
||||||
// Für Group-Phase ist die Paarung eindeutig (jedes Paar spielt 1x).
|
if (fifaMap.size === 0) return matches;
|
||||||
const byPair = new Map<string, Match>();
|
// Baue App-Code → App-Team-ID Map
|
||||||
for (const m of matches) {
|
const appCodeToId = new Map<string, string>();
|
||||||
if (m.group == null) continue; // nur Gruppenphase
|
|
||||||
if (!m.homeTeamId || !m.awayTeamId) continue;
|
|
||||||
const ht = teams.find((t) => t.id === m.homeTeamId);
|
|
||||||
const at = teams.find((t) => t.id === m.awayTeamId);
|
|
||||||
if (!ht || !at) continue;
|
|
||||||
const hn = normName(ht.name);
|
|
||||||
const an = normName(at.name);
|
|
||||||
// Beide Richtungen
|
|
||||||
byPair.set(`${hn}::${an}::${m.group}`, m);
|
|
||||||
byPair.set(`${an}::${hn}::${m.group}`, m);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = [...matches];
|
|
||||||
let applied = 0;
|
|
||||||
for (const ls of liveScores) {
|
|
||||||
const hn = normName(ls.homeName);
|
|
||||||
const an = normName(ls.awayName);
|
|
||||||
const key = `${hn}::${an}::${ls.group}`;
|
|
||||||
const fdMatch = byPair.get(key);
|
|
||||||
if (!fdMatch) continue;
|
|
||||||
|
|
||||||
const idx = result.findIndex((m) => m.id === fdMatch.id);
|
|
||||||
if (idx < 0) continue;
|
|
||||||
|
|
||||||
result[idx] = {
|
|
||||||
...result[idx],
|
|
||||||
homeScore: ls.homeScore ?? result[idx].homeScore,
|
|
||||||
awayScore: ls.awayScore ?? result[idx].awayScore,
|
|
||||||
status: ls.status as Match["status"],
|
|
||||||
};
|
|
||||||
applied++;
|
|
||||||
}
|
|
||||||
if (applied > 0) console.log("[worldcup26] auf matches angewandt:", applied);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Holt KO-Live-Daten von worldcup26 (Tore, Minute) und hängt sie an die Feed-Matches.
|
|
||||||
export async function fetchKOLiveData(): Promise<Wc26Game[]> {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${WC26_BASE}/get/games`, {
|
|
||||||
cache: "no-store",
|
|
||||||
headers: { "User-Agent": "wm2026-board/1.0" },
|
|
||||||
signal: AbortSignal.timeout(5000),
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error(`worldcup26 ${res.status}`);
|
|
||||||
const data = (await res.json()) as { games: Wc26Game[] };
|
|
||||||
return (data.games ?? []).filter(g => {
|
|
||||||
const grp = (g.group ?? "").toUpperCase();
|
|
||||||
return grp === "" || grp === "R32" || grp === "R16" || grp === "QF" || grp === "SF" || grp === "3RD" || grp === "FINAL" || grp === "FINALIST";
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hängt worldcup26-KO-Live-Daten an die Matches an (Tore, Minute, Scores).
|
|
||||||
export function attachKOLiveData(matches: Match[], teams: Team[], koGames: Wc26Game[]): Match[] {
|
|
||||||
if (koGames.length === 0) return matches;
|
|
||||||
|
|
||||||
const nameById = new Map<string, string>();
|
|
||||||
for (const t of teams) {
|
for (const t of teams) {
|
||||||
const nn = normName(t.name);
|
if (t.code) appCodeToId.set(t.code.toLowerCase(), t.id);
|
||||||
if (!nameById.has(nn)) nameById.set(nn, t.id);
|
|
||||||
}
|
}
|
||||||
|
let applied = 0;
|
||||||
return matches.map((m) => {
|
const result = matches.map((m) => {
|
||||||
if (m.group != null) return m; // nur K.o.-Spiele
|
const fs = fifaMap.get(m.matchNumber);
|
||||||
|
if (!fs) return m;
|
||||||
const hId = m.homeTeamId;
|
const r = { ...m };
|
||||||
const aId = m.awayTeamId;
|
if (fs.homeScore != null) r.homeScore = fs.homeScore;
|
||||||
const hName = hId ? teams.find(t => t.id === hId)?.name : null;
|
if (fs.awayScore != null) r.awayScore = fs.awayScore;
|
||||||
const aName = aId ? teams.find(t => t.id === aId)?.name : null;
|
if (fs.homePenalty != null) r.homePenalty = fs.homePenalty;
|
||||||
|
if (fs.awayPenalty != null) r.awayPenalty = fs.awayPenalty;
|
||||||
// Finde worldcup26-Spiel über Teamnamen
|
// FIFA-ID → App-Team-ID auflösen
|
||||||
const wm = koGames.find(g => {
|
if (fs.winnerTeamId) {
|
||||||
if (!hName || !aName) return false;
|
const appCode = fifaIdToAppCode.get(fs.winnerTeamId);
|
||||||
const gh = normName(g.home_team_name_en ?? "");
|
if (appCode) {
|
||||||
const ga = normName(g.away_team_name_en ?? "");
|
const appTeamId = appCodeToId.get(appCode.toLowerCase());
|
||||||
return (gh === normName(hName) && ga === normName(aName)) ||
|
if (appTeamId) {
|
||||||
(gh === normName(aName) && ga === normName(hName));
|
r.winnerTeamId = appTeamId;
|
||||||
});
|
|
||||||
|
|
||||||
if (!wm) return m;
|
|
||||||
|
|
||||||
const result = { ...m };
|
|
||||||
|
|
||||||
// Live-Score + Status
|
|
||||||
if (wm.time_elapsed === "live" || wm.time_elapsed === "finished") {
|
|
||||||
result.status = wm.time_elapsed === "finished" ? "FINISHED" : "IN_PLAY";
|
|
||||||
const hs = parseScore(wm.home_score);
|
|
||||||
const as = parseScore(wm.away_score);
|
|
||||||
if (hs != null) result.homeScore = hs;
|
|
||||||
if (as != null) result.awayScore = as;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spielminute
|
|
||||||
if (wm.current_minute) {
|
|
||||||
const min = parseInt(wm.current_minute, 10);
|
|
||||||
if (!isNaN(min)) result.minute = min;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Torereignisse aus home_scorers / away_scorers parsen (Format: "{\"Name Minute'\"}")
|
|
||||||
const goals: GoalEvent[] = [];
|
|
||||||
if (wm.home_scorers) parseScorers(wm.home_scorers, "home", goals);
|
|
||||||
if (wm.away_scorers) parseScorers(wm.away_scorers, "away", goals);
|
|
||||||
if (goals.length > 0) {
|
|
||||||
goals.sort((a, b) => a.minute - b.minute);
|
|
||||||
result.goals = goals;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parst worldcup26-Scorer-String: "{\"Casemiro 56'\"}" oder "{\"Player 12'\",\"Other 34'\"}"
|
|
||||||
function parseScorers(raw: string, team: "home" | "away", out: GoalEvent[]): void {
|
|
||||||
if (!raw || raw === "{}") return;
|
|
||||||
const cleaned = raw.replace(/[{}"\\]/g, "").trim();
|
|
||||||
if (!cleaned) return;
|
|
||||||
const parts = cleaned.split(",");
|
|
||||||
for (const p of parts) {
|
|
||||||
const m = /^(.+?)\s+([0-9+]+)'?(?:\s*\(.*?\))?\s*$/.exec(p.trim());
|
|
||||||
if (m) {
|
|
||||||
const name = m[1].trim();
|
|
||||||
const minStr = m[2];
|
|
||||||
let minute = 0;
|
|
||||||
const plusIdx = minStr.indexOf("+");
|
|
||||||
if (plusIdx >= 0) {
|
|
||||||
minute = parseInt(minStr.slice(0, plusIdx), 10) + parseInt(minStr.slice(plusIdx + 1), 10);
|
|
||||||
} else {
|
} else {
|
||||||
minute = parseInt(minStr, 10);
|
console.warn("[fifa] Winner-Team-Code nicht in App-Teams:", appCode, "| matchNumber:", m.matchNumber);
|
||||||
}
|
}
|
||||||
if (!isNaN(minute)) {
|
} else {
|
||||||
out.push({ scorer: name, minute, team });
|
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;
|
||||||
}
|
}
|
||||||
|
r.status = fs.status;
|
||||||
|
applied++;
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
if (applied > 0) console.log("[fifa] scores angewandt:", applied);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,8 @@ export interface ResolvedTie {
|
|||||||
away: ResolvedSide;
|
away: ResolvedSide;
|
||||||
status: string;
|
status: string;
|
||||||
prob?: { home: number; draw: number | null; away: number } | null;
|
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.
|
// 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.
|
// Bestimmt den Sieger eines abgeschlossenen Spiels (Feed) als Team-ID.
|
||||||
function winnerOf(m: Match | undefined): string | null {
|
function winnerOf(m: Match | undefined): string | null {
|
||||||
if (!m || m.status !== "FINISHED") return 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 == null || m.awayScore == null) return null;
|
||||||
if (m.homeScore > m.awayScore) return m.homeTeamId;
|
if (m.homeScore > m.awayScore) return m.homeTeamId;
|
||||||
if (m.awayScore > m.homeScore) return m.awayTeamId;
|
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 {
|
function loserOf(m: Match | undefined): string | null {
|
||||||
if (!m || m.status !== "FINISHED") return 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 == null || m.awayScore == null) return null;
|
||||||
if (m.homeScore > m.awayScore) return m.awayTeamId;
|
if (m.homeScore > m.awayScore) return m.awayTeamId;
|
||||||
if (m.awayScore > m.homeScore) return m.homeTeamId;
|
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 away = sideFrom(a.teamId, slotLabel(rm.away, assignment, winnerGroup), teams, feed, "away", a.provisional, a.tooltip);
|
||||||
|
|
||||||
const feedWinner = winnerOf(feed);
|
const feedWinner = winnerOf(feed);
|
||||||
|
winnerDbg(feed, feedWinner, "resolveR32Slot");
|
||||||
// Sim-Modus: Gewinner NUR aus Scores + Slot-Team-IDs ableiten.
|
// Sim-Modus: Gewinner NUR aus Scores + Slot-Team-IDs ableiten.
|
||||||
// feedWinner (aus feed.homeTeamId/awayTeamId) wird IGNORIERT,
|
// feedWinner (aus feed.homeTeamId/awayTeamId) wird IGNORIERT,
|
||||||
// weil matchNumber-Zuweisung (assignNumbersAndVenues) von der
|
// weil matchNumber-Zuweisung (assignNumbersAndVenues) von der
|
||||||
@@ -178,9 +197,16 @@ export function resolveBracket(
|
|||||||
losers.set(rm.matchNumber, loserOf(feed));
|
losers.set(rm.matchNumber, loserOf(feed));
|
||||||
}
|
}
|
||||||
decided.set(rm.matchNumber, feed?.status === "FINISHED");
|
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 {
|
return {
|
||||||
matchNumber: rm.matchNumber, stage: "R32",
|
matchNumber: rm.matchNumber, stage: "R32",
|
||||||
home, away, status: feed?.status ?? "SCHEDULED", prob: feed?.prob,
|
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 src = km.losers ? losers : winners;
|
||||||
const homeId = src.get(km.fromHome) ?? null;
|
const homeId = src.get(km.fromHome) ?? null;
|
||||||
const awayId = src.get(km.fromAway) ?? 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.
|
// Ein Folgeteam ist fix, wenn das speisende Spiel beendet ist.
|
||||||
const homeProv = !(decided.get(km.fromHome) ?? false);
|
const homeProv = !(decided.get(km.fromHome) ?? false);
|
||||||
@@ -217,6 +249,7 @@ export function resolveBracket(
|
|||||||
later[km.matchNumber] = {
|
later[km.matchNumber] = {
|
||||||
matchNumber: km.matchNumber, stage: km.stage,
|
matchNumber: km.matchNumber, stage: km.stage,
|
||||||
home, away, status: feed?.status ?? "SCHEDULED", prob: feed?.prob,
|
home, away, status: feed?.status ?? "SCHEDULED", prob: feed?.prob,
|
||||||
|
homePenalty: feed?.homePenalty, awayPenalty: feed?.awayPenalty,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export interface Match {
|
|||||||
awayTeamId: string | null;
|
awayTeamId: string | null;
|
||||||
homeScore: number | null;
|
homeScore: number | null;
|
||||||
awayScore: number | null;
|
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
|
// Wahrscheinlichkeiten 0..1 (Polymarket), falls vorhanden
|
||||||
prob?: { home: number; draw: number; away: number } | null;
|
prob?: { home: number; draw: number; away: number } | null;
|
||||||
venue?: string | null; // Austragungsort
|
venue?: string | null; // Austragungsort
|
||||||
|
|||||||
Reference in New Issue
Block a user