icu_ratings 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.
- data/.autotest +8 -0
- data/.gitignore +3 -0
- data/LICENCE +22 -0
- data/README.rdoc +64 -0
- data/Rakefile +44 -0
- data/VERSION.yml +5 -0
- data/lib/icu_ratings/player.rb +264 -0
- data/lib/icu_ratings/result.rb +172 -0
- data/lib/icu_ratings/tournament.rb +114 -0
- data/lib/icu_ratings.rb +6 -0
- data/spec/player_spec.rb +180 -0
- data/spec/result_spec.rb +106 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/tournament_spec.rb +618 -0
- metadata +72 -0
data/.autotest
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
#
|
2
|
+
# Since the default autotest mappings in gems/rspec-<version>/lib/autotest/rspec.rb
|
3
|
+
# assume lib/ and spec/ are structured in the same way, we have to replace them.
|
4
|
+
#
|
5
|
+
Autotest.add_hook :initialize do |at|
|
6
|
+
at.remove_mapping %r%^lib/(.*)\.rb$%
|
7
|
+
at.add_mapping(%r%^lib/icu_ratings/(.*)\.rb$%) { |_, m| "spec/#{m[1]}_spec.rb" }
|
8
|
+
end
|
data/.gitignore
ADDED
data/LICENCE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2010 Mark Orr
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
= ICU Chess Ratings
|
2
|
+
|
3
|
+
For calculating the Elo ratings of players in a chess tournament. The software is a port
|
4
|
+
the Irish Chess Union's existing rating software written in Microsoft Visual Basic and
|
5
|
+
intended to replace it in the near future.
|
6
|
+
|
7
|
+
The rating calculations are identical to FIDE's for tournaments that consist entirely
|
8
|
+
of established players (with the exception of player bonuses, which can be turned off).
|
9
|
+
However, the ICU has it's own peculiar way of dealing with unrated players (players
|
10
|
+
with only a provisional rating or without any prior rating) which is not the same as
|
11
|
+
FIDE's.
|
12
|
+
|
13
|
+
== Install
|
14
|
+
|
15
|
+
sudo gem install icu_ratings
|
16
|
+
|
17
|
+
== Usage
|
18
|
+
|
19
|
+
First, create a new ICU::RatedTournament object:
|
20
|
+
|
21
|
+
t = ICU::RatedTournament.new(:desc => "Irish Championships 2008")
|
22
|
+
|
23
|
+
Then add players (see ICU::RatedPlayer for details):
|
24
|
+
|
25
|
+
t.add_player(1, :rating => 2534, :desc => 'Alexander Baburin (7085)', :kfactor => 16)
|
26
|
+
t.add_player(2, :rating => 2525, :desc => 'Alon Greenfeld') # foreign (non-ICU) rated player
|
27
|
+
t.add_player(8, :rating => 2084, :desc => 'Anthony Fox (456)', :kfactor => 24)
|
28
|
+
# ...
|
29
|
+
|
30
|
+
Then add results (see ICU::RatedResult for details):
|
31
|
+
|
32
|
+
t.add_result(1, 1, 8, 'W') # players 1 and 8 played in round 1, player 1 won
|
33
|
+
t.add_result(4, 2, 1, 'D') # players 1 and 2 drew in round 4
|
34
|
+
# ...
|
35
|
+
|
36
|
+
Then call the <em>rate!</em> method.
|
37
|
+
|
38
|
+
t.rate!
|
39
|
+
|
40
|
+
If no exceptions have been raised yet, the tournament is now rated and the
|
41
|
+
main results of the rating calculations can be extracted by querying the
|
42
|
+
previously created player objects:
|
43
|
+
|
44
|
+
(1..32).each do |num|
|
45
|
+
player = t.player(num)
|
46
|
+
puts "Name: #{t.desc}"
|
47
|
+
puts "Score: #{p.score}/#{p.results.size}"
|
48
|
+
puts "New Rating: #{p.new_rating.round}"
|
49
|
+
puts "Performance Rating: #{p.performance.round}"
|
50
|
+
end
|
51
|
+
|
52
|
+
# Name: Alexander Baburin (7085)
|
53
|
+
# Score: 8.0/9
|
54
|
+
# New Rating: 2558
|
55
|
+
# Performance Rating: 2607
|
56
|
+
# ...
|
57
|
+
|
58
|
+
See ICU::RatedPlayer for further details. Further breakdown of the rating calculations
|
59
|
+
are available, if desired, from the results belonging to each player. See ICU::RatedResult
|
60
|
+
for more details.
|
61
|
+
|
62
|
+
== Author
|
63
|
+
|
64
|
+
Mark Orr, Irish Chess Union (ICU[http://icu.ie]) rating officer.
|
data/Rakefile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'spec/rake/spectask'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'jeweler'
|
8
|
+
Jeweler::Tasks.new do |gem|
|
9
|
+
gem.name = "icu_ratings"
|
10
|
+
gem.summary = "For rating chess tournaments."
|
11
|
+
gem.description = "Build an object that represents a chess tournament then get it to calculate ratings of all the players."
|
12
|
+
gem.homepage = "http://github.com/sanichi/icu_ratings"
|
13
|
+
gem.authors = ["Mark Orr"]
|
14
|
+
gem.email = "mark.j.l.orr@googlemail.com"
|
15
|
+
gem.files = FileList['[A-Z]*', '{lib,spec}/**/*', '.gitignore', '.autotest']
|
16
|
+
gem.has_rdoc = true
|
17
|
+
gem.rdoc_options = ["--charset", "UTF-8"]
|
18
|
+
end
|
19
|
+
Jeweler::GemcutterTasks.new
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler not available. Install it with: sudo gem install jeweler."
|
22
|
+
end
|
23
|
+
|
24
|
+
task :default => :spec
|
25
|
+
|
26
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
27
|
+
spec.libs << 'lib' << 'spec'
|
28
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
29
|
+
spec.spec_opts = ['--colour --format nested']
|
30
|
+
end
|
31
|
+
|
32
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
33
|
+
if File.exist?('VERSION.yml')
|
34
|
+
config = YAML.load(File.read('VERSION.yml'))
|
35
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
36
|
+
else
|
37
|
+
version = ""
|
38
|
+
end
|
39
|
+
|
40
|
+
rdoc.rdoc_dir = 'rdoc'
|
41
|
+
rdoc.title = "ICU Ratings #{version}"
|
42
|
+
rdoc.rdoc_files.include('README*')
|
43
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
44
|
+
end
|
data/VERSION.yml
ADDED
@@ -0,0 +1,264 @@
|
|
1
|
+
module ICU
|
2
|
+
|
3
|
+
=begin rdoc
|
4
|
+
|
5
|
+
== Adding Players to Tournaments
|
6
|
+
|
7
|
+
You don't directly create players, rather you add them to tournaments with the _add_player_ method.
|
8
|
+
|
9
|
+
t = ICU::RatedTournament.new
|
10
|
+
t.add_player(1)
|
11
|
+
|
12
|
+
There is only one mandatory parameter - the player number - which can be any integer value
|
13
|
+
except player numbers must be unique in each tournament:
|
14
|
+
|
15
|
+
t.add_player(2) # fine
|
16
|
+
t.add_player(2) # attempt to add a second player with the same number - exception!
|
17
|
+
|
18
|
+
== Retrieving Players from Tournaments
|
19
|
+
|
20
|
+
Player objects can be retrieved from ICU::RatedTournament objects with the latter's _player_ method
|
21
|
+
in conjunction with the appropriate player number:
|
22
|
+
|
23
|
+
p = t.player(2)
|
24
|
+
p.num # 2
|
25
|
+
|
26
|
+
Or the player object can be saved from the return value from _add_player_:
|
27
|
+
|
28
|
+
p = t.add_player(-2)
|
29
|
+
p.num # -2
|
30
|
+
|
31
|
+
If the number supplied to _player_ is an invalid player number, the method returns _nil_.
|
32
|
+
|
33
|
+
Different types of players are signalled by different combinations of the three optional
|
34
|
+
parameters: _rating_, _kfactor_ and _games_.
|
35
|
+
|
36
|
+
== Full Ratings
|
37
|
+
|
38
|
+
Rated players have a full rating and a K-factor and are added by including valid values for those two parameters:
|
39
|
+
|
40
|
+
p = t.add_player(3, :rating => 2000, :kfactor => 16)
|
41
|
+
p.type # :rated
|
42
|
+
|
43
|
+
== Provisional Ratings
|
44
|
+
|
45
|
+
Players that don't yet have a full rating but do have a provisonal rating estimated on some number
|
46
|
+
of games played prior to the tournament are indicated by values for the _rating_ and _games_ parameters:
|
47
|
+
|
48
|
+
p = t.add_player(4, :rating => 1600, :games => 10)
|
49
|
+
p.type # :provisional
|
50
|
+
|
51
|
+
The value for the number of games should not exceed 19 since players with 20 or more games
|
52
|
+
should have a full rating.
|
53
|
+
|
54
|
+
== Fixed Ratings
|
55
|
+
|
56
|
+
Players with fixed ratings just have a rating - no K-factor or number of previous games.
|
57
|
+
When the tournament is rated, these players will have their tournament performance ratings
|
58
|
+
calculated but the value returned by the method _new_rating_ will just be the rating they
|
59
|
+
started with. Typically these are foreign players with FIDE ratings who are not members of
|
60
|
+
the ICU and for whom ICU ratings are not desired.
|
61
|
+
|
62
|
+
p = t.add_player(6, :rating => 2500)
|
63
|
+
p.type # :foreign
|
64
|
+
|
65
|
+
== No Rating
|
66
|
+
|
67
|
+
Unrated players who do not have any rated games at all are indicated by leaving out any values for
|
68
|
+
_rating_, _kfactor_ or _games_.
|
69
|
+
|
70
|
+
p = t.add_player(5)
|
71
|
+
p.type # :unrated
|
72
|
+
|
73
|
+
== Invalid Combinations
|
74
|
+
|
75
|
+
The above four types of players (_rated_, _provisional_, _unrated_, _foreign_) are the only
|
76
|
+
valid ones and any attempt to add players with other combinations of the attributes
|
77
|
+
_rating_, _kfactor_ and _games_ will cause an exception. For example:
|
78
|
+
|
79
|
+
t.add_player(7, :rating => 2000, :kfactor => 16, :games => 10) # exception! - cannot have both kfactor and games
|
80
|
+
t.add_plater(7, :kfactor => 16) # exception! - kfactor makes no sense without a rating
|
81
|
+
|
82
|
+
== String Input Values
|
83
|
+
|
84
|
+
Although _rating_ and _kfactor_ are held as Float values and _games_ and _num_ (the player number) as Fixnums,
|
85
|
+
all these parameters can be specified using strings, even when padded with whitespace.
|
86
|
+
|
87
|
+
p = t.add_player(" 0 ", :rating => " 2000.5 ", :kfactor => " 20.5 ")
|
88
|
+
p.num # 0 (Fixnum)
|
89
|
+
p.rating # 2000.5 (Float)
|
90
|
+
p.kfactor # 20.5 (Float)
|
91
|
+
|
92
|
+
== Description Parameter
|
93
|
+
|
94
|
+
There is one other optional parameter, _desc_ (short for "description"). It has no effect on player
|
95
|
+
type or rating calculations and it cannot be used to retrieve players from a tournament (only the
|
96
|
+
player number can be used for that). Its only use is to attach additional arbitary data to players.
|
97
|
+
Any object can be used and descriptions don't have to be unique. The attribute's typical use,
|
98
|
+
if it's used at all, is expected to be for player names in the form of String values.
|
99
|
+
|
100
|
+
t.add_player(8, :rating => 2800, :desc => 'Gary Kasparov (4100018)')
|
101
|
+
t.player(8).desc # "Gary Kasparov (4100018)"
|
102
|
+
|
103
|
+
== After the Tournament is Rated
|
104
|
+
|
105
|
+
After the <em>rate!</em> method has been called on the ICU::RatedTournament object, the results
|
106
|
+
of the rating calculations are available via various methods of the player objects:
|
107
|
+
_new_rating_, _rating_change_, _performance_, _expected_score_.
|
108
|
+
|
109
|
+
== Unrateable Players
|
110
|
+
|
111
|
+
If a tournament contains groups of provisonal or unrated players who play games
|
112
|
+
only amongst themselves and not against any rated or foreign opponents, they can't
|
113
|
+
be rated. This is indicated by a value of _nil_ returned from the _new_rating_
|
114
|
+
method.
|
115
|
+
|
116
|
+
=end
|
117
|
+
|
118
|
+
class RatedPlayer
|
119
|
+
attr_reader :num, :rating, :kfactor, :games
|
120
|
+
attr_accessor :desc
|
121
|
+
|
122
|
+
# After the tournament has been rated, this is the player's new rating. For rated players this is the old rating
|
123
|
+
# plus the _rating_change_. For provisional players it is their performance rating, including their previous
|
124
|
+
# games. For unrated players it is their tournament performance rating. For foreign players it is the same
|
125
|
+
# as their start _rating_.
|
126
|
+
def new_rating
|
127
|
+
full_rating? ? rating + rating_change : performance
|
128
|
+
end
|
129
|
+
|
130
|
+
# After the tournament has been rated, this is the difference between the old and new ratings for
|
131
|
+
# rated players, based on sum of expected scores in each games and the player's K-factor.
|
132
|
+
# Zero for all other types of players.
|
133
|
+
def rating_change
|
134
|
+
@results.inject(0.0) { |c, r| c + (r.rating_change || 0.0) }
|
135
|
+
end
|
136
|
+
|
137
|
+
# After the tournament has been rated, this returns the sum of expected scores over all results.
|
138
|
+
# Although this is calculated for provisional and unrated players it is not used to estimate their
|
139
|
+
# new ratings. For rated players, this number times the K-factor gives the change in rating.
|
140
|
+
def expected_score
|
141
|
+
@results.inject(0.0) { |e, r| e + (r.expected_score || 0.0) }
|
142
|
+
end
|
143
|
+
|
144
|
+
# After the tournament has been rated, this returns the tournament rating performance for
|
145
|
+
# rated, unrated and foreign players. For provisional players it returns a weighted average
|
146
|
+
# of the player's tournament performance and their previous games. For provisional and
|
147
|
+
# unrated players it is the same as _new_rating_.
|
148
|
+
def performance
|
149
|
+
@performance
|
150
|
+
end
|
151
|
+
|
152
|
+
# Returns an array of the player's results (ICU::RatedResult) in round order.
|
153
|
+
def results
|
154
|
+
@results
|
155
|
+
end
|
156
|
+
|
157
|
+
# The sum of the player's scores in all rounds in which they have a result.
|
158
|
+
def score
|
159
|
+
@results.inject(0.0) { |e, r| e + r.score }
|
160
|
+
end
|
161
|
+
|
162
|
+
# Returns the type of player as a symbol: one of _rated_, _provisional_, _unrated_ or _foreign_.
|
163
|
+
def type
|
164
|
+
@type
|
165
|
+
end
|
166
|
+
|
167
|
+
def add_result(result) # :nodoc:
|
168
|
+
raise "invalid result (#{result.class})" unless result.is_a? ICU::RatedResult
|
169
|
+
raise "players cannot score results against themselves" if self == result.opponent
|
170
|
+
duplicate = false
|
171
|
+
@results.each do |r|
|
172
|
+
if r.round == result.round
|
173
|
+
raise "inconsistent result in round #{r.round}" unless r == result
|
174
|
+
duplicate = true
|
175
|
+
end
|
176
|
+
end
|
177
|
+
return if duplicate
|
178
|
+
@results << result
|
179
|
+
@results.sort!{ |a,b| a.round <=> b.round }
|
180
|
+
end
|
181
|
+
|
182
|
+
def rate! # :nodoc:
|
183
|
+
@results.each { |r| r.rate!(self) }
|
184
|
+
end
|
185
|
+
|
186
|
+
def full_rating? # :nodoc:
|
187
|
+
@type == :rated || @type == :foreign
|
188
|
+
end
|
189
|
+
|
190
|
+
def init_performance # :nodoc:
|
191
|
+
@performance = nil
|
192
|
+
@estimated_performance = nil
|
193
|
+
end
|
194
|
+
|
195
|
+
def estimate_performance # :nodoc:
|
196
|
+
new_games, new_performance = results.inject([0,0.0]) do |sum, result|
|
197
|
+
opponent = result.opponent
|
198
|
+
if opponent.full_rating? || opponent.performance
|
199
|
+
sum[0]+= 1
|
200
|
+
sum[1]+= (opponent.full_rating? ? opponent.rating : opponent.performance) + (2 * result.score - 1) * 400.0
|
201
|
+
end
|
202
|
+
sum
|
203
|
+
end
|
204
|
+
if new_games > 0
|
205
|
+
old_games, old_performance = type == :provisional ? [games, games * rating] : [0, 0.0]
|
206
|
+
@estimated_performance = (new_performance + old_performance) / (new_games + old_games)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def update_performance # :nodoc:
|
211
|
+
stable = case
|
212
|
+
when @performance && @estimated_performance then (@performance - @estimated_performance).abs < 0.5
|
213
|
+
when !@performance && !@estimated_performance then true
|
214
|
+
else false
|
215
|
+
end
|
216
|
+
@performance = @estimated_performance if @estimated_performance
|
217
|
+
stable
|
218
|
+
end
|
219
|
+
|
220
|
+
def ==(other) # :nodoc:
|
221
|
+
return false unless other.is_a? ICU::RatedPlayer
|
222
|
+
num == other.num
|
223
|
+
end
|
224
|
+
|
225
|
+
private
|
226
|
+
|
227
|
+
def initialize(num, opt={}) # :nodoc:
|
228
|
+
self.num = num
|
229
|
+
[:rating, :kfactor, :games, :desc].each { |atr| self.send("#{atr}=", opt[atr]) unless opt[atr].nil? }
|
230
|
+
@results = []
|
231
|
+
@type = deduce_type
|
232
|
+
end
|
233
|
+
|
234
|
+
def num=(num)
|
235
|
+
@num = num.to_i
|
236
|
+
raise "invalid player num (#{num})" if @num == 0 && !num.to_s.match(/^\s*\d/)
|
237
|
+
end
|
238
|
+
|
239
|
+
def rating=(rating)
|
240
|
+
@rating = rating.to_f
|
241
|
+
raise "invalid player rating (#{rating})" if @rating == 0.0 && !rating.to_s.match(/^\s*\d/)
|
242
|
+
end
|
243
|
+
|
244
|
+
def kfactor=(kfactor)
|
245
|
+
@kfactor = kfactor.to_f
|
246
|
+
raise "invalid player k-factor (#{kfactor})" if @kfactor <= 0.0
|
247
|
+
end
|
248
|
+
|
249
|
+
def games=(games)
|
250
|
+
@games = games.to_i
|
251
|
+
raise "invalid number of games (#{games})" if @games <= 0 || @games >= 20
|
252
|
+
end
|
253
|
+
|
254
|
+
def deduce_type
|
255
|
+
case
|
256
|
+
when @rating && @kfactor && !@games then :rated
|
257
|
+
when @rating && !@kfactor && @games then :provisional
|
258
|
+
when @rating && !@kfactor && !@games then :foreign
|
259
|
+
when !@rating && !@kfactor && !@games then :unrated
|
260
|
+
else raise "invalid combination of player attributes"
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module ICU
|
2
|
+
|
3
|
+
=begin rdoc
|
4
|
+
|
5
|
+
== Adding Results to Tournaments
|
6
|
+
|
7
|
+
You don't create results directly with a constructor, instead, you add them to a tournament
|
8
|
+
using the _add_result_ method, giving the round number, the player numbers and the score
|
9
|
+
of the first player relative to the second:
|
10
|
+
|
11
|
+
t = ICU::RatedTournament.new
|
12
|
+
t.add_player(10)
|
13
|
+
t.add_player(20)
|
14
|
+
t.add_result(1, 10, 20, 'W')
|
15
|
+
|
16
|
+
The above example expresses the result that in round 1 player 10 won against player 20. An exception is raised
|
17
|
+
if either of the two players does not already exist in the tournament, if either player already has a game
|
18
|
+
with another opponent in that round or if the two players already have a different result against each other
|
19
|
+
in that round. Note that the result is added to both players: in the above example a win in round 1 against
|
20
|
+
player 20 is added to player 10's results and a loss against player 10 in round 1 is added to player 20's results.
|
21
|
+
It's OK (but unnecessary) to add the same result again from the other player's prespective as long as
|
22
|
+
the score is consistent.
|
23
|
+
|
24
|
+
t.add_result(1, 20, 10, 'L') # unnecessary (nothing would change) but would not cause an exception
|
25
|
+
t.add_result(1, 20, 10, 'D') # inconsistent result - would raise an exception
|
26
|
+
|
27
|
+
== Specifying the Score
|
28
|
+
|
29
|
+
The method _score_ will always return a Float value (either 0.0, 0.5 or 1.0).
|
30
|
+
When specifying a score using the _add_result_ of ICU::Tourmanent the same values
|
31
|
+
can be used as can other, equally valid alternatives:
|
32
|
+
|
33
|
+
win:: "1", "1.0", "W", "w" (String), 1 (Fixnum), 1.0 (Float)
|
34
|
+
loss:: "0", "0.0", "L", "l" (String), 0 (Fixnum), 0.0 (Float)
|
35
|
+
draw:: "½", "D", "d" (String), 0.5 (Float)
|
36
|
+
|
37
|
+
Strings padded with whitespace also work (e.g. " 1.0 " and " W ").
|
38
|
+
|
39
|
+
== Specifying the Players
|
40
|
+
|
41
|
+
As described above, one way to specify the two players is via player numbers. Equally possible is player objects:
|
42
|
+
|
43
|
+
t = ICU::RatedTournament.new
|
44
|
+
p = t.add_player(10)
|
45
|
+
q = t.add_plater(20)
|
46
|
+
t.add_result(1, p, q, 'W')
|
47
|
+
|
48
|
+
Or indeed (although this is unnecessary):
|
49
|
+
|
50
|
+
t = ICU::RatedTournament.new
|
51
|
+
t.add_player(10)
|
52
|
+
t.add_plater(20)
|
53
|
+
t.add_result(1, t.player(10), t.player(20), 'W')
|
54
|
+
|
55
|
+
A players cannot have a results against themselves:
|
56
|
+
|
57
|
+
t.add_player(2, 10, 10, 'D') # exception!
|
58
|
+
|
59
|
+
== Retrieving Results
|
60
|
+
|
61
|
+
Results belong to players (ICU::RatedPlayer objects) and are stored in an array accessed by the method _results_.
|
62
|
+
Each result has a _round_ number, an _opponent_ object (also an ICU::RatedPlayer object) and a _score_ (1.0, 0.5 or 0.0):
|
63
|
+
|
64
|
+
p = t.player(10)
|
65
|
+
p.results.size # 1
|
66
|
+
r = p.results[0]
|
67
|
+
r.round # 1
|
68
|
+
r.opponent.num # 20
|
69
|
+
r.score # 1.0 (Float)
|
70
|
+
|
71
|
+
The _results_ method returns results in round order, irrespective of what order they were added in:
|
72
|
+
|
73
|
+
t = ICU::RatedTournament.new
|
74
|
+
[0,1,2,3,4].each { |num| t.add_player(num) }
|
75
|
+
[3,1].each { |rnd| t.add_result(rnd, 0, rnd, 'W') }
|
76
|
+
[4,2].each { |rnd| t.add_result(rnd, 0, rnd, 'L') }
|
77
|
+
t.player(0).results.map{ |r| r.round }.join(',') # "1,2,3,4"
|
78
|
+
|
79
|
+
== Unrated Results
|
80
|
+
|
81
|
+
Results that are not for rating, such as byes, walkovers and defaults, should not be
|
82
|
+
added to the tournament. Instead, players can simply have no results for certain rounds.
|
83
|
+
Indeed, it's even valid for players not to have any results at all (although, in that
|
84
|
+
case, for those players, no new rating can be calculated from the tournament).
|
85
|
+
|
86
|
+
== After the Tournament is Rated
|
87
|
+
|
88
|
+
The main rating calculations are avaiable from player methods (see ICU::RatedPlayer)
|
89
|
+
but additional details are available via methods of each player's individual results:
|
90
|
+
_expected_score_, _rating_change_.
|
91
|
+
|
92
|
+
=end
|
93
|
+
|
94
|
+
class RatedResult
|
95
|
+
# The round number.
|
96
|
+
def round
|
97
|
+
@round
|
98
|
+
end
|
99
|
+
|
100
|
+
# The player's opponent (an instance of ICU::RatedPlayer).
|
101
|
+
def opponent
|
102
|
+
@opponent
|
103
|
+
end
|
104
|
+
|
105
|
+
# The player's score in this game (1.0, 0.5 or 0.0).
|
106
|
+
def score
|
107
|
+
@score
|
108
|
+
end
|
109
|
+
|
110
|
+
# After the tournament has been rated, this returns the expected score (between 0 and 1)
|
111
|
+
# for the player based on the rating difference with the opponent scaled by 400.
|
112
|
+
# The standard Elo formula is used: 1/(1 + 10^(diff/400)).
|
113
|
+
def expected_score
|
114
|
+
@expected_score
|
115
|
+
end
|
116
|
+
|
117
|
+
# After the tournament has been rated, returns the change in rating due to this particular
|
118
|
+
# result. Only for rated players (returns _nil_ for other types of players). Computed from
|
119
|
+
# the difference between actual and expected scores multiplied by the player's K-factor.
|
120
|
+
# The sum of these changes is the overall rating change for rated players.
|
121
|
+
def rating_change
|
122
|
+
@rating_change
|
123
|
+
end
|
124
|
+
|
125
|
+
def rate!(player) # :nodoc:
|
126
|
+
player_rating = player.full_rating? ? player.rating : player.performance
|
127
|
+
opponent_rating = opponent.full_rating? ? opponent.rating : opponent.performance
|
128
|
+
if (player_rating && opponent_rating)
|
129
|
+
@expected_score = 1 / (1 + 10 ** ((opponent_rating - player_rating) / 400.0))
|
130
|
+
@rating_change = (@score - @expected_score) * player.kfactor if player.type == :rated
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def ==(other) # :nodoc:
|
135
|
+
return false unless other.round == round
|
136
|
+
return false unless other.opponent == opponent
|
137
|
+
return false unless other.score == score
|
138
|
+
true
|
139
|
+
end
|
140
|
+
|
141
|
+
def opponents_score # :nodoc:
|
142
|
+
1.0 - score
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def initialize(round, opponent, score) # :nodoc:
|
148
|
+
self.round = round
|
149
|
+
self.opponent = opponent
|
150
|
+
self.score = score
|
151
|
+
end
|
152
|
+
|
153
|
+
def round=(round)
|
154
|
+
@round = round.to_i
|
155
|
+
raise "invalid round number (#{round})" if @round <= 0
|
156
|
+
end
|
157
|
+
|
158
|
+
def opponent=(opponent)
|
159
|
+
raise "invalid opponent class (#{opponent.class})" unless opponent.is_a? ICU::RatedPlayer
|
160
|
+
@opponent = opponent
|
161
|
+
end
|
162
|
+
|
163
|
+
def score=(score)
|
164
|
+
@score = case score.to_s.strip
|
165
|
+
when /^(1\.0|1|\+|W|w)$/ then 1.0
|
166
|
+
when /^(0\.5|½|\=|D|d)$/ then 0.5
|
167
|
+
when /^(0\.0|0|\-|L|l)$/ then 0.0
|
168
|
+
else raise "invalid score (#{score})"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|