import { useEffect, useMemo, useState } from 'react' import type { ChampionshipHub } from '../types' import { teamColor } from '../utils' import { assignPosition, defaultRound, defaultScenario, normalizeScenario, pointsForPosition, projectStandings, } from '../lib/simulator' import type { Scenario } from '../lib/simulator' const STORAGE_PREFIX = 'box-box.champ.sim' function storageKey(season: number): string { return `${STORAGE_PREFIX}.${season}` } function loadScenario(hub: ChampionshipHub): Scenario { try { const raw = window.localStorage.getItem(storageKey(hub.season)) if (!raw) return defaultScenario(hub.drivers, hub.rounds_left) return normalizeScenario(JSON.parse(raw), hub.drivers, hub.rounds_left) } catch { return defaultScenario(hub.drivers, hub.rounds_left) } } function saveScenario(season: number, scenario: Scenario) { try { window.localStorage.setItem(storageKey(season), JSON.stringify(scenario)) } catch { // storage unavailable (private mode, quota) — simulator still works in memory } } /** Label for the i-th remaining round (0-based), e.g. "R7" or "Round 7". */ function roundLabel(hub: ChampionshipHub, index: number): string { return hub.round_labels[hub.round + index] ?? `Round ${hub.round + index + 1}` } function fmtPts(n: number): string { return Number.isInteger(n) ? String(n) : n.toFixed(1) } export function ChampionshipSimulator({ hub }: { hub: ChampionshipHub }) { const [scenario, setScenario] = useState(() => loadScenario(hub)) const [selected, setSelected] = useState(0) // Reload when the season changes (new hub, new storage key). useEffect(() => { setScenario(loadScenario(hub)) setSelected(0) // eslint-disable-next-line react-hooks/exhaustive-deps }, [hub.season, hub.rounds_left]) useEffect(() => { if (scenario.length > 0) saveScenario(hub.season, scenario) }, [hub.season, scenario]) const projected = useMemo( () => projectStandings(hub.drivers, scenario, hub.rounds_left), [hub.drivers, scenario, hub.rounds_left], ) if (hub.rounds_left <= 0 || hub.drivers.length === 0 || scenario.length === 0) { return (
Season complete — nothing left to simulate.
) } const roundIdx = Math.min(selected, scenario.length - 1) const round = scenario[roundIdx] const setRound = (nextRound: (number | null)[]) => { setScenario((prev) => prev.map((r, i) => (i === roundIdx ? nextRound : r))) } const driverByNumber = new Map(hub.drivers.map((d) => [d.driver_number, d])) return (
What-if simulator — remaining rounds
{scenario.map((_, i) => ( ))}
{roundLabel(hub, roundIdx)} finishing order
{round.map((driverNumber, p) => { const driver = driverNumber != null ? driverByNumber.get(driverNumber) : undefined return (
P{p + 1} +{pointsForPosition(p + 1)}
) })}
Projected standings
{projected.map((row) => { const d = row.driver const moved = row.delta !== 0 return ( ) })}
Pos Δ Driver Now +Sim Proj Title
P{row.projectedPosition} {row.delta > 0 && ( ▲{row.delta} )} {row.delta < 0 && ( ▼{-row.delta} )} {row.delta === 0 && }
{d.name_acronym} {d.full_name}
{fmtPts(row.currentPoints)} +{fmtPts(row.simPoints)} {fmtPts(row.projectedPoints)} {row.titleAlive ? 'ALIVE' : 'OUT'}

Simplified model: every remaining round is scored as a standard Grand Prix (25-18-15-12-10-8-6-4-2-1, no fastest-lap point). Sprint weekends are ignored. Title status uses the max-points-remaining bound against the leader's projected total.

) }