TL;DR

You read my Leaderboards: A How-To Guide post right? I revealed a dirty little secret about this world … we still don’t have flying cars. But, we do have a good way of creating leaderboards in video games. At least I think so. I want to continue on the leaderboard love train and expand on a concept I call “Meta Leaderboards”. Traditionally, leaderboards rank players using one criteria, e.g. XP, kills, etc. What if you wanted to combine leaderboards to form a larger, aggregate leaderboard? I’m going to show you how to do that.

WHOOMP! (THERE IT IS)

Let’s say you’ve got a game and its multiplayer mode can be played across 5 maps. We’ll also simplify things and say that we’re only ranking players on each map on XP gained when finishing the map. If you want to generate a leaderboard of players ranked by XP who have played in any of the maps, you would need to perform a merge of each of the leaderboards for the 5 maps. If you want to generate a leaderboard of players ranked by XP who have played in each of the maps, you would need to perform an intersection of each of the leaderboards for the 5 maps.

This functionality is now present in the Ruby leaderboard gem.

Check it to wreck it, let’s begin!

Generate a leaderboard of players ranked by XP who have played in any of the maps:

1
 
  2
 
  3
 
  4
 
  5
 
  6
 
  7
 
  8
 
  9
 
  10
 
  11
 
  12
 
  13
 
  14
 
  15
 
  16
 
  17
 
  18
 
  19
 
  20
 
  21
 
  22
 
  23
 
  24
 
  25
 
  26
 
  27
 
  28
 
  
ruby-1.8.7-p302 > map_1_xp_lb = Leaderboard.new('map_1_xp')
 
  => #, @host="localhost", @redis_options={:host=>"localhost", :port=>6379}>
 
  ruby-1.8.7-p302 > map_2_xp_lb = Leaderboard.new('map_2_xp')
 
  => #, @host="localhost", @redis_options={:host=>"localhost", :port=>6379}>
 
  ruby-1.8.7-p302 > map_3_xp_lb = Leaderboard.new('map_3_xp')
 
  => #, @host="localhost", @redis_options={:host=>"localhost", :port=>6379}>
 
  ruby-1.8.7-p302 > map_4_xp_lb = Leaderboard.new('map_4_xp')
 
  => #, @host="localhost", @redis_options={:host=>"localhost", :port=>6379}>
 
  ruby-1.8.7-p302 > map_5_xp_lb = Leaderboard.new('map_5_xp')
 
  => #, @host="localhost", @redis_options={:host=>"localhost", :port=>6379}>
 
  ruby-1.8.7-p302 > map_1_xp_lb.add_member('member_1', 10)
 
  => true
 
  ruby-1.8.7-p302 > map_2_xp_lb.add_member('member_2', 7)
 
  => true
 
  ruby-1.8.7-p302 > map_3_xp_lb.add_member('member_3', 7)
 
  => true
 
  ruby-1.8.7-p302 > map_4_xp_lb.add_member('member_4', 22)
 
  => true
 
  ruby-1.8.7-p302 > map_5_xp_lb.add_member('member_5', 3)
 
  => true
 
  ruby-1.8.7-p302 > map_1_xp_lb.merge_leaderboards('any_maps_xp_lb', ['map_2_xp', 'map_3_xp', 'map_4_xp', 'map_5_xp'])
 
  => 5
 
  ruby-1.8.7-p302 > any_maps_xp_lb = Leaderboard.new('any_maps_xp_lb')
 
  => #, @host="localhost", @redis_options={:host=>"localhost", :port=>6379}>
 
  ruby-1.8.7-p302 > any_maps_xp_lb.total_members
 
  => 5
 
  ruby-1.8.7-p302 > any_maps_xp_lb.leaders(1)
 
  => [{:rank=>1, :member=>"member_4", :score=>22.0}, {:rank=>2, :member=>"member_1", :score=>10.0}, {:rank=>3, :member=>"member_3", :score=>7.0}, {:rank=>4, :member=>"member_2", :score=>7.0}, {:rank=>5, :member=>"member_5", :score=>3.0}]

The default behavior for the merge leaderboards call is to sum the scores for members present in any of the leaderboards. You could also take a maximum or minimum score for members present in any of the leaderboards to generate a high score (or low score) leaderboard across the different maps.

Generate a leaderboard of players ranked by XP who have played in each of the maps:

1
 
  2
 
  3
 
  4
 
  5
 
  6
 
  7
 
  8
 
  9
 
  10
 
  11
 
  12
 
  13
 
  14
 
  15
 
  16
 
  17
 
  18
 
  19
 
  20
 
  21
 
  22
 
  23
 
  24
 
  25
 
  26
 
  27
 
  28
 
  29
 
  30
 
  31
 
  32
 
  33
 
  34
 
  35
 
  36
 
  37
 
  38
 
  39
 
  40
 
  41
 
  42
 
  43
 
  44
 
  45
 
  46
 
  47
 
  48
 
  49
 
  50
 
  51
 
  
