4 ## Copyright (C) 2006 Daigo Moriwaki <daigo at debian dot org>
6 ## This program is free software; you can redistribute it and/or modify
7 ## it under the terms of the GNU General Public License as published by
8 ## the Free Software Foundation; either version 2 of the License, or
9 ## (at your option) any later version.
11 ## This program is distributed in the hope that it will be useful,
12 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 ## GNU General Public License for more details.
16 ## You should have received a copy of the GNU General Public License
17 ## along with this program; if not, write to the Free Software
18 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 # This calculates rating scores of every players from CSA files, and outputs a
22 # yaml file (players.yaml) that Shogi Server can read.
25 # $ ./mk_rate . > players.yaml
27 # The conditions that games and players are rated as following:
28 # * Rated games, which were played by both rated players.
29 # * Rated players, who logged in the server with a name followed by a trip:
31 # * (Rated) players, who played more than $GAMES_LIMIT [ten] (rated) games.
37 # Ruby bindings for the GNU Scientific Library (GSL) is required.
38 # You can download it from http://rb-gsl.rubyforge.org/
39 # Or, if you use Debian,
40 # $ sudo aptitude install libgsl-ruby1.8
47 #################################################
51 # Count out players who play less games than $GAMES_LIMIT
52 $GAMES_LIMIT = $DEBUG ? 0 : 10
59 # Holds the last time when a player gamed
60 $players_time = Hash.new { Time.at(0) }
63 #################################################
64 # Keeps the value of the lowest key
72 if @lowest.empty? || key < @lowest[0]
73 @lowest = [key, value]
86 #################################################
87 # Calculates rates of every player from a Win Loss GSL::Matrix
92 # The model of the win possibility is 1/(1 + 10^(-d/400)).
93 # The equation in this class is 1/(1 + e^(-Kd)).
94 # So, K should be calculated like this.
95 K = Math.log(10.0) / 400.0
97 # Convergence limit to stop Newton method.
99 # Stop Newton method after this iterations.
102 # Average rate among the players
111 # Calcurates the average of the vector.
113 def Rating.average(vector, mean=0.0)
114 sum = Array(vector).inject(0.0) {|sum, n| sum + n}
115 vector -= GSL::Vector[*Array.new(vector.size, sum/vector.size - mean)]
122 def initialize(win_loss_matrix)
135 attr_reader :rate, :n
139 (0...@size).collect {|k| yield k}
144 (0...@size).each {|k| yield k}
148 # The possibility that the player k will beet the player i.
151 1.0/(1.0 + exp(@rate[i]-@rate[k]))
155 # Most possible equation
162 sum += @n[k,i] * win_rate(i,k) - @n[i,k] * win_rate(k,i)
169 # / f0/R0 f0/R1 f0/R2 ... \
170 # dfk/dRj = | f1/R0 f1/R1 f1/R2 ... |
171 # \ f2/R0 f2/R1 f2/R2 ... /
177 sum += win_rate(i,k) * win_rate(k,i) * (@n[k,i] + @n[i,k])
181 sum = 2.0 * win_rate(j,k) * win_rate(k,j) * (@n[k,j] + @n[j,k])
187 # Jacobi matrix of the func().
193 (0...@size).collect do |k|
194 (0...@size).collect do |j|
202 # The initial value of the rate, which is of very importance for Newton method.
203 # This is based on my huristics; the higher the win probablity of a player is,
204 # the greater points he takes.
209 v = GSL::Vector[0, 0]
212 v += GSL::Vector[@n[k,i], @n[i,k]]
214 v.nrm2 < 1 ? 0 : v[0] / (v[0] + v[1])
216 rank = possibility.sort_index
217 @rate = player_vector do |k|
218 K*500 * (rank[k]+1) / @size
224 # Resets @rate as the higher the current win probablity of a player is,
225 # the greater points he takes.
228 @rate = @record.get || @rate
229 rank = @rate.sort_index
230 @rate = player_vector do |k|
231 K*@count*1.5 * (rank[k]+1) / @size
236 # mu is the deaccelrating parameter in Deaccelerated Newton method
237 def deaccelrate(mu, old_rate, a, old_f_nrm2)
238 @rate = old_rate - a * mu
239 if func_vector.nrm2 < (1 - mu / 4.0 ) * old_f_nrm2 then
243 @record.set(func_vector.nrm2, @rate)
247 $stderr.puts "mu: %f " % [mu] if $DEBUG
248 deaccelrate(mu*0.5, old_rate, a, old_f_nrm2)
252 # Main process to calculate ratings.
255 # Counter to stop the process.
256 # Calulation in Newton method may fall in an infinite loop
261 # Solve the equation:
263 # @rate_(n+1) = @rate_(n) - a
265 # f.nrm2 should approach to zero.
269 # $stderr.puts "j: %s" % [j.inspect] if $DEBUG
270 $stderr.puts "f: %s -> %f" % [f.to_a.inspect, f.nrm2] if $DEBUG
272 # GSL::Linalg::LU.solve or GSL::Linalg::HH.solve would be available instead.
273 a = GSL::Linalg::SV.solve(j, f)
274 a = self.class.average(a)
275 # $stderr.puts "a: %s -> %f" % [a.to_a.inspect, a.nrm2] if $DEBUG
277 # Deaccelerated Newton method
278 # GSL::Vector object should be immutable.
281 old_f_nrm2 = old_f.nrm2
282 deaccelrate(1.0, old_rate, a, old_f_nrm2)
283 @record.set(func_vector.nrm2, @rate)
285 $stderr.printf "|error| : %5.2e\n", a.nrm2 if $DEBUG
288 if @count > COUNT_MAX
289 $stderr.puts "Values seem to oscillate. Stopped the process."
290 $stderr.puts "f: %s -> %f" % [func_vector.to_a.inspect, func_vector.nrm2]
294 end while (a.nrm2 > ERROR_LIMIT * @rate.nrm2)
297 $stderr.puts "resolved f: %s -> %f" %
298 [func_vector.to_a.inspect, func_vector.nrm2] if $DEBUG
306 # Make the values of @rate finite.
309 @rate = @rate.collect do |a|
311 a.infinite? * AVERAGE_RATE * 100
319 # Flatten the values of @rate.
321 def average!(mean=0.0)
322 @rate = self.class.average(@rate, mean)
326 # Make the values of @rate integer.
329 @rate = @rate.collect do |a|
335 a.infinite? * AVERAGE_RATE * 100
343 #################################################
347 def mk_win_loss_matrix(players)
348 keys = players.keys.sort.reject do |k|
349 players[k].values.inject(0) {|sum, v| sum + v[0] + v[1]} < $GAMES_LIMIT ||
350 players[k].values.inject(0) {|sum, v| sum + v[0]} < 1 || # Never win
351 players[k].values.inject(0) {|sum, v| sum + v[1]} < 1 # Never lose
358 ((0...size).collect do |k|
359 ((0...size).collect do |j|
363 v = players[keys[k]][keys[j]]
372 def _add_win_loss(winner, loser)
373 $players[winner] ||= Hash.new { GSL::Vector[0,0] }
374 $players[loser] ||= Hash.new { GSL::Vector[0,0] }
375 $players[winner][loser] += GSL::Vector[1,0]
376 $players[loser][winner] += GSL::Vector[0,1]
379 def _add_time(player, time)
380 $players_time[player] = time if $players_time[player] < time
383 def add(black_mark, black_name, white_name, white_mark, time)
384 if black_mark == WIN_MARK && white_mark == LOSS_MARK
385 _add_win_loss(black_name, white_name)
386 elsif black_mark == LOSS_MARK && white_mark == WIN_MARK
387 _add_win_loss(white_name, black_name)
388 elsif black_mark == DRAW_MARK && white_mark == DRAW_MARK
391 raise "Never reached!"
393 _add_time(black_name, time)
394 _add_time(white_name, time)
398 if /@NORATE\+/ =~ id # the player having @NORATE in the name should not be rated
401 id.gsub(/@.*?\+/,"+")
405 str = File.open(file).read
407 if /^N\+(.*)$/ =~ str then black_name = $1.strip end
408 if /^N\-(.*)$/ =~ str then white_name = $1.strip end
410 if /^'summary:(.*)$/ =~ str
411 dummy, p1, p2 = $1.split(":").map {|a| a.strip}
412 p1_name, p1_mark = p1.split(" ")
413 p2_name, p2_mark = p2.split(" ")
414 if p1_name == black_name
415 black_name, black_mark = p1_name, p1_mark
416 white_name, white_mark = p2_name, p2_mark
417 elsif p2_name == black_name
418 black_name, black_mark = p2_name, p2_mark
419 white_name, white_mark = p1_name, p1_mark
421 raise "Never reach!: #{black} #{white} #{p1} #{p2}"
424 if /^'\$END_TIME:(.*)$/ =~ str
425 time = Time.parse($1.strip)
427 if /^'rating:(.*)$/ =~ str
428 black_id, white_id = $1.split(":").map {|a| a.strip}
429 black_id = identify_id(black_id)
430 white_id = identify_id(white_id)
431 if black_id && white_id && (black_id != white_id)
432 add(black_mark, black_id, white_id, white_mark, time)
439 USAGE: #{$0} dir [...]
446 while dir = ARGV.shift do
447 Dir.glob( File.join(dir, "**", "*.csa") ) {|f| grep(f)}
450 win_loss_matrix, keys = mk_win_loss_matrix($players)
451 $stderr.puts keys.inspect if $DEBUG
452 $stderr.puts win_loss_matrix.inspect if $DEBUG
453 rating = Rating.new(win_loss_matrix)
455 rating.average!(Rating::AVERAGE_RATE)
459 keys.each_with_index do |p, i| # player_id, index#
460 win_loss = $players[p].values.inject(GSL::Vector[0,0]) {|sum, v| sum + v}
461 win = win_loss_matrix
463 { 'name' => p.split("+")[0],
464 'rate' => rating.rate[i],
465 'last_modified' => $players_time[p].dup,
466 'win' => win_loss[0],
467 'loss' => win_loss[1]}
476 # vim: ts=2 sw=2 sts=0