Files
wm-projekt/lib/simulation.ts
2026-07-01 20:04:55 -05:00

142 lines
5.0 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 { Match, Team, GroupTable, ThirdPlaceRow } from "./types";
import { ThirdAssignment, LATER_ROUNDS } from "./bracket";
import { ResolvedSide, ResolvedTie } from "./resolve-bracket";
export type SimOverrides = Record<string, { homeScore: number; awayScore: number }>;
const STORAGE_KEY = "wm2026-sim-overrides";
export function loadOverrides(): SimOverrides {
if (typeof window === "undefined") return {};
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : {};
} catch {
return {};
}
}
export function saveOverrides(overrides: SimOverrides): void {
if (typeof window === "undefined") return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(overrides));
} catch { /* quota exceeded */ }
}
// Plausibles Standardergebnis aus Polymarket-3-Wege-Wahrscheinlichkeiten.
//
// Schwellen (zentral):
// Draw am höchsten ODER max(home,away) < 0.40 → 0:0 (Gruppe) / Favorit 1:0 (K.o.)
// Favorit 0.400.60 → 1:0
// Favorit 0.600.78 → 2:0
// Favorit > 0.78 → 3:0
//
// Beispiele:
// SchweizKanada (0.385/0.315/0.295) → 0:0
// SchottlandBrasilien (0.10/0.17/0.75) → 0:2
// MarokkoHaiti (0.83/0.13/0.05) → 3:0
export function defaultScore(match: Match): { homeScore: number; awayScore: number } | null {
const prob = match.prob;
if (!prob) {
if (match.stage === "GROUP") return { homeScore: 1, awayScore: 1 };
return null;
}
const maxFA = Math.max(prob.home, prob.away);
const isDraw = prob.draw > prob.home && prob.draw > prob.away;
const isGroup = match.stage === "GROUP";
if (isDraw || maxFA < 0.40) {
if (isGroup) return { homeScore: 0, awayScore: 0 };
// K.o.: kein Remis möglich → der wahrscheinlichere von Heim/Auswärts gewinnt knapp,
// Draw wird ignoriert (kein gültiges K.o.-Ergebnis).
return prob.home >= prob.away
? { homeScore: 1, awayScore: 0 }
: { homeScore: 0, awayScore: 1 };
}
// Favorit bestimmen
const homeFav = prob.home >= prob.away;
let goals: number;
if (maxFA > 0.78) {
goals = 3;
} else if (maxFA >= 0.60) {
goals = 2;
} else {
goals = 1; // 0.400.60
}
if (homeFav) return { homeScore: goals, awayScore: 0 };
return { homeScore: 0, awayScore: goals };
}
// Baut die simulierte Match-Liste: gespielte Spiele unverändert,
// offene Spiele mit Override oder Polymarket-Default, status = "FINISHED".
export function buildSimMatches(realMatches: Match[], overrides: SimOverrides): Match[] {
return realMatches.map((m) => {
if (m.status === "FINISHED") return m;
const override = overrides[m.id];
if (override) {
return { ...m, homeScore: override.homeScore, awayScore: override.awayScore, status: "FINISHED" as const };
}
const def = defaultScore(m);
if (def) {
return { ...m, homeScore: def.homeScore, awayScore: def.awayScore, status: "FINISHED" as const };
}
return m;
});
}
// Ermittelt die Gewinner-Seite eines aufgelösten Ties anhand der Scores.
function winningSide(tie: ResolvedTie): ResolvedSide | null {
if (tie.home.score == null || tie.away.score == null) return null;
if (tie.home.score > tie.away.score) return tie.home;
if (tie.away.score > tie.home.score) return tie.away;
return tie.home; // unentschieden → Heimseite gewinnt
}
function losingSide(tie: ResolvedTie): ResolvedSide | null {
if (tie.home.score == null || tie.away.score == null) return null;
if (tie.home.score > tie.away.score) return tie.away;
if (tie.away.score > tie.home.score) return tie.home;
return tie.away;
}
// Propagiert Gewinner aus R32 durch alle Folgerunden (R16→QF→SF→Finale).
// Füllt teamId/label/code in den späteren Runden mit den Daten des Gewinners.
export function propagateSimWinners(
r32: ResolvedTie[],
later: Record<number, ResolvedTie>,
): Record<number, ResolvedTie> {
const winners = new Map<number, { teamId: string; label: string; code: string }>();
const losers = new Map<number, { teamId: string; label: string; code: string }>();
function update(tie: ResolvedTie) {
const w = winningSide(tie);
const l = losingSide(tie);
if (w?.teamId) winners.set(tie.matchNumber, { teamId: w.teamId, label: w.label, code: w.code });
if (l?.teamId) losers.set(tie.matchNumber, { teamId: l.teamId, label: l.label, code: l.code });
}
for (const tie of r32) update(tie);
const result: Record<number, ResolvedTie> = {};
for (const km of LATER_ROUNDS) {
const tie = later[km.matchNumber];
if (!tie) continue;
const src = km.losers ? losers : winners;
const hw = src.get(km.fromHome);
const aw = src.get(km.fromAway);
const home = hw
? { ...tie.home, teamId: hw.teamId, label: hw.label, code: hw.code, isWinner: false, provisional: false }
: tie.home;
const away = aw
? { ...tie.away, teamId: aw.teamId, label: aw.label, code: aw.code, isWinner: false, provisional: false }
: tie.away;
const newTie: ResolvedTie = { ...tie, home, away };
update(newTie);
result[km.matchNumber] = newTie;
}
return result;
}