icu_ratings 1.5.0 → 1.5.1
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/lib/icu_ratings/player.rb +177 -124
- data/lib/icu_ratings/result.rb +5 -20
- data/lib/icu_ratings/tournament.rb +3 -3
- data/lib/icu_ratings/version.rb +1 -1
- data/spec/player_spec.rb +42 -46
- data/spec/result_spec.rb +6 -6
- data/spec/tournament_spec.rb +29 -31
- metadata +4 -4
data/lib/icu_ratings/player.rb
CHANGED
@@ -117,8 +117,8 @@ module ICU
|
|
117
117
|
# There is one other optional parameter, _desc_ (short for "description"). It has no effect on player
|
118
118
|
# type or rating calculations and it cannot be used to retrieve players from a tournament (only the
|
119
119
|
# player number can be used for that). Its only use is to attach additional arbitary data to players.
|
120
|
-
# Any object can be used and descriptions don't have to be unique. The attribute's typical use,
|
121
|
-
#
|
120
|
+
# Any object can be used and descriptions don't have to be unique. The attribute's typical use, if
|
121
|
+
# it's used at all, is expected to be for player names and/or ID numbers, in the form of String values.
|
122
122
|
#
|
123
123
|
# t.add_player(8, :rating => 2800, :desc => 'Gary Kasparov (4100018)')
|
124
124
|
# t.player(8).desc # "Gary Kasparov (4100018)"
|
@@ -128,24 +128,24 @@ module ICU
|
|
128
128
|
# After the <em>rate!</em> method has been called on the ICU::RatedTournament object, the results
|
129
129
|
# of the rating calculations are available via various methods of the player objects:
|
130
130
|
#
|
131
|
-
# _new_rating_::
|
132
|
-
#
|
133
|
-
#
|
134
|
-
#
|
135
|
-
#
|
136
|
-
# _rating_change_::
|
137
|
-
#
|
138
|
-
#
|
139
|
-
# _performance_::
|
140
|
-
#
|
141
|
-
#
|
142
|
-
#
|
143
|
-
# _expected_score_::
|
144
|
-
#
|
145
|
-
#
|
146
|
-
#
|
147
|
-
#
|
148
|
-
# _bonus_::
|
131
|
+
# _new_rating_:: This is the player's new rating. For rated players it is their old rating
|
132
|
+
# plus their _rating_change_ plus their _bonus_ (if any). For provisional players
|
133
|
+
# it is their performance rating including their previous games. For unrated
|
134
|
+
# players it is their tournament performance rating. New ratings are not
|
135
|
+
# calculated for foreign players so this method just returns their start _rating_.
|
136
|
+
# _rating_change_:: This is calculated from a rated player's old rating, their K-factor and the sum
|
137
|
+
# of expected scores in each game. The same as the difference between the old and
|
138
|
+
# new ratings (unless there is a bonus). Not available for other player types.
|
139
|
+
# _performance_:: This returns the tournament rating performance for rated, unrated and
|
140
|
+
# foreign players. For provisional players it returns a weighted average
|
141
|
+
# of the player's tournament performance and their previous games. For
|
142
|
+
# provisional and unrated players it is the same as _new_rating_.
|
143
|
+
# _expected_score_:: This returns the sum of expected scores over all results for all player types.
|
144
|
+
# For rated players, this number times the K-factor gives their rating change.
|
145
|
+
# It is calculated for provisional, unrated and foreign players but not actually
|
146
|
+
# used to estimate new ratings (for provisional and unrated players performance
|
147
|
+
# estimates are used instead).
|
148
|
+
# _bonus_:: The bonus received by a rated player (usually zero). Not available for other player types.
|
149
149
|
#
|
150
150
|
# == Unrateable Players
|
151
151
|
#
|
@@ -155,53 +155,49 @@ module ICU
|
|
155
155
|
# method.
|
156
156
|
#
|
157
157
|
class RatedPlayer
|
158
|
-
attr_reader :num, :
|
158
|
+
attr_reader :num, :type, :performance, :results
|
159
159
|
attr_accessor :desc
|
160
160
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
def expected_score
|
175
|
-
@results.inject(0.0) { |e, r| e + (r.expected_score || 0.0) }
|
176
|
-
end
|
177
|
-
|
178
|
-
# After the tournament has been rated, this returns the tournament rating performance for
|
179
|
-
# rated, unrated and foreign players. Returns zero for rated players.
|
180
|
-
def performance
|
181
|
-
@performance
|
182
|
-
end
|
183
|
-
|
184
|
-
# Returns an array of the player's results (ICU::RatedResult) in round order.
|
185
|
-
def results
|
186
|
-
@results
|
161
|
+
def self.factory(num, args={}) # :nodoc:
|
162
|
+
num = check_num(num)
|
163
|
+
rating = check_rating(args[:rating])
|
164
|
+
kfactor = check_kfactor(args[:kfactor])
|
165
|
+
games = check_games(args[:games])
|
166
|
+
desc = args[:desc]
|
167
|
+
case
|
168
|
+
when rating && kfactor && !games then FullRating.new(num, desc, rating, kfactor)
|
169
|
+
when rating && !kfactor && games then ProvRating.new(num, desc, rating, games)
|
170
|
+
when rating && !kfactor && !games then FrgnRating.new(num, desc, rating)
|
171
|
+
when !rating && !kfactor && !games then NoneRating.new(num, desc)
|
172
|
+
else raise "invalid combination of player attributes"
|
173
|
+
end
|
187
174
|
end
|
188
175
|
|
189
|
-
|
190
|
-
|
191
|
-
|
176
|
+
def self.check_num(arg) # :nodoc:
|
177
|
+
num = arg.to_i
|
178
|
+
raise "invalid player num (#{arg})" if num == 0 && !arg.to_s.match(/^\s*\d/)
|
179
|
+
num
|
192
180
|
end
|
193
181
|
|
194
|
-
|
195
|
-
|
196
|
-
|
182
|
+
def self.check_rating(arg) # :nodoc:
|
183
|
+
return unless arg
|
184
|
+
rating = arg.to_f
|
185
|
+
raise "invalid player rating (#{arg})" if rating == 0.0 && !arg.to_s.match(/^\s*\d/)
|
186
|
+
rating
|
197
187
|
end
|
198
188
|
|
199
|
-
def
|
200
|
-
|
189
|
+
def self.check_kfactor(arg) # :nodoc:
|
190
|
+
return unless arg
|
191
|
+
kfactor = arg.to_f
|
192
|
+
raise "invalid player k-factor (#{arg})" if kfactor <= 0.0
|
193
|
+
kfactor
|
201
194
|
end
|
202
195
|
|
203
|
-
def
|
204
|
-
|
196
|
+
def self.check_games(arg) # :nodoc:
|
197
|
+
return unless arg
|
198
|
+
games = arg.to_i
|
199
|
+
raise "invalid number of games (#{arg})" if games <= 0 || games >= 20
|
200
|
+
games
|
205
201
|
end
|
206
202
|
|
207
203
|
# Calculate a K-factor according to ICU rules.
|
@@ -219,6 +215,117 @@ module ICU
|
|
219
215
|
end
|
220
216
|
end
|
221
217
|
|
218
|
+
def reset # :nodoc:
|
219
|
+
@performance = nil
|
220
|
+
@estimated_performance = nil
|
221
|
+
end
|
222
|
+
|
223
|
+
class FullRating < RatedPlayer # :nodoc:
|
224
|
+
attr_reader :rating, :kfactor, :bonus
|
225
|
+
|
226
|
+
def initialize(num, desc, rating, kfactor)
|
227
|
+
@type = :rated
|
228
|
+
@rating = rating
|
229
|
+
@kfactor = kfactor
|
230
|
+
@bonus = 0
|
231
|
+
super(num, desc)
|
232
|
+
end
|
233
|
+
|
234
|
+
def reset
|
235
|
+
@bonus_rating = nil
|
236
|
+
@bonus = 0
|
237
|
+
super
|
238
|
+
end
|
239
|
+
|
240
|
+
def rating_change
|
241
|
+
@results.inject(0.0) { |c, r| c + (r.rating_change || 0.0) }
|
242
|
+
end
|
243
|
+
|
244
|
+
def new_rating(type=nil)
|
245
|
+
case type
|
246
|
+
when :start
|
247
|
+
@rating # the player's start rating
|
248
|
+
when :opponent
|
249
|
+
@bonus_rating || @rating # the rating used for opponents during the calculations
|
250
|
+
else
|
251
|
+
@rating + rating_change + @bonus # the player's final rating
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def calculate_bonus
|
256
|
+
return if @kfactor <= 24 || @results.size <= 4 || @rating >= 2100
|
257
|
+
change = rating_change
|
258
|
+
return if change <= 35 || @rating + change >= 2100
|
259
|
+
threshold = 32 + 3 * (@results.size - 4)
|
260
|
+
bonus = (change - threshold).round
|
261
|
+
return if bonus <= 0
|
262
|
+
bonus = (1.25 * bonus).round if kfactor >= 40
|
263
|
+
[2100, @performance].each { |max| bonus = max - @rating if bonus + @rating > max }
|
264
|
+
return if bonus <= 0
|
265
|
+
bonus = bonus.round
|
266
|
+
@bonus_rating = @rating + change + bonus
|
267
|
+
@bonus = bonus
|
268
|
+
end
|
269
|
+
|
270
|
+
def update_bonus
|
271
|
+
@bonus_rating = @rating + @bonus + rating_change if @bonus_rating
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
class ProvRating < RatedPlayer # :nodoc:
|
276
|
+
attr_reader :rating, :games
|
277
|
+
|
278
|
+
def initialize(num, desc, rating, games)
|
279
|
+
@type = :provisional
|
280
|
+
@rating = rating
|
281
|
+
@games = games
|
282
|
+
super(num, desc)
|
283
|
+
end
|
284
|
+
|
285
|
+
def new_rating(type=nil)
|
286
|
+
performance
|
287
|
+
end
|
288
|
+
|
289
|
+
def average_performance(new_performance, new_games)
|
290
|
+
old_performance = games * rating
|
291
|
+
old_games = games
|
292
|
+
(new_performance + old_performance) / (new_games + old_games)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
class NoneRating < RatedPlayer # :nodoc:
|
297
|
+
def initialize(num, desc)
|
298
|
+
@type = :unrated
|
299
|
+
super(num, desc)
|
300
|
+
end
|
301
|
+
|
302
|
+
def new_rating(type=nil)
|
303
|
+
performance
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
class FrgnRating < RatedPlayer # :nodoc:
|
308
|
+
attr_reader :rating
|
309
|
+
|
310
|
+
def initialize(num, desc, rating)
|
311
|
+
@type = :foreign
|
312
|
+
@rating = rating
|
313
|
+
super(num, desc)
|
314
|
+
end
|
315
|
+
|
316
|
+
def new_rating(type=nil)
|
317
|
+
rating
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
def expected_score
|
322
|
+
@results.inject(0.0) { |e, r| e + (r.expected_score || 0.0) }
|
323
|
+
end
|
324
|
+
|
325
|
+
def score
|
326
|
+
@results.inject(0.0) { |e, r| e + r.score }
|
327
|
+
end
|
328
|
+
|
222
329
|
def add_result(result) # :nodoc:
|
223
330
|
raise "invalid result (#{result.class})" unless result.is_a? ICU::RatedResult
|
224
331
|
raise "players cannot score results against themselves" if self == result.opponent
|
@@ -234,31 +341,25 @@ module ICU
|
|
234
341
|
@results.sort!{ |a,b| a.round <=> b.round }
|
235
342
|
end
|
236
343
|
|
237
|
-
def init # :nodoc:
|
238
|
-
@performance = nil
|
239
|
-
@estimated_performance = nil
|
240
|
-
@bonus_rating = nil
|
241
|
-
@bonus = 0
|
242
|
-
end
|
243
|
-
|
244
344
|
def rate!(update_bonus=false) # :nodoc:
|
245
345
|
@results.each { |r| r.rate!(self) }
|
246
|
-
|
346
|
+
self.update_bonus if update_bonus && respond_to?(:update_bonus)
|
247
347
|
end
|
248
348
|
|
249
349
|
def estimate_performance # :nodoc:
|
250
|
-
|
251
|
-
|
252
|
-
if
|
350
|
+
games, performance = results.inject([0,0.0]) do |sum, result|
|
351
|
+
rating = result.opponent.new_rating(:opponent)
|
352
|
+
if rating
|
253
353
|
sum[0]+= 1
|
254
|
-
sum[1]+=
|
354
|
+
sum[1]+= rating + (2 * result.score - 1) * 400.0
|
255
355
|
end
|
256
356
|
sum
|
257
357
|
end
|
258
|
-
if
|
259
|
-
|
260
|
-
|
261
|
-
|
358
|
+
@estimated_performance = average_performance(performance, games) if games > 0
|
359
|
+
end
|
360
|
+
|
361
|
+
def average_performance(performance, games) # :nodoc:
|
362
|
+
performance / games
|
262
363
|
end
|
263
364
|
|
264
365
|
def update_performance(thresh) # :nodoc:
|
@@ -274,22 +375,6 @@ module ICU
|
|
274
375
|
stable
|
275
376
|
end
|
276
377
|
|
277
|
-
def calculate_bonus # :nodoc:
|
278
|
-
# Rounding is performed in places to emulate the older MSAccess implementation.
|
279
|
-
return if @type != :rated || @kfactor <= 24 || @results.size <= 4 || @rating >= 2100
|
280
|
-
change = rating_change
|
281
|
-
return if change <= 35 || @rating + change >= 2100
|
282
|
-
threshold = 32 + 3 * (@results.size - 4)
|
283
|
-
bonus = (change - threshold).round
|
284
|
-
return if bonus <= 0
|
285
|
-
bonus = (1.25 * bonus).round if kfactor >= 40
|
286
|
-
[2100, @performance].each { |max| bonus = max - @rating if bonus + @rating > max }
|
287
|
-
return if bonus <= 0
|
288
|
-
bonus = bonus.round
|
289
|
-
@bonus_rating = @rating + change + bonus
|
290
|
-
@bonus = bonus
|
291
|
-
end
|
292
|
-
|
293
378
|
def ==(other) # :nodoc:
|
294
379
|
return false unless other.is_a? ICU::RatedPlayer
|
295
380
|
num == other.num
|
@@ -297,42 +382,10 @@ module ICU
|
|
297
382
|
|
298
383
|
private
|
299
384
|
|
300
|
-
def initialize(num,
|
301
|
-
|
302
|
-
|
385
|
+
def initialize(num, desc) # :nodoc:
|
386
|
+
@num = num
|
387
|
+
@desc = desc
|
303
388
|
@results = []
|
304
|
-
@type = deduce_type
|
305
|
-
@bonus = 0
|
306
|
-
end
|
307
|
-
|
308
|
-
def num=(num)
|
309
|
-
@num = num.to_i
|
310
|
-
raise "invalid player num (#{num})" if @num == 0 && !num.to_s.match(/^\s*\d/)
|
311
|
-
end
|
312
|
-
|
313
|
-
def rating=(rating)
|
314
|
-
@rating = rating.to_f
|
315
|
-
raise "invalid player rating (#{rating})" if @rating == 0.0 && !rating.to_s.match(/^\s*\d/)
|
316
|
-
end
|
317
|
-
|
318
|
-
def kfactor=(kfactor)
|
319
|
-
@kfactor = kfactor.to_f
|
320
|
-
raise "invalid player k-factor (#{kfactor})" if @kfactor <= 0.0
|
321
|
-
end
|
322
|
-
|
323
|
-
def games=(games)
|
324
|
-
@games = games.to_i
|
325
|
-
raise "invalid number of games (#{games})" if @games <= 0 || @games >= 20
|
326
|
-
end
|
327
|
-
|
328
|
-
def deduce_type
|
329
|
-
case
|
330
|
-
when @rating && @kfactor && !@games then :rated
|
331
|
-
when @rating && !@kfactor && @games then :provisional
|
332
|
-
when @rating && !@kfactor && !@games then :foreign
|
333
|
-
when !@rating && !@kfactor && !@games then :unrated
|
334
|
-
else raise "invalid combination of player attributes"
|
335
|
-
end
|
336
389
|
end
|
337
390
|
end
|
338
391
|
end
|
data/lib/icu_ratings/result.rb
CHANGED
@@ -87,32 +87,17 @@ module ICU
|
|
87
87
|
#
|
88
88
|
# The main rating calculations are available from player methods (see ICU::RatedPlayer)
|
89
89
|
# but additional details are available via methods of each player's individual results:
|
90
|
-
# _expected_score_, _rating_change_.
|
90
|
+
# _expected_score_, _rating_change_ (rated players only).
|
91
91
|
#
|
92
92
|
class RatedResult
|
93
|
-
attr_reader :round, :opponent, :score
|
94
|
-
|
95
|
-
# After the tournament has been rated, this returns the expected score (between 0 and 1)
|
96
|
-
# for the player based on the rating difference with the opponent scaled by 400.
|
97
|
-
# The standard Elo formula is used: 1/(1 + 10^(diff/400)).
|
98
|
-
def expected_score
|
99
|
-
@expected_score
|
100
|
-
end
|
101
|
-
|
102
|
-
# After the tournament has been rated, returns the change in rating due to this particular
|
103
|
-
# result. Only for rated players (returns _nil_ for other types of players). Computed from
|
104
|
-
# the difference between actual and expected scores multiplied by the player's K-factor.
|
105
|
-
# The sum of these changes is the overall rating change for rated players.
|
106
|
-
def rating_change
|
107
|
-
@rating_change
|
108
|
-
end
|
93
|
+
attr_reader :round, :opponent, :score, :expected_score, :rating_change
|
109
94
|
|
110
95
|
def rate!(player) # :nodoc:
|
111
|
-
player_rating = player.
|
112
|
-
opponent_rating = opponent.
|
96
|
+
player_rating = player.new_rating(:start)
|
97
|
+
opponent_rating = opponent.new_rating(:opponent)
|
113
98
|
if player_rating && opponent_rating
|
114
99
|
@expected_score = 1 / (1 + 10 ** ((opponent_rating - player_rating) / 400.0))
|
115
|
-
@rating_change = (
|
100
|
+
@rating_change = (score - expected_score) * player.kfactor if player.type == :rated
|
116
101
|
end
|
117
102
|
end
|
118
103
|
|
@@ -86,7 +86,7 @@ module ICU
|
|
86
86
|
def add_player(num, args={})
|
87
87
|
raise "player with number #{num} already exists" if @player[num]
|
88
88
|
args[:kfactor] = ICU::RatedPlayer.kfactor(args[:kfactor].merge({ :start => start, :rating => args[:rating] })) if args[:kfactor].is_a?(Hash)
|
89
|
-
@player[num] = ICU::RatedPlayer.
|
89
|
+
@player[num] = ICU::RatedPlayer.factory(num, args)
|
90
90
|
end
|
91
91
|
|
92
92
|
# Add a new result to the tournament. Two instances of ICU::RatedResult are
|
@@ -126,7 +126,7 @@ module ICU
|
|
126
126
|
end
|
127
127
|
|
128
128
|
# Phase 1.
|
129
|
-
players.each { |p| p.
|
129
|
+
players.each { |p| p.reset }
|
130
130
|
@iterations1 = performance_ratings(max_iterations[0], threshold)
|
131
131
|
players.each { |p| p.rate! }
|
132
132
|
|
@@ -182,7 +182,7 @@ module ICU
|
|
182
182
|
|
183
183
|
# Calculate bonuses for all players and return the number who got one.
|
184
184
|
def calculate_bonuses
|
185
|
-
@player.values.inject(0) { |t,p| t + (p.calculate_bonus ? 1 : 0) }
|
185
|
+
@player.values.select{ |p| p.respond_to?(:bonus) }.inject(0) { |t,p| t + (p.calculate_bonus ? 1 : 0) }
|
186
186
|
end
|
187
187
|
end
|
188
188
|
end
|
data/lib/icu_ratings/version.rb
CHANGED
data/spec/player_spec.rb
CHANGED
@@ -3,48 +3,44 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
3
3
|
|
4
4
|
module ICU
|
5
5
|
describe RatedPlayer do
|
6
|
-
context "#
|
6
|
+
context "#factory - different types of players" do
|
7
7
|
before(:all) do
|
8
|
-
@r = ICU::RatedPlayer.
|
9
|
-
@p = ICU::RatedPlayer.
|
10
|
-
@f = ICU::RatedPlayer.
|
11
|
-
@u = ICU::RatedPlayer.
|
8
|
+
@r = ICU::RatedPlayer.factory(1, :rating => 2000, :kfactor => 10.0)
|
9
|
+
@p = ICU::RatedPlayer.factory(2, :rating => 1500, :games => 10)
|
10
|
+
@f = ICU::RatedPlayer.factory(3, :rating => 2500)
|
11
|
+
@u = ICU::RatedPlayer.factory(4)
|
12
12
|
end
|
13
13
|
|
14
14
|
it "rated players have a rating and k-factor" do
|
15
15
|
@r.num.should == 1
|
16
16
|
@r.rating.should == 2000
|
17
17
|
@r.kfactor.should == 10.0
|
18
|
-
@r.games.should be_nil
|
19
18
|
@r.type.should == :rated
|
20
|
-
@r.
|
19
|
+
@r.should_not respond_to(:games)
|
21
20
|
end
|
22
21
|
|
23
22
|
it "provisionally rated players have a rating and number of games" do
|
24
|
-
@p.num.should
|
25
|
-
@p.rating.should
|
26
|
-
@p.
|
27
|
-
@p.
|
28
|
-
@p.
|
29
|
-
@p.full_rating?.should be_false
|
23
|
+
@p.num.should == 2
|
24
|
+
@p.rating.should == 1500
|
25
|
+
@p.games.should == 10
|
26
|
+
@p.type.should == :provisional
|
27
|
+
@p.should_not respond_to(:kfactor)
|
30
28
|
end
|
31
29
|
|
32
30
|
it "foreign players just have a rating" do
|
33
|
-
@f.num.should
|
34
|
-
@f.rating.should
|
35
|
-
@f.
|
36
|
-
@f.
|
37
|
-
@f.
|
38
|
-
@f.full_rating?.should be_true
|
31
|
+
@f.num.should == 3
|
32
|
+
@f.rating.should == 2500
|
33
|
+
@f.type.should == :foreign
|
34
|
+
@f.should_not respond_to(:kfactor)
|
35
|
+
@f.should_not respond_to(:games)
|
39
36
|
end
|
40
37
|
|
41
38
|
it "unrated players just have nothing other than their number" do
|
42
|
-
@u.num.should
|
43
|
-
@u.
|
44
|
-
@u.
|
45
|
-
@u.
|
46
|
-
@u.
|
47
|
-
@u.full_rating?.should be_false
|
39
|
+
@u.num.should == 4
|
40
|
+
@u.type.should == :unrated
|
41
|
+
@u.should_not respond_to(:rating)
|
42
|
+
@u.should_not respond_to(:kfactor)
|
43
|
+
@u.should_not respond_to(:games)
|
48
44
|
end
|
49
45
|
|
50
46
|
it "other combinations are invalid" do
|
@@ -53,13 +49,13 @@ module ICU
|
|
53
49
|
{ :games => 10, :kfactor => 10 },
|
54
50
|
{ :games => 10, :kfactor => 10, :rating => 1000 },
|
55
51
|
{ :kfactor => 10 },
|
56
|
-
].each { |opts| lambda { ICU::RatedPlayer.
|
52
|
+
].each { |opts| lambda { ICU::RatedPlayer.factory(1, opts) }.should raise_error(/invalid.*combination/i) }
|
57
53
|
end
|
58
54
|
end
|
59
55
|
|
60
56
|
context "#new - miscellaneous" do
|
61
57
|
it "attribute values can be given by strings, even when space padded" do
|
62
|
-
p = ICU::RatedPlayer.
|
58
|
+
p = ICU::RatedPlayer.factory(' 1 ', :kfactor => ' 10.0 ', :rating => ' 1000 ')
|
63
59
|
p.num.should == 1
|
64
60
|
p.kfactor.should == 10.0
|
65
61
|
p.rating.should == 1000
|
@@ -68,46 +64,46 @@ module ICU
|
|
68
64
|
|
69
65
|
context "restrictions, or lack thereof, on attributes" do
|
70
66
|
it "the player number can be zero or even negative" do
|
71
|
-
lambda { ICU::RatedPlayer.
|
72
|
-
lambda { ICU::RatedPlayer.
|
67
|
+
lambda { ICU::RatedPlayer.factory(-1) }.should_not raise_error
|
68
|
+
lambda { ICU::RatedPlayer.factory(0) }.should_not raise_error
|
73
69
|
end
|
74
70
|
|
75
71
|
it "k-factors must be positive" do
|
76
|
-
lambda { ICU::RatedPlayer.
|
77
|
-
lambda { ICU::RatedPlayer.
|
72
|
+
lambda { ICU::RatedPlayer.factory(1, :kfactor => 0) }.should raise_error(/invalid.*factor/i)
|
73
|
+
lambda { ICU::RatedPlayer.factory(1, :kfactor => -1) }.should raise_error(/invalid.*factor/i)
|
78
74
|
end
|
79
75
|
|
80
76
|
it "the rating can be zero or even negative" do
|
81
|
-
lambda { ICU::RatedPlayer.
|
82
|
-
lambda { ICU::RatedPlayer.
|
77
|
+
lambda { ICU::RatedPlayer.factory(1, :rating => 0) }.should_not raise_error
|
78
|
+
lambda { ICU::RatedPlayer.factory(1, :rating => -1) }.should_not raise_error
|
83
79
|
end
|
84
80
|
|
85
81
|
it "ratings are stored as floats but can be specified with an integer" do
|
86
|
-
ICU::RatedPlayer.
|
87
|
-
ICU::RatedPlayer.
|
88
|
-
ICU::RatedPlayer.
|
82
|
+
ICU::RatedPlayer.factory(1, :rating => 1234.5).rating.should == 1234.5
|
83
|
+
ICU::RatedPlayer.factory(1, :rating => 1234.0).rating.should == 1234
|
84
|
+
ICU::RatedPlayer.factory(1, :rating => 1234).rating.should == 1234
|
89
85
|
end
|
90
86
|
|
91
87
|
it "the number of games shoud not exceed 20" do
|
92
|
-
lambda { ICU::RatedPlayer.
|
93
|
-
lambda { ICU::RatedPlayer.
|
94
|
-
lambda { ICU::RatedPlayer.
|
88
|
+
lambda { ICU::RatedPlayer.factory(1, :rating => 1000, :games => 19) }.should_not raise_error
|
89
|
+
lambda { ICU::RatedPlayer.factory(1, :rating => 1000, :games => 20) }.should raise_error
|
90
|
+
lambda { ICU::RatedPlayer.factory(1, :rating => 1000, :games => 21) }.should raise_error
|
95
91
|
end
|
96
92
|
|
97
93
|
it "a description, such as a name, but can be any object, is optional" do
|
98
|
-
ICU::RatedPlayer.
|
99
|
-
ICU::RatedPlayer.
|
100
|
-
ICU::RatedPlayer.
|
101
|
-
ICU::RatedPlayer.
|
94
|
+
ICU::RatedPlayer.factory(1, :desc => 'Fischer, Robert').desc.should == 'Fischer, Robert'
|
95
|
+
ICU::RatedPlayer.factory(1, :desc => 1).desc.should be_an_instance_of(Fixnum)
|
96
|
+
ICU::RatedPlayer.factory(1, :desc => 1.0).desc.should be_an_instance_of(Float)
|
97
|
+
ICU::RatedPlayer.factory(1).desc.should be_nil
|
102
98
|
end
|
103
99
|
end
|
104
100
|
|
105
101
|
context "results" do
|
106
102
|
before(:each) do
|
107
103
|
@p = ICU::RatedPlayer.new(1, :kfactor => 10, :rating => 1000)
|
108
|
-
@r1 = ICU::RatedResult.new(1, ICU::RatedPlayer.
|
109
|
-
@r2 = ICU::RatedResult.new(2, ICU::RatedPlayer.
|
110
|
-
@r3 = ICU::RatedResult.new(3, ICU::RatedPlayer.
|
104
|
+
@r1 = ICU::RatedResult.new(1, ICU::RatedPlayer.factory(2), 'W')
|
105
|
+
@r2 = ICU::RatedResult.new(2, ICU::RatedPlayer.factory(3), 'L')
|
106
|
+
@r3 = ICU::RatedResult.new(3, ICU::RatedPlayer.factory(4), 'D')
|
111
107
|
end
|
112
108
|
|
113
109
|
it "should be returned in round order" do
|
data/spec/result_spec.rb
CHANGED
@@ -5,20 +5,20 @@ module ICU
|
|
5
5
|
describe RatedResult do
|
6
6
|
context "a basic rated result" do
|
7
7
|
before(:all) do
|
8
|
-
@o = ICU::RatedPlayer.
|
8
|
+
@o = ICU::RatedPlayer.factory(2)
|
9
9
|
end
|
10
10
|
|
11
11
|
it "needs a round, opponent and score (win, loss or draw)" do
|
12
12
|
r = ICU::RatedResult.new(1, @o, 'W')
|
13
13
|
r.round.should == 1
|
14
|
-
r.opponent.should
|
14
|
+
r.opponent.should be_a_kind_of(ICU::RatedPlayer)
|
15
15
|
r.score.should == 1.0
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
19
|
context "restrictions, or lack thereof, on attributes" do
|
20
20
|
before(:each) do
|
21
|
-
@p = ICU::RatedPlayer.
|
21
|
+
@p = ICU::RatedPlayer.factory(2)
|
22
22
|
end
|
23
23
|
|
24
24
|
it "round numbers must be positive" do
|
@@ -41,7 +41,7 @@ module ICU
|
|
41
41
|
|
42
42
|
context "#opponents_score" do
|
43
43
|
before(:each) do
|
44
|
-
@p = ICU::RatedPlayer.
|
44
|
+
@p = ICU::RatedPlayer.factory(2)
|
45
45
|
end
|
46
46
|
|
47
47
|
it "should give the score from the opponent's perspective" do
|
@@ -53,8 +53,8 @@ module ICU
|
|
53
53
|
|
54
54
|
context "equality" do
|
55
55
|
before(:each) do
|
56
|
-
@p1 = ICU::RatedPlayer.
|
57
|
-
@p2 = ICU::RatedPlayer.
|
56
|
+
@p1 = ICU::RatedPlayer.factory(1)
|
57
|
+
@p2 = ICU::RatedPlayer.factory(2)
|
58
58
|
@r1 = ICU::RatedResult.new(1, @p1, 'W')
|
59
59
|
@r2 = ICU::RatedResult.new(1, @p1, 'W')
|
60
60
|
@r3 = ICU::RatedResult.new(2, @p1, 'W')
|
data/spec/tournament_spec.rb
CHANGED
@@ -343,7 +343,7 @@ module ICU
|
|
343
343
|
unless num == 13
|
344
344
|
p = @t.player(num)
|
345
345
|
p.expected_score.should_not == 0.0
|
346
|
-
p.rating_change
|
346
|
+
p.should_not respond_to(:rating_change)
|
347
347
|
p.new_rating.should == p.rating
|
348
348
|
end
|
349
349
|
end
|
@@ -762,17 +762,17 @@ module ICU
|
|
762
762
|
|
763
763
|
it "should agree with ICU rating database" do
|
764
764
|
[
|
765
|
-
[1, 3.5, 3.84, 977, 1073,
|
766
|
-
[2, 4.0, 3.51, 1068, 1042,
|
767
|
-
[3, 1.0, 1.05, 636, 636,
|
768
|
-
[4, 0.0, 0.78, 520, 537,
|
769
|
-
[5, 3.5, 1.74, 1026, 835,
|
770
|
-
[6, 3.0, 3.54, 907, 1010,
|
765
|
+
[1, 3.5, 3.84, 977, 1073, 0], # Austin
|
766
|
+
[2, 4.0, 3.51, 1068, 1042, 0], # Kevin
|
767
|
+
[3, 1.0, 1.05, 636, 636, nil], # Nikhil
|
768
|
+
[4, 0.0, 0.78, 520, 537, 0], # Sean
|
769
|
+
[5, 3.5, 1.74, 1026, 835, 45], # Michael
|
770
|
+
[6, 3.0, 3.54, 907, 1010, 0], # Eoin
|
771
771
|
].each do |item|
|
772
772
|
num, score, expected_score, performance, new_rating, bonus = item
|
773
773
|
p = @t.player(num)
|
774
774
|
p.score.should == score
|
775
|
-
p.bonus.should == bonus
|
775
|
+
p.bonus.should == bonus if bonus
|
776
776
|
p.performance.should be_within(0.5).of(performance)
|
777
777
|
p.expected_score.should be_within(0.01).of(expected_score)
|
778
778
|
p.new_rating.should be_within(0.5).of(new_rating)
|
@@ -817,13 +817,13 @@ module ICU
|
|
817
817
|
|
818
818
|
@m = [
|
819
819
|
# MSAccess results taken from rerun of original which is different (reason unknown).
|
820
|
-
[1, 4.0, 4.28, 1052, 1052,
|
821
|
-
[2, 3.5, 1.93, 920, 757,
|
822
|
-
[3, 3.5, 2.29, 932, 798,
|
823
|
-
[4, 1.0, 1.52, 588, 707,
|
824
|
-
[5, 1.0, 1.40, 828, 828,
|
825
|
-
[6, 1.0, 0.91, 627, 627,
|
826
|
-
[7, 0.0, 0.78, 460, 635,
|
820
|
+
[1, 4.0, 4.28, 1052, 1052, nil], # Fanjini
|
821
|
+
[2, 3.5, 1.93, 920, 757, 35], # Guinan
|
822
|
+
[3, 3.5, 2.29, 932, 798, 18], # Duffy
|
823
|
+
[4, 1.0, 1.52, 588, 707, 0], # Cooke
|
824
|
+
[5, 1.0, 1.40, 828, 828, nil], # Callaghan
|
825
|
+
[6, 1.0, 0.91, 627, 627, nil], # Montenegro
|
826
|
+
[7, 0.0, 0.78, 460, 635, 0], # Lowry-O'Reilly
|
827
827
|
]
|
828
828
|
end
|
829
829
|
|
@@ -832,7 +832,7 @@ module ICU
|
|
832
832
|
num, score, expected_score, performance, new_rating, bonus = item
|
833
833
|
p = @t.player(num)
|
834
834
|
p.score.should == score
|
835
|
-
p.bonus.should == bonus
|
835
|
+
p.bonus.should == bonus if bonus
|
836
836
|
p.performance.should be_within(num == 2 ? 0.6 : 0.5).of(performance)
|
837
837
|
p.expected_score.should be_within(0.01).of(expected_score)
|
838
838
|
p.new_rating.should be_within(0.5).of(new_rating)
|
@@ -845,7 +845,7 @@ module ICU
|
|
845
845
|
num, score, expected_score, performance, new_rating, bonus = item
|
846
846
|
p = @t.player(num)
|
847
847
|
p.score.should == score
|
848
|
-
p.bonus.should == bonus
|
848
|
+
p.bonus.should == bonus if bonus
|
849
849
|
p.performance.should be_within(num == 2 ? 0.6 : 0.5).of(performance)
|
850
850
|
p.expected_score.should be_within(0.01).of(expected_score)
|
851
851
|
p.new_rating.should be_within(0.5).of(new_rating)
|
@@ -859,7 +859,7 @@ module ICU
|
|
859
859
|
num, score, expected_score, performance, new_rating, bonus = item
|
860
860
|
p = @t.player(num)
|
861
861
|
p.score.should == score
|
862
|
-
p.bonus.should == 0
|
862
|
+
p.bonus.should == 0 if bonus
|
863
863
|
p.performance.should_not be_within(1.0).of(performance)
|
864
864
|
p.expected_score.should_not be_within(0.01).of(expected_score)
|
865
865
|
p.new_rating.should_not be_within(1.0).of(new_rating)
|
@@ -986,7 +986,7 @@ module ICU
|
|
986
986
|
end
|
987
987
|
|
988
988
|
it "should behave like ratings.ciu.ie" do
|
989
|
-
@p1.
|
989
|
+
@p1.instance_eval { @kfactor = 32 }
|
990
990
|
@t.rate!
|
991
991
|
@p1.new_rating.should be_within(0.5).of(1603)
|
992
992
|
@p1.expected_score.should be_within(0.001).of(2.868)
|
@@ -1230,7 +1230,7 @@ module ICU
|
|
1230
1230
|
@o5.new_rating.should == @o5.performance
|
1231
1231
|
@o6.new_rating.should == @o6.performance
|
1232
1232
|
|
1233
|
-
ratings = [@o1, @o2, @o3, @o4, @o5, @o6].map { |o| o.
|
1233
|
+
ratings = [@o1, @o2, @o3, @o4, @o5, @o6].map { |o| o.new_rating(:opponent) }
|
1234
1234
|
|
1235
1235
|
average_of_ratings = ratings.inject(0.0){ |m,r| m = m + r } / 6.0
|
1236
1236
|
average_of_ratings.should_not be_within(0.5).of(@p.new_rating)
|
@@ -1251,7 +1251,7 @@ module ICU
|
|
1251
1251
|
@o5.new_rating.should == @o5.performance
|
1252
1252
|
@o6.new_rating.should == @o6.performance
|
1253
1253
|
|
1254
|
-
ratings = [@o1, @o2, @o3, @o4, @o5, @o6].map { |o| o.
|
1254
|
+
ratings = [@o1, @o2, @o3, @o4, @o5, @o6].map { |o| o.new_rating(:opponent) }
|
1255
1255
|
|
1256
1256
|
average_of_ratings = ratings.inject(0.0){ |m,r| m = m + r } / 6.0
|
1257
1257
|
average_of_ratings.should be_within(0.5).of(@p.new_rating)
|
@@ -2079,12 +2079,12 @@ module ICU
|
|
2079
2079
|
end
|
2080
2080
|
end
|
2081
2081
|
|
2082
|
-
context "#rate -
|
2082
|
+
context "#rate - Deirdre Turner in the Limerick U1400 2012" do
|
2083
2083
|
before(:each) do
|
2084
2084
|
@t = ICU::RatedTournament.new(desc: "Limerick U1400 2012")
|
2085
2085
|
|
2086
|
-
# Add the players of most interest (
|
2087
|
-
@p = @t.add_player(6697, desc: "
|
2086
|
+
# Add the players of most interest (Deirdre Turner and her opponents).
|
2087
|
+
@p = @t.add_player(6697, desc: "Deirdre Turner")
|
2088
2088
|
@o1 = @t.add_player(6678, desc: "John P. Dunne", rating: 980, kfactor: 40)
|
2089
2089
|
@o2 = @t.add_player(6694, desc: "Jordan O'Sullivan")
|
2090
2090
|
@o3 = @t.add_player(6681, desc: "Ruairi Freeman", rating: 537, kfactor: 40)
|
@@ -2111,7 +2111,7 @@ module ICU
|
|
2111
2111
|
@t.add_player(6696, desc: "Mark David Tonita")
|
2112
2112
|
@t.add_player(6698, desc: "Eoghan Turner")
|
2113
2113
|
|
2114
|
-
#
|
2114
|
+
# Deirdre's results.
|
2115
2115
|
@t.add_result(1, 6697, 6678, "L")
|
2116
2116
|
@t.add_result(2, 6697, 6694, "W")
|
2117
2117
|
@t.add_result(3, 6697, 6681, "W")
|
@@ -2183,7 +2183,7 @@ module ICU
|
|
2183
2183
|
end
|
2184
2184
|
|
2185
2185
|
it "should be setup properly" do
|
2186
|
-
@p.desc.should == "
|
2186
|
+
@p.desc.should == "Deirdre Turner"
|
2187
2187
|
@o1.desc.should == "John P. Dunne"
|
2188
2188
|
@o2.desc.should == "Jordan O'Sullivan"
|
2189
2189
|
@o3.desc.should == "Ruairi Freeman"
|
@@ -2200,7 +2200,7 @@ module ICU
|
|
2200
2200
|
@o6.type.should == :provisional
|
2201
2201
|
|
2202
2202
|
@o1.rating.should == 980
|
2203
|
-
@o2.rating
|
2203
|
+
@o2.should_not respond_to(:rating)
|
2204
2204
|
@o3.rating.should == 537
|
2205
2205
|
@o4.rating.should == 682
|
2206
2206
|
@o5.rating.should == 1320
|
@@ -2216,16 +2216,14 @@ module ICU
|
|
2216
2216
|
@p.new_rating.should == @p.performance
|
2217
2217
|
|
2218
2218
|
@o1.bonus.should == 23
|
2219
|
-
@o2.bonus.should == 0
|
2220
2219
|
@o3.bonus.should == 0
|
2221
2220
|
@o4.bonus.should == 0
|
2222
2221
|
@o5.bonus.should == 0
|
2223
|
-
@o6.bonus.should == 0
|
2224
2222
|
|
2225
|
-
ratings = [@o1, @o2, @o3, @o4, @o5, @o6].map { |o| o.
|
2223
|
+
ratings = [@o1, @o2, @o3, @o4, @o5, @o6].map { |o| o.new_rating(:opponent) }
|
2226
2224
|
|
2227
2225
|
performance = ratings.inject(0.0){ |m,r| m = m + r } / 6.0
|
2228
|
-
performance.should be_within(0.
|
2226
|
+
performance.should be_within(0.1).of(@p.new_rating)
|
2229
2227
|
|
2230
2228
|
@t.iterations1.should be > 1
|
2231
2229
|
@t.iterations2.should be > 1
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: icu_ratings
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.5.
|
4
|
+
version: 1.5.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-12-
|
12
|
+
date: 2012-12-29 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -111,7 +111,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
111
111
|
version: '0'
|
112
112
|
segments:
|
113
113
|
- 0
|
114
|
-
hash:
|
114
|
+
hash: 1156445910149714022
|
115
115
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
116
|
none: false
|
117
117
|
requirements:
|
@@ -120,7 +120,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
120
|
version: '0'
|
121
121
|
segments:
|
122
122
|
- 0
|
123
|
-
hash:
|
123
|
+
hash: 1156445910149714022
|
124
124
|
requirements: []
|
125
125
|
rubyforge_project: icu_ratings
|
126
126
|
rubygems_version: 1.8.23
|