From fafb9dce4b721d0390acfcc1e73143642af482a2 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sat, 27 Jun 2026 23:42:27 -0500 Subject: [PATCH] third place --- app/components/Simulation.tsx | 246 +--------------------------------- lib/third-place-security.ts | 20 ++- 2 files changed, 23 insertions(+), 243 deletions(-) diff --git a/app/components/Simulation.tsx b/app/components/Simulation.tsx index eaa820e..16f98ec 100644 --- a/app/components/Simulation.tsx +++ b/app/components/Simulation.tsx @@ -1,73 +1,16 @@ "use client"; -import { useState, useMemo, useCallback, useEffect } from "react"; +import { useMemo } from "react"; import { Match, Team } from "@/lib/types"; import { computeGroupTables, computeThirdPlaceTable } from "@/lib/standings"; import { qualifiedThirdGroups, resolveAnnexC, LATER_ROUNDS } from "@/lib/bracket"; import { resolveBracket, ResolvedTie } from "@/lib/resolve-bracket"; -import { - loadOverrides, saveOverrides, buildSimMatches, - defaultScore, SimOverrides, -} from "@/lib/simulation"; +import { buildSimMatches } from "@/lib/simulation"; import Bracket from "./Bracket"; -import ThirdPlace from "./ThirdPlace"; -import Flag from "./Flag"; - -function teamById(teams: Team[], id: string | null) { - return id ? teams.find((t) => t.id === id) : undefined; -} - -function fmtDate(iso: string): string { - const d = new Date(iso); - return d.toLocaleString("de-DE", { - weekday: "short", day: "2-digit", month: "2-digit", - hour: "2-digit", minute: "2-digit", - }); -} - -// Label für die Spielphase in der Liste -function phaseLabel(match: Match): string { - if (match.stage === "GROUP" && match.group) return `Gruppe ${match.group}`; - if (match.stage === "R32") return "Sechzehntelfinale"; - if (match.stage === "R16") return "Achtelfinale"; - if (match.stage === "QF") return "Viertelfinale"; - if (match.stage === "SF") return "Halbfinale"; - if (match.stage === "3RD") return "Spiel um Platz 3"; - if (match.stage === "FINAL") return "Finale"; - return match.stage; -} export default function Simulation({ teams, matches }: { teams: Team[]; matches: Match[] }) { - const [overrides, setOverrides] = useState({}); - - useEffect(() => { - setOverrides(loadOverrides()); - }, []); - - const handleScore = useCallback((matchId: string, homeScore: number, awayScore: number) => { - setOverrides((prev) => { - const next = { ...prev, [matchId]: { homeScore, awayScore } }; - saveOverrides(next); - return next; - }); - }, []); - - const handleReset = useCallback(() => { - setOverrides({}); - saveOverrides({}); - }, []); - - const handleClear = useCallback((matchId: string) => { - setOverrides((prev) => { - const next = { ...prev }; - delete next[matchId]; - saveOverrides(next); - return next; - }); - }, []); - - // ---- Simulations-Pipeline ---- - const simMatches = useMemo(() => buildSimMatches(matches, overrides), [matches, overrides]); + // ---- Simulations-Pipeline (Polymarket-Defaults, keine manuellen Overrides) ---- + const simMatches = useMemo(() => buildSimMatches(matches, {}), [matches]); const simTables = useMemo(() => computeGroupTables(teams, simMatches), [teams, simMatches]); const simThirds = useMemo(() => computeThirdPlaceTable(simTables), [simTables]); const simQGroups = useMemo(() => qualifiedThirdGroups(simThirds), [simThirds]); @@ -149,95 +92,14 @@ export default function Simulation({ teams, matches }: { teams: Team[]; matches: return { r32: r32Fixed, later: laterFixed }; }, [simBracket, realProvisional, realDecided]); - // ---- Eingabeliste: offene Spiele aus dem echten Feed ---- - const openMatches = useMemo( - () => - matches - .filter((m) => m.status !== "FINISHED") - .sort((a, b) => +new Date(a.utcDate) - +new Date(b.utcDate)), - [matches], - ); - - const groupPhase = openMatches.filter((m) => m.stage === "GROUP"); - const koPhase = openMatches.filter((m) => m.stage !== "GROUP"); - - // KO-Eingaben erst freigeben, wenn alle Gruppenspiele beendet sind. - const groupPhaseComplete = matches - .filter((m) => m.group != null) - .every((m) => m.status === "FINISHED"); - - const hasChanges = Object.keys(overrides).length > 0; - return (
- Simulation — Ergebnisse frei wählbar, beeinflusst nicht die echten Daten. - Alle ungespielten Spiele sind vorbelegt mit dem Polymarket-Favoriten. - Eingaben werden im Browser gespeichert. + Simulation — alle ungespielten Spiele sind mit dem Polymarket-Favoriten + vorbelegt. Die angezeigten Wahrscheinlichkeiten stammen von Polymarket.
- {/* ---- Eingabeliste ---- */} -
-
-

