Files
wm-projekt/lib/secure-places.ts
2026-06-22 22:11:04 -05:00

94 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}