OSDN Git Service

a797e40f752d50e53ba6fa06f6da4e5e07b53b8d
[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 require 'yaml'
35 require 'time'
36 require 'gsl'
37
38 #################################################
39 # Constants
40 #
41 $GAMES_LIMIT = $DEBUG ? 0 : 10
42 WIN_MARK  = "win"
43 LOSS_MARK = "lose"
44
45 $players = Hash.new
46 $players_time = Hash.new { Time.at(0) }
47
48
49 #################################################
50 # Calculates rates of every player from a Win Loss GSL::Matrix
51 #
52 class Rating
53   include Math
54
55   # The model of the win possibility is 1/(1 + 10^(-d/400))
56   # The equation in this class is 1/(1 + e^(-Kd))
57   # So, K should be like this.
58   K = Math.log(10.0) / 400.0
59   
60   # Convergence limit to stop Newton method.
61   ERROR_LIMIT = 1.0e-3
62
63   # Average rate among the players
64   AVERAGE_RATE = 1000
65   
66   ###############
67   # Class methods
68   #
69   def Rating.average(vector, mean=0.0)
70     sum = Array(vector).inject(0.0) {|sum, n| sum + n}
71     vector -= GSL::Vector[*Array.new(vector.size, sum/vector.size - mean)]
72     vector
73   end
74
75   ##################
76   # Instance methods
77   #
78   def initialize(win_loss_matrix)
79     @n = win_loss_matrix
80     case @n
81     when GSL::Matrix
82       @size = @n.size1
83     when ::Matrix
84       @size = @n.row_size
85     else
86       raise ArgumentError
87     end
88     # 0 is the initial value
89     @rate = initial_rate
90   end
91   attr_reader :rate, :n
92
93   def player_vector
94     GSL::Vector[*
95       (0...@size).collect {|k| yield k}
96     ]
97   end
98
99   def each_player
100     (0...@size).each {|k| yield k}
101   end
102
103   ##
104   # The possibility that the player k will beet the player i.
105   #
106   def win_rate(k,i)
107     1.0/(1.0 + exp(@rate[i]-@rate[k]))
108   end
109
110   ##
111   # Most possible equation
112   #
113   def func_vector
114     player_vector do|k| 
115       sum = 0.0
116       each_player do |i|
117         next if i == k
118         sum += @n[k,i] * win_rate(i,k) - @n[i,k] * win_rate(k,i) 
119       end
120       sum * 2.0
121     end
122   end
123
124   ##
125   #         / f0/R0 f0/R1 f0/R2 ... \
126   # fk/Rj = | f1/R0 f1/R1 f1/R2 ... |
127   #         \ f2/R0 f2/R1 f2/R2 ... /
128   def d_funk(k,j)
129     sum = 0.0
130     if k == j
131       each_player do |i|
132         next if i == k
133         sum += win_rate(i,k) * win_rate(k,i) * (@n[k,i] + @n[i,k])
134         sum *= -2.0
135       end
136     else # k != j
137       sum = win_rate(j,k) * win_rate(k,j) * (@n[k,j] + @n[j,k])
138       sum *= 2.0
139     end
140     sum
141   end
142
143   ##
144   # Jacobi matrix of the func().
145   #   m00 m01
146   #   m10 m11
147   #
148   def j_matrix
149     GSL::Matrix[*
150       (0...@size).collect do |k|
151         (0...@size).collect do |j|
152           d_funk(k,j)
153         end
154       end
155     ]
156   end
157
158   ##
159   # The initial value of the rate, which is of very importance for Newton method.
160   # This is based on my huristics. 
161   #
162   def initial_rate
163     possibility = 
164       player_vector do |k|
165         v = GSL::Vector[0.0, 0.0]
166         each_player do |i|
167           next if k == i
168           v += GSL::Vector[@n[k,i], @n[i,k]]
169         end
170         v[0] + v[1] == 0 ? 0.001 : v[0] / (v[0] + v[1])
171       end
172     rank = possibility.sort_index
173     player_vector do |k|
174       K*500 * (rank[k]+1) / (@size)
175     end
176   end
177
178   ##
179   # Main method to calculate ratings.
180   #
181   def rating
182     # Counter to stop the process. 
183     # Calulation in Newton method may fall in an infinite loop
184     count = 0
185     # Mu parameter in Deaccelerated Newton method
186     mu = 1
187
188     # Main loop
189     begin
190       # Solve the equation: 
191       #   J*a=f
192       #   @rate_(n+1) = @rate_(n) - a
193       f = func_vector
194       j = j_matrix
195
196       # f.nrm2 should approach to zero.
197       $stderr.puts "f: %s -> %f" % [f.to_a.inspect, f.nrm2] if $DEBUG
198
199       # LU is not available because J may not be a normal matrix.
200       # a = GSL::Linalg::LU.solve(j, f)
201       a = GSL::Linalg::SV.solve(j, f)
202       a = self.class.average(a)
203       
204       # Deaccelerated Newton method
205       if mu == 1
206         old_rate = Vector.alloc(@rate)
207         old_f    = Vector.alloc(f)
208         @rate = old_rate - a * mu
209       end
210       if func_vector.nrm2 < (1.0 - mu / 4.0) * old_f.nrm2
211         mu = 1
212         break
213       else
214         mu *= 0.5
215         @rate = old_rate - a * mu
216       end
217
218       $stderr.printf "|error| : %5.2e\n", a.nrm2 if $DEBUG
219
220       count += 1
221       if count > 300
222         $stderr.puts "Values seem to oscillate. Stopped the process."
223         $stderr.puts "f: %s -> %f" % [f.to_a.inspect, f.nrm2]
224         break
225       end
226
227     end while (a.nrm2 > ERROR_LIMIT * @rate.nrm2)
228     #end while ( !(0..0.01).include?(func_vector.nrm2) )
229     
230     $stderr.puts "resolved f: %s -> %f" %
231       [func_vector.to_a.inspect, func_vector.nrm2] if $DEBUG
232
233     @rate *= 1.0/K
234     finite!
235     self
236   end
237
238   def finite!
239     @rate = @rate.collect do |a|
240       if a.infinite?
241         a.infinite? * AVERAGE_RATE * 100
242       else
243         a
244       end
245     end
246   end
247
248   def average!(mean=0.0)
249     @rate = self.class.average(@rate, mean)
250   end
251
252   def integer!
253     @rate = @rate.collect do |a|
254       if a.finite?
255         a.to_i
256       elsif a.nan?
257         0
258       elsif a.infinite?
259         a.infinite? * AVERAGE_RATE * 100
260       end
261     end
262   end
263 end
264
265
266
267 #################################################
268 # Main methods
269 #
270
271 def mk_win_loss_matrix(players)
272   keys = players.keys.sort.reject do |k|
273     players[k].values.inject(0) {|sum, v| sum + v[0] + v[1]} < $GAMES_LIMIT
274   end
275
276   size = keys.size
277
278   matrix =
279     GSL::Matrix[*
280     ((0...size).collect do |k|
281     ((0...size).collect do |j|
282       if k == j
283         0
284       else
285         v = players[keys[k]][keys[j]]
286         v[0]
287       end
288     end)
289     end)]
290   
291   return matrix, keys
292 end
293
294 def _add_win_loss(winner, loser)
295   $players[winner] ||= Hash.new { GSL::Vector[0,0] }
296   $players[loser]  ||= Hash.new { GSL::Vector[0,0] }
297   $players[winner][loser] += GSL::Vector[1,0]
298   $players[loser][winner] += GSL::Vector[0,1]
299 end
300
301 def _add_time(player, time)
302   $players_time[player] = time if $players_time[player] < time
303 end
304
305 def add(black_mark, black_name, white_name, white_mark, time)
306   if black_mark == WIN_MARK && white_mark == LOSS_MARK
307     _add_win_loss(black_name, white_name)
308   elsif black_mark == LOSS_MARK && white_mark == WIN_MARK
309     _add_win_loss(white_name, black_name)
310   else
311     raise "Never reached!"
312   end
313   _add_time(black_name, time)
314   _add_time(white_name, time)
315 end
316
317 def grep(file)
318   str = File.open(file).read
319
320   if /^N\+(.*)$/ =~ str then black_name = $1.strip end
321   if /^N\-(.*)$/ =~ str then white_name = $1.strip end
322
323   if /^'summary:(.*)$/ =~ str
324     dummy, p1, p2 = $1.split(":").map {|a| a.strip}    
325     p1_name, p1_mark = p1.split(" ")
326     p2_name, p2_mark = p2.split(" ")
327     if p1_name == black_name
328       black_name, black_mark = p1_name, p1_mark
329       white_name, white_mark = p2_name, p2_mark
330     elsif p2_name == black_name
331       black_name, black_mark = p2_name, p2_mark
332       white_name, white_mark = p1_name, p1_mark
333     else
334       raise "Never reach!: #{black} #{white} #{p1} #{p2}"
335     end
336   end
337   if /^'\$END_TIME:(.*)$/ =~ str
338     time = Time.parse($1.strip)
339   end
340   if /^'rating:(.*)$/ =~ str
341     black_id, white_id = $1.split(":").map {|a| a.strip}
342     add(black_mark, black_id, white_id, white_mark, time)
343   end
344 end
345
346 def usage
347   $stderr.puts <<-EOF
348 USAGE: #{$0} dir [...]
349   EOF
350   exit 1
351 end
352
353 def main
354   usage if ARGV.empty?
355   while dir = ARGV.shift do
356     Dir.glob( File.join(dir, "**", "*.csa") ) {|f| grep(f)}
357   end
358
359   win_loss_matrix, keys = mk_win_loss_matrix($players)
360   $stderr.puts keys.inspect if $DEBUG
361   $stderr.puts win_loss_matrix.inspect if $DEBUG
362   rating = Rating.new(win_loss_matrix)
363   rating.rating
364   rating.average!(Rating::AVERAGE_RATE)
365   rating.integer!
366
367   yaml = {}
368   keys.each_with_index do |p, i| # player_id, index#
369     win_loss = $players[p].values.inject(GSL::Vector[0,0]) {|sum, v| sum + v}
370     win = win_loss_matrix
371     yaml[p] = 
372       { 'name' => p.split("+")[0],
373         'rate' => rating.rate[i],
374         'last_modified' => $players_time[p].dup,
375         'win'  => win_loss[0],
376         'loss' => win_loss[1]}
377   end
378   puts yaml.to_yaml
379 end
380
381 if __FILE__ == $0
382   main
383 end
384
385 # vim: ts=2 sw=2 sts=0