// Shared domain types — used by the client, the server, and the build scripts. export type Stage = 'group' | 'r32' | 'r16' | 'qf' | 'sf' | 'third' | 'final'; export type MatchStatus = 'scheduled' | 'live' | 'finished'; /** One side of a fixture: a known team, or an unresolved knockout placeholder. */ export interface TeamSlot { /** Canonical team name when known, else null (unresolved knockout slot). */ team: string | null; /** Raw openfootball placeholder: '2A' | 'W74' | 'L101' | '3A/B/C/D/F' | null. */ placeholder: string | null; /** Display label: the team name, or a pretty placeholder ("Runner-up A"). */ label: string; } export interface Fixture { /** Canonical 1..104 match number (group matches 1-72, knockout 73-104). */ num: number; stage: Stage; /** 'A'..'L' for group stage, else null. */ group: string | null; /** Human round label from the source ("Matchday 1", "Round of 32", …). */ round: string; /** 1 | 2 | 3 — the team's nth group game, else null. */ groupRound: number | null; /** Kickoff as an ISO 8601 UTC timestamp. */ kickoff: string; venue: string; home: TeamSlot; away: TeamSlot; status: MatchStatus; homeScore: number | null; awayScore: number | null; /** Live match minute when status === 'live'. */ minute: number | null; } export interface FixturesFile { season: string; generatedAt: string; groups: string[]; venues: string[]; fixtures: Fixture[]; } export interface StandingRow { team: string; played: number; won: number; drawn: number; lost: number; gf: number; ga: number; gd: number; points: number; /** 1-based rank within the group after sorting. */ rank: number; } /** What the server pushes to clients: fixtures with any live overlay + tables. */ export interface Snapshot { season: string; /** When the live layer last refreshed (ISO), or null if seed-only. */ updatedAt: string | null; /** Which live source produced the current scores. */ source: 'seed' | 'football-data' | 'sofascore'; fixtures: Fixture[]; /** Group letter → ranked standings. */ tables: Record; } // ---- model / predictions ---- /** Per-team tournament odds from the Monte Carlo simulation. */ export interface TeamOdds { team: string; winGroup: number; qualify: number; reachR16: number; reachQF: number; reachSF: number; reachFinal: number; champion: number; } export interface MatchProbs { home: number; draw: number; away: number; } /** A single per-match prediction (group + resolved knockout games). */ export interface MatchPrediction { num: number; home: string; away: string; probs: MatchProbs; lambdaHome: number; lambdaAway: number; topScore: { home: number; away: number; p: number }; } /** A point on the championship-odds-over-time chart. */ export interface OddsHistoryPoint { t: string; label: string; top: { team: string; champion: number }[]; } export interface ModelSnapshot { generatedAt: string; /** Date the underlying ratings reflect (advances as results re-rate teams). */ asOf: string; iterations: number; matches: MatchPrediction[]; odds: TeamOdds[]; history: OddsHistoryPoint[]; } // ---- data-story viz (StatsBomb open data, a fixed historical match) ---- export interface VizShot { team: string; player: string; minute: number; /** StatsBomb pitch coords: x 0–120 (attacking toward 120), y 0–80. */ x: number; y: number; xg: number; outcome: string; goal: boolean; } export interface XgPoint { minute: number; cum: number; } export interface PassNode { player: string; x: number; y: number; passes: number; } export interface PassEdge { a: string; b: string; count: number; } export interface PassNetwork { players: PassNode[]; edges: PassEdge[]; } export interface TeamTotals { xg: number; shots: number; goals: number; passes: number; passAccuracy: number; } export interface StoryViz { matchId: number; competition: string; stage: string; date: string; home: string; away: string; homeScore: number; awayScore: number; /** Final result line, e.g. "Argentina won 4–2 on penalties". */ result: string; pitch: { length: number; width: number }; shots: VizShot[]; goals: { team: string; player: string; minute: number; xg: number }[]; xgRace: { home: XgPoint[]; away: XgPoint[] }; passNetwork: { home: PassNetwork; away: PassNetwork }; totals: { home: TeamTotals; away: TeamTotals }; } // ---- WebSocket protocol ---- // The snapshot is small (~104 fixtures), so the server simply re-pushes the full // snapshot on every change — no diffing, no client-side merge races. Predictions // are heavier to compute, so they ride a separate message sent only when the set // of finished results changes. /** Server → client. */ export type ServerMessage = | { t: 'snapshot'; snapshot: Snapshot } | { t: 'predictions'; model: ModelSnapshot };