import { Link } from "react-router"; import type { Route } from "./+types/admin.sports-seasons.$id.expected-values"; import { loader } from "./admin.sports-seasons.$id.expected-values.server"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import { ArrowLeft, Calculator } from "lucide-react"; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `Expected Values — ${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }]; } export { loader }; // DEFAULT scoring values — must match DEFAULT_SCORING_RULES in the simulate route. // Scoring: 1st=100, 2nd=70, 3rd/4th (FF losers)=45 each, 5th–8th (E8 losers)=20 each. // Sum = 100+70+45+45+20+20+20+20 = 340. // // Total EV invariant: Σ EV across all participants = Σ scoring values = 340, // because each probability column sums to 1.0 across all participants. // If total drifts from 340, likely causes: // 1. Stale EV records from a prior simulation run (fix: re-run simulation, which now // zeros non-bracket participants automatically) // 2. DB precision truncation (numeric(6,4) = 4dp; max drift ≈ ±1 for 68 teams) const SCORING = [100, 70, 45, 45, 20, 20, 20, 20] as const; function evFromProbs(ev: { probFirst: string; probSecond: string; probThird: string; probFourth: string; probFifth: string; probSixth: string; probSeventh: string; probEighth: string; }): number { return parseFloat(ev.probFirst) * SCORING[0] + parseFloat(ev.probSecond) * SCORING[1] + parseFloat(ev.probThird) * SCORING[2] + parseFloat(ev.probFourth) * SCORING[3] + parseFloat(ev.probFifth) * SCORING[4] + parseFloat(ev.probSixth) * SCORING[5] + parseFloat(ev.probSeventh) * SCORING[6] + parseFloat(ev.probEighth) * SCORING[7]; } function fmt(val: string | number) { return (parseFloat(val as string) * 100).toFixed(1) + "%"; } export default function ExpectedValuesPage({ loaderData }: Route.ComponentProps) { const { sportsSeason, participants, existingEVs } = loaderData; // Compute total and sort from stored prob columns, not stored expectedValue. // The simulator normalizes per-position column sums to exactly 1.0 (step 10), // so the total EV should always equal the sum of scoring values (340). // Sum only over participants shown in the table — excludes orphan EV records // from prior simulation runs for participants no longer in this season. const sorted = [...participants].toSorted((a, b) => { const evA = existingEVs.has(a.id) ? evFromProbs(existingEVs.get(a.id)!) : 0; const evB = existingEVs.has(b.id) ? evFromProbs(existingEVs.get(b.id)!) : 0; return evB - evA; }); const totalEV = sorted.reduce((sum, p) => { const ev = existingEVs.get(p.id); return ev ? sum + evFromProbs(ev) : sum; }, 0); return (
No participants found.
) : existingEVs.size === 0 ? (No EV data yet. Run a simulation from the sports season page.
) : (