leaderboard

Leaderboards backed by Redis in Ruby, redis.io.

Builds off ideas proposed in blog.agoragames.com/2011/01/01/creating-high-score-tables-leaderboards-using-redis/.

Installation

Install the gem:

gem install leaderboard

Make sure your redis server is running! Redis configuration is outside the scope of this README, but check out the Redis documentation, redis.io/documentation.

Compatibility

The gem has been built and tested under Ruby 1.8.7 and Ruby 1.9.2

Usage

Create a new leaderboard or attach to an existing leaderboard named ‘highscores’:

ruby-1.8.7-p302 > highscore_lb = Leaderboard.new('highscores')
 => #<Leaderboard:0x1018e4250 @page_size=25, @port=6379, @host="localhost", @redis_connection=#<Redis client v2.1.1 connected to redis://localhost:6379/0 (Redis v2.1.10)>, @leaderboard_name="highscores">

If you need to pass in options for Redis, you can do this with the redis_options parameter:

redis_options = {:host => 'localhost', :port => 6379, :password => 'password', :db => 'some_redis_db'}
highscore_lb = Leaderboard.new('highscores', redis_options[:host], redis_options[:port], Leaderboard::DEFAULT_PAGE_SIZE, redis_options))

You can set the page size to something other than the default page size (25):

ruby-1.8.7-p302 > highscore_lb.page_size = 5
 => 5 
ruby-1.8.7-p302 > highscore_lb
 => #<Leaderboard:0x1018e2130 @leaderboard_name="highscores", @page_size=5, @port=6379, @redis_connection=#<Redis client v2.1.1 connected to redis://localhost:6379/0 (Redis v2.1.10)>, @host="localhost", @redis_options={:host=>"localhost", :port=>6379}>

Add members to your leaderboard:

ruby-1.8.7-p302 > 1.upto(10) do |index|
ruby-1.8.7-p302 >     highscore_lb.add_member("member_#{index}", index)
ruby-1.8.7-p302 ?>  end
 => 1

Get some information about your leaderboard:

ruby-1.8.7-p302 > highscore_lb.total_members
 => 10 
ruby-1.8.7-p302 > highscore_lb.total_pages
 => 1

Get some information about a specific member(s) in the leaderboard:

ruby-1.8.7-p302 > highscore_lb.score_for('member_4')
 => 4.0 
ruby-1.8.7-p302 > highscore_lb.rank_for('member_4')
 => 7 
ruby-1.8.7-p302 > highscore_lb.rank_for('member_10')
 => 1

Get page 1 in the leaderboard:

ruby-1.8.7-p302 > highscore_lb.leaders(1)
       => [{:member=>"member_10", :rank=>1, :score=>"10"}, {:member=>"member_9", :rank=>2, :score=>"9"}, {:member=>"member_8", :rank=>3, :score=>"8"}, {:member=>"member_7", :rank=>4, :score=>"7"}, {:member=>"member_6", :rank=>5, :score=>"6"}, {:member=>"member_5", :rank=>6, :score=>"5"}, {:member=>"member_4", :rank=>7, :score=>"4"}, {:member=>"member_3", :rank=>8, :score=>"3"}, {:member=>"member_2", :rank=>9, :score=>"2"}, {:member=>"member_1", :rank=>10, :score=>"1"}]

Add more members to your leaderboard:

ruby-1.8.7-p302 > 50.upto(95) do |index|
ruby-1.8.7-p302 >     highscore_lb.add_member("member_#{index}", index)
ruby-1.8.7-p302 ?>  end
 => 50 
ruby-1.8.7-p302 > highscore_lb.total_pages
 => 3

Get an “Around Me” leaderboard for a member:

ruby-1.8.7-p302 > highscore_lb.around_me('member_53')
       => [{:member=>"member_65", :rank=>31, :score=>"65"}, {:member=>"member_64", :rank=>32, :score=>"64"}, {:member=>"member_63", :rank=>33, :score=>"63"}, {:member=>"member_62", :rank=>34, :score=>"62"}, {:member=>"member_61", :rank=>35, :score=>"61"}, {:member=>"member_60", :rank=>36, :score=>"60"}, {:member=>"member_59", :rank=>37, :score=>"59"}, {:member=>"member_58", :rank=>38, :score=>"58"}, {:member=>"member_57", :rank=>39, :score=>"57"}, {:member=>"member_56", :rank=>40, :score=>"56"}, {:member=>"member_55", :rank=>41, :score=>"55"}, {:member=>"member_54", :rank=>42, :score=>"54"}, {:member=>"member_53", :rank=>43, :score=>"53"}, {:member=>"member_52", :rank=>44, :score=>"52"}, {:member=>"member_51", :rank=>45, :score=>"51"}, {:member=>"member_50", :rank=>46, :score=>"50"}, {:member=>"member_10", :rank=>47, :score=>"10"}, {:member=>"member_9", :rank=>48, :score=>"9"}, {:member=>"member_8", :rank=>49, :score=>"8"}, {:member=>"member_7", :rank=>50, :score=>"7"}, {:member=>"member_6", :rank=>51, :score=>"6"}, {:member=>"member_5", :rank=>52, :score=>"5"}, {:member=>"member_4", :rank=>53, :score=>"4"}, {:member=>"member_3", :rank=>54, :score=>"3"}, {:member=>"member_2", :rank=>55, :score=>"2"}]

