elo_rating 0.2.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.
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