OSDN Git Service

Support @NORATE syntax in the name.
[shogi-server/shogi-server.git] / mk_rate
1 #!/usr/bin/ruby
2 ## $Id$
3
4 ## Copyright (C) 2006 Daigo Moriwaki <daigo at debian dot org>
5 ##
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.
10 ##
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.
15 ##
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
19
20 #
21 # This calculates rating scores of every players from CSA files, and outputs a
22 # yaml file (players.yaml) that Shogi Server can read.
23 #
24 # Sample:
25 #   $ ./mk_rate . > players.yaml
26 #
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:
30 #     "name,trip".
31 #   * (Rated) players, who played more than $GAMES_LIMIT [ten] (rated) games. 
32 #
33 #
34 # PREREQUIRE
35 # ==========
36 #
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
41 #
42
43 require 'yaml'
44 require 'time'
45 require 'gsl'
46
47 #################################################
48 # Constants
49 #
50
51 # Count out players who play less games than $GAMES_LIMIT
52 $GAMES_LIMIT = $DEBUG ? 0 : 10
53 WIN_MARK  = "win"
54 LOSS_MARK = "lose"
55
56 # Holds players
57 $players = Hash.new
58 # Holds the last time when a player gamed
59 $players_time = Hash.new { Time.at(0) }
60
61
62 #################################################
63 # Keeps the value of the lowest key
64 #
65 class Record
66   def initialize
67     @lowest = []
68   end
69
70   def set(key, value)
71     if @lowest.empty? || key < @lowest[0]
72       @lowest = [key, value]
73     end
74   end
75
76   def get
77     if @lowest.empty?
78       nil
79     else
80       @lowest[1]
81     end
82   end
83 end
84
85 #################################################
86 # Calculates rates of every player from a Win Loss GSL::Matrix
87 #
88 class Rating
89   include Math
90
91   # The model of the win possibility is 1/(1 + 10^(-d/400)).
92   # The equation in this class is 1/(1 + e^(-Kd)).
93   # So, K should be calculated like this.
94   K = Math.log(10.0) / 400.0
95   
96   # Convergence limit to stop Newton method.
97   ERROR_LIMIT = 1.0e-3
98   # Stop Newton method after this iterations.
99   COUNT_MAX = 500
100
101   # Average rate among the players
102   AVERAGE_RATE = 1000
103
104   
105   ###############
106   # Class methods
107   #  
108   
109   ##
110   # Calcurates the average of the vector.
111   #
112   def Rating.average(vector, mean=0.0)
113     sum = Array(vector).inject(0.0) {|sum, n| sum + n}
114     vector -= GSL::Vector[*Array.new(vector.size, sum/vector.size - mean)]
115     vector
116   end
117
118   ##################
119   # Instance methods
120   #
121   def initialize(win_loss_matrix)
122     @record = Record.new
123     @n = win_loss_matrix
124     case @n
125     when GSL::Matrix
126       @size = @n.size1
127     when ::Matrix
128       @size = @n.row_size
129     else
130       raise ArgumentError
131     end
132     initial_rate
133   end
134   attr_reader :rate, :n
135
136   def player_vector
137     GSL::Vector[*
138       (0...@size).collect {|k| yield k}
139     ]
140   end
141
142   def each_player
143     (0...@size).each {|k| yield k}
144   end
145
146   ##
147   # The possibility that the player k will beet the player i.
148   #
149   def win_rate(k,i)
150     1.0/(1.0 + exp(@rate[i]-@rate[k]))
151   end
152
153   ##
154   # Most possible equation
155   #
156   def func_vector
157     player_vector do|k| 
158       sum = 0.0
159       each_player do |i|
160         next if i == k
161         sum += @n[k,i] * win_rate(i,k) - @n[i,k] * win_rate(k,i) 
162       end
163       sum * 2.0
164     end
165   end
166
167   ##
168   #           / f0/R0 f0/R1 f0/R2 ... \
169   # dfk/dRj = | f1/R0 f1/R1 f1/R2 ... |
170   #           \ f2/R0 f2/R1 f2/R2 ... /
171   def d_func(k,j)
172     sum = 0.0
173     if k == j
174       each_player do |i|
175         next if i == k
176         sum += win_rate(i,k) * win_rate(k,i) * (@n[k,i] + @n[i,k])
177       end
178       sum *= -2.0
179     else # k != j
180       sum = 2.0 * win_rate(j,k) * win_rate(k,j) * (@n[k,j] + @n[j,k])
181     end
182     sum
183   end
184
185   ##
186   # Jacobi matrix of the func().
187   #   m00 m01
188   #   m10 m11
189   #
190   def j_matrix
191     GSL::Matrix[*
192       (0...@size).collect do |k|
193         (0...@size).collect do |j|
194           d_func(k,j)
195         end
196       end
197     ]
198   end
199
200   ##
201   # The initial value of the rate, which is of very importance for Newton method.
202   # This is based on my huristics; the higher the win probablity of a player is, 
203   # the greater points he takes.
204   #
205   def initial_rate
206     possibility = 
207       player_vector do |k|
208         v = GSL::Vector[0, 0]
209         each_player do |i|
210           next if k == i
211           v += GSL::Vector[@n[k,i], @n[i,k]]
212         end
213         v.nrm2 < 1 ? 0 : v[0] / (v[0] + v[1])
214       end
215     rank = possibility.sort_index
216     @rate = player_vector do |k|
217       K*500 * (rank[k]+1) / @size
218     end
219     average!
220   end
221
222   ##
223   # Resets @rate as the higher the current win probablity of a player is, 
224   # the greater points he takes. 
225   #
226   def initial_rate2
227     @rate = @record.get || @rate
228     rank = @rate.sort_index
229     @rate = player_vector do |k|
230       K*@count*1.5 * (rank[k]+1) / @size
231     end
232     average!
233   end
234
235   # mu is the deaccelrating parameter in Deaccelerated Newton method
236   def deaccelrate(mu, old_rate, a, old_f_nrm2)
237     @rate = old_rate - a * mu
238     if func_vector.nrm2 < (1 - mu / 4.0 ) * old_f_nrm2 then
239       return
240     end
241     if mu < 1e-4
242       @record.set(func_vector.nrm2, @rate)
243       initial_rate2
244       return
245     end
246     $stderr.puts "mu: %f " % [mu] if $DEBUG
247     deaccelrate(mu*0.5, old_rate, a, old_f_nrm2)
248   end
249
250   ##
251   # Main process to calculate ratings.
252   #
253   def rating
254     # Counter to stop the process. 
255     # Calulation in Newton method may fall in an infinite loop
256     @count = 0
257
258     # Main loop
259     begin
260       # Solve the equation: 
261       #   J*a=f
262       #   @rate_(n+1) = @rate_(n) - a
263       #
264       # f.nrm2 should approach to zero.
265       f = func_vector
266       j = j_matrix
267
268       # $stderr.puts "j: %s" % [j.inspect] if $DEBUG
269       $stderr.puts "f: %s -> %f" % [f.to_a.inspect, f.nrm2] if $DEBUG
270
271       # GSL::Linalg::LU.solve or GSL::Linalg::HH.solve would be available instead.
272       a = GSL::Linalg::SV.solve(j, f)
273       a = self.class.average(a)
274       # $stderr.puts "a: %s -> %f" % [a.to_a.inspect, a.nrm2] if $DEBUG
275       
276       # Deaccelerated Newton method
277       # GSL::Vector object should be immutable.
278       old_rate   = @rate
279       old_f      = f
280       old_f_nrm2 = old_f.nrm2
281       deaccelrate(1.0, old_rate, a, old_f_nrm2)
282       @record.set(func_vector.nrm2, @rate)
283
284       $stderr.printf "|error| : %5.2e\n", a.nrm2 if $DEBUG
285
286       @count += 1
287       if @count > COUNT_MAX
288         $stderr.puts "Values seem to oscillate. Stopped the process."
289         $stderr.puts "f: %s -> %f" % [func_vector.to_a.inspect, func_vector.nrm2]
290         break
291       end
292
293     end while (a.nrm2 > ERROR_LIMIT * @rate.nrm2)
294     
295     @rate = @record.get
296     $stderr.puts "resolved f: %s -> %f" %
297       [func_vector.to_a.inspect, func_vector.nrm2] if $DEBUG
298
299     @rate *= 1.0/K
300     finite!
301     self
302   end
303
304   ##
305   # Make the values of @rate finite.
306   #
307   def finite!
308     @rate = @rate.collect do |a|
309       if a.infinite?
310         a.infinite? * AVERAGE_RATE * 100
311       else
312         a
313       end
314     end
315   end
316
317   ##
318   # Flatten the values of @rate.
319   #
320   def average!(mean=0.0)
321     @rate = self.class.average(@rate, mean)
322   end
323
324   ##
325   # Make the values of @rate integer.
326   #
327   def integer!
328     @rate = @rate.collect do |a|
329       if a.finite?
330         a.to_i
331       elsif a.nan?
332         0
333       elsif a.infinite?
334         a.infinite? * AVERAGE_RATE * 100
335       end
336     end
337   end
338 end
339
340
341
342 #################################################
343 # Main methods
344 #
345
346 def mk_win_loss_matrix(players)
347   keys = players.keys.sort.reject do |k|
348     players[k].values.inject(0) {|sum, v| sum + v[0] + v[1]} < $GAMES_LIMIT
349   end
350
351   size = keys.size
352
353   matrix =
354     GSL::Matrix[*
355     ((0...size).collect do |k|
356     ((0...size).collect do |j|
357       if k == j
358         0
359       else
360         v = players[keys[k]][keys[j]]
361         v[0]
362       end
363     end)
364     end)]
365   
366   return matrix, keys
367 end
368
369 def _add_win_loss(winner, loser)
370   $players[winner] ||= Hash.new { GSL::Vector[0,0] }
371   $players[loser]  ||= Hash.new { GSL::Vector[0,0] }
372   $players[winner][loser] += GSL::Vector[1,0]
373   $players[loser][winner] += GSL::Vector[0,1]
374 end
375
376 def _add_time(player, time)
377   $players_time[player] = time if $players_time[player] < time
378 end
379
380 def add(black_mark, black_name, white_name, white_mark, time)
381   if black_mark == WIN_MARK && white_mark == LOSS_MARK
382     _add_win_loss(black_name, white_name)
383   elsif black_mark == LOSS_MARK && white_mark == WIN_MARK
384     _add_win_loss(white_name, black_name)
385   else
386     raise "Never reached!"
387   end
388   _add_time(black_name, time)
389   _add_time(white_name, time)
390 end
391
392 def identify_id(id)
393   if /@NORATE\+/ =~ id # the player having @NORATE in the name should not be rated
394     return nil
395   end
396   id.gsub(/@.*?\+/,"+")
397 end
398
399 def grep(file)
400   str = File.open(file).read
401
402   if /^N\+(.*)$/ =~ str then black_name = $1.strip end
403   if /^N\-(.*)$/ =~ str then white_name = $1.strip end
404
405   if /^'summary:(.*)$/ =~ str
406     dummy, p1, p2 = $1.split(":").map {|a| a.strip}    
407     p1_name, p1_mark = p1.split(" ")
408     p2_name, p2_mark = p2.split(" ")
409     if p1_name == black_name
410       black_name, black_mark = p1_name, p1_mark
411       white_name, white_mark = p2_name, p2_mark
412     elsif p2_name == black_name
413       black_name, black_mark = p2_name, p2_mark
414       white_name, white_mark = p1_name, p1_mark
415     else
416       raise "Never reach!: #{black} #{white} #{p1} #{p2}"
417     end
418   end
419   if /^'\$END_TIME:(.*)$/ =~ str
420     time = Time.parse($1.strip)
421   end
422   if /^'rating:(.*)$/ =~ str
423     black_id, white_id = $1.split(":").map {|a| a.strip}
424     black_id = identify_id(black_id)
425     white_id = identify_id(white_id)
426     if black_id && white_id && (black_id != white_id)
427       add(black_mark, black_id, white_id, white_mark, time)
428     end
429   end
430 end
431
432 def usage
433   $stderr.puts <<-EOF
434 USAGE: #{$0} dir [...]
435   EOF
436   exit 1
437 end
438
439 def main
440   usage if ARGV.empty?
441   while dir = ARGV.shift do
442     Dir.glob( File.join(dir, "**", "*.csa") ) {|f| grep(f)}
443   end
444
445   win_loss_matrix, keys = mk_win_loss_matrix($players)
446   $stderr.puts keys.inspect if $DEBUG
447   $stderr.puts win_loss_matrix.inspect if $DEBUG
448   rating = Rating.new(win_loss_matrix)
449   rating.rating
450   rating.average!(Rating::AVERAGE_RATE)
451   rating.integer!
452
453   yaml = {}
454   keys.each_with_index do |p, i| # player_id, index#
455     win_loss = $players[p].values.inject(GSL::Vector[0,0]) {|sum, v| sum + v}
456     win = win_loss_matrix
457     yaml[p] = 
458       { 'name' => p.split("+")[0],
459         'rate' => rating.rate[i],
460         'last_modified' => $players_time[p].dup,
461         'win'  => win_loss[0],
462         'loss' => win_loss[1]}
463   end
464   puts yaml.to_yaml
465 end
466
467 if __FILE__ == $0
468   main
469 end
470
471 # vim: ts=2 sw=2 sts=0