218 lines
14 KiB
Markdown
218 lines
14 KiB
Markdown
# Leaderboard
|
||
|
||
Open **Leaderboard** in the main navigation to compare players across the fleet,
|
||
with one entry per GUID combining Black Ops I (T5) and Black Ops II (T6). Game
|
||
and map filters limit which matches contribute to each player's statistics and
|
||
rank. Choose a ranking, search a name or GUID, and share the page URL with its
|
||
filters intact. Results refresh while the page is visible.
|
||
Matches only appear in server/global history once they reach round 2.
|
||
Leaderboards, summaries, map options, profiles and player history include only
|
||
completed matches with a received `game_end`. Active and interrupted matches do
|
||
not contribute. A player must also be observed at round 2 or later; players who
|
||
left before a completed game's end retain their recorded stats.
|
||
This also excludes existing round 1 records. Early telemetry is retained so
|
||
qualifying games keep their start time, roster and cumulative counters.
|
||
|
||
Click a player's name to open their shareable profile at
|
||
`/leaderboard/players/[guid]`. Profiles combine Black Ops I (T5) and Black Ops II
|
||
(T6) records for the same GUID. Existing `/leaderboard/players/[game]/[guid]`
|
||
links redirect to the combined profile. Profiles show all ten lifetime statistics,
|
||
headshots as a percentage of kills, kills per down, average kills and observed
|
||
rounds per game, and completion rate. Each map has its own complete set of
|
||
records; map records and match history identify their game. Match history shows
|
||
individual counters, server, date and completion credit in pages of 25 matches
|
||
across both games. Map names link back to that map's leaderboard in its game.
|
||
Use **Refresh stats** to load the latest reports. Ratios with a zero denominator
|
||
display a dash. Lifetime statistics and per-map records always cover all recorded
|
||
maps across both games.
|
||
|
||
Opening a player from the leaderboard preserves the originating game, map,
|
||
metric, search and page in the **Leaderboard** return link. The profile's
|
||
**Selected leaderboard** controls initialize from that game/map/metric and show
|
||
the player's rank within that competition, independent of the original search.
|
||
These controls also filter match history; lifetime totals stay unchanged.
|
||
History pagination retains the selection and return link. Map links preserve
|
||
the selected ranking metric. A player without qualifying matches in a selection
|
||
can still view their lifetime profile and choose another scope.
|
||
|
||
On phones, per-map records and match history use compact cards with the map,
|
||
game, highest round and kills up front. Expand **All stats and record runs**
|
||
or **More match stats** for the remaining counters and source evidence. Desktop
|
||
profiles retain the complete tables; both views use the same history filters
|
||
and pagination. History dates say **First observed**, since a missing start
|
||
report can make the first recorded observation later than the actual game start.
|
||
|
||
Highest-round, best-game-kill and highest-score records link to the completed
|
||
match that earned them on the leaderboard and in lifetime/per-map profile stats.
|
||
Each source shows its map, game, completion time, server, and whether the match
|
||
start was recorded. **Run details** shows when the player was first observed:
|
||
at the recorded start, at a reported round, or unknown for older stored records.
|
||
Presence is not proof of continuous survival. Equal personal bests link to the
|
||
earliest recorded completion, then the lowest match ID when times tie.
|
||
|
||
Completed matches link to their own results page at `/matches/[id]` from global
|
||
Match History, server history, and player profiles. The page shows the recorded
|
||
map and server, final round, start/end times, duration when the start was observed,
|
||
player counts, and the full saved roster. Players missing from the final report
|
||
retain their earlier counters; players with no stats display dashes. Counters and
|
||
score are the highest reported values within the match, not necessarily its final
|
||
scoreboard. Active and interrupted matches remain listed without results links.
|
||
Match pages remain available after the original server is removed.
|
||
|
||
The leaderboard uses the existing telemetry events; no reporter change is
|
||
required. The leaderboard and profiles identify a player by **GUID**, displaying
|
||
the latest reported name across both games, regardless of game or map filters.
|
||
Different names for the same GUID do not split their leaderboard entry or
|
||
profile; identical names with different GUIDs remain separate. Stored player
|
||
records remain keyed by **game + GUID** for telemetry ingestion; combining their
|
||
identity requires no rekeying. The record-source upgrade automatically adds
|
||
nullable participation-start fields; existing rows keep an unknown start instead
|
||
of inferring one. GUIDs stay strings.
|
||
|
||
## Recorded statistics
|
||
|
||
| Statistic | Meaning |
|
||
| --- | --- |
|
||
| Highest round | Highest round reported with that player present, including players who joined mid-game. |
|
||
| Total kills, headshots, revives, downs | Sum of each player's highest reported counter in each game. Round snapshots are cumulative, so they are never added together within a game. |
|
||
| Total rounds | Round progression during observed participation, added across games. A player observed at game start and then round 8 receives seven rounds. A player first seen at round 8 gets a highest-round record of 8, but zero total rounds; reaching round 9 then adds one. |
|
||
| Games played | Completed games in which a stats report at round 2 or later contains the player, even if they left before the end. Start-only and round 1 participation do not count. |
|
||
| Games completed | Games whose `game_end` roster contains the player. Disconnected players retain earlier stats without receiving completion credit. |
|
||
| Best game kills | Highest kill count recorded in any one game. |
|
||
| Highest reported score | Highest observed score balance. Spending points can lower the score, so this is not cumulative points earned. |
|
||
|
||
Every ranking sorts highest first, including downs (a count, not an efficiency
|
||
rating). Equal values share a rank; name and GUID provide a stable display order.
|
||
Searching preserves the player's rank for the selected game/map filters, or
|
||
across both games when unfiltered. Summary cards describe the same selection,
|
||
regardless of the search text. Player counts include each qualifying GUID once,
|
||
including when they have records in both games. Summary rounds are
|
||
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,
|
||
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
|
||
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,
|
||
spectating, or absent between reports. Mid-game snapshots still contribute their
|
||
reported kills and other counters, even when earlier participation is unknown.
|
||
|
||
## Persistence and match boundaries
|
||
|
||
Match and player records live in SQLite alongside the rest of Electric Cherry.
|
||
They persist across app restarts and server deletion, and are not subject to
|
||
campaign history retention. The latest scoreboard and leaderboard update in the
|
||
same transaction. Each match keeps player counters even after someone leaves.
|
||
|
||
A `game_start` opens a match; `game_end` completes it. A changed map, a lower round,
|
||
or a round report after a completed game can also establish a new match when the
|
||
start was missed. Superseded active matches remain in history as interrupted.
|
||
Repeated starts before any stats belong to the same match. Identical consecutive
|
||
payloads (ignoring receipt time and roster order) do not add records. Repeated
|
||
snapshots in a match cannot increase counters or rounds twice.
|
||
|
||
The first upgrade imports the latest stored full scoreboard from each server
|
||
once. That provides the available counters and highest round, but not discarded
|
||
earlier games or unobserved round progression. Old round-only snapshots have no
|
||
player identity and cannot be imported. Tracking continues from future reports.
|
||
|
||
T6 selections sharing an engine map, such as Town Survival and TranZit, remain
|
||
separate using the server's registered map variant. Unknown variants remain
|
||
explicitly identified instead of being labeled as a known selection. A match
|
||
retains the game/map identity recorded at ingestion even if the server is later
|
||
reconfigured or removed.
|
||
|
||
The current protocol sends events in gameplay order and has no match ID, event
|
||
ID, or source sequence. A late report from an older game cannot be reliably
|
||
distinguished from a new game. Likewise, identical starts before any stats are
|
||
indistinguishable from retries. Send reports once in order. Within a match,
|
||
counter decreases use the previous high-water mark; if a reconnect resets a
|
||
player's counters, subsequent activity may be undercounted until those counters
|
||
pass their earlier maxima. Exact replay handling and counter-reset accounting
|
||
would require additional reporter identity/ordering information.
|
||
|
||
## Read API
|
||
|
||
`GET /api/leaderboard` accepts optional `game` (`t5` or `t6`), `map` (the opaque
|
||
key returned in `maps`), `metric` (default `highestRound`), `q` (name/GUID search),
|
||
`limit` (1–100, default 50), and `offset` (default 0). It returns ranked `entries`,
|
||
available `maps`, the matching player `total`, pagination values, and `summary`.
|
||
Each entry represents one GUID and combines its qualifying matches within the
|
||
game/map filters. Entries have no game field; map options retain their game
|
||
identity. Both `total` and `summary.players` count unique GUIDs; `total` applies
|
||
the selected metric's qualification rules and player search, while the summary
|
||
covers all completed qualifying participation in the game/map scope.
|
||
|
||
Metric keys are `highestRound`, `kills`, `roundsSurvived`, `headshots`, `revives`,
|
||
`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.
|
||
Entries also include `records`, keyed by `highestRound`, `bestKills`, and
|
||
`bestScore`, with the completed match ID, value, map/game/server, timing and
|
||
observed participation behind each personal best.
|
||
|
||
## Query performance
|
||
|
||
Leaderboard reads materialize the selected completed participation once, reuse
|
||
one player aggregate for ranking and summary totals, and reuse the ranked/search
|
||
result for pagination and its matching count. Empty and out-of-range pages still
|
||
return accurate totals. Record evidence is fetched only for players on the page.
|
||
Exact player-rank reads share the ranking rules but skip unused fleet summaries
|
||
and map lists. No response cache delays visibility of a newly completed game.
|
||
|
||
Run the deterministic, isolated benchmark with:
|
||
|
||
```sh
|
||
node scripts/benchmark-leaderboard.mjs --scenario all --baseline-ref eac9ad4 --runs 3 --json /tmp/leaderboard-benchmark.json
|
||
```
|
||
|
||
The script creates temporary SQLite databases, loads the actual query modules,
|
||
checks complete-response parity against the selected Git revision, and reports
|
||
timings, source hashes and query plans. It never opens the application database.
|
||
The small fixture has 2,000 matches and 8,000 participation rows; the large fixture
|
||
has 50,000 matches and 200,000 participation rows. Both include completed,
|
||
active, interrupted and nonqualifying records.
|
||
|
||
On the local Intel i5-8279U / Node 25.9.0 / SQLite 3.49.2 test environment, three
|
||
measured runs after warmup gave these median query times on the large fixture:
|
||
|
||
| Request | Before | After |
|
||
| --- | ---: | ---: |
|
||
| Highest round, all games | 1,717 ms | 829 ms |
|
||
| Total kills, all games | 1,555 ms | 858 ms |
|
||
| One game | 899 ms | 491 ms |
|
||
| One map | 345 ms | 224 ms |
|
||
| Player search | 1,515 ms | 757 ms |
|
||
| Deep page | 1,421 ms | 752 ms |
|
||
| Headshot percentage | 1,444 ms | 760 ms |
|
||
| Exact player rank | 1,524 ms | 554 ms |
|
||
|
||
The small fixture's unfiltered request fell from 41 to 30 ms. All 150 additional
|
||
small-fixture parity cases and 22 timed small/large request cases matched the
|
||
baseline's complete responses, including ratios and record evidence. Timings
|
||
include SQL preparation and evidence lookup, exclude HTTP/browser rendering, and
|
||
use a warm filesystem cache with alternating implementation order. These are
|
||
local synthetic measurements, not production latency guarantees. Queries still
|
||
run synchronously, so larger histories remain a capacity consideration.
|
||
|
||
Validation for this change passed 1,019 unit tests, Svelte checking with no errors
|
||
or warnings, and a production build. Isolated Firefox walkthroughs at 1440px and
|
||
390px checked completed-only totals, record links, efficiency qualification,
|
||
scoped ranks/history, preserved navigation, empty scopes and mobile expansion.
|