ruby-1.9.2-p0 > map_1_xp_lb = Leaderboard.new('map_1_xp')
 
  => #"localhost", :port=>6379}, @redis_connection=#>
 
  ruby-1.9.2-p0 > map_2_xp_lb = Leaderboard.new('map_2_xp')
 
  => #"localhost", :port=>6379}, @redis_connection=#>
 
  ruby-1.9.2-p0 > map_3_xp_lb = Leaderboard.new('map_3_xp')
 
  => #"localhost", :port=>6379}, @redis_connection=#>
 
  ruby-1.9.2-p0 > map_4_xp_lb = Leaderboard.new('map_4_xp')
 
  => #"localhost", :port=>6379}, @redis_connection=#>
 
  ruby-1.9.2-p0 > map_5_xp_lb = Leaderboard.new('map_5_xp')
 
  => #"localhost", :port=>6379}, @redis_connection=#>
 
  ruby-1.9.2-p0 > map_1_xp_lb.add_member('member_1', 10)
 
  => true
 
  ruby-1.9.2-p0 > map_1_xp_lb.add_member('member_2', 19)
 
  => true
 
  ruby-1.9.2-p0 > map_2_xp_lb.add_member('member_2', 7)
 
  => true
 
  ruby-1.9.2-p0 > map_3_xp_lb.add_member('member_3', 7)
 
  => true
 
  ruby-1.9.2-p0 > map_3_xp_lb.add_member('member_4', 17)
 
  => true
 
  ruby-1.9.2-p0 > map_4_xp_lb.add_member('member_4', 22)
 
  => true
 
  ruby-1.9.2-p0 > map_4_xp_lb.add_member('member_5', 2)
 
  => true
 
  ruby-1.9.2-p0 > map_5_xp_lb.add_member('member_5', 3)
 
  => true
 
  ruby-1.9.2-p0 > map_1_xp_lb.intersect_leaderboards('all_maps_xp_lb', ['map_2_xp', 'map_3_xp', 'map_4_xp', 'map_5_xp'])
 
  => 0
 
  ruby-1.9.2-p0 > all_maps_xp_lb = Leaderboard.new('all_maps_xp_lb')
 
  => #"localhost", :port=>6379}, @redis_connection=#>
 
  ruby-1.9.2-p0 > all_maps_xp_lb.total_members
 
  => 0
 
  ruby-1.9.2-p0 > all_maps_xp_lb.leaders(1)
 
  => []
 
  ruby-1.9.2-p0 > map_2_xp_lb.add_member('member_1', 7)
 
  => true
 
  ruby-1.9.2-p0 > map_3_xp_lb.add_member('member_1', 8)
 
  => true
 
  ruby-1.9.2-p0 > map_4_xp_lb.add_member('member_1', 8)
 
  => true
 
  ruby-1.9.2-p0 > map_5_xp_lb.add_member('member_1', 2)
 
  => true
 
  ruby-1.9.2-p0 > map_1_xp_lb.intersect_leaderboards('all_maps_xp_lb', ['map_2_xp', 'map_3_xp', 'map_4_xp', 'map_5_xp'])
 
  => 1
 
  ruby-1.9.2-p0 > all_maps_xp_lb = Leaderboard.new('all_maps_xp_lb')
 
  => #"localhost", :port=>6379}, @redis_connection=#>
 
  ruby-1.9.2-p0 > all_maps_xp_lb.total_members
 
  => 1
 
  ruby-1.9.2-p0 > all_maps_xp_lb.leaders(1)
 
  => [{:member=>"member_1", :score=>35.0, :rank=>1}]
 
  ruby-1.9.2-p0 >

Notice in our first intersection here of the leaderboards, no one member has played in all of the different maps, so we have an empty leaderboard. After a member has played in all of the maps, we now have a populated leaderboard.

The default behavior for the intersect leaderboards call is to sum the scores for members present in any of the leaderboards. You could also take a maximum or minimum score for members present in any of the leaderboards to generate a high score (or low score) leaderboard across all of the different maps.

CAVEATS

Latin? Seriously?

When you create a merged or intersected leaderboard, this new leaderboard does not track changes made to any of the leaderboards that were combined. If you want to update the merged or intersected leaderboard, you have to perform the merge or intersection again. As such, generating meta leaderboards is something you probably want to do at some regular interval, not after every score update. Just make sure your users know when the leaderboard is going to be updated.

Unlike my previous post on leaderboards, I didn’t run any performance metrics on the merge or intersection operations. I know I need to do that at some point and I will. So … stay tuned?

FIN

Meta leaderboards are nothing more than an aggregate leaderboard. I’ve shown merge and intersection operations that allow you to create different types of meta leaderboards. Meta leaderboards can be generated on a regular, but not too frequent basis so as not to tax your servers too bad.

You can find more hilarity over on my Twitter account, @CzarneckiD.