- Offene Spiele ({openMatches.length}) -

- {hasChanges && ( - - )} -
- - {groupPhase.length > 0 && ( -
-
- Gruppenphase -
-
- {groupPhase.map((m) => ( - - ))} -
-
- )} - - {koPhase.length > 0 && ( -
-
- K.o.-Phase -
- {groupPhaseComplete ? ( -
- {koPhase.map((m) => ( - - ))} -
- ) : ( -
- K.o.-Spiele werden zur Eingabe freigegeben, sobald die Gruppenphase abgeschlossen ist. -
- )} -
- )} -
- - {/* ---- Drittplatzierte (simuliert) ---- */}

- Drittplatzierte (simuliert) -

- - - {/* ---- Simulierter K.o.-Baum ---- */} -

Simulierter K.o.-Baum

); } - -// ---- Einzelne Eingabezeile ---- -function SimRow({ - match, teams, overrides, onScore, onClear, -}: { - match: Match; teams: Team[]; overrides: SimOverrides; onScore: (id: string, h: number, a: number) => void; - onClear: (id: string) => void; -}) { - const home = teamById(teams, match.homeTeamId); - const away = teamById(teams, match.awayTeamId); - const def = defaultScore(match); - const ov = overrides[match.id]; - const homeVal = ov?.homeScore ?? def?.homeScore; - const awayVal = ov?.awayScore ?? def?.awayScore; - const homeChanged = ov != null; - const isKO = match.stage !== "GROUP"; - - const inputStyle = (changed: boolean): React.CSSProperties => ({ - width: 36, - height: 28, - textAlign: "center", - fontFamily: "var(--font-mono)", - fontSize: 14, - fontWeight: 700, - color: changed ? "var(--turf)" : "var(--ink)", - background: "var(--bg-raised)", - border: `1px solid ${changed ? "var(--turf)" : "var(--line)"}`, - borderRadius: "var(--radius-sm)", - outline: "none", - MozAppearance: "textfield", - }); - - return ( -
- - {phaseLabel(match)} - - - - - {home?.localisedName ?? home?.name ?? "—"} - - - { - const v = parseInt(e.target.value, 10); - if (!isNaN(v) && v >= 0) { - onScore(match.id, v, awayVal ?? (isKO ? 0 : 1)); - } else if (e.target.value === "") { - // Bei Leerung: Standard wiederherstellen - const fallback = def; - if (fallback) onScore(match.id, fallback.homeScore, fallback.awayScore); - } - }} - /> - : - { - const v = parseInt(e.target.value, 10); - if (!isNaN(v) && v >= 0) { - onScore(match.id, homeVal ?? (isKO ? 0 : 1), v); - } else if (e.target.value === "") { - const fallback = def; - if (fallback) onScore(match.id, fallback.homeScore, fallback.awayScore); - else onClear(match.id); - } - }} - /> - - - {away?.localisedName ?? away?.name ?? "—"} - - - - - {fmtDate(match.utcDate)} - -
- ); -} diff --git a/lib/third-place-security.ts b/lib/third-place-security.ts index ec0be41..f44c11a 100644 --- a/lib/third-place-security.ts +++ b/lib/third-place-security.ts @@ -1,6 +1,6 @@ import { GroupId, GROUP_IDS, Match, Team } from "./types"; -import { computeGroupTables } from "./standings"; -import { resolveAnnexC } from "./bracket"; +import { computeGroupTables, computeThirdPlaceTable } from "./standings"; +import { resolveAnnexC, qualifiedThirdGroups } from "./bracket"; // Repräsentative Ergebnis-Varianten je Restspiel (decken alle Punktkombinationen ab). const OUTCOMES: Array<[number, number]> = [ @@ -174,7 +174,21 @@ export function thirdSlotIsSecure(winnerGroup: GroupId, matches: Match[], teams: const states = buildThirdStates(matches, teams); const { lockedIn, contested } = classifyGroups(states); - if (lockedIn.size > 8) return false; + if (lockedIn.size > 8) { + // Alle Gruppen fertig oder zu viele lockedIn → die 8 tatsächlich qualifizierten + // Dritten bestimmen (nicht die 12 lockedIn-Gruppen). + const tables = computeGroupTables(teams, matches); + const thirds = computeThirdPlaceTable(tables); + const qGroups = qualifiedThirdGroups(thirds); + if (qGroups.length !== 8) return false; + const assignment = resolveAnnexC(qGroups); + if (!assignment) return false; + const ag = assignment[winnerGroup]; + if (!ag) return false; + const ss = states.get(ag); + if (!ss) return false; + return ss.finished && ss.third != null && ss.possibleTeamIds.size === 1; + } if (lockedIn.size === 8) { const assignment = resolveAnnexC([...lockedIn]);