Get rank and score for an arbitrary list of members (e.g. friends):

ruby-1.8.7-p302 > highscore_lb.ranked_in_list(['member_1', 'member_62', 'member_67'], true)
 => [{:rank=>55, :member=>"member_1", :score=>1.0}, {:rank=>33, :member=>"member_62", :score=>62.0}, {:rank=>28, :member=>"member_67", :score=>67.0}]

Other useful methods

remove_member(member): Remove a member from the leaderboard
total_members_in_score_range(min_score, max_score): Count the number of members within a score range in the leaderboard
change_score_for(member, delta): Change the score for a member by some amount delta (delta could be positive or negative)
check_member?(member): Check to see whether member is in the leaderboard
score_and_rank_for(member, use_zero_index_for_rank = false): Retrieve the score and rank for a member in a single call
remove_members_in_score_range(min_score, max_score): Remove members from the leaderboard within a score range
merge_leaderboards(destination, keys, options = {:aggregate => :min}): Merge leaderboards given by keys with this leaderboard into destination
intersect_leaderboards(destination, keys, options = {:aggregate => :min}): Intersect leaderboards given by keys with this leaderboard into destination

Performance Metrics

10 million sequential scores insert:

ruby-1.8.7-p302 > insert_time = Benchmark.measure do
ruby-1.8.7-p302 >       1.upto(10000000) do |index|
ruby-1.8.7-p302 >           highscore_lb.add_member("member_#{index}", index)
ruby-1.8.7-p302 ?>      end
ruby-1.8.7-p302 ?>  end
 => #<Benchmark::Tms:0x101605660 @label="", @stime=173.61, @total=577.52, @real=911.718175172806, @utime=403.91, @cstime=0.0, @cutime=0.0>

Average time to request an arbitrary page from the leaderboard:

ruby-1.8.7-p302 > requests_to_make = 50000
 => 50000 
ruby-1.8.7-p302 > lb_request_time = 0
 => 0 
ruby-1.8.7-p302 > 1.upto(requests_to_make) do
ruby-1.8.7-p302 >       lb_request_time += Benchmark.measure do
ruby-1.8.7-p302 >           highscore_lb.leaders(rand(highscore_lb.total_pages))
ruby-1.8.7-p302 ?>      end.total
ruby-1.8.7-p302 ?>  end
 => 1 
ruby-1.8.7-p302 > 
ruby-1.8.7-p302 >   p lb_request_time / requests_to_make
0.001808
 => nil

10 million random scores insert:

ruby-1.8.7-p302 > insert_time = Benchmark.measure do
ruby-1.8.7-p302 >       1.upto(10000000) do |index|
ruby-1.8.7-p302 >           highscore_lb.add_member("member_#{index}", rand(50000000))
ruby-1.8.7-p302 ?>      end
ruby-1.8.7-p302 ?>  end
 => #<Benchmark::Tms:0x10164ebf8 @label="", @stime=172.94, @total=577.91, @real=1356.57155895233, @utime=404.97, @cstime=0.0, @cutime=0.0>

Average time to request an arbitrary page from the leaderboard:

ruby-1.8.7-p302 > requests_to_make = 50000
 => 50000 
ruby-1.8.7-p302 > lb_request_time = 0
 => 0 
ruby-1.8.7-p302 > 1.upto(requests_to_make) do
ruby-1.8.7-p302 >       lb_request_time += Benchmark.measure do
ruby-1.8.7-p302 >           highscore_lb.leaders(rand(highscore_lb.total_pages))
ruby-1.8.7-p302 ?>      end.total
ruby-1.8.7-p302 ?>  end
 => 1 
ruby-1.8.7-p302 > 
ruby-1.8.7-p302 >   p lb_request_time / requests_to_make
0.00179680000000001
 => nil

Future Ideas

Contributing to leaderboard

Copyright © 2011 David Czarnecki. See LICENSE.txt for further details.