138 lines
4.8 KiB
TypeScript
138 lines
4.8 KiB
TypeScript
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) / 1:0 Heim (K.o.)
|
||
// Favorit 0.40–0.60 → 1:0
|
||
// Favorit 0.60–0.78 → 2:0
|
||
// Favorit > 0.78 → 3:0
|
||
//
|
||
// Beispiele:
|
||
// Schweiz–Kanada (0.385/0.315/0.295) → 0:0
|
||
// Schottland–Brasilien (0.10/0.17/0.75) → 0:2
|
||
// Marokko–Haiti (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 };
|
||
return { homeScore: 1, awayScore: 0 }; // K.o.: knapp Heim (kein Remis)
|
||
}
|
||
|
||
// 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.40–0.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;
|
||
}
|