Add qualified efficiency leaderboard rankings

This commit is contained in:
2026-10-01 12:16:27 -04:00
parent c7d71c63ba
commit 47ebe83106
9 changed files with 385 additions and 25 deletions
+19 -1
View File
@@ -72,6 +72,21 @@ player-rounds: four players progressing from game start to round 8 contribute 28
Highest-round records always use the round reached in a completed match, Highest-round records always use the round reached in a completed match,
including the final round and players who joined mid-game. including the final round and players who joined mid-game.
Efficiency rankings are also available for **Headshots / kills**, **Kills per
down**, **Average kills**, **Average rounds**, and **Completion rate**. Each
requires at least five completed games in the selected game/map scope. Headshot
rate also requires 100 kills; kills per down requires five downs. The board shows
the numerator, denominator and game count beside each ratio. Ratios are computed
from aggregate counters, not an average of individual match percentages.
Completion rate is the share of those ended games whose final report contained
the player; it is not a win rate. Active and interrupted games do not enter any
denominator. Zero-denominator ratios remain unknown and are not ranked.
Qualification is applied before ranking, search and pagination. Summary cards
still describe all players in the game/map selection, including those below the
threshold. Profile metrics show their values and whether the player qualifies
for the combined lifetime board; a scoped board can have a smaller sample.
Round totals require the player to appear in consecutive observations. If a Round totals require the player to appear in consecutive observations. If a
report omits them, their return starts a new participation interval and the gap report omits them, their return starts a new participation interval and the gap
is not credited. The payload cannot tell whether a present player was alive, is not credited. The payload cannot tell whether a present player was alive,
@@ -124,4 +139,7 @@ identity. Both `total` and `summary.players` count unique GUIDs; only `total` is
limited by the player search. limited by the player search.
Metric keys are `highestRound`, `kills`, `roundsSurvived`, `headshots`, `revives`, Metric keys are `highestRound`, `kills`, `roundsSurvived`, `headshots`, `revives`,
`downs`, `gamesPlayed`, `gamesCompleted`, `bestKills`, and `bestScore`. `downs`, `gamesPlayed`, `gamesCompleted`, `bestKills`, `bestScore`, `headshotRate`,
`killsPerDown`, `averageKills`, `averageRounds`, and `completionRate`.
Efficiency values in entries are unrounded ratios (for example, `0.5` means 50%)
or `null` when their denominator is zero. UI percentages multiply by 100.
+76
View File
@@ -0,0 +1,76 @@
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 ? '%' : ''}`;
}
+7 -3
View File
@@ -1,4 +1,6 @@
export const leaderboardMetrics = [ import { efficiencyMetrics, type EfficiencyMetric } from './leaderboard-efficiency.js';
export const playerStatMetrics = [
{ key: 'highestRound', label: 'Highest round' }, { key: 'highestRound', label: 'Highest round' },
{ key: 'kills', label: 'Total kills' }, { key: 'kills', label: 'Total kills' },
{ key: 'roundsSurvived', label: 'Total rounds' }, { key: 'roundsSurvived', label: 'Total rounds' },
@@ -11,9 +13,11 @@ export const leaderboardMetrics = [
{ key: 'bestScore', label: 'Highest reported score' } { key: 'bestScore', label: 'Highest reported score' }
] as const; ] as const;
export const leaderboardMetrics = [...playerStatMetrics, ...efficiencyMetrics] as const;
export type LeaderboardMetric = typeof leaderboardMetrics[number]['key']; export type LeaderboardMetric = typeof leaderboardMetrics[number]['key'];
export type PlayerStatMetric = typeof playerStatMetrics[number]['key'];
export type PlayerStats = Record<LeaderboardMetric, number>; export type PlayerStats = Record<PlayerStatMetric, number>;
export type PersonalBestMetric = 'highestRound' | 'bestKills' | 'bestScore'; export type PersonalBestMetric = 'highestRound' | 'bestKills' | 'bestScore';
export interface RecordEvidence { export interface RecordEvidence {
matchId: number; matchId: number;
@@ -71,7 +75,7 @@ export function playerProfileHref(guid: string): string {
return `/leaderboard/players/${encodeURIComponent(guid)}`; return `/leaderboard/players/${encodeURIComponent(guid)}`;
} }
export interface LeaderboardEntry { export interface LeaderboardEntry extends Record<EfficiencyMetric, number | null> {
rank: number; rank: number;
guid: string; guid: string;
name: string; name: string;
+15 -2
View File
@@ -1,6 +1,8 @@
import { db } from '../db/index.js'; import { db } from '../db/index.js';
import { MIN_RECORDED_ROUND } from '../../match-history.js'; import { MIN_RECORDED_ROUND } from '../../match-history.js';
import { getPersonalBestRecords } from './leaderboard-records.js'; import { getPersonalBestRecords } from './leaderboard-records.js';
import { EFFICIENCY_MIN_GAMES, EFFICIENCY_MIN_KILLS, EFFICIENCY_MIN_DOWNS,
isEfficiencyMetric } from '../../leaderboard-efficiency.js';
import { leaderboardMetrics, type LeaderboardEntry, type LeaderboardMap, type LeaderboardMetric, import { leaderboardMetrics, type LeaderboardEntry, type LeaderboardMap, type LeaderboardMetric,
type LeaderboardOptions, type LeaderboardResponse } from '../../leaderboard.js'; type LeaderboardOptions, type LeaderboardResponse } from '../../leaderboard.js';
@@ -25,6 +27,11 @@ export function getLeaderboard(options: LeaderboardOptions = {}): LeaderboardRes
const scoped = `FROM leaderboard_match_players mp const scoped = `FROM leaderboard_match_players mp
JOIN leaderboard_matches m ON m.id = mp.match_id JOIN leaderboard_matches m ON m.id = mp.match_id
WHERE ${conditions.join(' AND ')}`; WHERE ${conditions.join(' AND ')}`;
const qualification = isEfficiencyMetric(metric)
? `WHERE measures.gamesPlayed >= ${EFFICIENCY_MIN_GAMES} AND measures.${metric} IS NOT NULL`
+ (metric === 'headshotRate' ? ` AND measures.kills >= ${EFFICIENCY_MIN_KILLS}` : '')
+ (metric === 'killsPerDown' ? ` AND measures.downs >= ${EFFICIENCY_MIN_DOWNS}` : '')
: '';
const aggregated = `WITH totals AS ( const aggregated = `WITH totals AS (
SELECT mp.guid, MAX(mp.highest_round) AS highestRound, SELECT mp.guid, MAX(mp.highest_round) AS highestRound,
SUM(mp.kills) AS kills, SUM(mp.rounds_survived) AS roundsSurvived, SUM(mp.kills) AS kills, SUM(mp.rounds_survived) AS roundsSurvived,
@@ -32,12 +39,18 @@ export function getLeaderboard(options: LeaderboardOptions = {}): LeaderboardRes
COUNT(*) AS gamesPlayed, SUM(mp.completed) AS gamesCompleted, COUNT(*) AS gamesPlayed, SUM(mp.completed) AS gamesCompleted,
MAX(mp.kills) AS bestKills, MAX(mp.best_score) AS bestScore, MAX(mp.last_seen_at) AS lastSeen MAX(mp.kills) AS bestKills, MAX(mp.best_score) AS bestScore, MAX(mp.last_seen_at) AS lastSeen
${scoped} GROUP BY mp.guid ${scoped} GROUP BY mp.guid
), measures AS (
SELECT totals.*, 1.0 * headshots / NULLIF(kills, 0) AS headshotRate,
1.0 * kills / NULLIF(downs, 0) AS killsPerDown,
1.0 * kills / NULLIF(gamesPlayed, 0) AS averageKills,
1.0 * roundsSurvived / NULLIF(gamesPlayed, 0) AS averageRounds,
1.0 * gamesCompleted / NULLIF(gamesPlayed, 0) AS completionRate FROM totals
), identities AS ( ), identities AS (
SELECT guid, name, ROW_NUMBER() OVER (PARTITION BY guid ORDER BY last_seen_at DESC, game) AS recency SELECT guid, name, ROW_NUMBER() OVER (PARTITION BY guid ORDER BY last_seen_at DESC, game) AS recency
FROM leaderboard_players WHERE game IN ('t5', 't6') FROM leaderboard_players WHERE game IN ('t5', 't6')
), ranked AS ( ), ranked AS (
SELECT totals.*, p.name, RANK() OVER (ORDER BY totals.${metric} DESC) AS rank SELECT measures.*, p.name, RANK() OVER (ORDER BY measures.${metric} DESC) AS rank
FROM totals JOIN identities p ON p.guid = totals.guid AND p.recency = 1 FROM measures JOIN identities p ON p.guid = measures.guid AND p.recency = 1 ${qualification}
)`; )`;
const query = options.q?.trim() ?? ''; const query = options.q?.trim() ?? '';
const search = query ? 'WHERE instr(lower(name), lower(?)) > 0 OR instr(lower(guid), lower(?)) > 0' : ''; const search = query ? 'WHERE instr(lower(name), lower(?)) > 0 OR instr(lower(guid), lower(?)) > 0' : '';
+17 -7
View File
@@ -3,7 +3,8 @@
import { onMount, untrack } from 'svelte'; import { onMount, untrack } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { leaderboardMetrics, playerProfileHref, type LeaderboardMetric, type LeaderboardMap, type LeaderboardResponse } from '$lib/leaderboard.js'; import { leaderboardMetrics, playerProfileHref, type LeaderboardMetric, type PlayerStatMetric, type LeaderboardEntry, type LeaderboardMap, type LeaderboardResponse } from '$lib/leaderboard.js';
import { efficiencyMetrics, isEfficiencyMetric, efficiencyRequirement, efficiencySample, formatEfficiencyValue } from '$lib/leaderboard-efficiency.js';
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte'; import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
import Button from '$ui/components/primitives/Button.svelte'; import Button from '$ui/components/primitives/Button.svelte';
import EmptyState from '$ui/components/primitives/EmptyState.svelte'; import EmptyState from '$ui/components/primitives/EmptyState.svelte';
@@ -13,8 +14,8 @@
const number = new Intl.NumberFormat('en-US'); const number = new Intl.NumberFormat('en-US');
const pageSize = 50; const pageSize = 50;
const games = [{ key: 't5', label: 'Black Ops I' }, { key: 't6', label: 'Black Ops II' }]; const games = [{ key: 't5', label: 'Black Ops I' }, { key: 't6', label: 'Black Ops II' }];
const defaultColumns: LeaderboardMetric[] = ['highestRound', 'kills', 'roundsSurvived', 'headshots', 'revives', 'downs', 'gamesPlayed']; const defaultColumns: PlayerStatMetric[] = ['highestRound', 'kills', 'roundsSurvived', 'headshots', 'revives', 'downs', 'gamesPlayed'];
const descriptions: Record<LeaderboardMetric, string> = { const descriptions: Record<PlayerStatMetric, string> = {
highestRound: 'The highest round reached in a recorded game.', highestRound: 'The highest round reached in a recorded game.',
kills: 'Cumulative kills across recorded games.', kills: 'Cumulative kills across recorded games.',
roundsSurvived: 'Round progression during observed participation, added across games.', roundsSurvived: 'Round progression during observed participation, added across games.',
@@ -26,6 +27,10 @@
bestKills: 'The most kills reported for a player in a single game.', bestKills: 'The most kills reported for a player in a single game.',
bestScore: 'The highest reported points balance. Spending points reduces this balance; it is not total points earned.' bestScore: 'The highest reported points balance. Spending points reduces this balance; it is not total points earned.'
}; };
const description = (key: LeaderboardMetric) => isEfficiencyMetric(key)
? efficiencyMetrics.find(item => item.key === key)!.description : descriptions[key];
const formatValue = (entry: LeaderboardEntry, key: LeaderboardMetric) => isEfficiencyMetric(key)
? formatEfficiencyValue(entry[key], key) : number.format(entry[key]);
let data = $state<LeaderboardResponse | null>(null); let data = $state<LeaderboardResponse | null>(null);
let maps = $state<LeaderboardMap[]>([]); let maps = $state<LeaderboardMap[]>([]);
@@ -187,7 +192,7 @@
<section class="rankings" class:compact={!fullTable} aria-labelledby="rankings-title" aria-busy={loading}> <section class="rankings" class:compact={!fullTable} aria-labelledby="rankings-title" aria-busy={loading}>
<div class="rankings-header"> <div class="rankings-header">
<div><h2 id="rankings-title">{metricLabel}</h2><p>{descriptions[metric]}</p></div> <div><h2 id="rankings-title">{metricLabel}</h2><p>{description(metric)}</p>{#if isEfficiencyMetric(metric)}<p>Qualification is measured within the selected game and map. Fleet totals include all recorded players.</p>{/if}</div>
<div class="refresh-status" role="status">{loading ? 'Refreshing…' : checkedAt ? `Checked ${$timeFormat.time(checkedAt)}` : ''}<span>Refreshes every 15 seconds</span></div> <div class="refresh-status" role="status">{loading ? 'Refreshing…' : checkedAt ? `Checked ${$timeFormat.time(checkedAt)}` : ''}<span>Refreshes every 15 seconds</span></div>
</div> </div>
{#if data.entries.length} {#if data.entries.length}
@@ -201,7 +206,7 @@
<th scope="col" class="player">Player</th> <th scope="col" class="player">Player</th>
{#each columns as key (key)} {#each columns as key (key)}
<th scope="col" class="stat" class:selected={key === metric} aria-sort={key === metric ? 'descending' : 'none'}> <th scope="col" class="stat" class:selected={key === metric} aria-sort={key === metric ? 'descending' : 'none'}>
<button onclick={() => updateFilters({ metric: key })} title={descriptions[key]}>{leaderboardMetrics.find(item => item.key === key)!.label}{#if key === metric}<span aria-hidden="true"> ↓</span>{/if}</button> <button onclick={() => updateFilters({ metric: key })} title={description(key)}>{leaderboardMetrics.find(item => item.key === key)!.label}{#if key === metric}<span aria-hidden="true"> ↓</span>{/if}</button>
</th> </th>
{/each} {/each}
</tr></thead> </tr></thead>
@@ -213,16 +218,20 @@
<a class="player-name" href={playerProfileHref(entry.guid)}>{entry.name}</a> <a class="player-name" href={playerProfileHref(entry.guid)}>{entry.name}</a>
<div class="identity"><CopyValue value={entry.guid} label={`GUID for ${entry.name}`} /></div> <div class="identity"><CopyValue value={entry.guid} label={`GUID for ${entry.name}`} /></div>
</th> </th>
{#each columns as key (key)}<td class="stat" class:selected={key === metric}>{number.format(entry[key])}{#if key === 'highestRound' || key === 'bestKills' || key === 'bestScore'}<RecordSource evidence={entry.records[key]} />{/if}</td>{/each} {#each columns as key (key)}<td class="stat" class:selected={key === metric}>{formatValue(entry, key)}{#if isEfficiencyMetric(key)}<small class="sample">{efficiencySample(entry, key)} · {number.format(entry.gamesPlayed)} games</small>{/if}{#if key === 'highestRound' || key === 'bestKills' || key === 'bestScore'}<RecordSource evidence={entry.records[key]} />{/if}</td>{/each}
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="pagination"> <div class="pagination">
<span>{number.format(data.offset + 1)}–{number.format(data.offset + data.entries.length)} of {number.format(data.total)} {data.total === 1 ? 'player' : 'players'}{query ? ` matching “${query}”` : ''}</span> <span>{number.format(data.offset + 1)}–{number.format(data.offset + data.entries.length)} of {number.format(data.total)} {isEfficiencyMetric(metric) ? 'qualifying ' : ''}{data.total === 1 ? 'player' : 'players'}{query ? ` matching “${query}”` : ''}</span>
<div><Button size="sm" disabled={data.offset === 0 || loading} onclick={() => updateFilters({ offset: String(Math.max(0, offset - pageSize)) })}>Previous</Button><Button size="sm" disabled={data.offset + data.entries.length >= data.total || loading} onclick={() => updateFilters({ offset: String(offset + pageSize) })}>Next</Button></div> <div><Button size="sm" disabled={data.offset === 0 || loading} onclick={() => updateFilters({ offset: String(Math.max(0, offset - pageSize)) })}>Previous</Button><Button size="sm" disabled={data.offset + data.entries.length >= data.total || loading} onclick={() => updateFilters({ offset: String(offset + pageSize) })}>Next</Button></div>
</div> </div>
{:else if isEfficiencyMetric(metric)}
<EmptyState spacious title="No qualifying players found" description={`${efficiencyRequirement(metric)} in this game/map selection. Try another scope or player search.`}>
{#snippet actions()}<Button size="sm" onclick={() => updateFilters({ metric: 'highestRound' })}>Rank by highest round</Button>{/snippet}
</EmptyState>
{:else if query || game || map || offset} {:else if query || game || map || offset}
<EmptyState spacious title="No players found" description="Try a different game, map, or player name."> <EmptyState spacious title="No players found" description="Try a different game, map, or player name.">
{#snippet actions()}<Button size="sm" onclick={clearFilters}>Show all players</Button>{/snippet} {#snippet actions()}<Button size="sm" onclick={clearFilters}>Show all players</Button>{/snippet}
@@ -292,6 +301,7 @@
.stat { text-align: right; white-space: nowrap; } .stat { text-align: right; white-space: nowrap; }
td.stat { font-family: var(--font-mono); font-variant-numeric: tabular-nums; } td.stat { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.stat.selected { color: var(--accent-primary-hover); } .stat.selected { color: var(--accent-primary-hover); }
.sample { display: block; max-width: 22rem; margin-top: var(--space-2); white-space: normal; font-family: var(--font-body); font-size: var(--text-xs); font-weight: normal; color: var(--text-secondary); }
td.selected { font-weight: var(--weight-semibold); } td.selected { font-weight: var(--weight-semibold); }
th button { display: inline-flex; gap: 6px; align-items: center; background: transparent; border: 0; padding: 3px 0; cursor: pointer; color: inherit; white-space: nowrap; } th button { display: inline-flex; gap: 6px; align-items: center; background: transparent; border: 0; padding: 3px 0; cursor: pointer; color: inherit; white-space: nowrap; }
th button:hover { color: var(--accent-primary-hover); } th button:hover { color: var(--accent-primary-hover); }
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import { timeFormat } from '$ui/stores/time-zone.js'; import { timeFormat } from '$ui/stores/time-zone.js';
import { invalidateAll } from '$app/navigation'; import { invalidateAll } from '$app/navigation';
import { leaderboardMetrics, type PlayerProfile, type LeaderboardMetric } from '$lib/leaderboard.js'; import { playerStatMetrics, type PlayerProfile, type PlayerStatMetric } from '$lib/leaderboard.js';
import { efficiencyMetrics, efficiencyValue, efficiencyEligible, efficiencyRequirement, efficiencySample, formatEfficiencyValue } from '$lib/leaderboard-efficiency.js';
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte'; import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
import Button from '$ui/components/primitives/Button.svelte'; import Button from '$ui/components/primitives/Button.svelte';
import CopyValue from '$ui/components/CopyValue.svelte'; import CopyValue from '$ui/components/CopyValue.svelte';
@@ -14,8 +15,7 @@
const number = new Intl.NumberFormat('en-US', { maximumFractionDigits: 1 }); const number = new Intl.NumberFormat('en-US', { maximumFractionDigits: 1 });
const date = (value: string) => $timeFormat.dateTime(value); const date = (value: string) => $timeFormat.dateTime(value);
const gameName = (game: string) => game === 't5' ? 'Black Ops I' : game === 't6' ? 'Black Ops II' : game.toUpperCase(); const gameName = (game: string) => game === 't5' ? 'Black Ops I' : game === 't6' ? 'Black Ops II' : game.toUpperCase();
const ratio = (value: number, total: number, percent = false) => total ? `${number.format(value / total * (percent ? 100 : 1))}${percent ? '%' : ''}` : '—'; const historyColumns = ['highestRound', 'kills', 'roundsSurvived', 'headshots', 'revives', 'downs', 'bestScore'] as const satisfies readonly PlayerStatMetric[];
const historyColumns = ['highestRound', 'kills', 'roundsSurvived', 'headshots', 'revives', 'downs', 'bestScore'] as const satisfies readonly LeaderboardMetric[];
let refreshing = $state(false); let refreshing = $state(false);
let refreshError = $state(''); let refreshError = $state('');
async function refresh() { async function refresh() {
@@ -40,7 +40,7 @@
<section aria-labelledby="totals-title"> <section aria-labelledby="totals-title">
<h2 id="totals-title">Lifetime stats</h2> <h2 id="totals-title">Lifetime stats</h2>
<div class="stats"> <div class="stats">
{#each leaderboardMetrics as metric (metric.key)} {#each playerStatMetrics as metric (metric.key)}
<div class="stat" class:record={metric.key === 'highestRound'}><span>{metric.label}</span><strong>{number.format(stats[metric.key])}</strong>{#if metric.key === 'highestRound' || metric.key === 'bestKills' || metric.key === 'bestScore'}<RecordSource evidence={profile.records[metric.key]} />{/if}</div> <div class="stat" class:record={metric.key === 'highestRound'}><span>{metric.label}</span><strong>{number.format(stats[metric.key])}</strong>{#if metric.key === 'highestRound' || metric.key === 'bestKills' || metric.key === 'bestScore'}<RecordSource evidence={profile.records[metric.key]} />{/if}</div>
{/each} {/each}
</div> </div>
@@ -49,11 +49,9 @@
<section aria-labelledby="metrics-title"> <section aria-labelledby="metrics-title">
<h2 id="metrics-title">Player metrics</h2> <h2 id="metrics-title">Player metrics</h2>
<div class="stats metrics"> <div class="stats metrics">
<div class="stat"><span>Headshots / kills</span><strong>{ratio(stats.headshots, stats.kills, true)}</strong></div> {#each efficiencyMetrics as metric (metric.key)}
<div class="stat"><span>Kills per down</span><strong>{ratio(stats.kills, stats.downs)}</strong></div> <div class="stat"><span>{metric.label}</span><strong>{formatEfficiencyValue(efficiencyValue(stats, metric.key), metric.key)}</strong><small>{efficiencySample(stats, metric.key)}</small><small>{efficiencyEligible(stats, metric.key) ? 'Qualifies for lifetime ranking' : `${efficiencyRequirement(metric.key)} to rank`}</small></div>
<div class="stat"><span>Average kills per game</span><strong>{ratio(stats.kills, stats.gamesPlayed)}</strong></div> {/each}
<div class="stat"><span>Average rounds per game</span><strong>{ratio(stats.roundsSurvived, stats.gamesPlayed)}</strong></div>
<div class="stat"><span>Completion rate</span><strong>{ratio(stats.gamesCompleted, stats.gamesPlayed, true)}</strong></div>
</div> </div>
<p class="note">Only completed games contribute. Completion rate measures how often the player appeared in the final report. A dash means there is no denominator for that metric.</p> <p class="note">Only completed games contribute. Completion rate measures how often the player appeared in the final report. A dash means there is no denominator for that metric.</p>
</section> </section>
@@ -63,10 +61,10 @@
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Scrollable table for keyboard users.) --> <!-- svelte-ignore a11y_no_noninteractive_tabindex (Scrollable table for keyboard users.) -->
<div class="table-scroll" role="region" aria-label="Per-map player records" tabindex="0"> <div class="table-scroll" role="region" aria-label="Per-map player records" tabindex="0">
<table> <table>
<thead><tr><th scope="col">Map</th>{#each leaderboardMetrics as metric}<th scope="col">{metric.label}</th>{/each}</tr></thead> <thead><tr><th scope="col">Map</th>{#each playerStatMetrics as metric}<th scope="col">{metric.label}</th>{/each}</tr></thead>
<tbody>{#each profile.maps as map (`${map.game}:${map.key}`)} <tbody>{#each profile.maps as map (`${map.game}:${map.key}`)}
<tr><th scope="row"><a href={`/leaderboard?${new URLSearchParams({ game: map.game, map: map.key })}`}>{map.name}</a><small>{gameName(map.game)}</small><small>Last played {date(map.lastSeen)}</small></th> <tr><th scope="row"><a href={`/leaderboard?${new URLSearchParams({ game: map.game, map: map.key })}`}>{map.name}</a><small>{gameName(map.game)}</small><small>Last played {date(map.lastSeen)}</small></th>
{#each leaderboardMetrics as metric}<td class:best={metric.key === 'highestRound'}>{number.format(map[metric.key])}{#if metric.key === 'highestRound' || metric.key === 'bestKills' || metric.key === 'bestScore'}<RecordSource evidence={map.records[metric.key]} />{/if}</td>{/each}</tr> {#each playerStatMetrics as metric}<td class:best={metric.key === 'highestRound'}>{number.format(map[metric.key])}{#if metric.key === 'highestRound' || metric.key === 'bestKills' || metric.key === 'bestScore'}<RecordSource evidence={map.records[metric.key]} />{/if}</td>{/each}</tr>
{/each}</tbody> {/each}</tbody>
</table> </table>
</div> </div>
@@ -77,7 +75,7 @@
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Scrollable table for keyboard users.) --> <!-- svelte-ignore a11y_no_noninteractive_tabindex (Scrollable table for keyboard users.) -->
<div class="table-scroll" role="region" aria-label="Player match history" tabindex="0"> <div class="table-scroll" role="region" aria-label="Player match history" tabindex="0">
<table> <table>
<thead><tr><th scope="col">Map / server</th><th scope="col">Match started</th><th scope="col">Match status</th>{#each historyColumns as key}<th scope="col">{leaderboardMetrics.find(metric => metric.key === key)!.label}</th>{/each}</tr></thead> <thead><tr><th scope="col">Map / server</th><th scope="col">Match started</th><th scope="col">Match status</th>{#each historyColumns as key}<th scope="col">{playerStatMetrics.find(metric => metric.key === key)!.label}</th>{/each}</tr></thead>
<tbody>{#each profile.matches as match (match.id)} <tbody>{#each profile.matches as match (match.id)}
<tr><th scope="row">{#if match.status === 'completed'}<a href={`/matches/${match.id}`} aria-label={`View ${match.mapName} match from ${date(match.startedAt)}`}>{match.mapName}</a>{:else}{match.mapName}{/if}<small>{gameName(match.game)}</small><small>{match.server} {#if match.server_deleted_at}<Badge>Deleted</Badge>{/if}</small></th><td>{date(match.startedAt)}</td> <tr><th scope="row">{#if match.status === 'completed'}<a href={`/matches/${match.id}`} aria-label={`View ${match.mapName} match from ${date(match.startedAt)}`}>{match.mapName}</a>{:else}{match.mapName}{/if}<small>{gameName(match.game)}</small><small>{match.server} {#if match.server_deleted_at}<Badge>Deleted</Badge>{/if}</small></th><td>{date(match.startedAt)}</td>
<td class="status">{match.status}<small>{match.completed ? 'Completion credited' : 'No completion credit'}</small></td> <td class="status">{match.status}<small>{match.completed ? 'Completion credited' : 'No completion credit'}</small></td>
@@ -0,0 +1,143 @@
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import type Database from 'better-sqlite3';
import type { LeaderboardMetric } from '../../src/lib/leaderboard.js';
import { newFixture, type Fixture } from '../helpers/fixture.js';
let fixture: Fixture;
beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); });
afterEach(async () => fixture.cleanup());
interface Sample {
kills?: number; headshots?: number; downs?: number; rounds?: number; completed?: number;
game?: string; map?: string; status?: 'active' | 'completed' | 'interrupted';
}
function addSamples(handle: Database.Database, guid: string, samples: Sample[]) {
for (const sample of samples) {
const { kills = 20, headshots = 10, downs = 1, rounds = 2, completed = 1,
game = 't5', map = 'theater', status = 'completed' } = sample;
const inserted = handle.prepare(`INSERT INTO leaderboard_matches
(server, game, map, map_key, map_name, started_at, ended_at, last_received_at, status,
highest_round, last_event, last_fingerprint)
VALUES ('removed-server', ?, ?, ?, ?, '2026-09-01', '2026-09-02', '2026-09-02', ?, 20, 'game_end', 'test')`)
.run(game, map, `${game}:${map}`, map, status);
handle.prepare(`INSERT OR IGNORE INTO leaderboard_players (game, guid, name, first_seen_at, last_seen_at)
VALUES (?, ?, ?, '2026-09-01', '2026-09-02')`).run(game, guid, guid);
handle.prepare(`INSERT INTO leaderboard_match_players (match_id, guid, highest_round, kills, headshots,
downs, rounds_survived, completed, has_stats, first_seen_at, last_seen_at)
VALUES (?, ?, 20, ?, ?, ?, ?, ?, 1, '2026-09-01', '2026-09-02')`)
.run(inserted.lastInsertRowid, guid, kills, headshots, downs, rounds, completed);
}
}
const samples = (count: number, sample: Sample = {}) => Array.from({ length: count }, () => ({ ...sample }));
it('calculates every efficiency measure from aggregate counts and preserves raw ratios', async () => {
const { db } = await import('../../src/lib/server/db/index.js');
addSamples(db(), 'weighted', [
{ kills: 1, headshots: 1, downs: 1, rounds: 1, completed: 0 },
{ kills: 99, headshots: 9, downs: 9, rounds: 9 },
...samples(3, { kills: 0, headshots: 0, downs: 0, rounds: 0, completed: 0 })
]);
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
const entry = getLeaderboard().entries[0];
expect(entry).toMatchObject({ kills: 100, headshots: 10, downs: 10, roundsSurvived: 10,
gamesPlayed: 5, gamesCompleted: 1, headshotRate: 0.1, killsPerDown: 10,
averageKills: 20, averageRounds: 2, completionRate: 0.2 });
const { efficiencyMetrics, efficiencyValue } = await import('../../src/lib/leaderboard-efficiency.js');
for (const { key } of efficiencyMetrics) {
expect(entry[key]).toBe(efficiencyValue(entry, key));
expect(getLeaderboard({ metric: key }).entries).toEqual([entry]);
}
});
it('qualifies at five games, one hundred kills and five downs, with each metric enforcing its own sample', async () => {
const { db } = await import('../../src/lib/server/db/index.js');
addSamples(db(), 'exact', samples(5));
addSamples(db(), 'four-games', samples(4, { kills: 1000, headshots: 1000, downs: 5 }));
addSamples(db(), 'ninety-nine-kills', [{ kills: 19 }, ...samples(4)]);
addSamples(db(), 'four-downs', [{ downs: 0 }, ...samples(4)]);
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
const ids = (metric: LeaderboardMetric) =>
getLeaderboard({ metric }).entries.map(entry => entry.guid).sort();
expect(ids('headshotRate')).toEqual(['exact', 'four-downs']);
expect(ids('killsPerDown')).toEqual(['exact', 'ninety-nine-kills']);
for (const metric of ['averageKills', 'averageRounds', 'completionRate'] as const) {
expect(ids(metric)).toEqual(['exact', 'four-downs', 'ninety-nine-kills']);
}
expect(getLeaderboard({ metric: 'kills' }).total).toBe(4);
expect(getLeaderboard({ metric: 'headshotRate' }).summary).toEqual(getLeaderboard().summary);
});
it('keeps undefined ratios null and unranked while ranking legitimate zero efficiencies', async () => {
const { db } = await import('../../src/lib/server/db/index.js');
addSamples(db(), 'zero', samples(5, { kills: 0, headshots: 0, downs: 0, rounds: 0, completed: 0 }));
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
expect(getLeaderboard().entries[0]).toMatchObject({ headshotRate: null, killsPerDown: null,
averageKills: 0, averageRounds: 0, completionRate: 0 });
for (const metric of ['headshotRate', 'killsPerDown'] as const) {
expect(getLeaderboard({ metric })).toMatchObject({ entries: [], total: 0,
summary: { players: 1, matches: 5, kills: 0 } });
}
for (const metric of ['averageKills', 'averageRounds', 'completionRate'] as const) {
expect(getLeaderboard({ metric })).toMatchObject({ total: 1, entries: [{ guid: 'zero', rank: 1 }] });
}
addSamples(db(), 'no-kills', samples(5, { kills: 0, headshots: 0, downs: 1 }));
expect(getLeaderboard({ metric: 'killsPerDown' }).entries).toEqual([
expect.objectContaining({ guid: 'no-kills', killsPerDown: 0, rank: 1 })
]);
});
it('recomputes qualification within selected games and maps using completed matches only', async () => {
const { db } = await import('../../src/lib/server/db/index.js');
addSamples(db(), 'split', [...samples(3), ...samples(2, { game: 't6', map: 'town' })]);
addSamples(db(), 'unfinished', [...samples(4), { status: 'active', kills: 10000 },
{ status: 'interrupted', kills: 10000 }]);
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
expect(getLeaderboard({ metric: 'averageKills' })).toMatchObject({ total: 1,
entries: [{ guid: 'split', gamesPlayed: 5, averageKills: 20 }], summary: { players: 2, matches: 9, kills: 180 } });
expect(getLeaderboard({ metric: 'averageKills', game: 't5' })).toMatchObject({ total: 0, entries: [],
summary: { players: 2, matches: 7, kills: 140 } });
expect(getLeaderboard({ metric: 'averageKills', map: 't6:town' })).toMatchObject({ total: 0, entries: [],
summary: { players: 1, matches: 2, kills: 40 } });
addSamples(db(), 'split', samples(2));
expect(getLeaderboard({ metric: 'averageKills', game: 't5', map: 't5:theater' }).entries).toEqual([
expect.objectContaining({ guid: 'split', gamesPlayed: 5, rank: 1 })
]);
});
it('qualifies before competition ranking, then searches and paginates without changing scope summaries', async () => {
const { db } = await import('../../src/lib/server/db/index.js');
const handle = db();
addSamples(handle, 'Ace', samples(5, { kills: 50 }));
addSamples(handle, 'Blake', samples(5, { kills: 50 }));
addSamples(handle, 'Casey', samples(5, { kills: 10 }));
addSamples(handle, 'Small sample', samples(4, { kills: 1000 }));
handle.prepare(`INSERT INTO leaderboard_players (game, guid, name, first_seen_at, last_seen_at)
VALUES ('t6', 'Casey', 'Current name', '2026-09-01', '2026-09-03')`).run();
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
const full = getLeaderboard({ metric: 'averageKills', game: 't5' });
expect(full.entries.map(entry => [entry.name, entry.rank])).toEqual([['Ace', 1], ['Blake', 1], ['Current name', 3]]);
const searched = getLeaderboard({ metric: 'averageKills', game: 't5', q: 'current NAME', limit: 1 });
expect(searched).toMatchObject({ total: 1, entries: [{ guid: 'Casey', rank: 3 }], summary: full.summary });
expect(getLeaderboard({ metric: 'averageKills', game: 't5', q: 'Small' })).toMatchObject({ total: 0, entries: [], summary: full.summary });
const page = getLeaderboard({ metric: 'averageKills', game: 't5', limit: 1, offset: 1 });
expect(page).toMatchObject({ total: 3, entries: [{ guid: 'Blake', rank: 1 }], summary: full.summary });
expect(getLeaderboard({ metric: 'averageKills', offset: 3 })).toMatchObject({ total: 3, entries: [], summary: full.summary });
const { GET } = await import('../../src/routes/api/leaderboard/+server.js');
const response = GET({ url: new URL('http://localhost/api/leaderboard?metric=averageKills&game=t5&q=current&limit=1') });
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({ metric: 'averageKills', total: 1,
entries: [{ guid: 'Casey', rank: 3, averageKills: 10, gamesPlayed: 5 }], summary: full.summary });
});
it('accepts each efficiency metric through the API when the qualified group is empty', async () => {
const { db } = await import('../../src/lib/server/db/index.js');
addSamples(db(), 'small', samples(4));
const { efficiencyMetrics } = await import('../../src/lib/leaderboard-efficiency.js');
const { GET } = await import('../../src/routes/api/leaderboard/+server.js');
for (const { key } of efficiencyMetrics) {
const response = GET({ url: new URL(`http://localhost/api/leaderboard?metric=${key}`) });
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({ metric: key, entries: [], total: 0,
summary: { players: 1, matches: 4, kills: 80 } });
}
});
+92
View File
@@ -0,0 +1,92 @@
import { describe, expect, it } from 'vitest';
import type { PlayerStats } from '../../src/lib/leaderboard.js';
import {
EFFICIENCY_MIN_DOWNS, EFFICIENCY_MIN_GAMES, EFFICIENCY_MIN_KILLS,
efficiencyEligible, efficiencyMetrics, efficiencyRequirement, efficiencySample, efficiencyValue,
isEfficiencyMetric, type EfficiencyMetric
} from '../../src/lib/leaderboard-efficiency.js';
const stats = (overrides: Partial<PlayerStats> = {}): PlayerStats => ({
highestRound: 20, kills: 100, roundsSurvived: 47, headshots: 31, revives: 7, downs: 5,
gamesPlayed: 5, gamesCompleted: 3, bestKills: 50, bestScore: 12000, ...overrides
});
describe('efficiency ranking qualification', () => {
it.each(efficiencyMetrics)('requires five ended games for $key even when other counters are large', ({ key }) => {
expect(efficiencyEligible(stats({ gamesPlayed: EFFICIENCY_MIN_GAMES - 1, kills: 10000, downs: 100 }), key)).toBe(false);
expect(efficiencyEligible(stats({ gamesPlayed: EFFICIENCY_MIN_GAMES }), key)).toBe(true);
});
it('requires enough kills for headshot rate and enough downs for kills per down', () => {
expect(efficiencyEligible(stats({ kills: EFFICIENCY_MIN_KILLS - 1 }), 'headshotRate')).toBe(false);
expect(efficiencyEligible(stats({ kills: EFFICIENCY_MIN_KILLS }), 'headshotRate')).toBe(true);
expect(efficiencyEligible(stats({ downs: EFFICIENCY_MIN_DOWNS - 1 }), 'killsPerDown')).toBe(false);
expect(efficiencyEligible(stats({ downs: EFFICIENCY_MIN_DOWNS }), 'killsPerDown')).toBe(true);
expect(efficiencyEligible(stats({ kills: 0 }), 'killsPerDown')).toBe(true);
});
it('counts departed players in ended-game samples and applies no extra counter minimum to game averages', () => {
const departed = stats({ kills: 0, headshots: 0, downs: 0, roundsSurvived: 0, gamesCompleted: 0 });
for (const metric of ['averageKills', 'averageRounds', 'completionRate'] as const) {
expect(efficiencyEligible(departed, metric)).toBe(true);
expect(efficiencyValue(departed, metric)).toBe(0);
}
expect(efficiencyValue(stats({ gamesPlayed: 5, gamesCompleted: 3 }), 'completionRate')).toBe(3 / 5);
});
});
describe('efficiency values', () => {
it.each([
['headshotRate', { kills: 0 }], ['killsPerDown', { downs: 0 }],
['averageKills', { gamesPlayed: 0 }], ['averageRounds', { gamesPlayed: 0 }],
['completionRate', { gamesPlayed: 0 }]
] as Array<[EfficiencyMetric, Partial<PlayerStats>]>)('leaves %s undefined with no denominator', (metric, zeroDenominator) => {
expect(efficiencyValue(stats(zeroDenominator), metric)).toBeNull();
});
it('uses ratios of totals instead of equally weighting games with different sample sizes', () => {
// One game has 1/1 headshots and 1 kill/down; another has 9/99 and 99/9.
const aggregate = stats({ gamesPlayed: 2, headshots: 1 + 9, kills: 1 + 99, downs: 1 + 9 });
expect(efficiencyValue(aggregate, 'headshotRate')).toBe((1 + 9) / (1 + 99));
expect(efficiencyValue(aggregate, 'headshotRate')).not.toBe((1 / 1 + 9 / 99) / 2);
expect(efficiencyValue(aggregate, 'killsPerDown')).toBe((1 + 99) / (1 + 9));
expect(efficiencyValue(aggregate, 'killsPerDown')).not.toBe((1 / 1 + 99 / 9) / 2);
expect(efficiencyEligible(aggregate, 'headshotRate')).toBe(false);
});
it('preserves fractional ratios and low samples without rounding, capping or inventing a denominator', () => {
const small = stats({ gamesPlayed: 3, gamesCompleted: 1, kills: 2, headshots: 1, downs: 3, roundsSurvived: 7 });
expect(efficiencyValue(small, 'headshotRate')).toBe(1 / 2);
expect(efficiencyValue(small, 'killsPerDown')).toBe(2 / 3);
expect(efficiencyValue(small, 'averageKills')).toBe(2 / 3);
expect(efficiencyValue(small, 'averageRounds')).toBe(7 / 3);
expect(efficiencyValue(small, 'completionRate')).toBe(1 / 3);
expect(efficiencyValue(stats({ kills: 1000000, downs: 1 }), 'killsPerDown')).toBe(1000000);
});
});
describe('efficiency explanations', () => {
it('shows the actual numerator and denominator with their units', () => {
const player = stats({ kills: 1200 });
expect(efficiencySample(player, 'headshotRate')).toBe('31 headshots / 1,200 kills');
expect(efficiencySample(player, 'killsPerDown')).toBe('1,200 kills / 5 downs');
expect(efficiencySample(player, 'averageKills')).toBe('1,200 kills / 5 ended games');
expect(efficiencySample(player, 'averageRounds')).toBe('47 observed rounds / 5 ended games');
expect(efficiencySample(player, 'completionRate')).toBe('3 finishes / 5 ended games');
expect(efficiencySample(stats({ kills: 1, downs: 0 }), 'killsPerDown')).toBe('1 kill / 0 downs');
});
it('identifies ratio metrics and publishes the thresholds used to qualify them', () => {
for (const { key, description } of efficiencyMetrics) {
expect(isEfficiencyMetric(key)).toBe(true);
expect(description).toContain('divided by');
expect(efficiencyRequirement(key)).toContain(`${EFFICIENCY_MIN_GAMES} ended games`);
}
expect(efficiencyRequirement('headshotRate')).toContain(`${EFFICIENCY_MIN_KILLS} kills`);
expect(efficiencyRequirement('killsPerDown')).toContain(`${EFFICIENCY_MIN_DOWNS} downs`);
expect(efficiencyMetrics.filter(metric => metric.format === 'percent').map(metric => metric.key))
.toEqual(['headshotRate', 'completionRate']);
expect(isEfficiencyMetric('kills')).toBe(false);
expect(isEfficiencyMetric('')).toBe(false);
});
});
+6
View File
@@ -123,6 +123,8 @@ describe('persisted leaderboard queries', () => {
rank: 1, guid: '101', name: 'Ace', highestRound: 20, kills: 770, rank: 1, guid: '101', name: 'Ace', highestRound: 20, kills: 770,
roundsSurvived: 38, headshots: 320, revives: 8, downs: 4, roundsSurvived: 38, headshots: 320, revives: 8, downs: 4,
gamesPlayed: 4, gamesCompleted: 3, bestKills: 500, bestScore: 5000, gamesPlayed: 4, gamesCompleted: 3, bestKills: 500, bestScore: 5000,
headshotRate: 320 / 770, killsPerDown: 770 / 4, averageKills: 770 / 4,
averageRounds: 38 / 4, completionRate: 3 / 4,
lastSeen: '2026-09-05T00:00:00.000Z', records: { lastSeen: '2026-09-05T00:00:00.000Z', records: {
highestRound: expect.objectContaining({ matchId: 4, value: 20 }), highestRound: expect.objectContaining({ matchId: 4, value: 20 }),
bestKills: expect.objectContaining({ matchId: 4, value: 500 }), bestKills: expect.objectContaining({ matchId: 4, value: 500 }),
@@ -145,6 +147,8 @@ describe('persisted leaderboard queries', () => {
expect(t5.entries.find(entry => entry.guid === '101')).toEqual({ expect(t5.entries.find(entry => entry.guid === '101')).toEqual({
rank: 1, guid: '101', name: 'Ace', highestRound: 10, kills: 270, rank: 1, guid: '101', name: 'Ace', highestRound: 10, kills: 270,
roundsSurvived: 19, headshots: 120, revives: 4, downs: 3, gamesPlayed: 3, roundsSurvived: 19, headshots: 120, revives: 4, downs: 3, gamesPlayed: 3,
headshotRate: 120 / 270, killsPerDown: 270 / 3, averageKills: 270 / 3,
averageRounds: 19 / 3, completionRate: 2 / 3,
gamesCompleted: 2, bestKills: 150, bestScore: 3000, lastSeen: '2026-09-04T00:00:00.000Z', records: { gamesCompleted: 2, bestKills: 150, bestScore: 3000, lastSeen: '2026-09-04T00:00:00.000Z', records: {
highestRound: expect.objectContaining({ matchId: 1, value: 10 }), highestRound: expect.objectContaining({ matchId: 1, value: 10 }),
bestKills: expect.objectContaining({ matchId: 2, value: 150 }), bestKills: expect.objectContaining({ matchId: 2, value: 150 }),
@@ -156,6 +160,8 @@ describe('persisted leaderboard queries', () => {
expect(t6.entries.find(entry => entry.guid === '101')).toEqual({ expect(t6.entries.find(entry => entry.guid === '101')).toEqual({
rank: 1, guid: '101', name: 'Ace', highestRound: 20, kills: 500, rank: 1, guid: '101', name: 'Ace', highestRound: 20, kills: 500,
roundsSurvived: 19, headshots: 200, revives: 4, downs: 1, gamesPlayed: 1, roundsSurvived: 19, headshots: 200, revives: 4, downs: 1, gamesPlayed: 1,
headshotRate: 200 / 500, killsPerDown: 500, averageKills: 500,
averageRounds: 19, completionRate: 1,
gamesCompleted: 1, bestKills: 500, bestScore: 5000, lastSeen: '2026-09-05T00:00:00.000Z', records: { gamesCompleted: 1, bestKills: 500, bestScore: 5000, lastSeen: '2026-09-05T00:00:00.000Z', records: {
highestRound: expect.objectContaining({ matchId: 4, value: 20 }), highestRound: expect.objectContaining({ matchId: 4, value: 20 }),
bestKills: expect.objectContaining({ matchId: 4, value: 500 }), bestKills: expect.objectContaining({ matchId: 4, value: 500 }),