69 lines
2.3 KiB
TypeScript
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)
|
|
);
|
|
}
|