changes
This commit is contained in:
94
lib/secure-places.ts
Normal file
94
lib/secure-places.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user