• R/O
  • HTTP
  • SSH
  • HTTPS

Commit

Tags
No Tags

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

shogi-server source


Commit MetaInfo

Revisãoa218a00024866da7a502fbebdea53313dea4db8b (tree)
Hora2013-11-24 20:03:00
AutorDaigo Moriwaki <daigo@debi...>
CommiterDaigo Moriwaki

Mensagem de Log

* mk_rate-from-grep, mk_rate-grep: - Removed files that are no longer used.

Mudança Sumário

  • modified: changelog (diff)
  • delete: mk_rate-from-grep
  • delete: mk_rate-grep

Diff

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