An API for ranking players based on various criteria.
Leaderboard Service was originally created for Rumble Entertainment (which later became R Studios), a mobile gaming company. The service was responsible for sending player rewards for in-game activities and giving them ways to compete with each other. This service was also the most unfortunate project in Platform's domain in that it was hit by egregious levels of scope creep with consequently poorly communicated / understood / defined test plans. Regardless, it still did a wonderful job when stable, even if some features remain barely stubbed out / not polished / unused as a result of aforementioned scope creep.
R Studios unfortunately closed its doors in July 2024. This project has been released as open source with permission.
As of this writing, there may still be existing references to Rumble's resources, such as Confluence links, but their absence doesn't have any significant impact. Some documentation will also be missing until it can be recreated here, since with the company closure any feature specs and explainer articles originally written for Confluence / Slack channels were lost.
While Rumble is shutting down, I'm grateful for the opportunities and human connections I had working there.
This project is licensed under the MIT License - see the LICENSE file for details.
Leaderboards send regular rewards to deserving players. This is the players' driving motivation behind all sorts of in-game activities, including PvP. Leaderboards enable quantitative competition among participating players and encourage regular engagement with the game.
Once a Leaderboard has been created, it is a self-governing entity. It manages its list of players, scores, and hands out rewards on its own and lives on indefinitely assuming it has a rollover attribute. The only interaction the game server has with leaderboards is to:
- Create Leaderboards with rules
- Update Leaderboard rules
- Send scores for a specific leaderboard and player
Due to the sheer scale that leaderboards will face upon a global release, all queries must be optimized, and leaderboard data will need to be cached so as to not re-evaluate it constantly. Except in the case of aggregation, wherever possible, the MongoDB queries should perform updates directly, sort, and limit results. Performance with 200 daily active users is negligible, but with traffic in the millions, the service has a risk of scaling very poorly with calculations. Sharding (covered below) largely avoids this problem. Global leaderboards should never have to evaluate more than a handful of records at a time to avoid becoming compute-bound.
By default, the service returns both top and nearby ranks. While some game leaderboards will allow users to navigate any number of leaderboard records through pagination, the service should not support this. If there's a feature request for pagination, this can be achieved somewhat by retrieving slightly more records and allowing the client to add "pages" with a cap. Players generally won't care about who is 261,357th in a leaderboard - they'll only care about the best players and the scores around them.
Term | Definition |
---|---|
AccountId / aid | The MongoDB identifier for an account. |
Archive | A stored state of a completed leaderboard. When a leaderboard completes its rollover, a full copy of it is kept as an Archive. |
Demotion | If a user underperformed in a leaderboard, they move down in tier, which yields lesser rewards. |
Enrollment | A service record of a player's exploits within the leaderboard system, containing information about their tier and times participated. |
Global Leaderboard | A leaderboard containing every active player in the game. |
Minimum Percentile | For reward distribution. A value of 0% means all players receive the reward. A value of 95% in a leaderboard of 300 players means the top 15 receive the reward. |
Nearby Rank | Ranks close to the player in either direction. |
Promotion | If a user excelled in a leaderboard, they move up in tier, yielding greater rewards. |
Rank | The relative position of a player in the leaderboard. If three players have the same score, they share the same rank, and the rank below them skips two. |
Reward | Items to send to players meeting the reward rules. Rewards are distributed via mailbox-service and contain all information necessary, including subject and message body, to send to mailbox. |
Rollover | A time that a leaderboard locks down, distributes rewards, archives itself, and resets its scores. |
Rules | A generic term for how a leaderboard governs itself. Can refer to either reward distribution or rollover type. |
Score | An arbitrary numeric value indicating a player's progress. Like in an arcade machine, scores should be varied enough that ties are rare. |
Season | Only used in Ladder. As of 5/23, seasons were removed from leaderboards. |
Shard | A subset of a leaderboard's players. Shards are useful for driving competition and seeing immediate results. Sharded leaderboards operate the same as regular ones, but limited in player count. Shards can also be "Guild Shards", accessible only to members of a specific guild. |
Tier | A subset of a leaderboard type, ideally grouping players of similar skill together. Higher tiers yield better rewards. |
Winner | A player who received at least one reward from a leaderboard's rollover. |
To run locally, all platform-services require an environment.json
file in the top directory of the solution. This includes the PLATFORM_COMMON
value, which is shared across all services with later platform-common packages.
{
"MONGODB_NAME": "leaderboard-service-107",
"RUMBLE_COMPONENT": "leaderboard-service",
"RUMBLE_DEPLOYMENT": "wmaynard_local",
"PLATFORM_COMMON": {
"MONGODB_URI": {
"*": "mongodb://localhost:27017/leaderboard-service-107?retryWrites=true&w=majority&minPoolSize=2"
},
"CONFIG_SERVICE_URL": {
"*": "https://config-service.cdrentertainment.com/"
},
"GAME_GUKEY": {
"*": "{redacted}"
},
"GRAPHITE": {
"*": "graphite.rumblegames.com:2003"
},
"LOGGLY_BASE_URL": {
"*": "https://logs-01.loggly.com/bulk/f91d5019-e31d-4955-812c-31891b64b8d9/tag/{0}/"
},
"RUMBLE_KEY": {
"*": "{redacted}"
},
"RUMBLE_TOKEN_VALIDATION": {
"*": "https://dev.nonprod.tower.cdrentertainment.com/token/validate"
},
"SLACK_ENDPOINT_POST_MESSAGE": {
"*": "https://slack.com/api/chat.postMessage"
},
"SLACK_ENDPOINT_UPLOAD": {
"*": "https://slack.com/api/files.upload"
},
"SLACK_ENDPOINT_USER_LIST": {
"*": "https://slack.com/api/users.list"
},
"SLACK_LOG_BOT_TOKEN": {
"*": "xoxb-4937491542-3072841079041-s1VFRHXYg7BFFGLqtH5ks5pp"
},
"SLACK_LOG_CHANNEL": {
"*": "C031TKSGJ4T"
},
"SWARM_MODE": {
"*": false
},
"VERBOSE_LOGGING": {
"*": false
},
"RUMBLE_TOKEN_VERIFICATION": {
"*": ""
}
}
}
Name | Description |
---|---|
AdminController | Handles the operations for administrative tools, such as the CS portal, and updating leaderboards. |
TopController | Handles requests to score , rankings . |
Name | Description |
---|---|
AccountDisqualifiedException | Thrown when a user is not allowed to participate in leaderboards. |
UnknownLeaderboardException | Thrown when a leaderboard_id is unrecognized. The game server should hit /update to create the leaderboard if the leaderboard_id should exist. |
Name | Description |
---|---|
Account | Unused, to be removed |
Enrollment | A service record of a player's exploits within the leaderboard system, containing information about their tier and times participated. |
Entry | A minimal component tracked in the leaderboard data. One per player per leaderboard. |
Item | Part of a reward to give to players. Consists of a resource ID and quantity. |
Leaderboard | The meat of the service; contains all information necessary for tracking player scores, reward rules, and rollover rules. |
Ranking | Generated when leaderboards are evaluated. Indicates player positions in the leaderboard; each rank can have multiple players. |
Reward | Data required to send winners their respective messages in mailbox-service. |
ServiceRecord | Unused, to be removed |
ServiceSettings | Unused, to be removed |
TierRules | Dictates requirements for promotions, demotions, and rewards. |
Name | Description |
---|---|
ArchiveService | Stores copies of completed leaderboards in their entirety for a period of time. |
EnrollmentService | Handles player Enrollment documents. |
LeaderboardService | Handles scoring, data retrieval, and rollover mechanics. |
MasterService | Experimental PlatformTimerService. The goal is to allow exactly one instance of the service to perform certain work regardless of how many have been created in kubernetes for scaling. May be moved to platfrom-common at a later date. |
ResetService | Implementation of MasterService; locks down leaderboards and triggers rollovers. |
RewardsService | Registers winners and their rewards from completed leaderboards to create an auditable system. Sends messages to mailbox-service at the end of the rollover. |
SettingsService | Unused, to be removed |
All endpoints live off of the base {platform url}/leaderboard
.
Player authorization tokens are required.
Method | Endpoint | Description | Required Fields | Optional Fields |
---|---|---|---|---|
DELETE | /notification |
Acknowlegdes receipt of the promotionStatus after rollover. |
leaderboardId |
|
GET | /rankings |
Returns ranked and ordered information for a specific leaderboard ID. Response includes top scores and nearby scores. | leaderboardId score |
|
PATCH | /score |
Adds or subtracts a value to the player's score. While this value can be negative, scores are floored at 0. | leaderboardId |
GET /rankings?leaderboardId=pvp_daily&guildId=deadbeefdeadbeefdeadbeef
leaderboardId
accepts comma-separated values. To fetch rankings of multiple leaderboards, concatenate string IDs with commas (consequently, commas are no longer potentially valid as leaderboardIds)
guildId
is optional. When you specify a guildId
, you will retrieve both the regular shard and your guild's shard (provided you are a member of the guild). If your guild does not have a shard, one will be created. This process happens for all specified leaderboardIds
.
In the response, enrollment
changed to enrollments
, and is now an array instead of an object. The same is true for leaderboard
-> leaderboards
.
Response:
{
"enrollments": [
{
"tier": 1,
"activeTier": 0, // Indicates the last tier a user was in when DELETE /notification was called.
"isActive": false,
"archives": [
"6347060e3343959668563dd4",
"6347061b3343959668563de2"
],
"promotionStatus": -1, // Acknowledged = -1, Unchanged = 0, Demoted = 1, Promoted = 2
"id": "634705e83343959668563dcb"
},
..
]
"leaderboards": [
{
"shardId": "deadbeefdeadbeefdeadbeef",
"tier": 1,
"leaderboardId": "ldr_pvp_daily",
"guildId": null,
"allScores": [ // only shows up on small leaderboards
{
"rank": 1,
"accountId": "62e841925030343c6079e78d",
"score": 0,
"lastUpdated": 1665599430549
}
],
"nearbyScores": [
{
"rank": 1,
"accountId": "62e841925030343c6079e78d",
"score": 0,
"lastUpdated": 1665599430549
}
],
"topScores": [
{
"rank": 1,
"accountId": "62e841925030343c6079e78d",
"score": 0,
"lastUpdated": 1665599430549
}
]
},
...
]
}
Since leaderboards exist perpetually and continue to rollover and operate, it's important to be able to know when a rollover has happened, and to the same extent, know when that rollover was also the end of a season. The two important fields in the response to handle this are promotionStatus (enum)
and seasonEnded (bool)
. The UI should animate or display special information using these flags; a one-time operation to show that a player has been promoted / demoted / etc. Once the animation has played successfully, DELETE /notification
needs to be called to clear these flags.
Some notes on this:
- The end of a season should be calculated based on the
rolloversRemaining
rather than using CSV data from the game server. While they should be the same value when calculated,rolloversRemaining
will be the definitive source that the service uses. activeTier
indicates the most recent scoring event since theDELETE
call. When a season ends, this may be higher thantier + 1
.- You can also calculate what tier a player was in in the previous rollover by looking at the
promotionStatus
andtier
.
PATCH /score
Body:
{
"score": 55,
"leaderboardId": "pvp_daily",
"guildId": "deadbeefdeadbeefdeadbeef" // optional
}
Response:
No Content
Guild shards are automatically spawned off of a base shard definition as necessary. For example, let's say we want to have a Public Gold Blitz shard and a Guild Gold Blitz shard.
The gold_blitz
leaderboard has already been defined. For simplicity's sake, it has no tiers and no season. There is no special leaderboard ID you need to know to access the guild shard; it's the same between the public and guild shards. What you do need, however, is to send your guild ID with the request.
If there is no guildId
provided, Platform looks up the player's appropriate shard for scoring as normal.
If a guildId
is provided:
- Platform tries to find the guild's shard for the
gold_blitz
event. If it does not yet exist, it will be created. - Platform uses the player's token to fetch guild information. If this request fails, the score request fails.
- This is indicative of the player being kicked out of or leaving their guild, thus is ineligible for participation in the guild shard.
- Platform adds the player's score to the guild shard.
Player authorization tokens are required.
Method | Endpoint | Description | Required Fields | Optional Fields |
---|---|---|---|---|
GET | /archive |
Returns a list of past leaderboards the player has placed in. Archives are not kept indefinitely. By default, only one leaderboard is returned, but any positive number can be specified. Leaderboards are sorted by end time in descending order. | leaderboardId |
count |
GET /archive?leaderboardId=ldr_pvp_weekly_v1&count=2
Response:
{
"success": true,
"leaderboards": [
{ ... },
{ ... }
]
}
Admin authorization tokens are required. Use the leaderboard_AdminToken
from dynamic config.
Method | Endpoint | Description | Required Fields | Optional Fields |
---|---|---|---|---|
GET | /admin/list |
Returns a list of leaderboard IDs. This list should be checked against the CSV managed by the design team. If an ID does not exist, create it through /admin/update . |
||
POST | /admin/update |
Creates or modifies leaderboards per the request body. | leaderboard |
GET /admin/list
Response:
{
"success": true,
"leaderboardIds": [
"pvp_daily"
]
}
Due to its size, please see the dedicated LEADERBOARD MANAGEMENT README for a full example of how to format the message for POST /admin/update
.