acts_as_elo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +7 -0
- data/Gemfile +5 -0
- data/README.md +35 -0
- data/Rakefile +16 -0
- data/acts_as_elo.gemspec +21 -0
- data/init.rb +2 -0
- data/lib/acts_as_elo/acts/elo.rb +73 -0
- data/lib/acts_as_elo.rb +1 -0
- data/test/advance_test.rb +46 -0
- data/test/basic_test.rb +51 -0
- data/test/helper.rb +12 -0
- metadata +82 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
## ActsAsElo
|
2
|
+
|
3
|
+
### What is it?
|
4
|
+
|
5
|
+
Provides sophisticated yet easy to understand ranking system with minimum changes to the system.
|
6
|
+
Elo ranking system is heavily used in tournaments, player rankings in chess, sports, leader boards, multiplayer games, ranking of test questions, tasks etc.
|
7
|
+
|
8
|
+
Read more about it on [wiki](http://en.wikipedia.org/wiki/Elo_rating_system)
|
9
|
+
|
10
|
+
### Usage
|
11
|
+
|
12
|
+
`acts_as_elo` is very easy to use
|
13
|
+
|
14
|
+
1. Install: `gem install acts_as_elo`
|
15
|
+
2. Add `include Acts::Elo` and `acts_as_elo` to your model/record
|
16
|
+
|
17
|
+
### Example
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
|
21
|
+
class Player
|
22
|
+
include Acts::Elo
|
23
|
+
acts_as_elo
|
24
|
+
end
|
25
|
+
|
26
|
+
bob, jack = Player.new, Player.new
|
27
|
+
bob.elo_rank # => 1200
|
28
|
+
jack.elo_rank # => 1200
|
29
|
+
|
30
|
+
bob.elo_win!(jack)
|
31
|
+
bob.elo_rank # => 1205
|
32
|
+
jack.elo_rank # => 1995
|
33
|
+
```
|
34
|
+
|
35
|
+
That's it. No additional classes are involved.
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
#require 'rake'
|
5
|
+
require 'rake/testtask'
|
6
|
+
|
7
|
+
# Run the test with 'rake' or 'rake test'
|
8
|
+
desc 'Default: run acts_as_elo unit tests.'
|
9
|
+
task :default => :test
|
10
|
+
|
11
|
+
desc 'Test the acts_as_list plugin.'
|
12
|
+
Rake::TestTask.new(:test) do |t|
|
13
|
+
t.libs << 'lib' << 'test'
|
14
|
+
t.pattern = 'test/**/*_test.rb'
|
15
|
+
t.verbose = true
|
16
|
+
end
|
data/acts_as_elo.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
$:.push File.expand_path('../lib', __FILE__)
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'acts_as_elo'
|
5
|
+
s.version = '0.1.0'
|
6
|
+
s.platform = Gem::Platform::RUBY
|
7
|
+
s.authors = ['tjbladez']
|
8
|
+
s.email = ['nick@tjbladez.net']
|
9
|
+
s.homepage = 'http://github.com/tjbladez/acts_as_elo'
|
10
|
+
s.summary = %q{Elo ranking made easy}
|
11
|
+
s.description = %q{Provides sophisticated yet easy to understand ranking system with minimum changes to the system}
|
12
|
+
|
13
|
+
# Load Paths...
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.require_paths = ['lib']
|
17
|
+
|
18
|
+
# Dependencies (installed via 'bundle install')...
|
19
|
+
s.add_development_dependency("bundler", ["~> 1.0.0"])
|
20
|
+
s.add_development_dependency("riot")
|
21
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
module Acts
|
2
|
+
module Elo
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def acts_as_elo(opts = {})
|
9
|
+
default_rank = opts[:default_rank] || 1200
|
10
|
+
|
11
|
+
unless opts.empty?
|
12
|
+
class << self
|
13
|
+
attr_accessor :acts_as_elo_options
|
14
|
+
end
|
15
|
+
@acts_as_elo_options = opts
|
16
|
+
end
|
17
|
+
|
18
|
+
class_eval do
|
19
|
+
attr_writer :elo_rank
|
20
|
+
|
21
|
+
define_method(:elo_rank) do
|
22
|
+
@elo_rank ||= default_rank
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
include Acts::Elo::InstanceMethods unless self.included_modules.include?(Acts::Elo::InstanceMethods)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module InstanceMethods
|
31
|
+
def elo_win!(opponent, opts={})
|
32
|
+
elo_update(opponent, opts.merge!(:result => :win))
|
33
|
+
end
|
34
|
+
|
35
|
+
def elo_lose!(opponent, opts={})
|
36
|
+
elo_update(opponent, opts.merge!(:result => :lose))
|
37
|
+
end
|
38
|
+
|
39
|
+
def elo_draw!(opponent, opts={})
|
40
|
+
elo_update(opponent, opts.merge!(:result => :draw))
|
41
|
+
end
|
42
|
+
|
43
|
+
def elo_update(opponent, opts={})
|
44
|
+
begin
|
45
|
+
if self.class.respond_to?(:acts_as_elo_options) && self.class.acts_as_elo_options[:one_way]
|
46
|
+
one_way = true
|
47
|
+
end
|
48
|
+
one_way ||= opts[:one_way]
|
49
|
+
diff = (opponent.elo_rank.to_f - elo_rank.to_f).abs
|
50
|
+
expected = 1 / (1 + 10 ** (diff / 400))
|
51
|
+
send_opts = {one_way: true}
|
52
|
+
|
53
|
+
if opts[:result] == :win
|
54
|
+
points = 1
|
55
|
+
send_opts.merge!(result: :lose)
|
56
|
+
elsif opts[:result] == :lose
|
57
|
+
points = 0
|
58
|
+
send_opts.merge!(result: :win)
|
59
|
+
elsif opts[:result] == :draw
|
60
|
+
points = 0.5
|
61
|
+
send_opts.merge!(result: :draw)
|
62
|
+
end
|
63
|
+
|
64
|
+
opponent.elo_update(self, send_opts) unless one_way
|
65
|
+
|
66
|
+
@elo_rank = (elo_rank + 10*(points-expected)).round
|
67
|
+
rescue Exception => e
|
68
|
+
puts "Exception: #{e.message}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/acts_as_elo.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'acts_as_elo/acts/elo'
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require Pathname.new('.') + 'helper'
|
3
|
+
|
4
|
+
# When user answers the question based on the on current rank of the user
|
5
|
+
# and current rank of the question both user and question ranks will be updated
|
6
|
+
class User
|
7
|
+
include Acts::Elo
|
8
|
+
|
9
|
+
attr_accessor :questions
|
10
|
+
|
11
|
+
acts_as_elo :one_way => true
|
12
|
+
end
|
13
|
+
|
14
|
+
class Question
|
15
|
+
include Acts::Elo
|
16
|
+
|
17
|
+
attr_accessor :user
|
18
|
+
acts_as_elo
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
context "User with many questions" do
|
23
|
+
helper(:question) { |user| Question.new.tap {|q| q.user = user} }
|
24
|
+
|
25
|
+
setup { User.new }
|
26
|
+
hookup { topic.questions = [question(topic), question(topic)] }
|
27
|
+
|
28
|
+
asserts(:elo_rank).equals(1200)
|
29
|
+
asserts("questions have default ranks"){ topic.questions.map(&:elo_rank)}.equals([1200, 1200])
|
30
|
+
|
31
|
+
context "when question is answered correctly" do
|
32
|
+
hookup { topic.questions.each {|q| q.elo_rank = 1200 }}
|
33
|
+
hookup { topic.questions.first.elo_lose!(topic)}
|
34
|
+
|
35
|
+
asserts(:elo_rank).equals(1205)
|
36
|
+
asserts("questions have their ranks updated"){ topic.questions.map(&:elo_rank)}.equals([1195, 1200])
|
37
|
+
end
|
38
|
+
|
39
|
+
context "when question is answered incorrectly" do
|
40
|
+
hookup { topic.questions.each {|q| q.elo_rank = 1200 }}
|
41
|
+
hookup { topic.questions.first.elo_win!(topic) }
|
42
|
+
|
43
|
+
asserts(:elo_rank).equals(1195)
|
44
|
+
asserts("questions have their ranks updated"){ topic.questions.map(&:elo_rank)}.equals([1205, 1200])
|
45
|
+
end
|
46
|
+
end
|
data/test/basic_test.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require Pathname.new('.') + 'helper'
|
3
|
+
|
4
|
+
class Player
|
5
|
+
include Acts::Elo
|
6
|
+
|
7
|
+
acts_as_elo
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
context "Two players are opponents of each other" do
|
12
|
+
setup { [Player.new, Player.new] }
|
13
|
+
|
14
|
+
asserts("default ranks are 1200") {
|
15
|
+
topic.map(&:elo_rank)
|
16
|
+
}.equals([1200, 1200])
|
17
|
+
|
18
|
+
context "when first one wins" do
|
19
|
+
hookup {topic.each {|pl| pl.elo_rank = 1200 }}
|
20
|
+
hookup {topic.first.elo_win!(topic.last)}
|
21
|
+
|
22
|
+
asserts("ranks are updated") {topic.map(&:elo_rank)}.equals([1205, 1195])
|
23
|
+
end
|
24
|
+
|
25
|
+
context "when first one loses" do
|
26
|
+
hookup {topic.each {|pl| pl.elo_rank = 1200 }}
|
27
|
+
hookup {topic.first.elo_lose!(topic.last)}
|
28
|
+
|
29
|
+
asserts("ranks are updated") {topic.map(&:elo_rank)}.equals([1195, 1205])
|
30
|
+
end
|
31
|
+
|
32
|
+
context "when they draw" do
|
33
|
+
hookup {topic.each {|pl| pl.elo_rank = 1200 }}
|
34
|
+
hookup {topic.first.elo_draw!(topic.last)}
|
35
|
+
|
36
|
+
asserts("ranks are updated") {topic.map(&:elo_rank)}.equals([1200, 1200])
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
class PlayerWithDefaultRank
|
42
|
+
include Acts::Elo
|
43
|
+
acts_as_elo :default_rank => 1300
|
44
|
+
end
|
45
|
+
|
46
|
+
context "Testing default for elo rank" do
|
47
|
+
setup { PlayerWithDefaultRank.new }
|
48
|
+
asserts(:elo_rank).equals(1300)
|
49
|
+
end
|
50
|
+
|
51
|
+
|
data/test/helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
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
|
+
|
11
|
+
require 'riot'
|
12
|
+
require "#{File.dirname(__FILE__)}/../init"
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acts_as_elo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- tjbladez
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-11 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: &70308114821680 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.0
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70308114821680
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: riot
|
27
|
+
requirement: &70308114821280 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70308114821280
|
36
|
+
description: Provides sophisticated yet easy to understand ranking system with minimum
|
37
|
+
changes to the system
|
38
|
+
email:
|
39
|
+
- nick@tjbladez.net
|
40
|
+
executables: []
|
41
|
+
extensions: []
|
42
|
+
extra_rdoc_files: []
|
43
|
+
files:
|
44
|
+
- .gitignore
|
45
|
+
- Gemfile
|
46
|
+
- README.md
|
47
|
+
- Rakefile
|
48
|
+
- acts_as_elo.gemspec
|
49
|
+
- init.rb
|
50
|
+
- lib/acts_as_elo.rb
|
51
|
+
- lib/acts_as_elo/acts/elo.rb
|
52
|
+
- test/advance_test.rb
|
53
|
+
- test/basic_test.rb
|
54
|
+
- test/helper.rb
|
55
|
+
homepage: http://github.com/tjbladez/acts_as_elo
|
56
|
+
licenses: []
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ! '>='
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ! '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubyforge_project:
|
75
|
+
rubygems_version: 1.8.10
|
76
|
+
signing_key:
|
77
|
+
specification_version: 3
|
78
|
+
summary: Elo ranking made easy
|
79
|
+
test_files:
|
80
|
+
- test/advance_test.rb
|
81
|
+
- test/basic_test.rb
|
82
|
+
- test/helper.rb
|