Exclude same-round departures from games played

This commit is contained in:
2026-10-08 16:12:13 -04:00
parent 63e084e38f
commit a18635fb78
8 changed files with 73 additions and 7 deletions
@@ -141,3 +141,15 @@ it('accepts each efficiency metric through the API when the qualified group is e
summary: { players: 1, matches: 4, kills: 80 } });
}
});
it('does not use same-round departures to qualify for efficiency rankings', async () => {
const { db } = await import('../../src/lib/server/db/index.js');
addSamples(db(), 'short-visit', [...samples(4), { completed: 0 }]);
db().prepare(`UPDATE leaderboard_match_players SET first_observed_round = highest_round
WHERE completed = 0`).run();
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
expect(getLeaderboard().entries[0]).toMatchObject({ gamesPlayed: 4, gamesCompleted: 4,
kills: 100, averageKills: 25, completionRate: 1 });
const { efficiencyMetrics } = await import('../../src/lib/leaderboard-efficiency.js');
for (const { key } of efficiencyMetrics) expect(getLeaderboard({ metric: key }).entries).toEqual([]);
});
+34
View File
@@ -269,6 +269,40 @@ it('keeps disconnected player records while completing only the final roster', a
roundsSurvived: 3, gamesPlayed: 1, gamesCompleted: 0 }, matches: [{ status: 'completed', completed: 0 }] });
});
it.each([
[server, 'zombie_theater', 't5'],
['t6_zombie_town_1', 'zm_transit', 't6']
])('does not count same-round departures as games played on %s', async (source, map, game) => {
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js');
const departed = player('departed', { kills: 10 });
const advanced = player('advanced', { kills: 20 });
const finished = player('finished', { kills: 30 });
await report('game_start', 0, [], source, map);
await report('heartbeat', 5, [departed, advanced], source, map);
await report('heartbeat', 5, [player(departed.guid, { kills: 15 }), advanced], source, map);
await report('heartbeat', 5, [advanced], source, map);
await report('round_change', 6, [advanced, finished], source, map);
await report('game_end', 6, [finished], source, map);
const { closeDb } = await import('../../src/lib/server/db/index.js');
closeDb();
const board = getLeaderboard({ game });
expect(board.entries.find(entry => entry.guid === departed.guid)).toMatchObject({
gamesPlayed: 0, gamesCompleted: 0, kills: 15, highestRound: 5,
averageKills: null, averageRounds: null, completionRate: null
});
expect(board.entries.find(entry => entry.guid === advanced.guid)).toMatchObject({ gamesPlayed: 1, gamesCompleted: 0 });
expect(board.entries.find(entry => entry.guid === finished.guid)).toMatchObject({ gamesPlayed: 1, gamesCompleted: 1 });
const profile = getPlayerProfile(departed.guid)!;
expect(profile).toMatchObject({ stats: { gamesPlayed: 0, kills: 15 },
maps: [{ gamesPlayed: 0, kills: 15 }], matchesTotal: 1,
matches: [{ kills: 15, completed: 0 }], records: { bestKills: { value: 15 } } });
expect(getLeaderboard({ game, map: profile.maps[0].key, metric: 'gamesPlayed' }).entries)
.toContainEqual(expect.objectContaining({ guid: departed.guid, gamesPlayed: 0 }));
expect(getPlayerProfile(departed.guid, 0, { game, map: profile.maps[0].key })?.matchesTotal).toBe(1);
});
it('starts separate matches for game starts, map changes, round resets, and new rounds after an end', async () => {
await report('game_start');
await report('round_change', 5, [player(undefined, { kills: 50 })]);
+8
View File
@@ -387,6 +387,14 @@ describe('player profiles', () => {
for (const offset of [-1, NaN, 0.5]) expect(getPlayerProfile('101', offset)?.offset).toBe(0);
const { load } = await import('../../src/routes/leaderboard/players/[guid]/+page.server.js');
expect((await load({ params: { guid: '101' }, url: new URL('http://localhost/leaderboard/players/101?offset=25') })).profile).toEqual(second);
// Historical same-round departures retain both pages even with no counted games.
handle.prepare(`UPDATE leaderboard_match_players SET first_observed_round = highest_round,
completed = 0 WHERE guid = '101'`).run();
const departures = getPlayerProfile('101', 25)!;
expect(departures).toMatchObject({ stats: { gamesPlayed: 0, kills: 770 }, matchesTotal: 34, offset: 25 });
expect(departures.matches).toHaveLength(9);
expect(departures.maps.every(map => map.gamesPlayed === 0)).toBe(true);
});
it('loads canonical profile URLs, encodes GUIDs and returns 404 for missing players', async () => {