Files
electric-cherry/src/lib/leaderboard-efficiency.ts
T

107 lines
4.6 KiB
TypeScript

import type { PlayerStats } from './leaderboard.js';
export const EFFICIENCY_MIN_GAMES = 5;
export const EFFICIENCY_MIN_KILLS = 100;
export const EFFICIENCY_MIN_DOWNS = 5;
export const efficiencyMetrics = [
{
key: 'headshotRate',
label: 'Headshots / kills',
format: 'percent',
description: `Total headshots divided by total kills in ended games. Ranking requires at least ${EFFICIENCY_MIN_GAMES} games and ${EFFICIENCY_MIN_KILLS} kills.`
},
{
key: 'killsPerDown',
label: 'Kills per down',
format: 'number',
description: `Total kills divided by total downs in ended games. Ranking requires at least ${EFFICIENCY_MIN_GAMES} games and ${EFFICIENCY_MIN_DOWNS} downs.`
},
{
key: 'averageKills',
label: 'Average kills',
format: 'number',
description: `Total kills divided by games played, including games the player left before they ended. Ranking requires at least ${EFFICIENCY_MIN_GAMES} ended games.`
},
{
key: 'averageRounds',
label: 'Average rounds',
format: 'number',
description: `Total observed rounds divided by games played, including games the player left before they ended. Ranking requires at least ${EFFICIENCY_MIN_GAMES} ended games.`
},
{
key: 'completionRate',
label: 'Completion rate',
format: 'percent',
description: `Games with the player in the final report divided by games played. Only ended games count, including games the player left early. Ranking requires at least ${EFFICIENCY_MIN_GAMES} games.`
}
] as const;
export type EfficiencyMetric = (typeof efficiencyMetrics)[number]['key'];
const metrics = new Set<string>(efficiencyMetrics.map((metric) => metric.key));
export function isEfficiencyMetric(metric: string): metric is EfficiencyMetric {
return metrics.has(metric);
}
function sample(stats: PlayerStats, metric: EfficiencyMetric): [number, number] {
switch (metric) {
case 'headshotRate':
return [stats.headshots, stats.kills];
case 'killsPerDown':
return [stats.kills, stats.downs];
case 'averageKills':
return [stats.kills, stats.gamesPlayed];
case 'averageRounds':
return [stats.roundsSurvived, stats.gamesPlayed];
case 'completionRate':
return [stats.gamesCompleted, stats.gamesPlayed];
}
}
/** Calculate from aggregate counters; qualification never changes a recorded value. */
export function efficiencyValue(stats: PlayerStats, metric: EfficiencyMetric): number | null {
const [numerator, denominator] = sample(stats, metric);
return denominator === 0 ? null : numerator / denominator;
}
export function efficiencyEligible(stats: PlayerStats, metric: EfficiencyMetric): boolean {
if (stats.gamesPlayed < EFFICIENCY_MIN_GAMES) return false;
if (metric === 'headshotRate') return stats.kills >= EFFICIENCY_MIN_KILLS;
if (metric === 'killsPerDown') return stats.downs >= EFFICIENCY_MIN_DOWNS;
return true;
}
export function efficiencyRequirement(metric: EfficiencyMetric): string {
const games = `At least ${EFFICIENCY_MIN_GAMES} ended games`;
if (metric === 'headshotRate') return `${games} and ${EFFICIENCY_MIN_KILLS} kills`;
if (metric === 'killsPerDown') return `${games} and ${EFFICIENCY_MIN_DOWNS} downs`;
return games;
}
const number = new Intl.NumberFormat('en-US');
const count = (value: number, singular: string, plural = `${singular}s`) =>
`${number.format(value)} ${value === 1 ? singular : plural}`;
export function efficiencySample(stats: PlayerStats, metric: EfficiencyMetric): string {
switch (metric) {
case 'headshotRate':
return `${count(stats.headshots, 'headshot')} / ${count(stats.kills, 'kill')}`;
case 'killsPerDown':
return `${count(stats.kills, 'kill')} / ${count(stats.downs, 'down')}`;
case 'averageKills':
return `${count(stats.kills, 'kill')} / ${count(stats.gamesPlayed, 'ended game')}`;
case 'averageRounds':
return `${count(stats.roundsSurvived, 'observed round')} / ${count(stats.gamesPlayed, 'ended game')}`;
case 'completionRate':
return `${count(stats.gamesCompleted, 'finish', 'finishes')} / ${count(stats.gamesPlayed, 'ended game')}`;
}
}
const decimal = new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 });
export function formatEfficiencyValue(value: number | null, metric: EfficiencyMetric): string {
if (value === null) return '—';
const percent = metric === 'headshotRate' || metric === 'completionRate';
return `${decimal.format(value * (percent ? 100 : 1))}${percent ? '%' : ''}`;
}