January 30, 2017
One of the central features of my browser game, Directive Alpha, is the daily leaderboard, which is automatically reset at midnight. The Top 10 players of the day then receive a reward based on their ranking. I use Redis to implement this leaderboard and would like to share with you the process of building a daily leaderboard for a browser game using Redis.
Setting Up the Leaderboard
As a leaderboard, we are going to use Redis’ excellent SortedSet data type. The SortedSet is basically a list which contains a score and value. The score is used to keep track of how many points a player has made, the value represents who this player is (usually the player's name or ID).
For each score you want to track, you will need to make a separate Sorted Set. For keeping track of fights won you could create a Sorted Set with key score.fight , for tracking who made the most logins you could call it score.logins.
A daily leaderboard coupled with some rewards that are given out once a day can be a fantastic motivator for your players. The rankings are super-easy to implement with redis too. The trick here is to append today's date at the end of the Sorted Set key. Whenever you add a score, add it to both the global and the daily leaderboard.
So for the overall ranking you would use score.fight as the key for the Sorted Set, whereas for today's ranking you could use score.fight:2017-01-30.
Everytime you add a score, you determine today's date programmatically and append it to the Sorted Set key. This means that only scores that are made on a certain day are tracked and each day at 00:00, the ranking starts again from zero.
Of course, this technique can also be used if you want to keep track of points gained in a week, a year, or even a certain hour or minute if you need such fine-grained scoring.
Adding a Score
Now that we have set up the we can start adding up scores. Let's say we want to keep track of how many fights a player with userid 15 has made. Every time a player wins a fight, we simply call the zincrby command with a score of 1, like this:
zincrby score.fight 1 15
or more generally:
zincrby score.fight SCORE USERID
There is a lot of magic happening in this command. If the SortedSet “score.fight” doesn't exist, it is automatically created. Redis then adds a score of SCORE to the entry “USERID” in the SortedSet. If this entry doesn't exist yet, it is initialised with the SCORE given.
If we wanted to keep track of how much damage each player has dealt overall, we could simply use:
zincrby score.damage DAMAGE USERID
The same goes for daily leaderboards. If we want to keep track of how much damage a player has dealt today, we'd call:
zincrby score.damage:2017-01-30 DAMAGE USERID
If you just want to set the score to a certain value, you can use zadd. This can be useful to keep track of the highest damage a player has ever dealt. If it's higher than the previous score, call zadd to store it in the ranking.
Determining a Player's Rank
The Sorted Set uses the score to determine a player's rank in the leaderboard with zrevrank which sorts the ranking from highest score (rank 1) to lowest score.
Using the example above, if we wanted to display the rank in fights won by “USERID”, we can simply call zrevrank:
zrevrank score.fight USERID
Note: Ranks in Redis start at zero, so you need to add 1 to get the proper ranking.
Getting the Leaderboard
Now, we maybe want to display the top 10 rankings to the user. To retrieve a leaderboard, we can use the command zrevrange. The “rev” means that we order from highest to lowest. We want to retrieve the top 10, and since ranks are zero-indexed, we retrieve ranks 0 to 9.
zrevrange score.fight 0 9 WITHSCORES
Adding the option WITHSCORES gives us a list of tuples with the first entry being the player IDs and the second entry being their score. The entries are already in the correct (ranking) order.
All that's left now is to display these values to your players.
For more information on the Sorted Set, see here.
Leaderboards are an integral part of a game because they encourage competition between players. Redis is fantastic for this use case. Since it runs in-memory and has the specialised data type Sorted Set, it is ideally suited for a realtime leaderboard for a browser game.
Unfortunately, this specialisation comes at a price. Redis is not very well suited for relational data like you would often find in a browser game. Therefore, a hybrid approach is the best solution. Use Redis for the specialised scenarios it is good at (like a leaderboard) and leave the actual game data like a player's inventory to the databases that are good at handling these, such as MySQL, Postgres, MongoDB etc.
Directive Alpha originally started to learn Redis and therefore I'm using it as the only database. It's not exactly optimal as this has caused a few of situations where I ran into the limitations of Redis. For now, it's fine, I might rewrite some code in the future, but if I had to start over, I'd probably use a mix of SQL and Redis, each for the task they're best at.
If you haven't started playing yet, go to https://directivealpha.com and sign up now!