leaderboard 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rvmrc +1 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +143 -0
- data/Rakefile +89 -0
- data/VERSION +1 -0
- data/leaderboard.gemspec +66 -0
- data/lib/leaderboard.rb +143 -0
- data/test/db/.gitkeep +0 -0
- data/test/helper.rb +17 -0
- data/test/test.conf +8 -0
- data/test/test_leaderboard.rb +169 -0
- metadata +140 -0
data/.document
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm 1.8.7
|
data/Gemfile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
# Add dependencies to develop your gem here.
|
4
|
+
# Include everything needed to run rake, tests, features, etc.
|
5
|
+
group :development do
|
6
|
+
gem "bundler", "~> 1.0.0"
|
7
|
+
gem "jeweler", "~> 1.5.2"
|
8
|
+
gem "rcov", ">= 0"
|
9
|
+
end
|
10
|
+
|
11
|
+
gem 'redis'
|
12
|
+
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 David Czarnecki
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
= leaderboard
|
2
|
+
|
3
|
+
Leaderboards backed by Redis, http://redis.io.
|
4
|
+
|
5
|
+
Builds off ideas proposed in http://blog.agoragames.com/2011/01/01/creating-high-score-tables-leaderboards-using-redis/.
|
6
|
+
|
7
|
+
== Installation
|
8
|
+
|
9
|
+
Install the gem:
|
10
|
+
|
11
|
+
gem install leaderboard
|
12
|
+
|
13
|
+
Make sure your redis server is running! Redis configuration is outside the scope of this README, but
|
14
|
+
check out the Redis documentation, http://redis.io/documentation.
|
15
|
+
|
16
|
+
== Usage
|
17
|
+
|
18
|
+
Create a new leaderboard or attach to an existing leaderboard named 'highscores':
|
19
|
+
|
20
|
+
ruby-1.8.7-p302 > highscore_lb = Leaderboard.new('highscores')
|
21
|
+
=> #<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">
|
22
|
+
|
23
|
+
Add members to your leaderboard:
|
24
|
+
|
25
|
+
ruby-1.8.7-p302 > 1.upto(10) do |index|
|
26
|
+
ruby-1.8.7-p302 > highscore_lb.add_member("member_#{index}", index)
|
27
|
+
ruby-1.8.7-p302 ?> end
|
28
|
+
=> 1
|
29
|
+
|
30
|
+
Get some information about your leaderboard:
|
31
|
+
|
32
|
+
ruby-1.8.7-p302 > highscore_lb.total_members
|
33
|
+
=> 10
|
34
|
+
ruby-1.8.7-p302 > highscore_lb.total_pages
|
35
|
+
=> 1
|
36
|
+
|
37
|
+
Get some information about a specific member(s) in the leaderboard:
|
38
|
+
|
39
|
+
ruby-1.8.7-p302 > highscore_lb.score_for('member_4')
|
40
|
+
=> 4.0
|
41
|
+
ruby-1.8.7-p302 > highscore_lb.rank_for('member_4')
|
42
|
+
=> 7
|
43
|
+
ruby-1.8.7-p302 > highscore_lb.rank_for('member_10')
|
44
|
+
=> 1
|
45
|
+
|
46
|
+
Get page 1 in the leaderboard:
|
47
|
+
|
48
|
+
ruby-1.8.7-p302 > highscore_lb.leaders(1)
|
49
|
+
=> [{: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"}]
|
50
|
+
|
51
|
+
Add more members to your leaderboard:
|
52
|
+
|
53
|
+
ruby-1.8.7-p302 > 50.upto(95) do |index|
|
54
|
+
ruby-1.8.7-p302 > highscore_lb.add_member("member_#{index}", index)
|
55
|
+
ruby-1.8.7-p302 ?> end
|
56
|
+
=> 50
|
57
|
+
ruby-1.8.7-p302 > highscore_lb.total_pages
|
58
|
+
=> 3
|
59
|
+
|
60
|
+
Get an "Around Me" leaderboard for a member:
|
61
|
+
|
62
|
+
ruby-1.8.7-p302 > highscore_lb.around_me('member_53')
|
63
|
+
=> [{: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"}]
|
64
|
+
|
65
|
+
Get rank and score for an arbitrary list of members (e.g. friends):
|
66
|
+
|
67
|
+
ruby-1.8.7-p302 > highscore_lb.ranked_in_list(['member_1', 'member_62', 'member_67'], true)
|
68
|
+
=> [{:rank=>55, :member=>"member_1", :score=>1.0}, {:rank=>33, :member=>"member_62", :score=>62.0}, {:rank=>28, :member=>"member_67", :score=>67.0}]
|
69
|
+
|
70
|
+
== Performance Metrics
|
71
|
+
|
72
|
+
10 million sequential scores insert:
|
73
|
+
|
74
|
+
ruby-1.8.7-p302 > insert_time = Benchmark.measure do
|
75
|
+
ruby-1.8.7-p302 > 1.upto(10000000) do |index|
|
76
|
+
ruby-1.8.7-p302 > highscore_lb.add_member("member_#{index}", index)
|
77
|
+
ruby-1.8.7-p302 ?> end
|
78
|
+
ruby-1.8.7-p302 ?> end
|
79
|
+
=> #<Benchmark::Tms:0x101605660 @label="", @stime=173.61, @total=577.52, @real=911.718175172806, @utime=403.91, @cstime=0.0, @cutime=0.0>
|
80
|
+
|
81
|
+
Average time to request an arbitrary page from the leaderboard:
|
82
|
+
|
83
|
+
ruby-1.8.7-p302 > requests_to_make = 50000
|
84
|
+
=> 50000
|
85
|
+
ruby-1.8.7-p302 > lb_request_time = 0
|
86
|
+
=> 0
|
87
|
+
ruby-1.8.7-p302 > 1.upto(requests_to_make) do
|
88
|
+
ruby-1.8.7-p302 > lb_request_time += Benchmark.measure do
|
89
|
+
ruby-1.8.7-p302 > highscore_lb.leaders(rand(highscore_lb.total_pages))
|
90
|
+
ruby-1.8.7-p302 ?> end.total
|
91
|
+
ruby-1.8.7-p302 ?> end
|
92
|
+
=> 1
|
93
|
+
ruby-1.8.7-p302 >
|
94
|
+
ruby-1.8.7-p302 > p lb_request_time / requests_to_make
|
95
|
+
0.001808
|
96
|
+
=> nil
|
97
|
+
|
98
|
+
10 million random scores insert:
|
99
|
+
|
100
|
+
ruby-1.8.7-p302 > insert_time = Benchmark.measure do
|
101
|
+
ruby-1.8.7-p302 > 1.upto(10000000) do |index|
|
102
|
+
ruby-1.8.7-p302 > highscore_lb.add_member("member_#{index}", rand(50000000))
|
103
|
+
ruby-1.8.7-p302 ?> end
|
104
|
+
ruby-1.8.7-p302 ?> end
|
105
|
+
=> #<Benchmark::Tms:0x10164ebf8 @label="", @stime=172.94, @total=577.91, @real=1356.57155895233, @utime=404.97, @cstime=0.0, @cutime=0.0>
|
106
|
+
|
107
|
+
Average time to request an arbitrary page from the leaderboard:
|
108
|
+
|
109
|
+
ruby-1.8.7-p302 > requests_to_make = 50000
|
110
|
+
=> 50000
|
111
|
+
ruby-1.8.7-p302 > lb_request_time = 0
|
112
|
+
=> 0
|
113
|
+
ruby-1.8.7-p302 > 1.upto(requests_to_make) do
|
114
|
+
ruby-1.8.7-p302 > lb_request_time += Benchmark.measure do
|
115
|
+
ruby-1.8.7-p302 > highscore_lb.leaders(rand(highscore_lb.total_pages))
|
116
|
+
ruby-1.8.7-p302 ?> end.total
|
117
|
+
ruby-1.8.7-p302 ?> end
|
118
|
+
=> 1
|
119
|
+
ruby-1.8.7-p302 >
|
120
|
+
ruby-1.8.7-p302 > p lb_request_time / requests_to_make
|
121
|
+
0.00179680000000001
|
122
|
+
=> nil
|
123
|
+
|
124
|
+
== Future Ideas
|
125
|
+
|
126
|
+
* Bulk insert
|
127
|
+
* Is nil? OK to return if Redis returns no data or should it be []?
|
128
|
+
|
129
|
+
== Contributing to leaderboard
|
130
|
+
|
131
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
132
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
133
|
+
* Fork the project
|
134
|
+
* Start a feature/bugfix branch
|
135
|
+
* Commit and push until you are happy with your contribution
|
136
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
137
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
138
|
+
|
139
|
+
== Copyright
|
140
|
+
|
141
|
+
Copyright (c) 2011 David Czarnecki. See LICENSE.txt for
|
142
|
+
further details.
|
143
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
15
|
+
gem.name = "leaderboard"
|
16
|
+
gem.homepage = "http://github.com/agoragames/leaderboard"
|
17
|
+
gem.license = "MIT"
|
18
|
+
gem.summary = %Q{Leaderboards backed by Redis}
|
19
|
+
gem.description = %Q{Leaderboards backed by Redis}
|
20
|
+
gem.email = "dczarnecki@agoragames.com"
|
21
|
+
gem.authors = ["David Czarnecki"]
|
22
|
+
# Include your dependencies below. Runtime dependencies are required when using your gem,
|
23
|
+
# and development dependencies are only needed for development (ie running rake tasks, tests, etc)
|
24
|
+
# gem.add_runtime_dependency 'jabber4r', '> 0.1'
|
25
|
+
# gem.add_development_dependency 'rspec', '> 1.2.3'
|
26
|
+
end
|
27
|
+
Jeweler::RubygemsDotOrgTasks.new
|
28
|
+
|
29
|
+
require 'rake/testtask'
|
30
|
+
Rake::TestTask.new(:test) do |test|
|
31
|
+
test.libs << 'lib' << 'test'
|
32
|
+
test.pattern = 'test/**/test_*.rb'
|
33
|
+
test.verbose = true
|
34
|
+
end
|
35
|
+
|
36
|
+
require 'rcov/rcovtask'
|
37
|
+
Rcov::RcovTask.new do |test|
|
38
|
+
test.libs << 'test'
|
39
|
+
test.pattern = 'test/**/test_*.rb'
|
40
|
+
test.verbose = true
|
41
|
+
end
|
42
|
+
|
43
|
+
require 'rake/rdoctask'
|
44
|
+
Rake::RDocTask.new do |rdoc|
|
45
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
46
|
+
|
47
|
+
rdoc.rdoc_dir = 'rdoc'
|
48
|
+
rdoc.title = "leaderboard #{version}"
|
49
|
+
rdoc.rdoc_files.include('README*')
|
50
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
51
|
+
end
|
52
|
+
|
53
|
+
REDIS_DIR = File.expand_path(File.join("..", "test"), __FILE__)
|
54
|
+
REDIS_CNF = File.join(REDIS_DIR, "test.conf")
|
55
|
+
REDIS_PID = File.join(REDIS_DIR, "db", "redis.pid")
|
56
|
+
REDIS_LOCATION = ENV['REDIS_LOCATION']
|
57
|
+
|
58
|
+
task :default => :run
|
59
|
+
|
60
|
+
desc "Run tests and manage server start/stop"
|
61
|
+
task :run => [:start, :test, :stop]
|
62
|
+
|
63
|
+
desc "Run rcov and manage server start/stop"
|
64
|
+
task :rcoverage => [:start, :rcov, :stop]
|
65
|
+
|
66
|
+
desc "Start the Redis server"
|
67
|
+
task :start do
|
68
|
+
redis_running = \
|
69
|
+
begin
|
70
|
+
File.exists?(REDIS_PID) && Process.kill(0, File.read(REDIS_PID).to_i)
|
71
|
+
rescue Errno::ESRCH
|
72
|
+
FileUtils.rm REDIS_PID
|
73
|
+
false
|
74
|
+
end
|
75
|
+
|
76
|
+
if REDIS_LOCATION
|
77
|
+
system "#{REDIS_LOCATION}/redis-server #{REDIS_CNF}" unless redis_running
|
78
|
+
else
|
79
|
+
system "redis-server #{REDIS_CNF}" unless redis_running
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
desc "Stop the Redis server"
|
84
|
+
task :stop do
|
85
|
+
if File.exists?(REDIS_PID)
|
86
|
+
Process.kill "INT", File.read(REDIS_PID).to_i
|
87
|
+
FileUtils.rm REDIS_PID
|
88
|
+
end
|
89
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
data/leaderboard.gemspec
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{leaderboard}
|
8
|
+
s.version = "1.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["David Czarnecki"]
|
12
|
+
s.date = %q{2011-01-05}
|
13
|
+
s.description = %q{Leaderboards backed by Redis}
|
14
|
+
s.email = %q{dczarnecki@agoragames.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".rvmrc",
|
22
|
+
"Gemfile",
|
23
|
+
"LICENSE.txt",
|
24
|
+
"README.rdoc",
|
25
|
+
"Rakefile",
|
26
|
+
"VERSION",
|
27
|
+
"leaderboard.gemspec",
|
28
|
+
"lib/leaderboard.rb",
|
29
|
+
"test/db/.gitkeep",
|
30
|
+
"test/helper.rb",
|
31
|
+
"test/test.conf",
|
32
|
+
"test/test_leaderboard.rb"
|
33
|
+
]
|
34
|
+
s.homepage = %q{http://github.com/agoragames/leaderboard}
|
35
|
+
s.licenses = ["MIT"]
|
36
|
+
s.require_paths = ["lib"]
|
37
|
+
s.rubygems_version = %q{1.3.7}
|
38
|
+
s.summary = %q{Leaderboards backed by Redis}
|
39
|
+
s.test_files = [
|
40
|
+
"test/helper.rb",
|
41
|
+
"test/test_leaderboard.rb"
|
42
|
+
]
|
43
|
+
|
44
|
+
if s.respond_to? :specification_version then
|
45
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
46
|
+
s.specification_version = 3
|
47
|
+
|
48
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
49
|
+
s.add_runtime_dependency(%q<redis>, [">= 0"])
|
50
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
51
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
|
52
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
53
|
+
else
|
54
|
+
s.add_dependency(%q<redis>, [">= 0"])
|
55
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
56
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
|
57
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
58
|
+
end
|
59
|
+
else
|
60
|
+
s.add_dependency(%q<redis>, [">= 0"])
|
61
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
62
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
|
63
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
data/lib/leaderboard.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
class Leaderboard
|
4
|
+
VERSION = '1.0.0'.freeze
|
5
|
+
DEFAULT_PAGE_SIZE = 25
|
6
|
+
|
7
|
+
attr_reader :host
|
8
|
+
attr_reader :port
|
9
|
+
attr_reader :leaderboard_name
|
10
|
+
attr_reader :page_size
|
11
|
+
|
12
|
+
def initialize(leaderboard_name, host = 'localhost', port = 6379, page_size = DEFAULT_PAGE_SIZE)
|
13
|
+
@leaderboard_name = leaderboard_name
|
14
|
+
@host = host
|
15
|
+
@port = port
|
16
|
+
|
17
|
+
if page_size < 1
|
18
|
+
page_size = DEFAULT_PAGE_SIZE
|
19
|
+
end
|
20
|
+
|
21
|
+
@page_size = page_size
|
22
|
+
|
23
|
+
@redis_connection = Redis.new(:host => @host, :port => @port)
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_member(member, score)
|
27
|
+
@redis_connection.zadd(@leaderboard_name, score, member)
|
28
|
+
end
|
29
|
+
|
30
|
+
def remove_member(member)
|
31
|
+
@redis_connection.zrem(@leaderboard_name, member)
|
32
|
+
end
|
33
|
+
|
34
|
+
def total_members
|
35
|
+
@redis_connection.zcard(@leaderboard_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
def total_pages
|
39
|
+
(total_members / @page_size.to_f).ceil
|
40
|
+
end
|
41
|
+
|
42
|
+
def total_members_in_score_range(min_score, max_score)
|
43
|
+
@redis_connection.zcount(@leaderboard_name, min_score, max_score)
|
44
|
+
end
|
45
|
+
|
46
|
+
def change_score_for(member, delta)
|
47
|
+
@redis_connection.zincrby(@leaderboard_name, delta, member)
|
48
|
+
end
|
49
|
+
|
50
|
+
def rank_for(member, use_zero_index_for_rank = false)
|
51
|
+
if use_zero_index_for_rank
|
52
|
+
return @redis_connection.zrevrank(@leaderboard_name, member)
|
53
|
+
else
|
54
|
+
return @redis_connection.zrevrank(@leaderboard_name, member) + 1 rescue nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def score_for(member)
|
59
|
+
@redis_connection.zscore(@leaderboard_name, member).to_f
|
60
|
+
end
|
61
|
+
|
62
|
+
def leaders(current_page, with_scores = true, with_rank = true, use_zero_index_for_rank = false)
|
63
|
+
if current_page < 1
|
64
|
+
current_page = 1
|
65
|
+
end
|
66
|
+
|
67
|
+
if current_page > total_pages
|
68
|
+
current_page = total_pages
|
69
|
+
end
|
70
|
+
|
71
|
+
index_for_redis = current_page - 1
|
72
|
+
|
73
|
+
starting_offset = (index_for_redis * @page_size)
|
74
|
+
if starting_offset < 0
|
75
|
+
starting_offset = 0
|
76
|
+
end
|
77
|
+
|
78
|
+
ending_offset = (starting_offset + @page_size) - 1
|
79
|
+
|
80
|
+
raw_leader_data = @redis_connection.zrevrange(@leaderboard_name, starting_offset, ending_offset, :with_scores => with_scores)
|
81
|
+
if raw_leader_data
|
82
|
+
massage_leader_data(raw_leader_data, with_rank, use_zero_index_for_rank)
|
83
|
+
else
|
84
|
+
return nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def around_me(member, with_scores = true, with_rank = true, use_zero_index_for_rank = false)
|
89
|
+
reverse_rank_for_member = @redis_connection.zrevrank(@leaderboard_name, member)
|
90
|
+
|
91
|
+
starting_offset = reverse_rank_for_member - (@page_size / 2)
|
92
|
+
if starting_offset < 0
|
93
|
+
starting_offset = 0
|
94
|
+
end
|
95
|
+
|
96
|
+
ending_offset = (starting_offset + @page_size) - 1
|
97
|
+
|
98
|
+
raw_leader_data = @redis_connection.zrevrange(@leaderboard_name, starting_offset, ending_offset, :with_scores => with_scores)
|
99
|
+
if raw_leader_data
|
100
|
+
massage_leader_data(raw_leader_data, with_rank, use_zero_index_for_rank)
|
101
|
+
else
|
102
|
+
return nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def ranked_in_list(members, with_scores = true, use_zero_index_for_rank = false)
|
107
|
+
ranks_for_members = []
|
108
|
+
|
109
|
+
members.each do |member|
|
110
|
+
data = {}
|
111
|
+
data[:member] = member
|
112
|
+
data[:rank] = rank_for(member, use_zero_index_for_rank)
|
113
|
+
data[:score] = score_for(member) if with_scores
|
114
|
+
|
115
|
+
ranks_for_members << data
|
116
|
+
end
|
117
|
+
|
118
|
+
ranks_for_members
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def massage_leader_data(leaders, with_rank, use_zero_index_for_rank)
|
124
|
+
member_attribute = true
|
125
|
+
leader_data = []
|
126
|
+
|
127
|
+
data = {}
|
128
|
+
leaders.each do |leader_data_item|
|
129
|
+
if member_attribute
|
130
|
+
data[:member] = leader_data_item
|
131
|
+
else
|
132
|
+
data[:score] = leader_data_item
|
133
|
+
data[:rank] = rank_for(data[:member], use_zero_index_for_rank) if with_rank
|
134
|
+
leader_data << data
|
135
|
+
data = {}
|
136
|
+
end
|
137
|
+
|
138
|
+
member_attribute = !member_attribute
|
139
|
+
end
|
140
|
+
|
141
|
+
leader_data
|
142
|
+
end
|
143
|
+
end
|
data/test/db/.gitkeep
ADDED
File without changes
|
data/test/helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'test/unit'
|
11
|
+
|
12
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
13
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
14
|
+
require 'leaderboard'
|
15
|
+
|
16
|
+
class Test::Unit::TestCase
|
17
|
+
end
|
data/test/test.conf
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestLeaderboard < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@leaderboard = Leaderboard.new('name')
|
6
|
+
@redis_connection = Redis.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def teardown
|
10
|
+
@redis_connection.flushdb
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_version
|
14
|
+
assert_equal '1.0.0', Leaderboard::VERSION
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_initialize_with_defaults
|
18
|
+
assert_equal 'name', @leaderboard.leaderboard_name
|
19
|
+
assert_equal 'localhost', @leaderboard.host
|
20
|
+
assert_equal 6379, @leaderboard.port
|
21
|
+
assert_equal Leaderboard::DEFAULT_PAGE_SIZE, @leaderboard.page_size
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_page_size_is_default_page_size_if_set_to_invalid_value
|
25
|
+
@leaderboard = Leaderboard.new('name', 'localhost', 6379, 0)
|
26
|
+
|
27
|
+
assert_equal Leaderboard::DEFAULT_PAGE_SIZE, @leaderboard.page_size
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_add_member_and_total_members
|
31
|
+
@leaderboard.add_member('member', 1)
|
32
|
+
|
33
|
+
assert_equal 1, @leaderboard.total_members
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_total_members_in_score_range
|
37
|
+
add_members_to_leaderboard(5)
|
38
|
+
|
39
|
+
assert_equal 3, @leaderboard.total_members_in_score_range(2, 4)
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_rank_for
|
43
|
+
add_members_to_leaderboard(5)
|
44
|
+
|
45
|
+
assert_equal 2, @leaderboard.rank_for('member_4')
|
46
|
+
assert_equal 1, @leaderboard.rank_for('member_4', true)
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_score_for
|
50
|
+
add_members_to_leaderboard(5)
|
51
|
+
|
52
|
+
assert_equal 4, @leaderboard.score_for('member_4')
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_total_pages
|
56
|
+
add_members_to_leaderboard(10)
|
57
|
+
|
58
|
+
assert_equal 1, @leaderboard.total_pages
|
59
|
+
|
60
|
+
@redis_connection.flushdb
|
61
|
+
|
62
|
+
add_members_to_leaderboard(Leaderboard::DEFAULT_PAGE_SIZE + 1)
|
63
|
+
|
64
|
+
assert_equal 2, @leaderboard.total_pages
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_leaders
|
68
|
+
add_members_to_leaderboard(25)
|
69
|
+
|
70
|
+
assert_equal 25, @leaderboard.total_members
|
71
|
+
|
72
|
+
leaders = @leaderboard.leaders(1)
|
73
|
+
|
74
|
+
assert_equal 25, leaders.size
|
75
|
+
assert_equal 'member_25', leaders[0][:member]
|
76
|
+
assert_equal 'member_2', leaders[-2][:member]
|
77
|
+
assert_equal 'member_1', leaders[-1][:member]
|
78
|
+
assert_equal 1, leaders[-1][:score].to_i
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_leaders_with_multiple_pages
|
82
|
+
add_members_to_leaderboard(Leaderboard::DEFAULT_PAGE_SIZE * 3 + 1)
|
83
|
+
|
84
|
+
assert_equal Leaderboard::DEFAULT_PAGE_SIZE * 3 + 1, @leaderboard.total_members
|
85
|
+
|
86
|
+
leaders = @leaderboard.leaders(1)
|
87
|
+
assert_equal @leaderboard.page_size, leaders.size
|
88
|
+
|
89
|
+
leaders = @leaderboard.leaders(2)
|
90
|
+
assert_equal @leaderboard.page_size, leaders.size
|
91
|
+
|
92
|
+
leaders = @leaderboard.leaders(3)
|
93
|
+
assert_equal @leaderboard.page_size, leaders.size
|
94
|
+
|
95
|
+
leaders = @leaderboard.leaders(4)
|
96
|
+
assert_equal 1, leaders.size
|
97
|
+
|
98
|
+
leaders = @leaderboard.leaders(-5)
|
99
|
+
assert_equal @leaderboard.page_size, leaders.size
|
100
|
+
|
101
|
+
leaders = @leaderboard.leaders(10)
|
102
|
+
assert_equal 1, leaders.size
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_around_me
|
106
|
+
add_members_to_leaderboard(Leaderboard::DEFAULT_PAGE_SIZE * 3 + 1)
|
107
|
+
|
108
|
+
assert_equal Leaderboard::DEFAULT_PAGE_SIZE * 3 + 1, @leaderboard.total_members
|
109
|
+
|
110
|
+
leaders_around_me = @leaderboard.around_me('member_30')
|
111
|
+
assert_equal @leaderboard.page_size / 2, leaders_around_me.size / 2
|
112
|
+
|
113
|
+
leaders_around_me = @leaderboard.around_me('member_1')
|
114
|
+
assert_equal @leaderboard.page_size / 2 + 1, leaders_around_me.size
|
115
|
+
|
116
|
+
leaders_around_me = @leaderboard.around_me('member_76')
|
117
|
+
assert_equal @leaderboard.page_size / 2, leaders_around_me.size / 2
|
118
|
+
end
|
119
|
+
|
120
|
+
def test_ranked_in_list
|
121
|
+
add_members_to_leaderboard(Leaderboard::DEFAULT_PAGE_SIZE)
|
122
|
+
|
123
|
+
assert_equal Leaderboard::DEFAULT_PAGE_SIZE, @leaderboard.total_members
|
124
|
+
|
125
|
+
members = ['member_1', 'member_5', 'member_10']
|
126
|
+
ranked_members = @leaderboard.ranked_in_list(members, true)
|
127
|
+
|
128
|
+
assert_equal 3, ranked_members.size
|
129
|
+
|
130
|
+
assert_equal 25, ranked_members[0][:rank]
|
131
|
+
assert_equal 1, ranked_members[0][:score]
|
132
|
+
|
133
|
+
assert_equal 21, ranked_members[1][:rank]
|
134
|
+
assert_equal 5, ranked_members[1][:score]
|
135
|
+
|
136
|
+
assert_equal 16, ranked_members[2][:rank]
|
137
|
+
assert_equal 10, ranked_members[2][:score]
|
138
|
+
end
|
139
|
+
|
140
|
+
def test_remove_member
|
141
|
+
add_members_to_leaderboard(Leaderboard::DEFAULT_PAGE_SIZE)
|
142
|
+
|
143
|
+
assert_equal Leaderboard::DEFAULT_PAGE_SIZE, @leaderboard.total_members
|
144
|
+
|
145
|
+
@leaderboard.remove_member('member_1')
|
146
|
+
|
147
|
+
assert_equal Leaderboard::DEFAULT_PAGE_SIZE - 1, @leaderboard.total_members
|
148
|
+
assert_nil @leaderboard.rank_for('member_1')
|
149
|
+
end
|
150
|
+
|
151
|
+
def test_change_score_for
|
152
|
+
@leaderboard.add_member('member_1', 5)
|
153
|
+
assert_equal 5, @leaderboard.score_for('member_1')
|
154
|
+
|
155
|
+
@leaderboard.change_score_for('member_1', 5)
|
156
|
+
assert_equal 10, @leaderboard.score_for('member_1')
|
157
|
+
|
158
|
+
@leaderboard.change_score_for('member_1', -5)
|
159
|
+
assert_equal 5, @leaderboard.score_for('member_1')
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
def add_members_to_leaderboard(members_to_add = 5)
|
165
|
+
1.upto(members_to_add) do |index|
|
166
|
+
@leaderboard.add_member("member_#{index}", index)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
metadata
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: leaderboard
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 1.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- David Czarnecki
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-01-05 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
name: redis
|
25
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 3
|
31
|
+
segments:
|
32
|
+
- 0
|
33
|
+
version: "0"
|
34
|
+
requirement: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
type: :development
|
37
|
+
prerelease: false
|
38
|
+
name: bundler
|
39
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 23
|
45
|
+
segments:
|
46
|
+
- 1
|
47
|
+
- 0
|
48
|
+
- 0
|
49
|
+
version: 1.0.0
|
50
|
+
requirement: *id002
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
type: :development
|
53
|
+
prerelease: false
|
54
|
+
name: jeweler
|
55
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ~>
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 7
|
61
|
+
segments:
|
62
|
+
- 1
|
63
|
+
- 5
|
64
|
+
- 2
|
65
|
+
version: 1.5.2
|
66
|
+
requirement: *id003
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
name: rcov
|
71
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
hash: 3
|
77
|
+
segments:
|
78
|
+
- 0
|
79
|
+
version: "0"
|
80
|
+
requirement: *id004
|
81
|
+
description: Leaderboards backed by Redis
|
82
|
+
email: dczarnecki@agoragames.com
|
83
|
+
executables: []
|
84
|
+
|
85
|
+
extensions: []
|
86
|
+
|
87
|
+
extra_rdoc_files:
|
88
|
+
- LICENSE.txt
|
89
|
+
- README.rdoc
|
90
|
+
files:
|
91
|
+
- .document
|
92
|
+
- .rvmrc
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.rdoc
|
96
|
+
- Rakefile
|
97
|
+
- VERSION
|
98
|
+
- leaderboard.gemspec
|
99
|
+
- lib/leaderboard.rb
|
100
|
+
- test/db/.gitkeep
|
101
|
+
- test/helper.rb
|
102
|
+
- test/test.conf
|
103
|
+
- test/test_leaderboard.rb
|
104
|
+
has_rdoc: true
|
105
|
+
homepage: http://github.com/agoragames/leaderboard
|
106
|
+
licenses:
|
107
|
+
- MIT
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options: []
|
110
|
+
|
111
|
+
require_paths:
|
112
|
+
- lib
|
113
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
hash: 3
|
119
|
+
segments:
|
120
|
+
- 0
|
121
|
+
version: "0"
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
none: false
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
hash: 3
|
128
|
+
segments:
|
129
|
+
- 0
|
130
|
+
version: "0"
|
131
|
+
requirements: []
|
132
|
+
|
133
|
+
rubyforge_project:
|
134
|
+
rubygems_version: 1.3.7
|
135
|
+
signing_key:
|
136
|
+
specification_version: 3
|
137
|
+
summary: Leaderboards backed by Redis
|
138
|
+
test_files:
|
139
|
+
- test/helper.rb
|
140
|
+
- test/test_leaderboard.rb
|