OSDN Git Service

* [utils/csa-filter.rb]
[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-2008  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     end
85
86     def reset
87       if @flag
88         @comments << nil
89       end
90       @flag = false
91     end
92
93     def <<(comment)
94       case comment
95       when @regexp_move
96         @flag = true
97         @theOther.reset
98       when @regexp_time
99         if @flag
100           @times << $1.to_i
101         end
102       when @regexp_comment
103         if @flag
104           @comments << EvalGraph::parse_comment($1)
105           @flag = false
106         end
107       when @regexp_name
108         @name = $1
109       when /\$START_TIME:(.*)/
110         @start_time = $1
111       end
112     end
113
114     # Return times for each move which the player played. 
115     # return[0] is the initial play_time.
116     #
117     def time_values(y_max, play_time)
118       consume = play_time
119       values = []
120       values << 1.0*y_max/play_time*consume
121       @times.each do |t|
122         if consume == 0
123           break
124         end
125         consume -= t
126         if consume < 0
127           consume = 0
128         end
129         values << 1.0*y_max/play_time*consume
130       end
131       return values
132     end
133   end # Player
134
135   class Black < Player
136     def name
137       @name ? "#{@name} (B)" : "black"
138     end
139
140     # Gluplot can not show nil vlaues so that a return value has to contain
141     # [[0], [0]] at least.
142     def eval_values
143       moves = []
144       comments.each_with_index do |c, i|
145         moves << i*2 + 1 if c
146       end
147       moves.unshift 0
148       [moves, comments.compact.unshift(0)]
149     end
150
151     # Return moves and times. For example, [[0,1,3], [900, 899, 898]]
152     def time_values(y_max, play_time)
153       values = super
154       moves = [0]
155       return [moves, values] if values.size <= 1
156
157       i = 1
158       values[1, values.size-1].each do |v|
159         moves << i
160         i += 2
161       end
162       return [moves, values]
163     end
164   end # Black
165
166   class White < Player
167     def name
168       @name ? "#{@name} (W)" : "white"
169     end
170
171     def eval_values
172       moves = []
173       comments.each_with_index do |c, i|
174         moves << i*2 if c
175       end
176       moves.unshift 0
177       [moves, comments.compact.unshift(0)]
178     end
179
180     def time_values(y_max, play_time)
181       values = super
182       moves = [0]
183       return [moves, values] if values.size <= 1
184
185       i = 2
186       values[1, values.size-1].each do |v|
187         moves << i
188         i += 2
189       end
190       return [moves, values]
191     end
192   end # White
193
194   
195   def create_players
196     black = Black.new("+")
197     white = White.new("-")
198     black.theOther = white
199     white.theOther = black
200     return black,white
201   end
202   module_function :create_players
203 end # module EvalGraph
204
205
206 def plot(csa_file, title, black, white, a_play_time)
207   width = [black.comments.size, white.comments.size].max * 2 + 1
208   Gnuplot.open do |gp|
209     Gnuplot::Plot.new( gp ) do |plot|
210       plot.terminal "svg" # or png
211       plot.output   to_svg_file(csa_file)
212       
213       plot.title  title
214       plot.size   "ratio #{1/1.618}"
215       plot.xlabel "Moves"
216       plot.ylabel "Evaluation Value"
217       plot.xrange "[0:#{width}]"
218       plot.yrange "[-3000:3000]"
219       plot.xtics  "20"
220       plot.mxtics "2"
221       plot.ytics  %Q!("2000" 2000, "-2000" -2000)!
222       plot.xzeroaxis "lt -1"
223       plot.grid
224       plot.size   "0.9,0.9"
225       plot.key "left"
226      
227       plot.style "line 1 linewidth 5 linetype 0 linecolor rgbcolor \"red\"" 
228       plot.style "line 2 linewidth 4 linetype 0 linecolor rgbcolor \"dark-green\"" 
229
230       plot.data << Gnuplot::DataSet.new( black.eval_values ) do |ds|
231         ds.with  = "lines ls 1"
232         ds.title = black.name
233       end
234       
235       plot.data << Gnuplot::DataSet.new( white.eval_values ) do |ds|
236         ds.with  = "lines ls 2"
237         ds.title = white.name
238       end
239
240       if a_play_time > 0
241         plot.style "line 5 linewidth 1 linetype 0 linecolor rgbcolor \"red\"" 
242         plot.style "line 6 linewidth 1 linetype 0 linecolor rgbcolor \"green\"" 
243         plot.style "fill solid 0.25 noborder"
244
245         plot.data << Gnuplot::DataSet.new( black.time_values(3000, a_play_time) ) do |ds|
246           ds.with  = "boxes notitle ls 5"
247         end
248         
249         plot.data << Gnuplot::DataSet.new( white.time_values(-3000, a_play_time) ) do |ds|
250           ds.with  = "boxes notitle ls 6"
251         end
252       end # if
253
254     end
255   end  
256 end
257
258
259 # Read kifu, a record of moves, to generate a graph file. 
260 # lines are contents of the kifu.
261 # file is a file name of a genrating image file.
262 # original_file_name is a file name of the csa file.
263 #
264 def read(lines, file_name, original_file_name=nil)
265   lines.map! {|l| l.strip}
266   original_file_name ||=  file_name 
267   
268   black,white = EvalGraph.create_players
269   while l = lines.shift do
270     black << l
271     white << l
272   end
273   
274   title = "#{file_name}" 
275   a_play_time = play_time(original_file_name)
276   plot(file_name, title, black, white, a_play_time)
277 end
278
279
280 if $0 == __FILE__
281   def usage
282     puts "Usage: #{$0} [--update] <csa_files>..."
283     puts "Options:"
284     puts "  --update        Update .svg files if exist."
285     exit 1
286   end
287
288   usage if ARGV.empty?
289
290   parser = GetoptLong.new
291   parser.set_options(['--update', GetoptLong::NO_ARGUMENT])
292   begin
293     parser.each_option do |name, arg|
294       eval "$OPT_#{name.sub(/^--/, '').gsub(/-/, '_').upcase} = '#{arg}'"
295     end
296   rescue
297     usage
298   end
299   
300   while file = ARGV.shift
301     next if !$OPT_UPDATE && File.exists?(to_svg_file(file))
302     read(Pathname.new(file).readlines, file)
303     str = reformat_svg(Pathname.new(to_svg_file(file)).read)
304     open(to_svg_file(file),"w+") {|f| f << str}
305   end
306 end