third place fix und polymarket
This commit is contained in:
284
lib/feeds.ts
284
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<ParsedOdds[]> {
|
||||
|
||||
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<string[]>(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<ParsedOdds[]> {
|
||||
}
|
||||
|
||||
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<ParsedOdds>();
|
||||
|
||||
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<number, { homeTeamId: string; awayTeamId: string }> {
|
||||
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<number, { homeTeamId: string; awayTeamId: string }>();
|
||||
|
||||
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<string, string>();
|
||||
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;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -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<string>,
|
||||
): { 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<number, string | null>();
|
||||
// Map: Match-Nummer -> ist das Spiel beendet (Sieger fix)?
|
||||
const decided = new Map<number, boolean>();
|
||||
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);
|
||||
|
||||
|
||||
@@ -132,17 +132,19 @@ function classifyGroups(states: Map<GroupId, GroupThirdState>): {
|
||||
// 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<string> {
|
||||
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<string>();
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user