Rank each player once across T5 and T6
This commit is contained in:
@@ -82,21 +82,42 @@ describe('persisted leaderboard queries', () => {
|
||||
expect(serverMatchHistory('removed-server', 2)).toEqual(scopedHistory);
|
||||
});
|
||||
|
||||
it('sums counters across matches, retains personal bests and separates games for the same GUID', async () => {
|
||||
it('ranks each GUID once with combined game totals and personal bests', async () => {
|
||||
await seedRecords();
|
||||
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
|
||||
const board = getLeaderboard();
|
||||
expect(board.entries.find(entry => entry.game === 't5' && entry.guid === '101')).toEqual({
|
||||
rank: 3, game: 't5', guid: '101', name: 'Ace', highestRound: 10, kills: 270,
|
||||
roundsSurvived: 19, headshots: 120, revives: 4, downs: 3,
|
||||
gamesPlayed: 3, gamesCompleted: 2, bestKills: 150, bestScore: 3000,
|
||||
lastSeen: '2026-09-04T00:00:00.000Z'
|
||||
});
|
||||
expect(board.entries.find(entry => entry.game === 't6' && entry.guid === '101'))
|
||||
.toMatchObject({ name: 'Other game', kills: 500, gamesPlayed: 1, rank: 1 });
|
||||
expect(board.entries.filter(entry => entry.guid === '101')).toEqual([{
|
||||
rank: 1, guid: '101', name: 'Ace', highestRound: 20, kills: 770,
|
||||
roundsSurvived: 38, headshots: 320, revives: 8, downs: 4,
|
||||
gamesPlayed: 4, gamesCompleted: 3, bestKills: 500, bestScore: 5000,
|
||||
lastSeen: '2026-09-05T00:00:00.000Z'
|
||||
}]);
|
||||
expect(board.total).toBe(4);
|
||||
expect(board.entries.map(entry => [entry.guid, entry.rank])).toEqual([['101', 1], ['505', 2], ['202', 3], ['303', 4]]);
|
||||
expect(board.entries.some(entry => entry.guid === '404')).toBe(false);
|
||||
expect(board.summary).toEqual({ players: 5, matches: 5, kills: 1140, roundsSurvived: 68,
|
||||
expect(board.summary).toEqual({ players: 4, matches: 5, kills: 1140, roundsSurvived: 68,
|
||||
highestRound: 20, firstTrackedAt: '2026-09-01T00:00:00.000Z', updatedAt: '2026-09-06T00:00:00.000Z' });
|
||||
const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js');
|
||||
expect(board.entries[0]).toMatchObject(getPlayerProfile('101')!.stats);
|
||||
});
|
||||
|
||||
it('scopes contributions by game while keeping one shared player identity', async () => {
|
||||
await seedRecords();
|
||||
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
|
||||
const t5 = getLeaderboard({ game: 't5' });
|
||||
expect(t5.entries.find(entry => entry.guid === '101')).toEqual({
|
||||
rank: 1, guid: '101', name: 'Ace', highestRound: 10, kills: 270,
|
||||
roundsSurvived: 19, headshots: 120, revives: 4, downs: 3, gamesPlayed: 3,
|
||||
gamesCompleted: 2, bestKills: 150, bestScore: 3000, lastSeen: '2026-09-04T00:00:00.000Z'
|
||||
});
|
||||
expect(t5).toMatchObject({ total: 3, summary: { players: 3, matches: 3, kills: 440, roundsSurvived: 35 } });
|
||||
const t6 = getLeaderboard({ game: 't6' });
|
||||
expect(t6.entries.find(entry => entry.guid === '101')).toEqual({
|
||||
rank: 1, guid: '101', name: 'Ace', highestRound: 20, kills: 500,
|
||||
roundsSurvived: 19, headshots: 200, revives: 4, downs: 1, gamesPlayed: 1,
|
||||
gamesCompleted: 1, bestKills: 500, bestScore: 5000, lastSeen: '2026-09-05T00:00:00.000Z'
|
||||
});
|
||||
expect(t6).toMatchObject({ total: 2, summary: { players: 2, matches: 2, kills: 700, roundsSurvived: 33 } });
|
||||
});
|
||||
|
||||
it('filters exact map variants and retains historical and custom map options independently of selection', async () => {
|
||||
@@ -107,7 +128,8 @@ describe('persisted leaderboard queries', () => {
|
||||
expect(theater.summary).toMatchObject({ players: 3, matches: 2, kills: 420, roundsSurvived: 32, highestRound: 10 });
|
||||
expect(theater.maps.map(map => map.key)).toEqual(['t5:coast', 't5:custom_map', 't5:theater']);
|
||||
const town = getLeaderboard({ map: 't6:town' });
|
||||
expect(town.entries.map(entry => entry.name)).toEqual(['Other game']);
|
||||
expect(town.entries.map(entry => [entry.guid, entry.name, entry.kills])).toEqual([['101', 'Ace', 500]]);
|
||||
expect(getLeaderboard({ game: 't5', map: 't6:town' })).toMatchObject({ entries: [], total: 0, summary: { players: 0, kills: 0 } });
|
||||
expect(town.maps).toContainEqual({ key: 't6:farm', game: 't6', map: 'zm_transit', name: 'Farm Survival' });
|
||||
});
|
||||
|
||||
@@ -122,7 +144,58 @@ describe('persisted leaderboard queries', () => {
|
||||
expect(search.summary).toEqual(board.summary);
|
||||
expect(getLeaderboard({ game: 't5', metric: 'kills', limit: 1, offset: 1 }).entries[0])
|
||||
.toMatchObject({ name: 'Blake', rank: 2 });
|
||||
expect(getLeaderboard({ q: '303' }).entries[0]).toMatchObject({ name: 'Casey', rank: 5 });
|
||||
expect(getLeaderboard({ q: '303' }).entries[0]).toMatchObject({ name: 'Casey', rank: 4 });
|
||||
});
|
||||
|
||||
it('uses and searches the latest shared name regardless of game or map filters without merging namesakes', async () => {
|
||||
const handle = await seedRecords();
|
||||
handle.prepare("UPDATE leaderboard_players SET last_seen_at = '2026-09-09' WHERE game = 't6' AND guid = '101'").run();
|
||||
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
|
||||
for (const options of [{}, { game: 't5' }, { game: 't6' }, { map: 't5:theater' }, { game: 't6', map: 't6:town' }]) {
|
||||
const board = getLeaderboard(options);
|
||||
expect(board.entries.find(entry => entry.guid === '101')?.name).toBe('Other game');
|
||||
expect(getLeaderboard({ ...options, q: 'other GAME' }).entries.map(entry => entry.guid)).toEqual(['101']);
|
||||
expect(getLeaderboard({ ...options, q: 'Ace' }).entries).toEqual([]);
|
||||
}
|
||||
handle.prepare("UPDATE leaderboard_players SET name = 'Other game' WHERE guid = '202'").run();
|
||||
const namesakes = getLeaderboard({ q: 'Other game' });
|
||||
expect(namesakes.entries.map(entry => [entry.guid, entry.name, entry.rank])).toEqual([
|
||||
['101', 'Other game', 1], ['202', 'Other game', 3]
|
||||
]);
|
||||
expect(namesakes).toMatchObject({ total: 2, summary: { players: 4 } });
|
||||
});
|
||||
|
||||
it('paginates unique players after ranking their combined totals', async () => {
|
||||
await seedRecords();
|
||||
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
|
||||
const first = getLeaderboard({ metric: 'kills', limit: 2 });
|
||||
const second = getLeaderboard({ metric: 'kills', limit: 2, offset: 2 });
|
||||
expect(first.entries.map(entry => [entry.guid, entry.kills, entry.rank])).toEqual([['101', 770, 1], ['505', 200, 2]]);
|
||||
expect(second.entries.map(entry => [entry.guid, entry.kills, entry.rank])).toEqual([['202', 100, 3], ['303', 70, 4]]);
|
||||
expect(first.total).toBe(4);
|
||||
expect(second.total).toBe(4);
|
||||
expect(second.summary).toEqual(first.summary);
|
||||
expect(getLeaderboard({ metric: 'kills', limit: 2, offset: 4 }).entries).toEqual([]);
|
||||
});
|
||||
|
||||
it('ignores unsupported games in identities, totals, rankings and map options', async () => {
|
||||
const handle = await seedRecords();
|
||||
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
|
||||
const original = getLeaderboard();
|
||||
handle.prepare(`INSERT INTO leaderboard_matches
|
||||
(id, server, game, map, map_key, map_name, started_at, last_received_at, status, highest_round, last_event, last_fingerprint)
|
||||
VALUES (7, 'removed-server', 't7', 'excluded', 't7:excluded', 'Excluded', '2020-01-01', '2030-01-01', 'completed', 99, 'game_end', 'test')`).run();
|
||||
for (const guid of ['101', 'unsupported-only']) {
|
||||
handle.prepare(`INSERT INTO leaderboard_match_players
|
||||
(match_id, guid, kills, highest_round, has_stats, first_seen_at, last_seen_at)
|
||||
VALUES (7, ?, 99999, 99, 1, '2020-01-01', '2030-01-01')`).run(guid);
|
||||
handle.prepare(`INSERT INTO leaderboard_players (game, guid, name, first_seen_at, last_seen_at)
|
||||
VALUES ('t7', ?, 'Unsupported name', '2020-01-01', '2030-01-01')`).run(guid);
|
||||
}
|
||||
expect(getLeaderboard()).toEqual(original);
|
||||
expect(getLeaderboard({ q: 'Unsupported' }).entries).toEqual([]);
|
||||
expect(getLeaderboard({ game: 't7' })).toMatchObject({ entries: [], maps: [], total: 0,
|
||||
summary: { players: 0, matches: 0, kills: 0 } });
|
||||
});
|
||||
|
||||
it('treats SQL and LIKE punctuation as literal input and allows only known ranking columns', async () => {
|
||||
@@ -271,6 +344,19 @@ describe('player profiles', () => {
|
||||
});
|
||||
|
||||
describe('leaderboard API', () => {
|
||||
it('returns one combined ranking row when a search matches the same player in both games', async () => {
|
||||
const handle = await seedRecords();
|
||||
handle.prepare("UPDATE leaderboard_players SET name = 'JoshWright' WHERE guid = '101'").run();
|
||||
const { GET } = await import('../../src/routes/api/leaderboard/+server.js');
|
||||
const response = GET({ url: new URL('http://localhost/api/leaderboard?q=JoshWright&metric=kills') });
|
||||
expect(response.status).toBe(200);
|
||||
const result = await response.json() as LeaderboardResponse;
|
||||
expect(result).toMatchObject({ total: 1, summary: { players: 4, matches: 5, kills: 1140 } });
|
||||
expect(result.entries).toEqual([expect.objectContaining({ guid: '101', name: 'JoshWright', rank: 1,
|
||||
kills: 770, gamesPlayed: 4, gamesCompleted: 3, highestRound: 20, bestKills: 500, bestScore: 5000 })]);
|
||||
expect(result.entries[0]).not.toHaveProperty('game');
|
||||
});
|
||||
|
||||
it('returns the requested ranking and pagination', async () => {
|
||||
await seedRecords();
|
||||
const { GET } = await import('../../src/routes/api/leaderboard/+server.js');
|
||||
|
||||
Reference in New Issue
Block a user