|
|
|
|
@@ -1,6 +1,6 @@
|
|
|
|
|
import { GroupId, GROUP_IDS, Match, Team } from "./types";
|
|
|
|
|
import { computeGroupTables, computeThirdPlaceTable } from "./standings";
|
|
|
|
|
import { resolveAnnexC, ANNEX_WINNER_ORDER } from "./bracket";
|
|
|
|
|
import { computeGroupTables } from "./standings";
|
|
|
|
|
import { resolveAnnexC } from "./bracket";
|
|
|
|
|
|
|
|
|
|
// Repräsentative Ergebnis-Varianten je Restspiel (decken alle Punktkombinationen ab).
|
|
|
|
|
const OUTCOMES: Array<[number, number]> = [
|
|
|
|
|
@@ -25,7 +25,6 @@ interface GroupThirdState {
|
|
|
|
|
possibleTeamIds: Set<string>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Offene Spiele einer Gruppe, die noch kein Ergebnis haben.
|
|
|
|
|
function openMatches(group: GroupId, matches: Match[]): Match[] {
|
|
|
|
|
return matches.filter(
|
|
|
|
|
(m) => m.group === group && m.status !== "FINISHED"
|
|
|
|
|
@@ -33,8 +32,6 @@ function openMatches(group: GroupId, matches: Match[]): Match[] {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Simuliert alle Restspiel-Ergebnisse für eine Gruppe und sammelt die möglichen
|
|
|
|
|
// Punktzahlen und Team-IDs des Drittplatzierten.
|
|
|
|
|
function enumerateThirdPossibilities(
|
|
|
|
|
group: GroupId, teams: Team[], matches: Match[],
|
|
|
|
|
): { minPoints: number; maxPoints: number; teamIds: Set<string> } {
|
|
|
|
|
@@ -47,21 +44,14 @@ function enumerateThirdPossibilities(
|
|
|
|
|
const teamIds = new Set<string>();
|
|
|
|
|
|
|
|
|
|
if (open.length === 0) {
|
|
|
|
|
// Keine offenen Spiele: aktuellen Tabellenstand nehmen
|
|
|
|
|
const table = computeGroupTables(teams, matches).find((t) => t.group === group);
|
|
|
|
|
const third = table?.rows.find((r) => r.rank === 3);
|
|
|
|
|
if (third) {
|
|
|
|
|
minP = maxP = third.points;
|
|
|
|
|
teamIds.add(third.teamId);
|
|
|
|
|
}
|
|
|
|
|
if (third) { minP = maxP = third.points; teamIds.add(third.teamId); }
|
|
|
|
|
return { minPoints: minP, maxPoints: maxP, teamIds };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const totalCombos = Math.pow(OUTCOMES.length, open.length);
|
|
|
|
|
// Safety: bei sehr vielen offenen Spielen → breites Intervall
|
|
|
|
|
if (totalCombos > 500) {
|
|
|
|
|
return { minPoints: 0, maxPoints: 9, teamIds };
|
|
|
|
|
}
|
|
|
|
|
if (totalCombos > 500) return { minPoints: 0, maxPoints: 9, teamIds };
|
|
|
|
|
|
|
|
|
|
for (let combo = 0; combo < totalCombos; combo++) {
|
|
|
|
|
let c = combo;
|
|
|
|
|
@@ -74,62 +64,34 @@ function enumerateThirdPossibilities(
|
|
|
|
|
const all = [...played, ...simulated];
|
|
|
|
|
const table = computeGroupTables(teams, all).find((t) => t.group === group);
|
|
|
|
|
const third = table?.rows.find((r) => r.rank === 3);
|
|
|
|
|
if (third) {
|
|
|
|
|
minP = Math.min(minP, third.points);
|
|
|
|
|
maxP = Math.max(maxP, third.points);
|
|
|
|
|
teamIds.add(third.teamId);
|
|
|
|
|
}
|
|
|
|
|
if (third) { minP = Math.min(minP, third.points); maxP = Math.max(maxP, third.points); teamIds.add(third.teamId); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { minPoints: minP === Infinity ? 0 : minP, maxPoints: maxP === -Infinity ? 9 : maxP, teamIds };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Bestimmt, ob ein Gruppendritter eines bestimmten Slots (Gegner des Siegers
|
|
|
|
|
// der Gruppe `winnerGroup`) in ALLEN noch möglichen Konstellationen identisch
|
|
|
|
|
// bleibt und somit als „fix" markiert werden kann.
|
|
|
|
|
//
|
|
|
|
|
// Strategie: Enumeriert alle möglichen Mengen von 8 qualifizierten Dritten-Gruppen,
|
|
|
|
|
// prüft per Annex C, welche Gruppe in jeder Konstellation in diesen Slot fällt.
|
|
|
|
|
export function thirdSlotIsSecure(
|
|
|
|
|
winnerGroup: GroupId,
|
|
|
|
|
matches: Match[],
|
|
|
|
|
teams: Team[],
|
|
|
|
|
): boolean {
|
|
|
|
|
// 1. Pro Gruppe den Status des Dritten analysieren
|
|
|
|
|
function buildThirdStates(matches: Match[], teams: Team[]): Map<GroupId, GroupThirdState> {
|
|
|
|
|
const states = new Map<GroupId, GroupThirdState>();
|
|
|
|
|
for (const g of GROUP_IDS) {
|
|
|
|
|
const groupMatches = matches.filter((m) => m.group === g);
|
|
|
|
|
const allDone = groupMatches.length > 0 && groupMatches.every((m) => m.status === "FINISHED");
|
|
|
|
|
const gm = matches.filter((m) => m.group === g);
|
|
|
|
|
const allDone = gm.length > 0 && gm.every((m) => m.status === "FINISHED");
|
|
|
|
|
const table = computeGroupTables(teams, matches).find((t) => t.group === g);
|
|
|
|
|
const thirdRow = table?.rows.find((r) => r.rank === 3);
|
|
|
|
|
|
|
|
|
|
let third: ThirdInfo | null = null;
|
|
|
|
|
if (thirdRow) {
|
|
|
|
|
third = {
|
|
|
|
|
teamId: thirdRow.teamId,
|
|
|
|
|
points: thirdRow.points,
|
|
|
|
|
goalsFor: thirdRow.goalsFor,
|
|
|
|
|
goalsAgainst: thirdRow.goalsAgainst,
|
|
|
|
|
goalDiff: thirdRow.goalDiff,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (thirdRow) third = { teamId: thirdRow.teamId, points: thirdRow.points, goalsFor: thirdRow.goalsFor, goalsAgainst: thirdRow.goalsAgainst, goalDiff: thirdRow.goalDiff };
|
|
|
|
|
if (allDone && third) {
|
|
|
|
|
states.set(g, {
|
|
|
|
|
finished: true, third, minPoints: third.points, maxPoints: third.points,
|
|
|
|
|
possibleTeamIds: new Set([third.teamId]),
|
|
|
|
|
});
|
|
|
|
|
states.set(g, { finished: true, third, minPoints: third.points, maxPoints: third.points, possibleTeamIds: new Set([third.teamId]) });
|
|
|
|
|
} else {
|
|
|
|
|
const { minPoints, maxPoints, teamIds } = enumerateThirdPossibilities(g, teams, matches);
|
|
|
|
|
states.set(g, {
|
|
|
|
|
finished: false, third, minPoints, maxPoints, possibleTeamIds: teamIds,
|
|
|
|
|
});
|
|
|
|
|
states.set(g, { finished: false, third, minPoints, maxPoints, possibleTeamIds: teamIds });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return states;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. lockedIn / lockedOut / contested bestimmen
|
|
|
|
|
// lockedIn = Dritter liegt selbst im Worst-Case in den Top 8
|
|
|
|
|
// lockedOut = Dritter liegt selbst im Best-Case nicht in den Top 8
|
|
|
|
|
function classifyGroups(states: Map<GroupId, GroupThirdState>): {
|
|
|
|
|
lockedIn: Set<GroupId>; lockedOut: Set<GroupId>; contested: Set<GroupId>;
|
|
|
|
|
} {
|
|
|
|
|
const lockedIn = new Set<GroupId>();
|
|
|
|
|
const lockedOut = new Set<GroupId>();
|
|
|
|
|
const contested = new Set<GroupId>();
|
|
|
|
|
@@ -138,51 +100,6 @@ export function thirdSlotIsSecure(
|
|
|
|
|
const st = states.get(g)!;
|
|
|
|
|
if (st.finished && !st.third) { lockedOut.add(g); continue; }
|
|
|
|
|
|
|
|
|
|
// Prüfen, ob dieser Dritte garantiert in den Top 8 liegt (Worst-Case)
|
|
|
|
|
let alwaysTop8 = true;
|
|
|
|
|
let neverTop8 = true;
|
|
|
|
|
|
|
|
|
|
for (const other of GROUP_IDS) {
|
|
|
|
|
if (other === g) continue;
|
|
|
|
|
const os = states.get(other)!;
|
|
|
|
|
|
|
|
|
|
// Kann der andere Dritte diesen hier überholen?
|
|
|
|
|
// Best-Case für den anderen: maxPoints; Worst-Case für diesen: maxPoints (Worst) / minPoints (Best)
|
|
|
|
|
// Für alwaysTop8: dieser mit minPoints vs andere mit maxPoints — liegen ≤ 7 andere über ihm?
|
|
|
|
|
// Für neverTop8: dieser mit maxPoints vs andere mit minPoints — sind mindestens 8 andere besser?
|
|
|
|
|
|
|
|
|
|
if (os.maxPoints > st.minPoints ||
|
|
|
|
|
(os.maxPoints === st.minPoints && !st.finished)) {
|
|
|
|
|
// Der andere KÖNNTE diesen überholen
|
|
|
|
|
alwaysTop8 = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (st.maxPoints > os.minPoints ||
|
|
|
|
|
(st.maxPoints === os.minPoints && !os.finished)) {
|
|
|
|
|
// Dieser KÖNNTE den anderen überholen
|
|
|
|
|
neverTop8 = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Exakte Prüfung: zähle wie viele Groups definitiv besser/schlechter sind
|
|
|
|
|
let definitelyBetter = 0;
|
|
|
|
|
let definitelyWorse = 0;
|
|
|
|
|
for (const other of GROUP_IDS) {
|
|
|
|
|
if (other === g) continue;
|
|
|
|
|
const os = states.get(other)!;
|
|
|
|
|
// other garantiert besser als g?
|
|
|
|
|
if (os.minPoints > st.maxPoints) definitelyBetter++;
|
|
|
|
|
else if (os.minPoints === st.maxPoints && os.finished && !st.finished) definitelyBetter++;
|
|
|
|
|
// g garantiert besser als other?
|
|
|
|
|
if (st.minPoints > os.maxPoints) definitelyWorse++;
|
|
|
|
|
else if (st.minPoints === os.maxPoints && st.finished && !os.finished) definitelyWorse++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (definitelyBetter <= 7) alwaysTop8 = false; // mehr als 7 definitiv besser → nicht garantiert Top 8
|
|
|
|
|
if (definitelyWorse >= 8) neverTop8 = true; // 8+ definitiv schlechter → garantiert Top 8
|
|
|
|
|
|
|
|
|
|
// Neuberechnung mit korrekter Logik:
|
|
|
|
|
// Garantiert Top 8: höchstens 7 andere Gruppen haben Best-Case ≥ mein Worst-Case
|
|
|
|
|
let countCouldBeBetter = 0;
|
|
|
|
|
for (const other of GROUP_IDS) {
|
|
|
|
|
if (other === g) continue;
|
|
|
|
|
@@ -190,9 +107,8 @@ export function thirdSlotIsSecure(
|
|
|
|
|
if (os.maxPoints > st.minPoints) countCouldBeBetter++;
|
|
|
|
|
else if (os.maxPoints === st.minPoints && !st.finished && os.finished) countCouldBeBetter++;
|
|
|
|
|
}
|
|
|
|
|
alwaysTop8 = countCouldBeBetter <= 7;
|
|
|
|
|
const alwaysTop8 = countCouldBeBetter <= 7;
|
|
|
|
|
|
|
|
|
|
// Garantiert NICHT Top 8: mindestens 8 andere haben Worst-Case > mein Best-Case
|
|
|
|
|
let countDefinitelyBetter = 0;
|
|
|
|
|
for (const other of GROUP_IDS) {
|
|
|
|
|
if (other === g) continue;
|
|
|
|
|
@@ -200,135 +116,135 @@ export function thirdSlotIsSecure(
|
|
|
|
|
if (os.minPoints > st.maxPoints) countDefinitelyBetter++;
|
|
|
|
|
else if (os.minPoints === st.maxPoints && os.finished && !st.finished) countDefinitelyBetter++;
|
|
|
|
|
}
|
|
|
|
|
neverTop8 = countDefinitelyBetter >= 8;
|
|
|
|
|
const neverTop8 = countDefinitelyBetter >= 8;
|
|
|
|
|
|
|
|
|
|
if (alwaysTop8) lockedIn.add(g);
|
|
|
|
|
else if (neverTop8) lockedOut.add(g);
|
|
|
|
|
else contested.add(g);
|
|
|
|
|
}
|
|
|
|
|
return { lockedIn, lockedOut, contested };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Public API ---
|
|
|
|
|
|
|
|
|
|
// Liefert die Team-IDs der Dritten, die mathematisch sicher unter den Top 8
|
|
|
|
|
// der gruppenübergreifenden Dritten-Tabelle sind. Prüft für jedes Team aus
|
|
|
|
|
// 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> {
|
|
|
|
|
const states = buildThirdStates(matches, teams);
|
|
|
|
|
const secureTeams = new Set<string>();
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
const os = states.get(og)!;
|
|
|
|
|
if (os.finished && os.third) {
|
|
|
|
|
if (os.third.points > my.points) couldBeBetter++;
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (couldBeBetter <= 7) secureTeams.add(my.teamId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return secureTeams;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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[] {
|
|
|
|
|
const states = buildThirdStates(matches, teams);
|
|
|
|
|
const { lockedIn } = classifyGroups(states);
|
|
|
|
|
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 {
|
|
|
|
|
const states = buildThirdStates(matches, teams);
|
|
|
|
|
const { lockedIn, contested } = classifyGroups(states);
|
|
|
|
|
|
|
|
|
|
// 3. lockedIn >= 8: wenn genau 8 → Zuordnung EINDEUTIG (nur eine Kombination).
|
|
|
|
|
// Wenn > 8 → mehrdeutig (versch. 8er-Teilmengen möglich) → konservativ false.
|
|
|
|
|
if (lockedIn.size > 8) {
|
|
|
|
|
console.log("[3RD-SECURE] false: >8 lockedIn | wg:", winnerGroup, "| lockedIn:", [...lockedIn].join(","));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Genau 8 lockedIn → nur eine mögliche Kombination → Annex C ist determiniert.
|
|
|
|
|
// Direkt prüfen, da Enumeration entfällt.
|
|
|
|
|
if (lockedIn.size === 8) {
|
|
|
|
|
const qGroups = [...lockedIn];
|
|
|
|
|
const assignment = resolveAnnexC(qGroups);
|
|
|
|
|
const assignment = resolveAnnexC([...lockedIn]);
|
|
|
|
|
if (!assignment) { console.log("[3RD-SECURE] false: no annex with 8 locked | wg:", winnerGroup); return false; }
|
|
|
|
|
const assignedGroup = assignment[winnerGroup];
|
|
|
|
|
if (!assignedGroup) { console.log("[3RD-SECURE] false: no assignedGroup (8 locked) | wg:", winnerGroup); return false; }
|
|
|
|
|
const stableState = states.get(assignedGroup);
|
|
|
|
|
if (!stableState) return false;
|
|
|
|
|
const result = stableState.finished && stableState.third != null && stableState.possibleTeamIds.size === 1;
|
|
|
|
|
console.log("[3RD-SECURE] final (8 locked):", result, "| wg:", winnerGroup, "| stableGroup:", assignedGroup, "| finished:", stableState.finished, "| hasThird:", !!stableState.third, "| uniqueTeam:", stableState.possibleTeamIds.size);
|
|
|
|
|
const ag = assignment[winnerGroup];
|
|
|
|
|
if (!ag) { console.log("[3RD-SECURE] false: no assignedGroup (8 locked) | wg:", winnerGroup); 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) {
|
|
|
|
|
// Nicht genug Gruppen, um 8 Dritte zu füllen → Annex C nicht auflösbar
|
|
|
|
|
console.log("[3RD-SECURE] false: <8 possible | wg:", winnerGroup, "| lockedIn:", lockedIn.size, "| contested:", contested.size);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Alle Kombinationen der contested Groups enumerieren,
|
|
|
|
|
// die zusammen mit lockedIn genau 8 qualifizierte Dritte ergeben
|
|
|
|
|
const contestedArr = [...contested];
|
|
|
|
|
const need = 8 - lockedIn.size;
|
|
|
|
|
const lockedArr = [...lockedIn];
|
|
|
|
|
|
|
|
|
|
// Begrenzung der Kombinationszahl
|
|
|
|
|
const combos = combinations(contestedArr.length, need);
|
|
|
|
|
if (combos > 200) { console.log("[3RD-SECURE] false: >200 combos | wg:", winnerGroup); return false; }
|
|
|
|
|
|
|
|
|
|
let stableGroup: GroupId | null = null;
|
|
|
|
|
|
|
|
|
|
for (const indices of enumerateCombinations(contestedArr.length, need)) {
|
|
|
|
|
const qGroups: GroupId[] = [...lockedArr, ...indices.map((i) => contestedArr[i])];
|
|
|
|
|
const qGroups: GroupId[] = [...lockedIn, ...indices.map((i) => contestedArr[i])];
|
|
|
|
|
if (qGroups.length !== 8) continue;
|
|
|
|
|
|
|
|
|
|
// Prüfen, ob diese Kombination tatsächlich möglich ist
|
|
|
|
|
// (die ausgewählten contested müssen in mindestens einem Szenario Top 8 sein,
|
|
|
|
|
// die nicht ausgewählten contested in mindestens einem Szenario nicht Top 8)
|
|
|
|
|
// Vereinfachung: alle Kombinationen als möglich betrachten, solange
|
|
|
|
|
// keine offensichtlichen Widersprüche vorliegen
|
|
|
|
|
const feasible = isCombinationFeasible(qGroups, states);
|
|
|
|
|
if (!feasible) continue;
|
|
|
|
|
|
|
|
|
|
const assignment = resolveAnnexC(qGroups);
|
|
|
|
|
if (!assignment) continue;
|
|
|
|
|
|
|
|
|
|
const assignedGroup = assignment[winnerGroup];
|
|
|
|
|
if (!assignedGroup) { console.log("[3RD-SECURE] false: no assignedGroup | wg:", winnerGroup); return false; }
|
|
|
|
|
|
|
|
|
|
if (stableGroup === null) {
|
|
|
|
|
stableGroup = assignedGroup;
|
|
|
|
|
} else if (stableGroup !== assignedGroup) {
|
|
|
|
|
console.log("[3RD-SECURE] false: unstable slot | wg:", winnerGroup, "| groups:", stableGroup, "vs", assignedGroup);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const ag = assignment[winnerGroup];
|
|
|
|
|
if (!ag) { console.log("[3RD-SECURE] false: no assignedGroup | wg:", winnerGroup); return false; }
|
|
|
|
|
if (stableGroup === null) stableGroup = ag;
|
|
|
|
|
else if (stableGroup !== ag) { console.log("[3RD-SECURE] false: unstable slot | wg:", winnerGroup); return false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. Slot ist stabil → prüfe, ob die Quellgruppe selbst bereits feststeht
|
|
|
|
|
if (stableGroup === null) { console.log("[3RD-SECURE] false: stableGroup=null | wg:", winnerGroup); return false; }
|
|
|
|
|
const stableState = states.get(stableGroup);
|
|
|
|
|
if (!stableState) return false;
|
|
|
|
|
const result = stableState.finished && stableState.third != null && stableState.possibleTeamIds.size === 1;
|
|
|
|
|
console.log("[3RD-SECURE] final:", result, "| wg:", winnerGroup, "| stableGroup:", stableGroup, "| finished:", stableState.finished, "| hasThird:", !!stableState.third, "| uniqueTeam:", stableState.possibleTeamIds.size);
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Berechnet C(n, k) — die Anzahl der Kombinationen.
|
|
|
|
|
// --- Combinatorics helpers ---
|
|
|
|
|
|
|
|
|
|
function combinations(n: number, k: number): number {
|
|
|
|
|
if (k < 0 || k > n) return 0;
|
|
|
|
|
if (k === 0 || k === n) return 1;
|
|
|
|
|
let result = 1;
|
|
|
|
|
for (let i = 1; i <= k; i++) {
|
|
|
|
|
result = result * (n - i + 1) / i;
|
|
|
|
|
}
|
|
|
|
|
return Math.round(result);
|
|
|
|
|
let r = 1;
|
|
|
|
|
for (let i = 1; i <= k; i++) r = r * (n - i + 1) / i;
|
|
|
|
|
return Math.round(r);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generator für alle k-Kombinationen aus n Elementen (0-basierte Indizes).
|
|
|
|
|
function* enumerateCombinations(n: number, k: number): Generator<number[]> {
|
|
|
|
|
if (k === 0) { yield []; return; }
|
|
|
|
|
if (k > n) return;
|
|
|
|
|
const indices = Array.from({ length: k }, (_, i) => i);
|
|
|
|
|
const idx = Array.from({ length: k }, (_, i) => i);
|
|
|
|
|
while (true) {
|
|
|
|
|
yield [...indices];
|
|
|
|
|
yield [...idx];
|
|
|
|
|
let i = k - 1;
|
|
|
|
|
while (i >= 0 && indices[i] === n - k + i) i--;
|
|
|
|
|
while (i >= 0 && idx[i] === n - k + i) i--;
|
|
|
|
|
if (i < 0) break;
|
|
|
|
|
indices[i]++;
|
|
|
|
|
for (let j = i + 1; j < k; j++) indices[j] = indices[j - 1] + 1;
|
|
|
|
|
idx[i]++;
|
|
|
|
|
for (let j = i + 1; j < k; j++) idx[j] = idx[j - 1] + 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prüft, ob eine Kombination von 8 qualifizierten Gruppen tatsächlich möglich ist.
|
|
|
|
|
// Vereinfacht: jede Gruppe in der Kombination muss mindestens einen Punktestand
|
|
|
|
|
// haben können, der Top 8 reicht; jede Gruppe außerhalb muss mindestens einen
|
|
|
|
|
// Punktestand haben können, der NICHT Top 8 reicht.
|
|
|
|
|
function isCombinationFeasible(
|
|
|
|
|
qGroups: GroupId[],
|
|
|
|
|
states: Map<GroupId, GroupThirdState>,
|
|
|
|
|
): boolean {
|
|
|
|
|
const qSet = new Set(qGroups);
|
|
|
|
|
// Jede qualifizierte Gruppe muss potentiell in die Top 8 kommen können
|
|
|
|
|
for (const g of qGroups) {
|
|
|
|
|
const st = states.get(g)!;
|
|
|
|
|
if (st.third && st.finished && st.maxPoints === 0 && qGroups.length > 8) {
|
|
|
|
|
// Dritter mit 0 Punkten kann nicht qualifiziert sein, wenn mehr als 8 Kandidaten
|
|
|
|
|
// Vereinfachte Prüfung
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Jede nicht-qualifizierte Gruppe muss potentiell rausfallen können
|
|
|
|
|
for (const g of GROUP_IDS) {
|
|
|
|
|
if (qSet.has(g)) continue;
|
|
|
|
|
// OK, diese Gruppe ist nicht qualifiziert — muss möglich sein
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|