Advertisement
Guest User

Untitled

a guest
Mar 1st, 2017
166
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.93 KB | None | 0 0
  1. # pachi game reviewer, by sander314@reddit
  2. #settings
  3. 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?
  4. THR_IGNORE = -2 # ignore even top 10 worst moves if better than a 2% decrease, keeps down noise in games with few mistakes
  5. THR_BAD = -5 # what change in win rate is a 'bad' move : 5% decrease in expected win rate
  6. THR_VERYBAD = -10 # what change in win rate is a 'very bad' move: 10% decrease
  7. NWORST = 10 # output the worst this many moves on the terminal and possibly to 'deep analysis'
  8. NDEEP = 20 # for deep analysis, play out this many moves
  9.  
  10. # Constants, don't touch these
  11. ABC = ('a'..'z').to_a.join # SGF ordering
  12. ABC_NO_I = ABC.sub('i','').upcase # GTP ordering
  13. DONE = "MOVE_CALC_DONE"
  14.  
  15. ### Helper functions
  16. def g2s(m) # converts pachi/gtp style move to sgf
  17. return "" if m['pass']
  18. "#{ABC[ABC_NO_I.index(m[0])]}#{ABC[SIZE - m[1..-1].to_i]}"
  19. end
  20. def s2g(m) # ...and the other way around
  21. return "pass" if m=='' || m=='tt'
  22. "#{ABC_NO_I[ABC.index(m[0])]}#{SIZE - ABC.index(m[1])}"
  23. end
  24. def bw(player,j) # whose turn is it j moves after move by player
  25. (player=='B'&&j%2==0||player=='W'&&j%2==1) ? 'B':'W'
  26. end
  27. def pachi_move(pachi,player)
  28. pachi.puts "genmove #{player}", "echo #{DONE}_#{player}"
  29. output_lines = ['']
  30. while !output_lines[-1]["#{DONE}_#{player}"] || output_lines[-1]["IN:"]
  31. output_lines << pachi.gets
  32. break if output_lines[-1].nil?
  33. progr = output_lines[-1][/\[\d+\]\s+best/]
  34. printf "#{progr[1..-1].to_i/1000}k," if progr # ok progress output, could be better
  35. end
  36. output = output_lines.join
  37. 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
  38. m = output.scan(/best (\S+) xkomi (\S+) \| seq ([^|]+)\| can \w ([^\n]+)/) # last match because match can capture 'pondering' output
  39. abort(output + "Could not find proper output matching move result pattern") if m.empty?
  40. score = m[-1][0].to_f
  41. xkomi = m[-1][1].to_f
  42. can = m[-1][3].strip.split(/\s+/).map{|ms| b=ms.index('('); [ms[0...b],ms[b+1..-2].to_f]}
  43. 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
  44. return [player=='B' ? score : 1-score,xkomi,seq,can]
  45. end
  46.  
  47. ### Parse arguments and sgf input file
  48.  
  49. if !ARGV[0]
  50. 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"
  51. end
  52.  
  53. file = ARGV[0]
  54. abort("File #{file} not found") if !File.exists?(file)
  55. puts "Analyzing #{file}"
  56.  
  57. sgf = File.read(ARGV[0])
  58. all,header,movetxt = /(\(;FF.*?(?=;))(.*)/m.match(sgf).to_a
  59.  
  60. SIZE = header[/(?<=SZ\[)(\d+)/].to_i rescue 19
  61. KOMI = header[/(?<=KM\[)([^\]]+)/].to_f rescue 6.5
  62.  
  63. moves = movetxt.scan(/([BW])\[(\w\w|)\]/).map{|p,m| [p, m, s2g(m)] } # extract moves
  64.  
  65. move_i = (0...moves.size).to_a
  66. deep_move_i = []
  67. autodeep = {'W'=>false,'B'=>false}
  68. pachiargs = DEFAULT_PACHI
  69. moves_finish = 0
  70.  
  71. a = 1
  72. while a+1 < ARGV.size
  73. if ARGV[a]=='-m'
  74. from,to = ARGV[a+1].split('-').map(&:to_i)
  75. move_i = ((from-1)...[to,moves.size].min).to_a
  76. puts "-m: Only look at moves #{move_i[0]+1}-#{move_i[-1]+1}"
  77. end
  78. if ARGV[a]=='-d'
  79. deep_move_i = ARGV[a+1].split(',')
  80. ['B','W'].each{|p| autodeep[p] = ARGV[a+1].upcase[p] }
  81. deep_move_i = deep_move_i.select{|x|x[/\d+/]}.map{|m|m.to_i-1}
  82. end
  83. if ARGV[a]=='-f'
  84. moves_finish = ARGV[a+1].to_i
  85. end
  86. if ARGV[a]=='-e'
  87. pachiargs = ARGV[a+1..-1].join(' ')
  88. break
  89. end
  90. if ARGV[a]=='+e'
  91. pachiargs << ARGV[a+1..-1].join(' ')
  92. break
  93. end
  94. a=a+2
  95. end
  96.  
  97.  
  98. setup = "boardsize #{SIZE}\nkomi #{KOMI}"
  99. puts "Settings:\n", setup, "Engine args: ", pachiargs
  100.  
  101. pachi_moves = []
  102. puts "Analyzing moves #{move_i[0]+1} to #{move_i[-1]+1}"
  103.  
  104. IO.popen("./pachi #{pachiargs} 2>&1","w+"){|pachi|
  105. # Normal analysis
  106. pachi.puts setup
  107. move_i.each{|i|
  108. print "Analyzing move #{i+1} ... t = "
  109. pachi.puts "clear_board\n", moves[0...i].map{|p,sm,gm| "play #{p} #{gm}" }
  110. pachi_moves[i] = pachi_move(pachi,moves[i][0])
  111. puts "\tB Win rate %.1f Move %s" %[100*pachi_moves[i][0],pachi_moves[i][2][0]]
  112. }
  113. move_comments = moves.map{''}
  114.  
  115. # Process win rates
  116. b_win = [50]
  117. 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
  118. b_win += [b_win[-1],b_win[-1]] # add some extra values for looking ahead, can result in some weird comments anyway
  119.  
  120. worst_moves = {'B'=>[],'W'=>[]}
  121. theor_wr_change = []; actual_wr_change = []
  122. moves.each_with_index{|(p,sgfmove,gtpmove),i|
  123. 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
  124. 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
  125. 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
  126. }
  127. worst_move_sum = ''
  128. worst_moves.each{|p,mv|
  129. mv.select!{|a_wr_change,twr,i,actual,pachi| actual!=pachi} # do not look silly by suggesting optimal moves are worst
  130. actual_worst = mv.sort_by{|actual,theor|actual}[0...NWORST]
  131. worst_move_sum += "\nActual worst moves for #{p} (assuming actual opponent response):\n"
  132. actual_worst.each_with_index{|(a_wr_change,twr,i,actual,pachi),order|
  133. break if a_wr_change >= THR_IGNORE
  134. deep_move_i << i if autodeep[p]
  135. move_comments[i] += "\##{order+1} actual worst move for #{p}\n"
  136. 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]
  137. }
  138. worst_move_sum += "\nTheoretical worst moves for #{p} (assuming opponent responds optimally):\n"
  139. mv.sort_by{|actual,theor|theor }[0...NWORST].each_with_index{|(awr,t_wr_change,i,actual,pachi),order|
  140. move_comments[i] += "\##{order+1} theoretical worst move for #{p}\n"
  141. break if t_wr_change >= THR_IGNORE
  142. 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]
  143. }
  144.  
  145. }
  146. puts worst_move_sum
  147. pachi_deep_move = []
  148. deep_move_i.uniq.each{|i|
  149. print "Deep analyzing move #{i+1} ... t = "
  150. pachi.puts "clear_board\n", moves[0...i].map{|p,sm,gm| "play #{p} #{gm}" }
  151. playout = (0...NDEEP).map{|j| pachi_move(pachi,bw(moves[i][0],j)) }
  152. seq = playout.map{|score,xkomi,seq,can| seq[0] }
  153. puts "\nMove playout: #{seq.join(" ")}"
  154. pachi_deep_move[i] = seq
  155. }
  156.  
  157.  
  158. f_out = "#{file.sub('.sgf','')}_review.sgf"
  159. File.open(f_out,"w"){|f| # ensure .sgf extension even if missing from original
  160. f << header << "C[Review generated by sander314's reviewer script using ./pachi #{pachiargs}\n#{worst_move_sum}]\n" # copy header
  161.  
  162. var_stack = []
  163. (0...moves.size).each{|i|
  164. player = moves[i][0]
  165. actual_move = "#{player}[#{moves[i][1]}]"
  166. # puts "#{pachi_deep_move[i][0]}!=#{moves[i][2]}? #{pachi_deep_move[i][0]!=moves[i][2]}!" if pachi_deep_move[i]
  167. if pachi_deep_move[i] && pachi_deep_move[i][0]!=moves[i][2]
  168. move_comments[i] += "#{NDEEP} move deep 'best' variation available\n"
  169. f << "(" # include longer variation
  170. variation = pachi_deep_move[i].each_with_index.map{|m,j| ";#{bw(player,j)}[#{g2s(m)}]" }
  171. var_stack.unshift ")(#{variation.join})"
  172. end
  173.  
  174. if pachi_moves[i]
  175. pachi_move = g2s(pachi_moves[i][2][0]) # first in SEQ
  176. pachi_alt = ("TR[" + pachi_moves[i][3][1..-1].map{|m,s| g2s(m)}.join("][") + "]") rescue ''
  177. pachi_alt_desc = pachi_moves[i][3].map{|m,s| "#{m} : #{'%.1f'%(100*(player=='B' ? s : 1-s))}%" }.join("\n")
  178.  
  179. marks = "#{pachi_alt}MA[#{pachi_move}]"
  180. move_comment = move_comments[i]
  181. if pachi_move != moves[i][2] && theor_wr_change[i] < THR_BAD # pachi==actual can actually happen for low engine time
  182. move_comment += theor_wr_change[i] < THR_VERYBAD ? "Very bad move" : "Bad move"
  183. 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'
  184. move_comment += punished ? " which was punished" : " which was not punished"
  185. move_comment += "\nExpected #{player} Win Rate %+.1f after best reply" % theor_wr_change[i] # These require some subtle interpretation
  186. move_comment += "\nActual #{player} Win Rate %+.1f in two moves\n" % actual_wr_change[i]
  187. if !pachi_deep_move[i] # include a (short) better variation on this move
  188. f << "("
  189. variation = pachi_moves[i][2].each_with_index.map{|m,j| ";#{bw(player,j)}[#{g2s(m)}]" } #
  190. var_stack.unshift ")(#{variation.join})"
  191. end
  192. end
  193. 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"
  194. end
  195. f << ";" << actual_move << move_comment << "\n"
  196. }
  197.  
  198. fmove = []
  199. pachi.puts "clear_board\n", moves.map{|p,sm,gm| "play #{p} #{gm}" }
  200. moves_finish.times{|i|
  201. pl = bw(moves[-1][0],i+1)
  202. fmove[i] = pachi_move(pachi,pl)
  203. 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(" ")]
  204. puts "\tFinishing game move #{i}: #{out}"
  205. f.puts ";#{pl}[#{g2s(fmove[i][2][0])}] C[Not in actual game, likely continuation\n#{out}]"
  206. break if fmove[-1][2][0]=='pass' && fmove[-2][2][0]=='pass'
  207. }
  208.  
  209. f << var_stack.join("\n")
  210. f << ")"
  211. }
  212. puts "Done! See #{f_out} for review"
  213.  
  214.  
  215.  
  216. pachi.close_write
  217. } # pipe to pachi
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement