Files
esports/src/lib/broadcast/director.ts

69 lines
2.3 KiB
TypeScript

/**
* 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)
);
}