OSDN Git Service

* [shogi-server] Support a graceful shutdown. (Closes #38544)
[shogi-server/shogi-server.git] / utils / eval_graph.rb
1 #!/usr/bin/ruby
2 # This generates graphs of evaluation values from comments in CSA files.
3 # Ruby libraries that are required: 
4 # * RubyGems: http://rubyforge.org/projects/rubygems/
5 # * rgplot:   http://rubyforge.org/projects/rgplot/
6 # OS librariles that is required:
7 # * Gnuplot:  http://www.gnuplot.info/
8 #   * On Debian, $ sudo apt-get install gnuplot
9 #
10 # Author::    Daigo Moriwaki <daigo at debian dot org>
11 # Copyright:: Copyright (C) 2006-2012 Daigo Moriwaki <daigo at debian dot org>
12 #
13 # $Id$
14 #
15 #--
16 # This program is free software; you can redistribute it and/or modify
17 # it under the terms of the GNU General Public License as published by
18 # the Free Software Foundation; either version 2 of the License, or
19 # (at your option) any later version.
20 #
21 # This program is distributed in the hope that it will be useful,
22 # but WITHOUT ANY WARRANTY; without even the implied warranty of
23 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24 # GNU General Public License for more details.
25 #
26 # You should have received a copy of the GNU General Public License
27 # along with this program; if not, write to the Free Software
28 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
29 #++
30
31 require 'pathname'
32 require 'getoptlong'
33 require 'rubygems'
34 require 'gnuplot'
35
36 def to_svg_file(csa_file)
37   "#{csa_file}.svg"
38 end
39
40 def reformat_svg(str)
41   str.gsub(%r!<svg.*?>!m, <<-END) 
42 <svg viewBox="0 0 800 600" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
43 END
44 end
45
46 # Parse play time from the game_name, then return it. If the game_name is
47 # not valid, return 0.
48 #
49 def play_time(game_name)
50   if /.*?\+.*?\-(\d*?)\-/ =~ game_name
51     return $1.to_i
52   end
53   return 0  
54 end
55
56 module EvalGraph
57   def parse_comment(str)
58     return nil unless str
59     str.strip!
60     items = str.split(" ")
61     if items.size > 0
62       return items[0]
63     else
64       return nil
65     end
66   end
67   module_function :parse_comment
68   
69   class Player
70     attr_accessor :theOther
71     attr_reader   :name, :comments, :start_time
72     
73     # type is '+' or '-'
74     def initialize(type)
75       @comments = []
76       @times = []
77       @type = type
78       @regexp_move    = Regexp.new("^\\#{@type}\\d{4}\\w{2}")
79       @regexp_name    = Regexp.new("^N\\#{@type}(.*)")
80       @regexp_time    = Regexp.new(/^T(\d+)/)
81       @regexp_comment = Regexp.new(/^'\*\*(.*)/)
82       @flag = false
83       @name = nil
84       @increment = 0
85     end
86
87     def reset
88       if @flag
89         @comments << nil
90       end
91       @flag = false
92     end
93
94     def <<(comment)
95       case comment
96       when @regexp_move
97         @flag = true
98         @theOther.reset
99       when @regexp_time
100         if @flag
101           @times << $1.to_i
102         end
103       when @regexp_comment
104         if @flag
105           @comments << EvalGraph::parse_comment($1)
106           @flag = false
107         end
108       when @regexp_name
109         @name = $1
110       when /\$START_TIME:(.*)/
111         @start_time = $1
112       when /^'Increment:(.*)/
113         @increment = $1.to_i
114       end
115     end
116
117     # Return times for each move which the player played. 
118     # return[0] is the initial play_time.
119     #
120     def time_values(y_max, play_time)
121       consume = play_time
122       values = []
123       values << 1.0*y_max/play_time*consume
124       @times.each do |t|
125         consume += @increment
126         if consume == 0
127           break
128         end
129         consume -= t
130         if consume < 0
131           consume = 0
132         end
133         values << 1.0*y_max/play_time*consume
134       end
135       return values
136     end
137   end # Player
138
139   class Black < Player
140     def name
141       @name ? "#{@name} (B)" : "black"
142     end
143
144     # Gluplot can not show nil vlaues so that a return value has to contain
145     # [[0], [0]] at least.
146     def eval_values
147       moves = []
148       comments.each_with_index do |c, i|
149         moves << i*2 if c
150       end
151       moves.unshift 0
152       [moves, comments.compact.unshift(0)]
153     end
154
155     # Return moves and times. For example, [[0,1,3], [900, 899, 898]]
156     def time_values(y_max, play_time)
157       values = super
158       moves = [0]
159       return [moves, values] if values.size <= 1
160
161       i = 1
162       values[1, values.size-1].each do |v|
163         moves << i
164         i += 2
165       end
166       return [moves, values]
167     end
168   end # Black
169
170   class White < Player
171     def name
172       @name ? "#{@name} (W)" : "white"
173     end
174
175     def eval_values
176       moves = []
177       comments.each_with_index do |c, i|
178         moves << i*2+1 if c
179       end
180       moves.unshift 0
181       [moves, comments.compact.unshift(0)]
182     end
183
184     def time_values(y_max, play_time)
185       values = super
186       moves = [0]
187       return [moves, values] if values.size <= 1
188
189       i = 2
190       values[1, values.size-1].each do |v|
191         moves << i
192         i += 2
193       end
194       return [moves, values]
195     end
196   end # White
197
198   
199   def create_players
200     black = Black.new("+")
201     white = White.new("-")
202     black.theOther = white
203     white.theOther = black
204     return black,white
205   end
206   module_function :create_players
207 end # module EvalGraph
208
209
210 def plot(csa_file, title, black, white, a_play_time)
211   width = [black.comments.size, white.comments.size].max * 2 + 1
212   Gnuplot.open do |gp|
213     Gnuplot::Plot.new( gp ) do |plot|
214       plot.terminal "svg size 800 500 fixed" # or png
215       plot.output   to_svg_file(csa_file)
216       
217       plot.title  title
218       plot.size   "ratio #{1/1.618}"
219       plot.xlabel "Moves"
220       plot.ylabel "Evaluation Value"
221       plot.xrange "[0:#{width}]"
222       plot.yrange "[-3000:3000]"
223       plot.xtics  "20"
224       plot.mxtics "2"
225       plot.ytics  %Q!("2000" 2000, "-2000" -2000)!
226       plot.xzeroaxis "lt -1"
227       plot.grid
228       plot.size   "0.9,0.9"
229       plot.key "left"
230      
231       plot.style "line 1 linewidth 5 linetype -1 linecolor rgbcolor \"red\""
232       plot.style "line 2 linewidth 4 linetype -1 linecolor rgbcolor \"dark-green\""
233
234       plot.data << Gnuplot::DataSet.new( black.eval_values ) do |ds|
235         ds.with  = "lines ls 1"
236         ds.title = black.name
237       end
238       
239       plot.data << Gnuplot::DataSet.new( white.eval_values ) do |ds|
240         ds.with  = "lines ls 2"
241         ds.title = white.name
242       end
243
244       if a_play_time > 0
245         plot.style "line 5 linewidth 1 linetype -1 linecolor rgbcolor \"red\""
246         plot.style "line 6 linewidth 1 linetype -1 linecolor rgbcolor \"green\""
247         plot.style "fill solid 0.25 noborder"
248
249         plot.data << Gnuplot::DataSet.new( black.time_values(3000, a_play_time) ) do |ds|
250           ds.with  = "boxes notitle ls 5"
251         end
252         
253         plot.data << Gnuplot::DataSet.new( white.time_values(-3000, a_play_time) ) do |ds|
254           ds.with  = "boxes notitle ls 6"
255         end
256       end # if
257
258     end
259   end  
260 end
261
262
263 # Read kifu, a record of moves, to generate a graph file. 
264 # lines are contents of the kifu.
265 # file is a file name of a genrating image file.
266 # original_file_name is a file name of the csa file.
267 #
268 def read(lines, file_name, original_file_name=nil)
269   lines.map! {|l| l.strip}
270   original_file_name ||=  file_name 
271   
272   black,white = EvalGraph.create_players
273   while l = lines.shift do
274     black << l
275     white << l
276   end
277   
278   title = "#{file_name}" 
279   a_play_time = play_time(original_file_name)
280   plot(file_name, title, black, white, a_play_time)
281 end
282
283
284 if $0 == __FILE__
285   def usage
286     puts "Usage: #{$0} [--update] <csa_files>..."
287     puts "Options:"
288     puts "  --update        Update .svg files if exist."
289     exit 1
290   end
291
292   usage if ARGV.empty?
293
294   parser = GetoptLong.new
295   parser.set_options(['--update', GetoptLong::NO_ARGUMENT])
296   begin
297     parser.each_option do |name, arg|
298       eval "$OPT_#{name.sub(/^--/, '').gsub(/-/, '_').upcase} = '#{arg}'"
299     end
300   rescue
301     usage
302   end
303   
304   while file = ARGV.shift
305     next if !$OPT_UPDATE && File.exists?(to_svg_file(file))
306     read(Pathname.new(file).readlines, file)
307     str = reformat_svg(Pathname.new(to_svg_file(file)).read)
308     open(to_svg_file(file),"w+") {|f| f << str}
309   end
310 end