154 lines
6.2 KiB
TypeScript
154 lines
6.2 KiB
TypeScript
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);
|
|
});
|
|
});
|