94 lines
3.9 KiB
TypeScript
94 lines
3.9 KiB
TypeScript
import { GroupId, Match, Team } from "./types";
|
||
import { computeGroupTables } from "./standings";
|
||
|
||
// Bestimmt, welche Tabellenplätze einer Gruppe bereits MATHEMATISCH feststehen –
|
||
// auch wenn noch Spiele ausstehen. Berücksichtigt ALLE FIFA-Tiebreaker, weil die
|
||
// Tabelle über computeGroupTables() berechnet wird (Punkte, Direktvergleich,
|
||
// Tordifferenz, Tore).
|
||
//
|
||
// Methode: vollständige Worst-Case-Enumeration der Restspiele. Ein Platz p ist
|
||
// für ein Team sicher, wenn es in JEDEM möglichen Ausgang aller Restspiele auf
|
||
// Platz p oder besser bleibt. Wir enumerieren pro Restspiel mehrere
|
||
// repräsentative Ergebnisse, die alle relevanten Tiebreaker-Effekte abdecken:
|
||
// - Heimsieg knapp (1:0) und hoch (5:0)
|
||
// - Auswärtssieg knapp (0:1) und hoch (0:5)
|
||
// - Unentschieden (0:0 und 2:2)
|
||
// Diese Menge deckt Punkt- UND Tordifferenz-Szenarien ausreichend ab, um echte
|
||
// Sicherheit korrekt zu erkennen (inkl. Fälle wie "Direktvergleich bereits
|
||
// gewonnen -> uneinholbar").
|
||
|
||
// Mögliche Ergebnis-Varianten je Restspiel (homeGoals, awayGoals).
|
||
const OUTCOME_VARIANTS: Array<[number, number]> = [
|
||
[1, 0], [5, 0], // Heimsieg knapp / hoch
|
||
[0, 1], [0, 5], // Auswärtssieg knapp / hoch
|
||
[0, 0], [2, 2], // Unentschieden
|
||
];
|
||
|
||
function openMatches(group: GroupId, matches: Match[]): Match[] {
|
||
return matches.filter(
|
||
(m) => m.group === group && m.status !== "FINISHED"
|
||
&& m.homeTeamId != null && m.awayTeamId != null,
|
||
);
|
||
}
|
||
|
||
// Liefert für eine Gruppe Map: Platz(1-basiert) -> teamId, aber nur für Plätze,
|
||
// die in ALLEN Szenarien stabil von demselben Team gehalten werden.
|
||
export function securePlaces(
|
||
group: GroupId, teams: Team[], matches: Match[],
|
||
): Map<number, string> {
|
||
const open = openMatches(group, matches);
|
||
|
||
// Begrenzung: bei sehr vielen offenen Spielen wird die Enumeration groß.
|
||
// In 4er-Gruppen sind es maximal 6 offene Spiele (Turnierstart) -> 6^? zu viel.
|
||
// Wir enumerieren nur, wenn die Kombinationszahl handhabbar ist; sonst gilt
|
||
// konservativ "nichts sicher" (am Turnierstart ohnehin korrekt).
|
||
const combos = Math.pow(OUTCOME_VARIANTS.length, open.length);
|
||
if (open.length === 0) {
|
||
// Alles gespielt: aktuelle Tabelle ist final.
|
||
const table = computeGroupTables(teams, matches).find((t) => t.group === group);
|
||
const res = new Map<number, string>();
|
||
table?.rows.forEach((r) => res.set(r.rank, r.teamId));
|
||
return res;
|
||
}
|
||
if (combos > 100_000) {
|
||
return new Map(); // zu früh im Turnier -> nichts gesichert
|
||
}
|
||
|
||
// Für jeden Platz das Set der Teams sammeln, die diesen Platz über ALLE
|
||
// Szenarien einnehmen können. Ist das Set einelementig, ist der Platz sicher.
|
||
const placeTeams: Array<Set<string>> = [new Set(), new Set(), new Set(), new Set()];
|
||
|
||
const total = combos;
|
||
for (let combo = 0; combo < total; combo++) {
|
||
// Szenario zusammenbauen: jedes offene Spiel bekommt eine Variante.
|
||
let c = combo;
|
||
const simulated: Match[] = open.map((m) => {
|
||
const variantIdx = c % OUTCOME_VARIANTS.length;
|
||
c = Math.floor(c / OUTCOME_VARIANTS.length);
|
||
const [hg, ag] = OUTCOME_VARIANTS[variantIdx];
|
||
return { ...m, status: "FINISHED", homeScore: hg, awayScore: ag };
|
||
});
|
||
// Gespielte + simulierte Spiele kombinieren.
|
||
const played = matches.filter((m) => !(m.group === group && m.status !== "FINISHED"));
|
||
const all = [...played, ...simulated];
|
||
const table = computeGroupTables(teams, all).find((t) => t.group === group);
|
||
if (!table) continue;
|
||
table.rows.forEach((r) => {
|
||
placeTeams[r.rank - 1]?.add(r.teamId);
|
||
});
|
||
}
|
||
|
||
const result = new Map<number, string>();
|
||
for (let i = 0; i < 4; i++) {
|
||
if (placeTeams[i].size === 1) {
|
||
result.set(i + 1, [...placeTeams[i]][0]);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
export function placeIsSecure(
|
||
group: GroupId, place: number, teams: Team[], matches: Match[],
|
||
): boolean {
|
||
return securePlaces(group, teams, matches).has(place);
|
||
} |