Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # pachi game reviewer, by sander314@reddit
- #settings
- DEFAULT_PACHI = "-t =100000 threads=4,max_tree_size=3072,resign_threshold=0 " # -f book.dat does not work since syntax for book matches is different TODO: parse book match?
- THR_IGNORE = -2 # ignore even top 10 worst moves if better than a 2% decrease, keeps down noise in games with few mistakes
- THR_BAD = -5 # what change in win rate is a 'bad' move : 5% decrease in expected win rate
- THR_VERYBAD = -10 # what change in win rate is a 'very bad' move: 10% decrease
- NWORST = 10 # output the worst this many moves on the terminal and possibly to 'deep analysis'
- NDEEP = 20 # for deep analysis, play out this many moves
- # Constants, don't touch these
- ABC = ('a'..'z').to_a.join # SGF ordering
- ABC_NO_I = ABC.sub('i','').upcase # GTP ordering
- DONE = "MOVE_CALC_DONE"
- ### Helper functions
- def g2s(m) # converts pachi/gtp style move to sgf
- return "" if m['pass']
- "#{ABC[ABC_NO_I.index(m[0])]}#{ABC[SIZE - m[1..-1].to_i]}"
- end
- def s2g(m) # ...and the other way around
- return "pass" if m=='' || m=='tt'
- "#{ABC_NO_I[ABC.index(m[0])]}#{SIZE - ABC.index(m[1])}"
- end
- def bw(player,j) # whose turn is it j moves after move by player
- (player=='B'&&j%2==0||player=='W'&&j%2==1) ? 'B':'W'
- end
- def pachi_move(pachi,player)
- pachi.puts "genmove #{player}", "echo #{DONE}_#{player}"
- output_lines = ['']
- while !output_lines[-1]["#{DONE}_#{player}"] || output_lines[-1]["IN:"]
- output_lines << pachi.gets
- break if output_lines[-1].nil?
- progr = output_lines[-1][/\[\d+\]\s+best/]
- printf "#{progr[1..-1].to_i/1000}k," if progr # ok progress output, could be better
- end
- output = output_lines.join
- return [-1,0,['pass'],[]] if output['No moves left'] || output['= pass'] || output['= resign'] # do not output 'best' if actual result =pass or =resign, since it can be an occupied square
- m = output.scan(/best (\S+) xkomi (\S+) \| seq ([^|]+)\| can \w ([^\n]+)/) # last match because match can capture 'pondering' output
- abort(output + "Could not find proper output matching move result pattern") if m.empty?
- score = m[-1][0].to_f
- xkomi = m[-1][1].to_f
- can = m[-1][3].strip.split(/\s+/).map{|ms| b=ms.index('('); [ms[0...b],ms[b+1..-2].to_f]}
- seq = m[-1][2].strip != '' ? m[-1][2].strip.split(/\s+/) : [can.max_by{|m,s|s}[0]] # for very low time, sometimes only can is output and not seq
- return [player=='B' ? score : 1-score,xkomi,seq,can]
- end
- ### Parse arguments and sgf input file
- if !ARGV[0]
- abort "Usage: ruby rev.rb file.sgf\n\t-m a-b\tOnly analyze moves a-b\n\t-e options\tall further arguments 'options' passed to pachi\tDefault: #{DEFAULT_PACHI}\n\t+e options\tall further options passed in addition to default\n"
- end
- file = ARGV[0]
- abort("File #{file} not found") if !File.exists?(file)
- puts "Analyzing #{file}"
- sgf = File.read(ARGV[0])
- all,header,movetxt = /(\(;FF.*?(?=;))(.*)/m.match(sgf).to_a
- SIZE = header[/(?<=SZ\[)(\d+)/].to_i rescue 19
- KOMI = header[/(?<=KM\[)([^\]]+)/].to_f rescue 6.5
- moves = movetxt.scan(/([BW])\[(\w\w|)\]/).map{|p,m| [p, m, s2g(m)] } # extract moves
- move_i = (0...moves.size).to_a
- deep_move_i = []
- autodeep = {'W'=>false,'B'=>false}
- pachiargs = DEFAULT_PACHI
- moves_finish = 0
- a = 1
- while a+1 < ARGV.size
- if ARGV[a]=='-m'
- from,to = ARGV[a+1].split('-').map(&:to_i)
- move_i = ((from-1)...[to,moves.size].min).to_a
- puts "-m: Only look at moves #{move_i[0]+1}-#{move_i[-1]+1}"
- end
- if ARGV[a]=='-d'
- deep_move_i = ARGV[a+1].split(',')
- ['B','W'].each{|p| autodeep[p] = ARGV[a+1].upcase[p] }
- deep_move_i = deep_move_i.select{|x|x[/\d+/]}.map{|m|m.to_i-1}
- end
- if ARGV[a]=='-f'
- moves_finish = ARGV[a+1].to_i
- end
- if ARGV[a]=='-e'
- pachiargs = ARGV[a+1..-1].join(' ')
- break
- end
- if ARGV[a]=='+e'
- pachiargs << ARGV[a+1..-1].join(' ')
- break
- end
- a=a+2
- end
- setup = "boardsize #{SIZE}\nkomi #{KOMI}"
- puts "Settings:\n", setup, "Engine args: ", pachiargs
- pachi_moves = []
- puts "Analyzing moves #{move_i[0]+1} to #{move_i[-1]+1}"
- IO.popen("./pachi #{pachiargs} 2>&1","w+"){|pachi|
- # Normal analysis
- pachi.puts setup
- move_i.each{|i|
- print "Analyzing move #{i+1} ... t = "
- pachi.puts "clear_board\n", moves[0...i].map{|p,sm,gm| "play #{p} #{gm}" }
- pachi_moves[i] = pachi_move(pachi,moves[i][0])
- puts "\tB Win rate %.1f Move %s" %[100*pachi_moves[i][0],pachi_moves[i][2][0]]
- }
- move_comments = moves.map{''}
- # Process win rates
- b_win = [50]
- moves.each_with_index{|(p,sgfmove),i| b_win[i] = pachi_moves[i] ? 100*pachi_moves[i][0] : b_win[i-1] } # in the absence of data, crudely assume a static win rate
- b_win += [b_win[-1],b_win[-1]] # add some extra values for looking ahead, can result in some weird comments anyway
- worst_moves = {'B'=>[],'W'=>[]}
- theor_wr_change = []; actual_wr_change = []
- moves.each_with_index{|(p,sgfmove,gtpmove),i|
- theor_wr_change[i] = p=='B' ? b_win[i+1] - b_win[i] : b_win[i] - b_win[i+1] # Theoretical win rate change after best response
- actual_wr_change[i] = p=='B' ? b_win[i+2] - b_win[i] : b_win[i] - b_win[i+2] # Actual win rate change after opponents actual response
- worst_moves[p] << [actual_wr_change[i],theor_wr_change[i],i,gtpmove,pachi_moves[i][2][0]] if pachi_moves[i] && pachi_moves[i+1] && pachi_moves[i+2] # ignore if no analysis
- }
- worst_move_sum = ''
- worst_moves.each{|p,mv|
- mv.select!{|a_wr_change,twr,i,actual,pachi| actual!=pachi} # do not look silly by suggesting optimal moves are worst
- actual_worst = mv.sort_by{|actual,theor|actual}[0...NWORST]
- worst_move_sum += "\nActual worst moves for #{p} (assuming actual opponent response):\n"
- actual_worst.each_with_index{|(a_wr_change,twr,i,actual,pachi),order|
- break if a_wr_change >= THR_IGNORE
- deep_move_i << i if autodeep[p]
- move_comments[i] += "\##{order+1} actual worst move for #{p}\n"
- worst_move_sum += "Move %3d: %s\tWin rate change %+.1f%% (B %.1f%% -> %.1f%%)\tSuggested %s\n" % [i+1, actual, a_wr_change,b_win[i],b_win[i+2], pachi]
- }
- worst_move_sum += "\nTheoretical worst moves for #{p} (assuming opponent responds optimally):\n"
- mv.sort_by{|actual,theor|theor }[0...NWORST].each_with_index{|(awr,t_wr_change,i,actual,pachi),order|
- move_comments[i] += "\##{order+1} theoretical worst move for #{p}\n"
- break if t_wr_change >= THR_IGNORE
- worst_move_sum += "Move %3d: %s\tWin rate change %+.1f%% (B %.1f%% -> %.1f%%)\tSuggested %s\n" % [i+1, actual, t_wr_change,b_win[i],b_win[i+1], pachi]
- }
- }
- puts worst_move_sum
- pachi_deep_move = []
- deep_move_i.uniq.each{|i|
- print "Deep analyzing move #{i+1} ... t = "
- pachi.puts "clear_board\n", moves[0...i].map{|p,sm,gm| "play #{p} #{gm}" }
- playout = (0...NDEEP).map{|j| pachi_move(pachi,bw(moves[i][0],j)) }
- seq = playout.map{|score,xkomi,seq,can| seq[0] }
- puts "\nMove playout: #{seq.join(" ")}"
- pachi_deep_move[i] = seq
- }
- f_out = "#{file.sub('.sgf','')}_review.sgf"
- File.open(f_out,"w"){|f| # ensure .sgf extension even if missing from original
- f << header << "C[Review generated by sander314's reviewer script using ./pachi #{pachiargs}\n#{worst_move_sum}]\n" # copy header
- var_stack = []
- (0...moves.size).each{|i|
- player = moves[i][0]
- actual_move = "#{player}[#{moves[i][1]}]"
- # puts "#{pachi_deep_move[i][0]}!=#{moves[i][2]}? #{pachi_deep_move[i][0]!=moves[i][2]}!" if pachi_deep_move[i]
- if pachi_deep_move[i] && pachi_deep_move[i][0]!=moves[i][2]
- move_comments[i] += "#{NDEEP} move deep 'best' variation available\n"
- f << "(" # include longer variation
- variation = pachi_deep_move[i].each_with_index.map{|m,j| ";#{bw(player,j)}[#{g2s(m)}]" }
- var_stack.unshift ")(#{variation.join})"
- end
- if pachi_moves[i]
- pachi_move = g2s(pachi_moves[i][2][0]) # first in SEQ
- pachi_alt = ("TR[" + pachi_moves[i][3][1..-1].map{|m,s| g2s(m)}.join("][") + "]") rescue ''
- pachi_alt_desc = pachi_moves[i][3].map{|m,s| "#{m} : #{'%.1f'%(100*(player=='B' ? s : 1-s))}%" }.join("\n")
- marks = "#{pachi_alt}MA[#{pachi_move}]"
- move_comment = move_comments[i]
- if pachi_move != moves[i][2] && theor_wr_change[i] < THR_BAD # pachi==actual can actually happen for low engine time
- move_comment += theor_wr_change[i] < THR_VERYBAD ? "Very bad move" : "Bad move"
- punished = (actual_wr_change[i] < THR_BAD || (actual_wr_change[i]-theor_wr_change[i]).abs<2) # second clause prevents '-5.1% bad move, to -4.9% not punished etc'
- move_comment += punished ? " which was punished" : " which was not punished"
- move_comment += "\nExpected #{player} Win Rate %+.1f after best reply" % theor_wr_change[i] # These require some subtle interpretation
- move_comment += "\nActual #{player} Win Rate %+.1f in two moves\n" % actual_wr_change[i]
- if !pachi_deep_move[i] # include a (short) better variation on this move
- f << "("
- variation = pachi_moves[i][2].each_with_index.map{|m,j| ";#{bw(player,j)}[#{g2s(m)}]" } #
- var_stack.unshift ")(#{variation.join})"
- end
- end
- move_comment = "C[#{move_comment}Black win rate #{'%.1f' % b_win[i+1]} after actual move\nBlack win rate #{'%.1f' % b_win[i]}% after #{pachi_moves[i][2][0]}\nAlternative Moves:\n#{pachi_alt_desc}]#{marks}\n"
- end
- f << ";" << actual_move << move_comment << "\n"
- }
- fmove = []
- pachi.puts "clear_board\n", moves.map{|p,sm,gm| "play #{p} #{gm}" }
- moves_finish.times{|i|
- pl = bw(moves[-1][0],i+1)
- fmove[i] = pachi_move(pachi,pl)
- out = "B Win rate %.1f \tMove %s \tAlt %s" %[100*fmove[i][0],fmove[i][2][0],fmove[i][3].map{|m,p| "#{m}(#{p})" }.join(" ")]
- puts "\tFinishing game move #{i}: #{out}"
- f.puts ";#{pl}[#{g2s(fmove[i][2][0])}] C[Not in actual game, likely continuation\n#{out}]"
- break if fmove[-1][2][0]=='pass' && fmove[-2][2][0]=='pass'
- }
- f << var_stack.join("\n")
- f << ")"
- }
- puts "Done! See #{f_out} for review"
- pachi.close_write
- } # pipe to pachi
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement