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