elo_rating 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9b56895aa2ec414462c5b2480ed2ebe93fa9f642
4
+ data.tar.gz: 50b2f10121650d74f5344d28eb440663d3b3a672
5
+ SHA512:
6
+ metadata.gz: 7bd15401b85f7c9b8a0b5414ed45d44755afbb70b15b3308ca5cede8f41775f108b00763a3521ce6b2a9856673ea39e3e5b94b3ed41cd79eb6a493d3c19d8d1a
7
+ data.tar.gz: 63b7eb3a19af7eccca15b9c07c9c93ee0d77b01b142d25dbcb17fc5575d8ddecaa3460f94f65cd292df50aa7dd3ca1327153886f2f63e0c13e4334c8b8e5faa6
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Maxwell Holder
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # elo_rating
2
+
3
+ `elo_rating` helps you calculate [Elo ratings](https://en.wikipedia.org/wiki/Elo_rating_system), a rating system used primary for Chess.
4
+
5
+ It can handle multiple players in one game and allows for custom K-factor functions.
6
+
7
+ - [API Documentation]()
8
+
9
+ ## Getting started
10
+
11
+ ```ruby
12
+ gem install elo_rating
13
+ ```
14
+
15
+ or add it to your Gemfile and run `bundle`:
16
+
17
+ ```ruby
18
+ gem 'elo_rating'
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Say you have two players, both have ratings of 2000. The second player wins. To
24
+ determine both player's updated ratings:
25
+
26
+
27
+ ```ruby
28
+ match = EloRating::Match.new
29
+ match.add_player(rating: 2000)
30
+ match.add_player(rating: 2000, winner: true)
31
+ match.updated_ratings # => [1988, 2012]
32
+ ```
33
+
34
+ The first player's rating goes down 12 points and the second player's goes up 12 points.
35
+
36
+ ### >2 players
37
+
38
+ Most Elo rating calculators only allow for matches of just 2 players, but the
39
+ formula can be extended games with more players by calculating rating adjustments
40
+ for each player against each of their opponents.
41
+
42
+ Say you have three players, rated 1900, 2000, and 2000. They are playing a game
43
+ like [Monopoly](https://en.wikipedia.org/wiki/Monopoly_(game)) where there is
44
+ only one winner. The third player wins.
45
+ To determine their new scores:
46
+
47
+ ```ruby
48
+ match = EloRating::Match.new
49
+ match.add_player(rating: 1900, winner: true)
50
+ match.add_player(rating: 2000)
51
+ match.add_player(rating: 2000)
52
+ match.updated_ratings # => [1931, 1985, 1985]
53
+ ```
54
+
55
+ ### Ranked games
56
+
57
+ Some games like [Mario Kart](https://en.wikipedia.org/wiki/Mario_Kart) have
58
+ multiple, ranked winners. Let's say you have three players like before, rated
59
+ 1900, 2000, and 2000. Instead of indicating the winner, you can specify the
60
+ ranks:
61
+
62
+ ```ruby
63
+ match = EloRating::Match.new
64
+ match.add_player(rating: 1900, rank: 1)
65
+ match.add_player(rating: 2000, rank: 2)
66
+ match.add_player(rating: 2000, rank: 3)
67
+ match.updated_ratings # => [1973, 1997, 1931]
68
+ ```
69
+
70
+ ## Elo rating functions
71
+
72
+ The functions used in the above calculations are available for use directly:
73
+
74
+ ### Expected score
75
+
76
+ Say you have 2 players, rated 1900 and 2000.
77
+
78
+ ```ruby
79
+ EloRating.expected_score(1900, 2000) # => 0.360
80
+ ```
81
+
82
+ The player rated 1900 has a 36% chance of winning.
83
+
84
+ ### Rating adjustment
85
+
86
+ You can use the expected score and the results of an actual match to determine
87
+ how an Elo rating should change.
88
+
89
+ Let's say we have the expected rating from above of 0.36. If the first player rated 1900 won the match, their actual score would be 1.
90
+
91
+ We can use this to determine how much their rating should change:
92
+
93
+ ```ruby
94
+ EloRating.rating_adjustment(0.36, 1) # => 15.36
95
+ ```
96
+
97
+ Their rating should go up about 15 points if they win.
98
+
99
+ ## K-factor
100
+
101
+ The K-factor is used in calculating the rating adjustment and determines how much impact the most recent game has on a player's rating.
102
+
103
+ It defaults to 24:
104
+
105
+ ```ruby
106
+ EloRating::k_factor # => 24
107
+ ```
108
+
109
+ You can change this to any number. With a lower K-factor, ratings are less volatile and change slower:
110
+
111
+ ```ruby
112
+ EloRating::k_factor = 10
113
+ match = EloRating::Match.new
114
+ match.add_player(rating: 2000, winner: true)
115
+ match.add_player(rating: 2000)
116
+ match.updated_ratings # => [1995, 2005]
117
+ ```
118
+
119
+ You can also pass a block to provide a custom function to calculate the K-factor
120
+ based on the player's rating:
121
+
122
+ ```ruby
123
+ EloRating::set_k_factor do |rating|
124
+ rating ||= 2000
125
+ if rating < 2100
126
+ 32
127
+ elsif 2100 <= rating && rating <= 2400
128
+ 24
129
+ else
130
+ 16
131
+ end
132
+ end
133
+ ```
134
+
135
+ You can provide a rating to `EloRating.rating_adjustment` that will be used in
136
+ your custom K-factor function:
137
+
138
+ ```ruby
139
+ EloRating.rating_adjustment(0.75, 0) # => -24.0
140
+ EloRating.rating_adjustment(0.75, 0, rating: 2200) # => -18.0
141
+ EloRating.rating_adjustment(0.75, 0, rating: 2500) # => -12.0
142
+ ```
143
+
144
+ You can also specify a K-factor for a single rating adjustment:
145
+
146
+ ```ruby
147
+ EloRating.rating_adjustment(0.75, 0, k_factor: 24) # => -18.0
148
+ ```
149
+
150
+ Note: custom K-factor functions must not raise any exceptions when the rating is nil:
151
+
152
+ ```ruby
153
+ EloRating::set_k_factor do |rating|
154
+ rating / 100
155
+ end
156
+ # => ArgumentError: Error encountered in K-factor block when passed nil rating: undefined method `/' for nil:NilClass
157
+ ```
@@ -0,0 +1,118 @@
1
+ ##
2
+ # This class represents a single game between a number of players.
3
+ class EloRating::Match
4
+
5
+ attr_reader :players
6
+
7
+ # Creates a new match
8
+ def initialize
9
+ @players = []
10
+ end
11
+
12
+ # Adds a player to the match
13
+ #
14
+ # ==== Attributes
15
+ # * +rating+: the Elo rating of the player
16
+ # * +winner+ (optional): boolean, whether this player is the winner of the match
17
+ # * +place+ (optional): a number representing the rank of the player within the match; the lower the number, the higher they placed
18
+ #
19
+ # Raises an +ArgumentError+ if the rating or place is not numeric, or if
20
+ # both winner and place is specified.
21
+ def add_player(player_attributes)
22
+ players << Player.new(player_attributes.merge(match: self))
23
+ self
24
+ end
25
+
26
+ # Calculates the updated ratings for each of the players in the match.
27
+ #
28
+ # Raises an +ArgumentError+ if more than one player is marked as the winner or
29
+ # if some but not all players have +place+ specified.
30
+ def updated_ratings
31
+ validate_players!
32
+ players.map(&:updated_rating)
33
+ end
34
+
35
+ private
36
+
37
+ def validate_players!
38
+ raise ArgumentError, 'Only one player can be the winner' if multiple_winners?
39
+ raise ArgumentError, 'All players must have places if any do' if inconsistent_places?
40
+ end
41
+
42
+ def multiple_winners?
43
+ players.select { |player| player.winner? }.size > 1
44
+ end
45
+
46
+ def inconsistent_places?
47
+ players.select { |player| player.place }.any? &&
48
+ players.select { |player| !player.place }.any?
49
+ end
50
+
51
+ class Player
52
+ # :nodoc:
53
+ attr_reader :rating, :place, :match
54
+ def initialize(match:, rating:, place: nil, winner: nil)
55
+ validate_attributes!(rating: rating, place: place, winner: winner)
56
+ @match = match
57
+ @rating = rating
58
+ @place = place
59
+ @winner = winner
60
+ end
61
+
62
+ def winner?
63
+ @winner
64
+ end
65
+
66
+ def validate_attributes!(rating:, place:, winner:)
67
+ raise ArgumentError, 'Rating must be numeric' unless rating.is_a? Numeric
68
+ raise ArgumentError, 'Winner and place cannot both be specified' if place && winner
69
+ raise ArgumentError, 'Place must be numeric' unless place.nil? || place.is_a?(Numeric)
70
+ end
71
+
72
+ def opponents
73
+ match.players - [self]
74
+ end
75
+
76
+ def updated_rating
77
+ (rating + total_rating_adjustments).round
78
+ end
79
+
80
+ def total_rating_adjustments
81
+ opponents.map do |opponent|
82
+ rating_adjustment_against(opponent)
83
+ end.reduce(0, :+)
84
+ end
85
+
86
+ def rating_adjustment_against(opponent)
87
+ EloRating.rating_adjustment(
88
+ expected_score_against(opponent),
89
+ actual_score_against(opponent)
90
+ )
91
+ end
92
+
93
+ def expected_score_against(opponent)
94
+ EloRating.expected_score(rating, opponent.rating)
95
+ end
96
+
97
+ def actual_score_against(opponent)
98
+ if won_against?(opponent)
99
+ 1
100
+ elsif opponent.won_against?(self)
101
+ 0
102
+ else # draw
103
+ 0.5
104
+ end
105
+ end
106
+
107
+ def won_against?(opponent)
108
+ winner? || placed_ahead_of?(opponent)
109
+ end
110
+
111
+ def placed_ahead_of?(opponent)
112
+ if place && opponent.place
113
+ place < opponent.place
114
+ end
115
+ end
116
+ end
117
+ end
118
+
data/lib/elo_rating.rb ADDED
@@ -0,0 +1,73 @@
1
+ # :main: README.md
2
+
3
+ ##
4
+ # EloRating helps you calculate Elo ratings.
5
+ #
6
+ # See the README for an introduction.
7
+ module EloRating
8
+
9
+ # Default K-factor.
10
+ @k_factor = Proc.new do |rating|
11
+ 24
12
+ end
13
+
14
+ # Calls the K-factor function with the provided rating.
15
+ def self.k_factor(rating = nil)
16
+ @k_factor.call(rating)
17
+ end
18
+
19
+ # Sets the K-factor by providing a block that optionally takes a +rating+
20
+ # argument:
21
+ #
22
+ # EloRating::set_k_factor do |rating|
23
+ # if rating && rating > 2500
24
+ # 24
25
+ # else
26
+ # 16
27
+ # end
28
+ # end
29
+ #
30
+ # Raises an ArgumentError if an exception is encountered when calling the provided block with nil as the argument
31
+ def self.set_k_factor(&k_factor)
32
+ k_factor.call(nil)
33
+ @k_factor = k_factor
34
+ rescue => e
35
+ raise ArgumentError, "Error encountered in K-factor block when passed nil rating: #{e}"
36
+ end
37
+
38
+ # Sets the K-factor to a number.
39
+ def self.k_factor=(k_factor)
40
+ @k_factor = Proc.new do
41
+ k_factor
42
+ end
43
+ end
44
+
45
+ # Calculates the expected score of a player given their rating (+player_rating+)
46
+ # and their opponent's rating (+opponent_rating+).
47
+ #
48
+ # Returns a float between 0 and 1 where 0.999 represents high certainty of the
49
+ # first player winning.
50
+ def self.expected_score(player_rating, opponent_rating)
51
+ 1.0/(1 + (10 ** ((opponent_rating - player_rating)/400.0)))
52
+ end
53
+
54
+ # Calculates the amount a player's rating should change.
55
+ #
56
+ # ==== Arguments
57
+ # * +expected_score+: a float between 0 and 1, representing the probability of
58
+ # the player winning
59
+ # * +actual_score+: 0, 0.5, or 1, whether the outcome was a loss, draw, or win
60
+ # (respectively)
61
+ # * +rating+ (optional): the rating of the player, used by the K-factor function
62
+ # * +k_factor+ (optional): the K-factor to use for this calculation to be used
63
+ # instead of the normal K-factor or K-factor function
64
+ #
65
+ # Returns a positive or negative float representing the amount the player's
66
+ # rating should change.
67
+ def self.rating_adjustment(expected_score, actual_score, rating: nil, k_factor: nil)
68
+ k_factor ||= k_factor(rating)
69
+ k_factor * (actual_score - expected_score)
70
+ end
71
+ end
72
+
73
+ require 'elo_rating/match'
@@ -0,0 +1,84 @@
1
+ require_relative '../lib/elo_rating.rb'
2
+
3
+ describe EloRating do
4
+ after(:each) do
5
+ EloRating::k_factor = 24
6
+ end
7
+ describe '::k_factor' do
8
+ it 'defaults to 24' do
9
+ expect(EloRating::k_factor).to eql(24)
10
+ end
11
+ end
12
+ describe '::k_factor=' do
13
+ it 'sets the K-factor to an integer' do
14
+ EloRating::k_factor = 10
15
+ expect(EloRating::k_factor).to eql(10)
16
+ end
17
+ end
18
+ describe '::set_k_factor' do
19
+ it 'takes a block to determine the K-factor based on a player\'s rating' do
20
+ EloRating::set_k_factor do |rating|
21
+ if rating && rating > 1000
22
+ 15
23
+ else
24
+ 24
25
+ end
26
+ end
27
+ expect(EloRating::k_factor(1001)).to eql 15
28
+ end
29
+ context 'given a block that doesn\'t handle nil ratings' do
30
+ it 'raises an error' do
31
+ expect do
32
+ EloRating::set_k_factor do |rating|
33
+ rating * 10
34
+ end
35
+ end.to raise_error ArgumentError
36
+ end
37
+ end
38
+ end
39
+ describe '.expected_score' do
40
+ it 'returns the odds of a player winning given their rating and their opponent\'s rating' do
41
+ expect(EloRating.expected_score(1200, 1000)).to be_within(0.0001).of(0.7597)
42
+ end
43
+ end
44
+ describe '.rating_adjustment' do
45
+ it 'returns the amount a rating should change given an expected score and an actual score' do
46
+ expect(EloRating.rating_adjustment(0.75, 0)).to be_within(0.0001).of(-18.0)
47
+ end
48
+ it 'uses the K-factor' do
49
+ expect(EloRating).to receive(:k_factor).and_return(24)
50
+
51
+ EloRating.rating_adjustment(0.75, 0)
52
+ end
53
+ context 'custom numeric k-factor' do
54
+ it 'uses the custom k-factor' do
55
+ EloRating::k_factor = 10
56
+
57
+ expect(EloRating.rating_adjustment(0.75, 0)).to be_within(0.0001).of(-7.5)
58
+ end
59
+ end
60
+ context 'custom k-factor function' do
61
+ it 'calls the function with the provided rating to determine the k-factor' do
62
+ EloRating::set_k_factor do |rating|
63
+ rating ||= 2000
64
+ if rating < 2100
65
+ 32
66
+ elsif 2100 <= rating && rating <= 2400
67
+ 24
68
+ else
69
+ 16
70
+ end
71
+ end
72
+
73
+ expect(EloRating.rating_adjustment(0.75, 0)).to be_within(0.0001).of(-24.0)
74
+ expect(EloRating.rating_adjustment(0.75, 0, rating: 2200)).to be_within(0.0001).of(-18.0)
75
+ expect(EloRating.rating_adjustment(0.75, 0, rating: 2500)).to be_within(0.0001).of(-12.0)
76
+ end
77
+ end
78
+ context 'provided with a nonce numeric k-factor' do
79
+ it 'uses the provided k-factor' do
80
+ expect(EloRating.rating_adjustment(0.75, 0, k_factor: 24)).to be_within(0.0001).of(-18.0)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,95 @@
1
+ require_relative '../lib/elo_rating.rb'
2
+
3
+ describe EloRating::Match do
4
+ describe '#updated_ratings' do
5
+ context 'simple match with two players and one winner' do
6
+ it 'returns the updated ratings of all the players' do
7
+ match = EloRating::Match.new
8
+ match.add_player(rating: 2000)
9
+ match.add_player(rating: 2000, winner: true)
10
+ expect(match.updated_ratings).to eql [1988, 2012]
11
+ end
12
+ end
13
+ context 'match with 3 players and one winner' do
14
+ it 'returns the updated ratings of all the players' do
15
+ match = EloRating::Match.new
16
+ match.add_player(rating: 1900, winner: true)
17
+ match.add_player(rating: 2000)
18
+ match.add_player(rating: 2000)
19
+ expect(match.updated_ratings).to eql [1931, 1985, 1985]
20
+ end
21
+ end
22
+ context 'ranked game with 3 players' do
23
+ it 'returns the updated ratings of all the players' do
24
+ match = EloRating::Match.new
25
+ match.add_player(rating: 1900, place: 1)
26
+ match.add_player(rating: 2000, place: 2)
27
+ match.add_player(rating: 2000, place: 3)
28
+ expect(match.updated_ratings).to eql [1931, 1997, 1973]
29
+ end
30
+ end
31
+ context 'multiple winners specified' do
32
+ it 'raises an error' do
33
+ match = EloRating::Match.new
34
+ match.add_player(rating: 1000, winner: true)
35
+ match.add_player(rating: 1000, winner: true)
36
+ expect { match.updated_ratings }.to raise_error ArgumentError
37
+ end
38
+ end
39
+ context 'place specified for one player but not all' do
40
+ it 'raises an error' do
41
+ match = EloRating::Match.new
42
+ match.add_player(rating: 1000)
43
+ match.add_player(rating: 1000, place: 2)
44
+ expect { match.updated_ratings }.to raise_error ArgumentError
45
+ end
46
+ end
47
+ end
48
+
49
+ describe '#add_player' do
50
+ context 'adding a player with a rating' do
51
+ it 'creates a new player with the specified rating' do
52
+ match = EloRating::Match.new
53
+ expect(EloRating::Match::Player).to receive(:new).with(rating: 2000, match: match)
54
+
55
+ match.add_player(rating: 2000)
56
+ end
57
+ it 'appends the new player to the match\'s player' do
58
+ match = EloRating::Match.new
59
+ match.add_player(rating: 2000)
60
+
61
+ expect(match.players.size).to eql 1
62
+ end
63
+ it 'returns the match itself so multiple calls can be chained' do
64
+ match = EloRating::Match.new
65
+ match.add_player(rating: 1000).add_player(rating: 2000)
66
+
67
+ expect(match.players.size). to eql 2
68
+ end
69
+ end
70
+
71
+ context 'adding a player with a non-numeric rating' do
72
+ it 'raises an error' do
73
+ match = EloRating::Match.new
74
+
75
+ expect { match.add_player(rating: :foo) }.to raise_error(ArgumentError)
76
+ end
77
+ end
78
+
79
+ context 'adding a player with a non-numeric place' do
80
+ it 'raises an error' do
81
+ match = EloRating::Match.new
82
+
83
+ expect { match.add_player(rating: 1000, place: :foo) }.to raise_error(ArgumentError)
84
+ end
85
+ end
86
+
87
+ context 'adding a player with both winner and place specified' do
88
+ it 'raises an error' do
89
+ match = EloRating::Match.new
90
+
91
+ expect { match.add_player(rating: 1000, place: 2, winner: true) }.to raise_error ArgumentError
92
+ end
93
+ end
94
+ end
95
+ end
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: elo_rating
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Max Holder
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-28 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Tiny library for calculating Elo ratings. Handles multiplayer matches
14
+ and allows for custom k-factor functions.
15
+ email: mxhold@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE
21
+ - README.md
22
+ - lib/elo_rating.rb
23
+ - lib/elo_rating/match.rb
24
+ - spec/elo_rating_spec.rb
25
+ - spec/match_spec.rb
26
+ homepage: http://github.com/mxhold/elo_rating
27
+ licenses:
28
+ - MIT
29
+ metadata: {}
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubyforge_project:
46
+ rubygems_version: 2.2.0
47
+ signing_key:
48
+ specification_version: 4
49
+ summary: Tiny library for calculating Elo ratings
50
+ test_files:
51
+ - spec/elo_rating_spec.rb
52
+ - spec/match_spec.rb