OSDN Git Service

Merge branch 'wdoor-stable'
[shogi-server/shogi-server.git] / mk_rate
1 #!/usr/bin/ruby
2 # $Id$
3 #
4 # Author:: Daigo Moriwaki
5 # Homepage:: http://sourceforge.jp/projects/shogi-server/
6 #
7 #--
8 # Copyright (C) 2006-2012 Daigo Moriwaki <daigo at debian dot org>
9 #
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
14 #
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with this program; if not, write to the Free Software
22 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
23 #++
24 #
25 # == Synopsis
26 #
27 # mk_rate reads game results files generated by the mk_game_results command,
28 # calculates rating scores of each player, and then outputs a yaml file 
29 # (players.yaml) that Shogi-server can recognize.
30 #
31 # == Usage
32 #
33 # ./mk_rate [options] GAME_RESULTS_FILE [...]
34 #
35 # ./mk_rate [options]
36
37 # GAME_RESULTS_FILE::
38 #   a path to a file listing results of games, which is generated by the
39 #   mk_game_results command.
40 #   In the second style above, the file content can be read from the stdin.
41 #
42 # --abnormal-threshold::
43 #   n [plies] (default 30)
44 #   Games that end with the 'abnormal' status are counted in win/lost games
45 #   for the rating calculation if a game plays more than n plies. Otherwise
46 #   (or if n is zero), abnormal games are counted out of rating games.
47 #
48 # --base-date::
49 #   a base time point for this calculation (default now). Ex. '2009-10-31'
50 #
51 # --half-life::
52 #   n [days] (default 60)
53 #   
54 # --half-life-ignore::
55 #   m [days] (default  7)
56 #   after m days, the half-life effect works
57 #
58 # --ignore::
59 #   m [days] (default  365*2)
60 #   old results will be ignored
61 #
62 # --fixed-rate-player::
63 #   player whose rate is fixed at the rate
64 #
65 # --fixed-rate::
66 #   rate 
67 #
68 # --skip-draw-games::
69 #   skip draw games. [default: draw games are counted in as 0.5 win and 0.5
70 #   lost.]
71 #
72 # --help::
73 #   show this message
74 #
75 # == PREREQUIRE
76 #
77 # Sample Command lines that install prerequires will work on Debian.
78 #
79 # * Ruby 2.0.0 or later (including Rubygems)
80 #
81 #   $ sudo aptitude install ruby
82 #
83 # * Ruby bindings for the GNU Scientific Library (GSL[http://rb-gsl.rubyforge.org/])
84 #
85 #   $ sudo aptitude install ruby-gsl
86 #
87 # * RGL: {Ruby Graph Library}[http://rubyforge.org/projects/rgl/]
88 #
89 #   $ sudo gem install rgl
90 #
91 # == Examples
92 #
93 #   $ ./mk_rate game_results.txt > players.yaml
94 #
95 #   $ ./mk_game_results . | ./mk_rate > players.yaml
96 #
97 # If you do not want the file to be update in case of errors, 
98 #
99 #   $ ./mk_rate game_results.txt && ./mk_rate game_results.txt > players.yaml
100 #
101 # == How players are rated
102 #
103 # The conditions that games and players are rated as following:
104 #
105 # * Rated games, which were played by both rated players.
106 # * Rated players, who logged in the server with a name followed by a trip: "name,trip".
107 # * (Rated) players, who played more than $GAMES_LIMIT [15] (rated) games. 
108 #
109
110 require 'pathname'
111 $:.unshift(File.dirname(Pathname.new(__FILE__).realpath))
112 require 'utils/csa-filter'
113 require 'yaml'
114 require 'time'
115 require 'getoptlong'
116 require 'set'
117 require 'rubygems'
118 require 'gsl'
119 require 'rgl/adjacency'
120 require 'rgl/connected_components'
121
122 #################################################
123 # Constants
124 #
125
126 # Count out players who play less games than $GAMES_LIMIT
127 $GAMES_LIMIT = $DEBUG ? 0 : 15
128 WIN_MARK  = "win"
129 LOSS_MARK = "lose"
130 DRAW_MARK = "draw"
131
132 # Holds players
133 $players = Hash.new
134 # Holds the last time when a player gamed
135 $players_time = Hash.new { Time.at(0) }
136 # Holds history of input lines to check duplicated inputs
137 $history = Set.new
138
139
140 #################################################
141 # Keeps the value of the lowest key
142 #
143 class Record
144   def initialize
145     @lowest = []
146   end
147
148   def set(key, value)
149     if @lowest.empty? || key < @lowest[0]
150       @lowest = [key, value]
151     end
152   end
153
154   def get
155     if @lowest.empty?
156       nil
157     else
158       @lowest[1]
159     end
160   end
161 end
162
163 #################################################
164 # Calculates rates of every player from a Win Loss GSL::Matrix
165 #
166 class Rating
167   include Math
168
169   # The model of the win possibility is 1/(1 + 10^(-d/400)).
170   # The equation in this class is 1/(1 + e^(-Kd)).
171   # So, K should be calculated like this.
172   K = Math.log(10.0) / 400.0
173   
174   # Convergence limit to stop Newton method.
175   ERROR_LIMIT = 1.0e-3
176   # Stop Newton method after this iterations.
177   COUNT_MAX = 500
178
179   # Average rate among the players
180   AVERAGE_RATE = 1000
181
182   
183   ###############
184   # Class methods
185   #  
186   
187   ##
188   # Calcurates the average of the vector.
189   #
190   def Rating.average(vector, mean=0.0)
191     sum = Array(vector).inject(0.0) {|sum, n| sum + n}
192     vector -= GSL::Vector[*Array.new(vector.size, sum/vector.size - mean)]
193     vector
194   end
195
196   ##################
197   # Instance methods
198   #
199   def initialize(win_loss_matrix)
200     @record = Record.new
201     @n = win_loss_matrix
202     case @n
203     when GSL::Matrix, GSL::Matrix::Int
204       @size = @n.size1
205     when ::Matrix
206       @size = @n.row_size
207     else
208       raise ArgumentError
209     end
210     initial_rate
211   end
212   attr_reader :rate, :n
213
214   def player_vector
215     GSL::Vector[*
216       (0...@size).collect {|k| yield k}
217     ]
218   end
219
220   def each_player
221     (0...@size).each {|k| yield k}
222   end
223
224   ##
225   # The possibility that the player k will beet the player i.
226   #
227   def win_rate(k,i)
228     1.0/(1.0 + exp(@rate[i]-@rate[k]))
229   end
230
231   ##
232   # Most possible equation
233   #
234   def func_vector
235     player_vector do|k| 
236       sum = 0.0
237       each_player do |i|
238         next if i == k
239         sum += @n[k,i] * win_rate(i,k) - @n[i,k] * win_rate(k,i) 
240       end
241       sum * 2.0
242     end
243   end
244
245   ##
246   #           / f0/R0 f0/R1 f0/R2 ... \
247   # dfk/dRj = | f1/R0 f1/R1 f1/R2 ... |
248   #           \ f2/R0 f2/R1 f2/R2 ... /
249   def d_func(k,j)
250     sum = 0.0
251     if k == j
252       each_player do |i|
253         next if i == k
254         sum += win_rate(i,k) * win_rate(k,i) * (@n[k,i] + @n[i,k])
255       end
256       sum *= -2.0
257     else # k != j
258       sum = 2.0 * win_rate(j,k) * win_rate(k,j) * (@n[k,j] + @n[j,k])
259     end
260     sum
261   end
262
263   ##
264   # Jacobi matrix of the func().
265   #   m00 m01
266   #   m10 m11
267   #
268   def j_matrix
269     GSL::Matrix[*
270       (0...@size).collect do |k|
271         (0...@size).collect do |j|
272           d_func(k,j)
273         end
274       end
275     ]
276   end
277
278   ##
279   # The initial value of the rate, which is of very importance for Newton
280   # method.  This is based on my huristics; the higher the win probablity of
281   # a player is, the greater points he takes.
282   #
283   def initial_rate
284     possibility = 
285       player_vector do |k|
286         v = GSL::Vector[0, 0]
287         each_player do |i|
288           next if k == i
289           v += GSL::Vector[@n[k,i], @n[i,k]]
290         end
291         v.nrm2 < 1 ? 0 : v[0] / (v[0] + v[1])
292       end
293     rank = possibility.sort_index
294     @rate = player_vector do |k|
295       K*500 * (rank[k]+1) / @size
296     end
297     average!
298   end
299
300   ##
301   # Resets @rate as the higher the current win probablity of a player is, 
302   # the greater points he takes. 
303   #
304   def initial_rate2
305     @rate = @record.get || @rate
306     rank = @rate.sort_index
307     @rate = player_vector do |k|
308       K*@count*1.5 * (rank[k]+1) / @size
309     end
310     average!
311   end
312
313   # mu is the deaccelrating parameter in Deaccelerated Newton method
314   def deaccelrate(mu, old_rate, a, old_f_nrm2)
315     @rate = old_rate - a * mu
316     if func_vector.nrm2 < (1 - mu / 4.0 ) * old_f_nrm2 then
317       return
318     end
319     if mu < 1e-4
320       @record.set(func_vector.nrm2, @rate)
321       initial_rate2
322       return
323     end
324     $stderr.puts "mu: %f " % [mu] if $DEBUG
325     deaccelrate(mu*0.5, old_rate, a, old_f_nrm2)
326   end
327
328   ##
329   # Main process to calculate ratings.
330   #
331   def rating
332     # Counter to stop the process. 
333     # Calulation in Newton method may fall in an infinite loop
334     @count = 0
335
336     # Main loop
337     begin
338       # Solve the equation: 
339       #   J*a=f
340       #   @rate_(n+1) = @rate_(n) - a
341       #
342       # f.nrm2 should approach to zero.
343       f = func_vector
344       j = j_matrix
345
346       # $stderr.puts "j: %s" % [j.inspect] if $DEBUG
347       $stderr.puts "f: %s -> %f" % [f.to_a.inspect, f.nrm2] if $DEBUG
348
349       # GSL::Linalg::LU.solve or GSL::Linalg::HH.solve would be available instead.
350       #a = GSL::Linalg::HH.solve(j, f)
351       a, = GSL::MultiFit::linear(j, f)
352       a = self.class.average(a)
353       # $stderr.puts "a: %s -> %f" % [a.to_a.inspect, a.nrm2] if $DEBUG
354       
355       # Deaccelerated Newton method
356       # GSL::Vector object should be immutable.
357       old_rate   = @rate
358       old_f      = f
359       old_f_nrm2 = old_f.nrm2
360       deaccelrate(1.0, old_rate, a, old_f_nrm2)
361       #@rate -= a # Instead, do not deaccelerate
362       @record.set(func_vector.nrm2, @rate)
363
364       $stderr.printf "|error| : %5.2e\n", a.nrm2 if $DEBUG
365
366       @count += 1
367       if @count > COUNT_MAX
368         $stderr.puts "Values seem to oscillate. Stopped the process."
369         $stderr.puts "f: %s -> %f" % [func_vector.to_a.inspect, func_vector.nrm2]
370         break
371       end
372
373     end while (a.nrm2 > ERROR_LIMIT * @rate.nrm2)
374     
375     @rate = @record.get
376     $stderr.puts "resolved f: %s -> %f" %
377       [func_vector.to_a.inspect, func_vector.nrm2] if $DEBUG
378     $stderr.puts "Count: %d" % [@count] if $DEBUG
379
380     @rate *= 1.0/K
381     finite!
382     self
383   end
384
385   ##
386   # Make the values of @rate finite.
387   #
388   def finite!
389     @rate = @rate.collect do |a|
390       if a.infinite?
391         a.infinite? * AVERAGE_RATE * 100
392       else
393         a
394       end
395     end
396   end
397
398   ##
399   # Flatten the values of @rate.
400   #
401   def average!(mean=0.0)
402     @rate = self.class.average(@rate, mean)
403   end
404
405   ##
406   # Translate by value
407   #
408   def translate!(value)
409     @rate += value
410   end
411
412   ##
413   # Make the values of @rate integer.
414   #
415   def integer!
416     @rate = @rate.collect do |a|
417       if a.finite?
418         a.to_i
419       elsif a.nan?
420         0
421       elsif a.infinite?
422         a.infinite? * AVERAGE_RATE * 100
423       end
424     end
425   end
426 end
427
428 #################################################
429 # Encapsulate a pair of keys and win loss matrix.
430 #   - keys is an array of player IDs; [gps+123, foo+234, ...]
431 #   - matrix holds games # where player i (row index) beats player j (column index).
432 #     The row and column indexes match with the keys.
433 #
434 # This object should be immutable. If an internal state is being modified, a
435 # new object is always returned.
436 #
437 class WinLossMatrix
438
439   ###############
440   # Class methods
441   #  
442
443   def self.mk_matrix(players)
444     keys = players.keys.sort
445     size = keys.size
446     matrix =
447       GSL::Matrix[*
448       ((0...size).collect do |k|
449         p1 = keys[k]
450         p1_hash = players[p1]
451         ((0...size).collect do |j|
452           if k == j
453             0
454           else
455             p2 = keys[j]
456             v = p1_hash[p2] || GSL::Vector[0,0]
457             v[0]
458           end
459         end)
460       end)]
461     return WinLossMatrix.new(keys, matrix)
462   end
463
464   def self.mk_win_loss_matrix(players)
465     obj = mk_matrix(players)
466     return obj.filter
467   end
468
469   ##################
470   # Instance methods
471   #
472
473   # an array of player IDs; [gps+123, foo+234, ...]
474   attr_reader :keys
475
476   # matrix holds games # where player i (row index) beats player j (column index).
477   # The row and column indexes match with the keys.
478   attr_reader :matrix
479
480   def initialize(keys, matrix)
481     @keys   = keys
482     @matrix = matrix
483   end
484
485   ##
486   # Returns the size of the keys/matrix
487   #
488   def size
489     if @keys
490       @keys.size
491     else
492       nil
493     end
494   end
495
496   ##
497   # Removes players in a rows such as [1,3,5], and then returns a new
498   # object.
499   #
500   def delete_rows(rows)
501     rows = rows.sort.reverse
502
503     copied_cols = []
504     (0...size).each do |i|
505       next if rows.include?(i)
506       row = @matrix.row(i).clone
507       rows.each do |j|
508         row.delete_at(j)
509       end
510       copied_cols << row
511     end
512     if copied_cols.size == 0
513       new_matrix = GSL::Matrix.new
514     else
515       new_matrix = GSL::Matrix[*copied_cols]
516     end
517
518     new_keys = @keys.clone
519     rows.each do |j|
520       new_keys.delete_at(j)
521     end
522
523     return WinLossMatrix.new(new_keys, new_matrix)
524   end
525
526   ##
527   # Removes players who do not pass a criteria to be rated, and returns a
528   # new object.
529   # 
530   def filter
531     $stderr.puts @keys.inspect if $DEBUG
532     $stderr.puts @matrix.inspect if $DEBUG
533     delete = []  
534     (0...size).each do |i|
535       row = @matrix.row(i)
536       col = @matrix.col(i)
537       win  = row.sum
538       loss = col.sum
539       if win < 1 || loss < 1 || win + loss < $GAMES_LIMIT
540         delete << i
541       end
542     end
543
544     # The recursion ends if there is nothing to delete
545     return self if delete.empty?
546
547     new_obj = delete_rows(delete)
548     new_obj.filter
549   end
550
551   ##
552   # Cuts self into connecting groups such as each player in a group has at least
553   # one game with other players in the group. Returns them as an array.
554   #
555   def connected_subsets
556     g = RGL::AdjacencyGraph.new
557     (0...size).each do |k|
558       (0...size).each do |i|
559         next if k == i
560         if @matrix[k,i] > 0
561           g.add_edge(k,i)
562         end
563       end
564     end
565
566     subsets = []
567     g.each_connected_component do |c|
568       new_keys = []      
569       c.each do |v|
570         new_keys << keys[v.to_s.to_i]
571       end
572       subsets << new_keys
573     end
574
575     subsets = subsets.sort {|a,b| b.size <=> a.size}
576
577     result = subsets.collect do |keys|
578       matrix =
579         GSL::Matrix[*
580         ((0...keys.size).collect do |k|
581           p1 = @keys.index(keys[k])
582           ((0...keys.size).collect do |j|
583             if k == j
584               0
585             else
586               p2 = @keys.index(keys[j])
587               @matrix[p1,p2]
588             end
589           end)
590         end)]
591       WinLossMatrix.new(keys, matrix)
592     end
593
594     return result
595   end
596
597   def to_s
598     "size : #{@keys.size}" + "\n" +
599     @keys.inspect + "\n" + 
600     @matrix.inspect
601   end
602
603 end
604
605
606 #################################################
607 # Main methods
608 #
609
610 # Half-life effect
611 # After NHAFE_LIFE days value will get half.
612 # 0.693 is constant, where exp(0.693) ~ 0.5
613 def half_life(days)
614   if days < $options["half-life-ignore"]
615     return 1.0
616   else
617     Math::exp(-0.693/$options["half-life"]*(days-$options["half-life-ignore"]))
618   end
619 end
620
621 def _add_win_loss(winner, loser, time)
622   how_long_days = ($options["base-date"] - time)/(3600*24)
623   $players[winner] ||= Hash.new { GSL::Vector[0,0] }
624   $players[loser]  ||= Hash.new { GSL::Vector[0,0] }
625   $players[winner][loser] += GSL::Vector[1.0*half_life(how_long_days),0]
626   $players[loser][winner] += GSL::Vector[0,1.0*half_life(how_long_days)]
627 end
628
629 def _add_draw(player1, player2, time)
630   how_long_days = ($options["base-date"] - time)/(3600*24)
631   $players[player1] ||= Hash.new { GSL::Vector[0,0] }
632   $players[player2] ||= Hash.new { GSL::Vector[0,0] }
633   $players[player1][player2] += GSL::Vector[0.5*half_life(how_long_days),0.5*half_life(how_long_days)]
634   $players[player2][player1] += GSL::Vector[0.5*half_life(how_long_days),0.5*half_life(how_long_days)]
635 end
636
637 def _add_time(player, time)
638   $players_time[player] = time if $players_time[player] < time
639 end
640
641 def add(black_mark, black_name, white_name, white_mark, time)
642   if black_mark == WIN_MARK && white_mark == LOSS_MARK
643     _add_win_loss(black_name, white_name, time)
644   elsif black_mark == LOSS_MARK && white_mark == WIN_MARK
645     _add_win_loss(white_name, black_name, time)
646   elsif black_mark == DRAW_MARK && white_mark == DRAW_MARK
647     if $options["skip-draw-games"]
648       return
649     else
650       _add_draw(black_name, white_name, time)
651     end
652   else
653     raise "Never reached!"
654   end
655   _add_time(black_name, time)
656   _add_time(white_name, time)
657 end
658
659 def identify_id(id)
660   if /@NORATE\+/ =~ id # the player having @NORATE in the name should not be rated
661     return nil
662   end
663   id.gsub(/@.*?\+/,"+")
664 end
665
666 # Parse a game result line
667 #
668 def parse(line)
669   if $history.include? line
670     $stderr.puts "[WARNING] Duplicated: #{line}"
671     return
672   end
673   $history.add line
674
675   time, state, black_mark, black_id, white_id, white_mark, file = line.split("\t")
676   unless time && state && black_mark && black_id &&
677          white_id && white_mark && file
678     $stderr.puts "Failed to parse the line : #{line}"
679     return
680   end
681
682   if state == "abnormal"
683     csa = CsaFileReader.new(file, "EUC-JP")
684     if $options["abnormal-threshold"] == 0 || csa.ply <= $options["abnormal-threshold"]
685       return
686     end
687   end
688   time = Time.parse(time)
689   return if $options["base-date"] < time
690   how_long_days = ($options["base-date"] - time)/(3600*24)
691   if (how_long_days > $options["ignore"])
692     return
693   end
694
695   black_id = identify_id(black_id)
696   white_id = identify_id(white_id)
697
698   if black_id && white_id && (black_id != white_id) &&
699      black_mark && white_mark
700     add(black_mark, black_id, white_id, white_mark, time)
701   end
702 end
703
704 def validate(yaml)
705   yaml["players"].each do |group_key, group|
706     group.each do |player_key, player|
707       rate = player['rate']
708       next unless rate
709       if rate > 10000 || rate < -10000
710         return false
711       end
712     end
713   end
714   return true
715 end
716
717 def usage(io)
718     io.puts <<EOF
719 USAGE: #{$0} [options] GAME_RESULTS_FILE [...]
720        #{$0} [options]
721        
722 GAME_RESULTS_FILE:
723   a path to a file listing results of games, which is genrated by the
724   mk_game_results command.
725   In the second style above, the file content can be read from the stdin.
726
727 OPTOINS:
728   --base-date         a base time point for this calicuration (default now). Ex. '2009-10-31'
729   --half-life         n [days] (default 60)
730   --half-life-ignore  m [days] (default  7)
731                       after m days, half-life effect works
732   --ignore            n [days] (default 730 [=365*2]).
733                       Results older than n days from the 'base-date' are ignored.
734   --fixed-rate-player player whose rate is fixed at the rate
735   --fixed-rate        rate 
736   --skip-draw-games   skip draw games. [default: draw games are counted in
737                       as 0.5 win and 0.5 lost]
738   --help              show this message
739 EOF
740 end
741
742 def main
743   $options = Hash::new
744   parser = GetoptLong.new(
745     ["--abnormal-threshold", GetoptLong::REQUIRED_ARGUMENT],
746     ["--base-date",          GetoptLong::REQUIRED_ARGUMENT],
747     ["--half-life",          GetoptLong::REQUIRED_ARGUMENT],
748     ["--half-life-ignore",   GetoptLong::REQUIRED_ARGUMENT],
749     ["--help", "-h",         GetoptLong::NO_ARGUMENT],
750     ["--ignore",             GetoptLong::REQUIRED_ARGUMENT],
751     ["--fixed-rate-player",  GetoptLong::REQUIRED_ARGUMENT],
752     ["--fixed-rate",         GetoptLong::REQUIRED_ARGUMENT],
753     ["--skip-draw-games",    GetoptLong::NO_ARGUMENT])
754   parser.quiet = true
755   begin
756     parser.each_option do |name, arg|
757       name.sub!(/^--/, '')
758       $options[name] = arg.dup
759     end
760     if ( $options["fixed-rate-player"] && !$options["fixed-rate"]) ||
761        (!$options["fixed-rate-player"] &&  $options["fixed-rate"]) ||
762        ( $options["fixed-rate-player"] &&  $options["fixed-rate"].to_i <= 0) 
763       usage($stderr)
764       exit 1
765     end
766   rescue
767     usage($stderr)
768     raise parser.error_message
769   end
770   if $options["help"]
771     usage($stdout) 
772     exit 0
773   end
774   if $options["base-date"]
775     $options["base-date"] = Time::parse $options["base-date"]
776   else
777     $options["base-date"] = Time.now
778   end
779   $options["abnormal-threshold"] ||= 30
780   $options["abnormal-threshold"] = $options["abnormal-threshold"].to_i
781   $options["half-life"] ||= 60
782   $options["half-life"] = $options["half-life"].to_i
783   $options["half-life-ignore"] ||= 7
784   $options["half-life-ignore"] = $options["half-life-ignore"].to_i
785   $options["ignore"] ||= 365*2
786   $options["ignore"] = $options["ignore"].to_i
787   $options["fixed-rate"] = $options["fixed-rate"].to_i if $options["fixed-rate"]
788
789   if ARGV.empty?
790     while line = $stdin.gets do
791       parse line.strip
792     end
793   else
794     while file = ARGV.shift do
795       File.open(file) do |f|
796         f.each_line do |line|
797           parse line.strip
798         end
799       end 
800     end
801   end
802
803   yaml = {} 
804   yaml["players"] = {}
805   rating_group = 0
806   if $players.size > 0
807     obj = WinLossMatrix::mk_win_loss_matrix($players)
808     obj.connected_subsets.each do |win_loss_matrix|
809       yaml["players"][rating_group] = {}
810
811       rating = Rating.new(win_loss_matrix.matrix)
812       rating.rating
813       rating.average!(Rating::AVERAGE_RATE)
814       rating.integer!
815
816       if $options["fixed-rate-player"]
817         # first, try exact match
818         index = win_loss_matrix.keys.index($options["fixed-rate-player"])
819         # second, try regular match
820         unless index
821           win_loss_matrix.keys.each_with_index do |p, i|
822             if %r!#{$options["fixed-rate-player"]}! =~ p
823               index = i
824             end
825           end
826         end
827         if index
828           the_rate = rating.rate[index]
829           rating.translate!($options["fixed-rate"] - the_rate)
830         end
831       end
832
833       win_loss_matrix.keys.each_with_index do |p, i| # player_id, index#
834         win  = win_loss_matrix.matrix.row(i).sum
835         loss = win_loss_matrix.matrix.col(i).sum
836
837         yaml["players"][rating_group][p] = 
838           { 'name' => p.split("+")[0],
839             'rating_group' => rating_group,
840             'rate' => rating.rate[i],
841             'last_modified' => $players_time[p].dup,
842             'win'  => win,
843             'loss' => loss}
844       end
845       rating_group += 1
846     end
847   end
848   rating_group -= 1
849   non_rated_group = 999 # large enough
850   yaml["players"][non_rated_group] = {}
851   $players.each_key do |id|
852     # skip players who have already been rated
853     found = false
854     (0..rating_group).each do |i|
855        found = true if yaml["players"][i][id]
856        break if found
857     end
858     next if found
859
860     v = GSL::Vector[0, 0]
861     $players[id].each_value {|value| v += value}
862     next if v[0] < 1 && v[1] < 1
863
864     yaml["players"][non_rated_group][id] =
865       { 'name' => id.split("+")[0],
866         'rating_group' => non_rated_group,
867         'rate' => 0,
868         'last_modified' => $players_time[id].dup,
869         'win'  => v[0],
870         'loss' => v[1]}
871   end
872   unless validate(yaml)
873     $stderr.puts "Aborted. It did not result in valid ratings."
874     $stderr.puts yaml.to_yaml if $DEBUG
875     exit 10
876   end
877   puts yaml.to_yaml
878 end
879
880 if __FILE__ == $0
881   main
882 end
883
884 # vim: ts=2 sw=2 sts=0