fixes
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { NextResponse } from "next/server";
|
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 { computeGroupTables, computeThirdPlaceTable } from "@/lib/standings";
|
||||||
import { qualifiedThirdGroups, resolveAnnexC } from "@/lib/bracket";
|
import { qualifiedThirdGroups, resolveAnnexC } from "@/lib/bracket";
|
||||||
|
|
||||||
@@ -31,6 +31,9 @@ export async function GET() {
|
|||||||
const groupTables = computeGroupTables(teams, matches);
|
const groupTables = computeGroupTables(teams, matches);
|
||||||
const groupTablesLive = computeGroupTables(teams, matches, true);
|
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
|
// prob-Feld normalisieren: immer null statt undefined, damit JSON konsistent ist
|
||||||
const normalizedMatches = matches.map((m) => ({ ...m, prob: m.prob ?? null }));
|
const normalizedMatches = matches.map((m) => ({ ...m, prob: m.prob ?? null }));
|
||||||
|
|
||||||
|
|||||||
79
lib/feeds.ts
79
lib/feeds.ts
@@ -1,6 +1,8 @@
|
|||||||
import { GroupId, 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 { computeGroupTables, computeThirdPlaceTable } from "./standings";
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Caching: einfacher In-Memory-Cache mit TTL. Verhindert, dass jede Browser-
|
// Caching: einfacher In-Memory-Cache mit TTL. Verhindert, dass jede Browser-
|
||||||
@@ -88,26 +90,74 @@ const STAGE_ORDER: Record<Match["stage"], number> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Setzt Spielnummern und Stadien.
|
// Setzt Spielnummern und Stadien.
|
||||||
// - K.o.-Spiele: chronologisch ab 73 (Anstöße dort eindeutig) -> für Bracket nötig.
|
// K.o.-Spiele: Nummerierung über Slot-Auflösung (assignKONumbersBySlots),
|
||||||
// - Venue: Gruppenspiele über die Teampaarung, K.o.-Spiele über die Spielnummer.
|
// 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 {
|
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).
|
// Stadien setzen (Gruppenphase per Paarung, K.o. per Nummer).
|
||||||
for (const m of matches) {
|
for (const m of matches) {
|
||||||
m.venue = venueFor(m, teams);
|
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<string, Match>();
|
||||||
|
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).
|
// Holt alle Spiele + leitet die Teamliste daraus ab (spart einen Extra-Call).
|
||||||
export async function fetchMatchesAndTeams(): Promise<{ matches: Match[]; teams: Team[] }> {
|
export async function fetchMatchesAndTeams(): Promise<{ matches: Match[]; teams: Team[] }> {
|
||||||
return cached("fd:matches", 60_000, async () => {
|
return cached("fd:matches", 60_000, async () => {
|
||||||
@@ -154,6 +204,7 @@ export async function fetchMatchesAndTeams(): Promise<{ matches: Match[]; teams:
|
|||||||
});
|
});
|
||||||
|
|
||||||
const teams = [...teamMap.values()];
|
const teams = [...teamMap.values()];
|
||||||
|
|
||||||
assignNumbersAndVenues(matches, teams);
|
assignNumbersAndVenues(matches, teams);
|
||||||
|
|
||||||
return { matches, teams };
|
return { matches, teams };
|
||||||
@@ -227,9 +278,9 @@ export async function fetchOdds(): Promise<ParsedOdds[]> {
|
|||||||
allEvents.push(...page);
|
allEvents.push(...page);
|
||||||
if (page.length < 100) break;
|
if (page.length < 100) break;
|
||||||
}
|
}
|
||||||
//console.log("[polymarket] events fetched:", allEvents.length);
|
|
||||||
|
|
||||||
const parsed: ParsedOdds[] = [];
|
const parsed: ParsedOdds[] = [];
|
||||||
|
|
||||||
for (const ev of allEvents) {
|
for (const ev of allEvents) {
|
||||||
// Slug: fifwc-{home}-{away}-{yyyy}-{mm}-{dd}
|
// Slug: fifwc-{home}-{away}-{yyyy}-{mm}-{dd}
|
||||||
const slugParts = ev.slug.replace("fifwc-", "").split("-");
|
const slugParts = ev.slug.replace("fifwc-", "").split("-");
|
||||||
|
|||||||
@@ -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 away = sideFrom(a.teamId, slotLabel(rm.away, assignment, winnerGroup), teams, feed, "away", a.provisional, a.tooltip);
|
||||||
|
|
||||||
const feedWinner = winnerOf(feed);
|
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) {
|
&& feed.homeScore != null && feed.awayScore != null) {
|
||||||
// Simulation: Gewinner aus Scores und aufgelösten Team-IDs ableiten
|
// Simulation: Gewinner aus Scores und aufgelösten Team-IDs ableiten.
|
||||||
if (feed.homeScore > feed.awayScore) {
|
// Nutze NICHT feed.homeTeamId/awayTeamId (sind null für KO-Matches),
|
||||||
winners.set(rm.matchNumber, h.teamId);
|
// sondern die aufgelösten h.teamId / a.teamId aus resolveR32Slot.
|
||||||
losers.set(rm.matchNumber, loserOf(feed) ?? a.teamId);
|
const homeWins = feed.homeScore > feed.awayScore;
|
||||||
} else if (feed.awayScore > feed.homeScore) {
|
const awayWins = feed.awayScore > feed.homeScore;
|
||||||
winners.set(rm.matchNumber, a.teamId);
|
const winnerId = homeWins ? h.teamId : awayWins ? a.teamId : h.teamId;
|
||||||
losers.set(rm.matchNumber, loserOf(feed) ?? h.teamId);
|
winners.set(rm.matchNumber, winnerId);
|
||||||
} else {
|
losers.set(rm.matchNumber, loserOf(feed) ?? (homeWins ? a.teamId : awayWins ? h.teamId : a.teamId));
|
||||||
// Unentschieden → Heim gewinnt
|
|
||||||
winners.set(rm.matchNumber, h.teamId);
|
|
||||||
losers.set(rm.matchNumber, loserOf(feed) ?? a.teamId);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
winners.set(rm.matchNumber, feedWinner);
|
winners.set(rm.matchNumber, feedWinner);
|
||||||
losers.set(rm.matchNumber, loserOf(feed));
|
losers.set(rm.matchNumber, loserOf(feed));
|
||||||
@@ -188,6 +188,7 @@ 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;
|
||||||
|
|
||||||
// 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);
|
||||||
const awayProv = !(decided.get(km.fromAway) ?? 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 away = sideFrom(awayId, awayLabel, teams, feed, "away", awayProv, awayTtip);
|
||||||
|
|
||||||
const feedWinner = winnerOf(feed);
|
const feedWinner = winnerOf(feed);
|
||||||
if (resolveWinners && !feedWinner && feed && homeId && awayId
|
if (resolveWinners && feed && homeId && awayId
|
||||||
&& feed.homeScore != null && feed.awayScore != null) {
|
&& feed.homeScore != null && feed.awayScore != null) {
|
||||||
if (feed.homeScore > feed.awayScore) {
|
const homeWins = feed.homeScore > feed.awayScore;
|
||||||
winners.set(km.matchNumber, homeId);
|
const awayWins = feed.awayScore > feed.homeScore;
|
||||||
losers.set(km.matchNumber, loserOf(feed) ?? awayId);
|
const winnerId = homeWins ? homeId : awayWins ? awayId : homeId;
|
||||||
} else if (feed.awayScore > feed.homeScore) {
|
winners.set(km.matchNumber, winnerId);
|
||||||
winners.set(km.matchNumber, awayId);
|
losers.set(km.matchNumber, loserOf(feed) ?? (homeWins ? awayId : awayWins ? homeId : awayId));
|
||||||
losers.set(km.matchNumber, loserOf(feed) ?? homeId);
|
|
||||||
} else {
|
|
||||||
winners.set(km.matchNumber, homeId);
|
|
||||||
losers.set(km.matchNumber, loserOf(feed) ?? awayId);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
winners.set(km.matchNumber, feedWinner);
|
winners.set(km.matchNumber, feedWinner);
|
||||||
losers.set(km.matchNumber, loserOf(feed));
|
losers.set(km.matchNumber, loserOf(feed));
|
||||||
|
|||||||
Reference in New Issue
Block a user