/** * The director: which match a channel shows. Pure, so it is easy to test. * * It never cuts away from a map in progress, lingers on a result long enough * for it to be seen, and otherwise goes where it is most exciting: tournament * matches first, then close series near match point, tiebreakers, and crowds. */ import { DECIDED, type LiveMatch } from '$lib/live'; import type { ChannelMode } from './channel'; export const RESULT_LINGER_MS = 30_000; export interface DirectorInput { mode: ChannelMode; matchId: string | null; tournamentId: string | null; current: string | null; /** When the current match was first seen decided. */ decidedSince: number | null; live: LiveMatch[]; now: number; } export function pickMatch(input: DirectorInput): string | null { if (input.mode === 'match') return input.matchId; const candidates = input.live.filter( (match) => input.mode === 'auto' || (input.tournamentId != null && match.tournament?.id === input.tournamentId) ); const current = candidates.find((match) => match.id === input.current); if (current) { const decided = DECIDED.includes(current.phase); if (!decided) { // Stay through a map; between maps, only leave for a much better match. const best = best_(candidates); if (current.phase === 'playing' || !best || best.id === current.id) return current.id; return interest(best) > interest(current) + 60 ? best.id : current.id; } if (input.decidedSince != null && input.now - input.decidedSince < RESULT_LINGER_MS) return current.id; } return best_(candidates)?.id ?? null; } function best_(candidates: LiveMatch[]): LiveMatch | null { return ( candidates .filter((match) => !DECIDED.includes(match.phase)) .sort((a, b) => interest(b) - interest(a) || a.id.localeCompare(b.id))[0] ?? null ); } /** How much a match is worth watching right now. */ export function interest(match: LiveMatch): number { const [a, b] = match.sides; const needed = Math.max(1, match.winsNeeded); const closeness = 1 - Math.abs(a.points - b.points) / needed; const progress = (a.points + b.points) / (2 * needed - 1); return ( (match.tournament ? 100 : 0) + (match.simulated ? 0 : 40) + Math.min(80, match.viewers * 4) + closeness * progress * 60 + (match.phase === 'tb' || match.map?.category === 'TB' ? 50 : 0) + (match.phase === 'playing' ? 15 : 0) ); }