feat: redesign match play and add broadcast tooling
This commit is contained in:
68
src/lib/broadcast/director.ts
Normal file
68
src/lib/broadcast/director.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user