whole_history_rating 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Pete Schwamb
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,19 @@
1
+
2
+ # Whole History Rating
3
+
4
+ A system for ranking game players by skill, based on Rémi Coulom's Whole History Rating algorithm.
5
+
6
+ Developed for use on [GoShrine](http://goshrine.com), but the code is not go specific. It can support any two player game, as long as the outcome is a simple win/loss. An addition to the algorithm is support for handicaps.
7
+
8
+ Installation
9
+ ------------
10
+
11
+ * gem install whole-history-rating
12
+
13
+ Enjoy!
14
+
15
+ -Pete
16
+
17
+
18
+
19
+
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+
3
+ module WholeHistoryRating
4
+
5
+ VERSION = "0.1.0"
6
+
7
+ STDOUT.sync = true
8
+
9
+ ROOT = File.expand_path(File.dirname(__FILE__))
10
+
11
+ %w[ base
12
+ player
13
+ game
14
+ player_day
15
+ ].each do |lib|
16
+ require File.join(ROOT, 'whole_history_rating', lib)
17
+ end
18
+
19
+ end
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env ruby
2
+ require 'date'
3
+
4
+ module WholeHistoryRating
5
+
6
+ class Base
7
+
8
+ class UnstableRatingException < RuntimeError; end
9
+
10
+ attr_accessor :players, :games
11
+
12
+ def initialize(config = {})
13
+ @config = config
14
+ @config[:w2] ||= 300.0 # elo^2
15
+ @games = []
16
+ @players = {}
17
+ @start_date = nil
18
+ end
19
+
20
+ def print_ordered_ratings
21
+ players = @players.values.select {|p| p.days.count > 0}
22
+ players.sort_by { |p| p.days.last.gamma }.each_with_index do |p,idx|
23
+ if p.days.count > 0
24
+ puts "#{p.name} => #{p.days.map(&:elo)}"
25
+ end
26
+ end
27
+ end
28
+
29
+ def log_likelihood
30
+ score = 0.0
31
+ @players.values.each do |p|
32
+ unless p.days.empty?
33
+ score += p.log_likelihood
34
+ end
35
+ end
36
+ score
37
+ end
38
+
39
+ def player_by_name(name)
40
+ players[name] || players[name] = Player.new(name, @config)
41
+ end
42
+
43
+ def create_game(options)
44
+ if options['finished_at'].nil?
45
+ puts "Skipping (not finished) #{options.inspect}"
46
+ return nil
47
+ end
48
+
49
+ game_date = Date.parse(options['finished_at'])
50
+
51
+ @start_date ||= game_date
52
+
53
+ # Avoid self-played games (no info)
54
+ if options['w_name'] == options['b_name']
55
+ puts "Skipping (black name == white name ?) #{options.inspect}"
56
+ return nil
57
+ end
58
+
59
+ day_num = (game_date - @start_date).to_i
60
+
61
+ #puts "day num = #{day_num}"
62
+
63
+ white_player = player_by_name(options['w_name'])
64
+ white_player.id = options['w_id']
65
+ black_player = player_by_name(options['b_name'])
66
+ black_player.id = options['b_id']
67
+ game = Game.new(day_num, white_player, black_player, options['winner'], options['handicap'], options['extras'])
68
+ game.date = game_date
69
+ game
70
+ end
71
+
72
+ def add_game(game)
73
+ game.white_player.add_game(game)
74
+ game.black_player.add_game(game)
75
+ if game.bpd.nil?
76
+ puts "Bad game: #{options.inspect} -> #{game.inspect}"
77
+ end
78
+ @games << game
79
+ game
80
+ end
81
+
82
+ def run_one_iteration
83
+ players.each do |name,player|
84
+ player.run_one_newton_iteration
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,67 @@
1
+ module WholeHistoryRating
2
+ class Game
3
+ attr_accessor :day, :date, :white_player, :black_player, :handicap, :winner, :wpd, :bpd, :extras
4
+
5
+ def initialize(day, white_player, black_player, winner, handicap, extras)
6
+ @day = day
7
+ @white_player = white_player
8
+ @black_player = black_player
9
+ @winner = winner
10
+ @extras = extras
11
+ @handicap = handicap || 0
12
+ @handicap_proc = handicap if handicap.is_a?(Proc)
13
+ end
14
+
15
+ def opponents_adjusted_gamma(player)
16
+ black_advantage = @handicap_proc ? @handicap_proc.call(self) : @handicap
17
+ #puts "black_advantage = #{black_advantage}"
18
+
19
+ if player == white_player
20
+ opponent_elo = bpd.elo + black_advantage
21
+ elsif player == black_player
22
+ opponent_elo = wpd.elo - black_advantage
23
+ else
24
+ raise "No opponent for #{player.inspect}, since they're not in this game: #{self.inspect}."
25
+ end
26
+ rval = 10**(opponent_elo/400.0)
27
+ if rval == 0 || rval.infinite? || rval.nan?
28
+ raise WHR::UnstableRatingException, "bad adjusted gamma: #{inspect}"
29
+ end
30
+ rval
31
+ end
32
+
33
+ def opponent(player)
34
+ if player == white_player
35
+ black_player
36
+ elsif player == black_player
37
+ white_player
38
+ end
39
+ end
40
+
41
+ def prediction_score
42
+ if white_win_probability == 0.5
43
+ 0.5
44
+ else
45
+ ((winner == "W" && white_win_probability > 0.5) || (winner == "B" && white_win_probability < 0.5)) ? 1.0 : 0.0
46
+ end
47
+ end
48
+
49
+ def inspect
50
+ "#{self}: W:#{white_player.name}(r=#{wpd ? wpd.r : '?'}) B:#{black_player.name}(r=#{bpd ? bpd.r : '?'}) winner = #{winner}, komi = #{@komi}, handicap = #{@handicap}"
51
+ end
52
+
53
+ #def likelihood
54
+ # winner == "W" ? white_win_probability : 1-white_win_probability
55
+ #end
56
+
57
+ # This is the Bradley-Terry Model
58
+ def white_win_probability
59
+ wpd.gamma/(wpd.gamma + opponents_adjusted_gamma(white_player))
60
+ end
61
+
62
+ def black_win_probability
63
+ bpd.gamma/(bpd.gamma + opponents_adjusted_gamma(black_player))
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,254 @@
1
+ require 'matrix'
2
+
3
+ module WholeHistoryRating
4
+ class Player
5
+ attr_accessor :name, :anchor_gamma, :days, :w2, :debug, :id
6
+
7
+ def initialize(name, config)
8
+ @name = name
9
+ @debug = config[:debug]
10
+ @w2 = (Math.sqrt(config[:w2])*Math.log(10)/400)**2 # Convert from elo^2 to r^2
11
+ @days = []
12
+ end
13
+
14
+ def inspect
15
+ "#{self}:(#{name})"
16
+ end
17
+
18
+ def log_likelihood
19
+ sum = 0.0
20
+ sigma2 = compute_sigma2
21
+ n = days.count
22
+ 0.upto(n-1) do |i|
23
+ prior = 0
24
+ if i < (n-1)
25
+ rd = days[i].r - days[i+1].r
26
+ prior += (1/(Math.sqrt(2*Math::PI*sigma2[i]))) * Math.exp(-(rd**2)/2*sigma2[i])
27
+ end
28
+ if i > 0
29
+ rd = days[i].r - days[i-1].r
30
+ prior += (1/(Math.sqrt(2*Math::PI*sigma2[i-1]))) * Math.exp(-(rd**2)/2*sigma2[i-1])
31
+ end
32
+ if prior == 0
33
+ sum += days[i].log_likelihood
34
+ else
35
+ if (days[i].log_likelihood.infinite? || Math.log(prior).infinite?)
36
+ puts "Infinity at #{inspect}: #{days[i].log_likelihood} + #{Math.log(prior)}: prior = #{prior}, days = #{days.inspect}"
37
+ exit
38
+ end
39
+ sum += days[i].log_likelihood + Math.log(prior)
40
+ end
41
+ end
42
+ sum
43
+ end
44
+
45
+ def hessian(days, sigma2)
46
+ n = days.count
47
+ Matrix.build(n) do |row,col|
48
+ if row == col
49
+ prior = 0
50
+ prior += -1.0/sigma2[row] if row < (n-1)
51
+ prior += -1.0/sigma2[row-1] if row > 0
52
+ days[row].log_likelihood_second_derivative + prior - 0.001
53
+ elsif row == col-1
54
+ 1.0/sigma2[row]
55
+ elsif row == col+1
56
+ 1.0/sigma2[col]
57
+ else
58
+ 0
59
+ end
60
+ end
61
+ end
62
+
63
+ def gradient(r, days, sigma2)
64
+ g = []
65
+ n = days.count
66
+ days.each_with_index do |day,idx|
67
+ prior = 0
68
+ prior += -(r[idx]-r[idx+1])/sigma2[idx] if idx < (n-1)
69
+ prior += -(r[idx]-r[idx-1])/sigma2[idx-1] if idx > 0
70
+ if @debug
71
+ puts "g[#{idx}] = #{day.log_likelihood_derivative} + #{prior}"
72
+ end
73
+ g << day.log_likelihood_derivative + prior
74
+ end
75
+ g
76
+ end
77
+
78
+ def run_one_newton_iteration
79
+ days.each do |day|
80
+ day.clear_game_terms_cache
81
+ end
82
+
83
+ if days.count == 1
84
+ days[0].update_by_1d_newtons_method
85
+ elsif days.count > 1
86
+ update_by_ndim_newton
87
+ end
88
+ end
89
+
90
+ def compute_sigma2
91
+ sigma2 = []
92
+ days.each_cons(2) do |d1,d2|
93
+ sigma2 << (d2.day - d1.day).abs * @w2
94
+ end
95
+ sigma2
96
+ end
97
+
98
+ def update_by_ndim_newton
99
+ # r
100
+ r = days.map(&:r)
101
+
102
+ if @debug
103
+ puts "Updating #{inspect}"
104
+ days.each do |day|
105
+ puts "day[#{day.day}] r = #{day.r}"
106
+ puts "day[#{day.day}] win terms = #{day.won_game_terms}"
107
+ puts "day[#{day.day}] win games = #{day.won_games}"
108
+ puts "day[#{day.day}] lose terms = #{day.lost_game_terms}"
109
+ puts "day[#{day.day}] lost games = #{day.lost_games}"
110
+ puts "day[#{day.day}] log(p) = #{day.log_likelihood}"
111
+ puts "day[#{day.day}] dlp = #{day.log_likelihood_derivative}"
112
+ puts "day[#{day.day}] dlp2 = #{day.log_likelihood_second_derivative}"
113
+ end
114
+ end
115
+
116
+ # sigma squared (used in the prior)
117
+ sigma2 = compute_sigma2
118
+
119
+ h = hessian(days, sigma2)
120
+ g = gradient(r, days, sigma2)
121
+
122
+ a = []
123
+ d = [h[0,0]]
124
+ b = [h[0,1]]
125
+
126
+ n = r.size
127
+ (1..(n-1)).each do |i|
128
+ a[i] = h[i,i-1] / d[i-1]
129
+ d[i] = h[i,i] - a[i] * b[i-1]
130
+ b[i] = h[i,i+1]
131
+ end
132
+
133
+
134
+ y = [g[0]]
135
+ (1..(n-1)).each do |i|
136
+ y[i] = g[i] - a[i] * y[i-1]
137
+ end
138
+
139
+ x = []
140
+ x[n-1] = y[n-1] / d[n-1]
141
+ (n-2).downto(0) do |i|
142
+ x[i] = (y[i] - b[i] * x[i+1]) / d[i]
143
+ end
144
+
145
+ new_r = r.zip(x).map {|ri,xi| ri-xi}
146
+
147
+ new_r.each do |r|
148
+ if r > 650
149
+ raise WHR::UnstableRatingException, "Unstable r (#{new_r}) on player #{inspect}"
150
+ end
151
+ end
152
+
153
+ if @debug
154
+ puts "Hessian = #{h}"
155
+ puts "gradient = #{g}"
156
+ puts "a = #{a}"
157
+ puts "d = #{d}"
158
+ puts "b = #{b}"
159
+ puts "y = #{y}"
160
+ puts "x = #{x}"
161
+ puts "#{inspect} (#{r}) => (#{new_r})"
162
+ end
163
+
164
+ days.each_with_index do |day,idx|
165
+ day.r = day.r - x[idx]
166
+ end
167
+ end
168
+
169
+ def covariance
170
+ r = days.map(&:r)
171
+
172
+ sigma2 = compute_sigma2
173
+ h = hessian(days, sigma2)
174
+ g = gradient(r, days, sigma2)
175
+
176
+ n = days.count
177
+
178
+ a = []
179
+ d = [h[0,0]]
180
+ b = [h[0,1]]
181
+
182
+ n = r.size
183
+ (1..(n-1)).each do |i|
184
+ a[i] = h[i,i-1] / d[i-1]
185
+ d[i] = h[i,i] - a[i] * b[i-1]
186
+ b[i] = h[i,i+1]
187
+ end
188
+
189
+ dp = []
190
+ dp[n-1] = h[n-1,n-1]
191
+ bp = []
192
+ bp[n-1] = h[n-1,n-2]
193
+ ap = []
194
+ (n-2).downto(0) do |i|
195
+ ap[i] = h[i,i+1] / dp[i+1]
196
+ dp[i] = h[i,i] - ap[i]*bp[i+1]
197
+ bp[i] = h[i,i-1]
198
+ end
199
+
200
+ v = []
201
+ 0.upto(n-2) do |i|
202
+ v[i] = dp[i+1]/(b[i]*bp[i+1] - d[i]*dp[i+1])
203
+ end
204
+ v[n-1] = -1/d[n-1]
205
+
206
+ #puts "a = #{a}"
207
+ #puts "b = #{b}"
208
+ #puts "bp = #{bp}"
209
+ #puts "d = #{d}"
210
+ #puts "dp = #{dp}"
211
+ #puts "v = #{v}"
212
+
213
+ Matrix.build(n) do |row,col|
214
+ if row == col
215
+ v[row]
216
+ elsif row == col-1
217
+ -1*a[col]*v[col]
218
+ else
219
+ 0
220
+ end
221
+ end
222
+ end
223
+
224
+ def update_uncertainty
225
+ if days.count > 0
226
+ c = covariance
227
+ u = (0..(days.count-1)).collect{|i| c[i,i]}
228
+ days.zip(u) {|d,u| d.uncertainty = u}
229
+ else
230
+ 5
231
+ end
232
+ end
233
+
234
+ def add_game(game)
235
+ if days.last.nil? || days.last.day != game.day
236
+ new_pday = PlayerDay.new(self, game.day)
237
+ new_pday.date = game.date
238
+ if days.empty?
239
+ new_pday.is_first_day = true
240
+ new_pday.gamma = 1
241
+ else
242
+ new_pday.gamma = days.last.gamma
243
+ end
244
+ days << new_pday
245
+ end
246
+ if (game.white_player == self)
247
+ game.wpd = days.last
248
+ else
249
+ game.bpd = days.last
250
+ end
251
+ days.last.add_game(game)
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,119 @@
1
+
2
+ module WholeHistoryRating
3
+ class PlayerDay
4
+ attr_accessor :won_games, :lost_games, :name, :kyudan, :day, :date, :player, :r, :is_first_day, :uncertainty
5
+ def initialize(player, day)
6
+ @day = day
7
+ @player = player
8
+ @is_first_day = false
9
+ @won_games = []
10
+ @lost_games = []
11
+ end
12
+
13
+ def gamma=(gamma)
14
+ @r = Math.log(gamma)
15
+ end
16
+
17
+ def gamma
18
+ Math.exp(@r)
19
+ end
20
+
21
+ def elo=(elo)
22
+ @r = elo * (Math.log(10)/400.0)
23
+ end
24
+
25
+ def elo
26
+ (@r * 400.0)/(Math.log(10))
27
+ end
28
+
29
+ def clear_game_terms_cache
30
+ @won_game_terms = nil
31
+ @lost_game_terms = nil
32
+ end
33
+
34
+ def won_game_terms
35
+ if @won_game_terms.nil?
36
+ @won_game_terms = @won_games.map do |g|
37
+ other_gamma = g.opponents_adjusted_gamma(player)
38
+ if other_gamma == 0 || other_gamma.nan? || other_gamma.infinite?
39
+ puts "other_gamma (#{g.opponent(player).inspect}) = #{other_gamma}"
40
+ end
41
+ [1.0,0.0,1.0,other_gamma]
42
+ end
43
+ if is_first_day
44
+ @won_game_terms << [1.0,0.0,1.0,1.0] # win against virtual player ranked with gamma = 1.0
45
+ end
46
+ end
47
+ @won_game_terms
48
+ end
49
+
50
+ def lost_game_terms
51
+ if @lost_game_terms.nil?
52
+ @lost_game_terms = @lost_games.map do |g|
53
+ other_gamma = g.opponents_adjusted_gamma(player)
54
+ if other_gamma == 0 || other_gamma.nan? || other_gamma.infinite?
55
+ puts "other_gamma (#{g.opponent(player).inspect}) = #{other_gamma}"
56
+ end
57
+ [0.0,other_gamma,1.0,other_gamma]
58
+ end
59
+ if is_first_day
60
+ @lost_game_terms << [0.0,1.0,1.0,1.0] # loss against virtual player ranked with gamma = 1.0
61
+ end
62
+ end
63
+ @lost_game_terms
64
+ end
65
+
66
+ def log_likelihood_second_derivative
67
+ sum = 0.0
68
+ (won_game_terms + lost_game_terms).each do |a,b,c,d|
69
+ sum += (c*d) / ((c*gamma + d)**2.0)
70
+ end
71
+ if gamma.nan? || sum.nan?
72
+ puts "won_game_terms = #{won_game_terms}"
73
+ puts "lost_game_terms = #{lost_game_terms}"
74
+ end
75
+ -1 * gamma * sum
76
+ end
77
+
78
+ def log_likelihood_derivative
79
+ tally = 0.0
80
+ (won_game_terms + lost_game_terms).each do |a,b,c,d|
81
+ tally += c/(c*gamma + d)
82
+ end
83
+ won_game_terms.count - gamma * tally
84
+ end
85
+
86
+ def log_likelihood
87
+ tally = 0.0
88
+ won_game_terms.each do |a,b,c,d|
89
+ tally += Math.log(a*gamma)
90
+ tally -= Math.log(c*gamma + d)
91
+ end
92
+ lost_game_terms.each do |a,b,c,d|
93
+ tally += Math.log(b)
94
+ tally -= Math.log(c*gamma + d)
95
+ end
96
+ tally
97
+ end
98
+
99
+ def add_game(game)
100
+ if (game.winner == "W" && game.white_player == @player) ||
101
+ (game.winner == "B" && game.black_player == @player)
102
+ @won_games << game
103
+ else
104
+ @lost_games << game
105
+ end
106
+ end
107
+
108
+ def update_by_1d_newtons_method
109
+ dlogp = log_likelihood_derivative
110
+ d2logp = log_likelihood_second_derivative
111
+ dr = (log_likelihood_derivative / log_likelihood_second_derivative)
112
+ new_r = @r - dr
113
+ #new_r = [0, @r - dr].max
114
+ #puts "(#{player.name}) #{new_r} = #{@r} - (#{log_likelihood_derivative}/#{log_likelihood_second_derivative})"
115
+ @r = new_r
116
+ end
117
+
118
+ end
119
+ end
@@ -0,0 +1,31 @@
1
+ DIR = File.dirname(__FILE__)
2
+ LIB = File.join(DIR, *%w[lib whole_history_rating.rb])
3
+ VERSION = open(LIB) { |lib|
4
+ lib.each { |line|
5
+ if v = line[/^\s*VERSION\s*=\s*(['"])(\d+\.\d+\.\d+)\1/, 2]
6
+ break v
7
+ end
8
+ }
9
+ }
10
+
11
+ SPEC = Gem::Specification.new do |s|
12
+ s.name = "whole_history_rating"
13
+ s.version = VERSION
14
+ s.platform = Gem::Platform::RUBY
15
+ s.authors = ["Pete Schwamb"]
16
+ s.email = ["pete@schwamb.net"]
17
+ s.homepage = "http://github.com/goshrine/whole_history_rating"
18
+ s.summary = "A pure ruby implementation of Remi Coulom's Whole-History Rating algorithm."
19
+ s.description = <<-END_DESCRIPTION.gsub(/\s+/, " ").strip
20
+ This gem provides a library and executables that take as input as set of games and output a set of skill rankings for the players of those games. The algorithm is a version of Remi Coulom's Whole-History Rating modified to support handicaps, and implemented in Ruby.
21
+ END_DESCRIPTION
22
+
23
+ #s.add_dependency('json', '>= 1.5.0')
24
+
25
+ #s.add_development_dependency "rspec"
26
+
27
+ #s.executables = ['whr_rate']
28
+
29
+ s.files = `git ls-files`.split("\n")
30
+ s.require_paths = %w[lib]
31
+ end
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: whole_history_rating
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Pete Schwamb
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-17 00:00:00.000000000Z
13
+ dependencies: []
14
+ description: This gem provides a library and executables that take as input as set
15
+ of games and output a set of skill rankings for the players of those games. The
16
+ algorithm is a version of Remi Coulom's Whole-History Rating modified to support
17
+ handicaps, and implemented in Ruby.
18
+ email:
19
+ - pete@schwamb.net
20
+ executables: []
21
+ extensions: []
22
+ extra_rdoc_files: []
23
+ files:
24
+ - LICENSE
25
+ - README.md
26
+ - lib/whole_history_rating.rb
27
+ - lib/whole_history_rating/base.rb
28
+ - lib/whole_history_rating/game.rb
29
+ - lib/whole_history_rating/player.rb
30
+ - lib/whole_history_rating/player_day.rb
31
+ - whole_history_rating.gemspec
32
+ homepage: http://github.com/goshrine/whole_history_rating
33
+ licenses: []
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ! '>='
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubyforge_project:
52
+ rubygems_version: 1.8.6
53
+ signing_key:
54
+ specification_version: 3
55
+ summary: A pure ruby implementation of Remi Coulom's Whole-History Rating algorithm.
56
+ test_files: []