icu_ratings 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|