├── .gitignore ├── AUTHORS ├── LICENSE ├── Makefile ├── README.md ├── aq_analysis.py ├── config.ini ├── dual_view.py ├── file_association.bat ├── gnugo_analysis.py ├── goban.py ├── gomill ├── README.txt ├── __init__.py ├── allplayalls.py ├── ascii_boards.py ├── ascii_tables.py ├── boards.py ├── cem_tuners.py ├── common.py ├── compact_tracebacks.py ├── competition_schedulers.py ├── competitions.py ├── game_jobs.py ├── gtp_controller.py ├── gtp_engine.py ├── gtp_games.py ├── gtp_proxy.py ├── gtp_states.py ├── handicap_layout.py ├── job_manager.py ├── mcts_tuners.py ├── playoffs.py ├── ringmaster_command_line.py ├── ringmaster_presenters.py ├── ringmasters.py ├── settings.py ├── sgf.py ├── sgf_grammar.py ├── sgf_moves.py ├── sgf_properties.py ├── terminal_input.py ├── tournament_results.py ├── tournaments.py └── utils.py ├── goreviewpartner.desktop ├── goreviewpartner.png ├── gtp.py ├── gtp_bot.py ├── gtp_terminal.py ├── icon.gif ├── leela_analysis.py ├── leela_zero_analysis.py ├── live_analysis.py ├── live_analysis └── .gitignore ├── main.py ├── mss ├── __init__.py ├── __main__.py ├── base.py ├── darwin.py ├── exception.py ├── factory.py ├── linux.py ├── screenshot.py ├── tools.py └── windows.py ├── pachi_analysis.py ├── phoenixgo_analysis.py ├── playsound.py ├── r2csv.py ├── r2sgf.py ├── ray_analysis.py ├── settings.py ├── tabbed.py ├── toolbox.py └── translations ├── check.py ├── de.po ├── fr.po ├── kr.po ├── new_translation.po ├── pl.po ├── ru.po ├── translation_help.odg ├── translation_help.pdf └── zh.po /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.sgf 3 | *.rsgf 4 | translations/en.po 5 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # git shortlog -sn | cut -c8- with manually removed duplicates (taken from Leela Zero project) 2 | 3 | pnprog 4 | Boris NA 5 | Wonsik Kim 6 | wonderingabout 7 | Isaac Deutsch 8 | Tomasz Warniełło 9 | chermes 10 | Adrian Petrescu 11 | Adam Bender 12 | Brandon Sloane 13 | J. Brian Jordan 14 | Michael Kelley 15 | Mikita Herasiutsin 16 | Tomasz 17 | Vladimir Medvedev 18 | Wuyou Jiang 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | install -d -Dm755 . /opt/goreviewpartner 3 | cp -rp * /opt/goreviewpartner 4 | chmod a+w /opt/goreviewpartner/config.ini 5 | install -Dm644 goreviewpartner.png /usr/share/pixmaps/goreviewpartner.png 6 | install -Dm644 goreviewpartner.desktop /usr/local/share/applications/goreviewpartner.desktop 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Go Review Partner logo](http://yuntingdian.com/goreviewpartner/grp-documentation/img/goreviewpartner.png "Go Review Partner logo") 2 | 3 | # goreviewpartner 4 | A tool to help analyse and review your games of Go ([weiqi, baduk, igo](https://en.wikipedia.org/wiki/Go_(game))) using bots. Currently supporting [Leela Zero](https://github.com/gcp/leela-zero/), [AQ](https://github.com/ymgaq/AQ), [Pachi](https://github.com/pasky/pachi), [PhoenixGo](https://github.com/Tencent/PhoenixGo/), [Leela](https://www.sjeng.org/leela.html), [Ray](https://github.com/zakki/Ray) and [GnuGo](https://www.gnu.org/software/gnugo/). 5 | 6 | Project home page: [http://yuntingdian.com/goreviewpartner/](http://yuntingdian.com/goreviewpartner/). 7 | Windows ready downloads are available from the home page. 8 | 9 | Online Documentation: [http://yuntingdian.com/goreviewpartner/grp-documentation/doc.htm](http://yuntingdian.com/goreviewpartner/grp-documentation/doc.htm) 10 | 11 | More informations on L19 forum in this thread: [Announcing GoReviewPartner](https://lifein19x19.com/forum/viewtopic.php?f=9&t=14050) 12 | 13 | Feel free to open issues or ask questions on Github or L19. 14 | 15 | ![Screen-shot of GoReviewPartner: Main menu](http://yuntingdian.com/goreviewpartner/grp-documentation/img/main_screen.png "Screen-shot of GoReviewPartner: Main menu") 16 | 17 | ![Screen-shot of GoReviewPartner: Analyse selection](http://yuntingdian.com/goreviewpartner/grp-documentation/img/analysis_panel.png "Screen-shot of GoReviewPartner: Analyse selection") 18 | 19 | ![Screen-shot of GoReviewPartner: Analyse running](http://yuntingdian.com/goreviewpartner/grp-documentation/img/analysing.png "Screen-shot of GoReviewPartner: Analyse running") 20 | 21 | ![Screen-shot of GoReviewPartner: Game Review (alternative moves)](http://yuntingdian.com/goreviewpartner/grp-documentation/img/displaying_sequence.png "Screen-shot of GoReviewPartner: Game Review (alternative moves)") 22 | 23 | ![Screen-shot of GoReviewPartner: Game Review (displaying influence map)](http://yuntingdian.com/goreviewpartner/grp-documentation/img/territories.png "Screen-shot of GoReviewPartner: Game Review (displaying influence map)") 24 | 25 | ![Screen-shot of GoReviewPartner: Game Review (testing other sequences)](http://yuntingdian.com/goreviewpartner/grp-documentation/img/open_move.png "Screen-shot of GoReviewPartner: Game Review (testing other sequences)") 26 | 27 | ![Screen-shot of GoReviewPartner: Game Review (comparison graph)](http://yuntingdian.com/goreviewpartner/grp-documentation/img/black_comparison_graph.png "Screen-shot of GoReviewPartner: Game Review (comparison graph)") 28 | -------------------------------------------------------------------------------- /aq_analysis.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from gtp import gtp 5 | import sys 6 | from Tkinter import * 7 | from time import sleep 8 | import threading 9 | from toolbox import * 10 | from toolbox import _ 11 | 12 | class AQAnalysis(): 13 | 14 | def win_rate(self,current_move,value,roll): 15 | #see discussion at https://github.com/ymgaq/AQ/issues/20 16 | if current_move<=160: 17 | lmbd=0.8 18 | else: 19 | lmbd=0.8-min(0.3,max(0.0,(current_move-160)/600)) 20 | rate=lmbd*value+(1-lmbd)*roll 21 | #print "winrate(m=",current_move,",v=",value,",r=",roll,")=",rate 22 | return rate 23 | 24 | def run_analysis(self,current_move): 25 | one_move=go_to_move(self.move_zero,current_move) 26 | player_color=guess_color_to_play(self.move_zero,current_move) 27 | aq=self.aq 28 | log() 29 | log("==============") 30 | log("move",str(current_move)) 31 | 32 | if player_color in ('w',"W"): 33 | log("AQ plays white") 34 | answer=aq.play_white() 35 | else: 36 | log("AQ plays black") 37 | answer=aq.play_black() 38 | 39 | if current_move>1: 40 | es=aq.final_score() 41 | #one_move.set("ES",es) 42 | node_set(one_move,"ES",es) 43 | 44 | log("AQ preferred move:",answer) 45 | node_set(one_move,"CBM",answer) #Computer Best Move 46 | 47 | all_moves=aq.get_all_aq_moves() 48 | 49 | if (answer not in ["PASS","RESIGN"]): 50 | best_move=True 51 | 52 | log("Number of alternative sequences:",len(all_moves)) 53 | #log(all_moves) 54 | 55 | #for sequence_first_move,one_sequence,one_score,one_monte_carlo,one_value_network,one_policy_network,one_evaluation,one_rave,one_nodes in all_moves: 56 | for sequence_first_move,count,value,roll,prob,one_sequence in all_moves[:self.maxvariations]: 57 | log("Adding sequence starting from",sequence_first_move) 58 | previous_move=one_move.parent 59 | current_color=player_color 60 | one_score=self.win_rate(current_move,value,roll) 61 | first_variation_move=True 62 | for one_deep_move in one_sequence.split(' '): 63 | 64 | if one_deep_move in ["PASS","RESIGN"]: 65 | log("Leaving the variation when encountering",one_deep_move) 66 | break 67 | i,j=gtp2ij(one_deep_move) 68 | new_child=previous_move.new_child() 69 | node_set(new_child,current_color,(i,j)) 70 | 71 | 72 | if player_color=='b': 73 | bwwr=str(one_score)+'%/'+str(100-one_score)+'%' 74 | mcwr=str(roll)+'%/'+str(100-roll)+'%' 75 | vnwr=str(value)+'%/'+str(100-value)+'%' 76 | else: 77 | bwwr=str(100-one_score)+'%/'+str(one_score)+'%' 78 | mcwr=str(100-roll)+'%/'+str(roll)+'%' 79 | vnwr=str(100-value)+'%/'+str(value)+'%' 80 | 81 | if first_variation_move: 82 | first_variation_move=False 83 | node_set(new_child,"BWWR",bwwr) 84 | node_set(new_child,"PLYO",str(count)) 85 | node_set(new_child,"VNWR",vnwr) 86 | node_set(new_child,"MCWR",mcwr) 87 | node_set(new_child,"PNV",str(prob)+"%") 88 | 89 | 90 | if best_move: 91 | best_move=False 92 | node_set(one_move,"BWWR",bwwr) 93 | node_set(one_move,"MCWR",mcwr) 94 | node_set(one_move,"VNWR",vnwr) 95 | 96 | previous_move=new_child 97 | if current_color in ('w','W'): 98 | current_color='b' 99 | else: 100 | current_color='w' 101 | log("==== no more sequences =====") 102 | aq.undo() 103 | else: 104 | log('adding "'+answer+'" to the sgf file') 105 | aq.undo() 106 | 107 | 108 | return answer 109 | 110 | def initialize_bot(self): 111 | aq=aq_starting_procedure(self.g,self.profile) 112 | self.aq=aq 113 | self.time_per_move=0 114 | return aq 115 | 116 | def aq_starting_procedure(sgf_g,profile,silentfail=False): 117 | return bot_starting_procedure("AQ","AQ",AQ_gtp,sgf_g,profile,silentfail) 118 | 119 | 120 | class RunAnalysis(AQAnalysis,RunAnalysisBase): 121 | def __init__(self,parent,filename,move_range,intervals,variation,komi,profile,existing_variations="remove_everything"): 122 | RunAnalysisBase.__init__(self,parent,filename,move_range,intervals,variation,komi,profile,existing_variations) 123 | 124 | class LiveAnalysis(AQAnalysis,LiveAnalysisBase): 125 | def __init__(self,g,filename,profile): 126 | LiveAnalysisBase.__init__(self,g,filename,profile) 127 | 128 | import ntpath 129 | import subprocess 130 | import Queue 131 | 132 | class AQ_gtp(gtp): 133 | 134 | def quit(self): 135 | self.write('\x03') 136 | 137 | def __init__(self,command): 138 | self.c=1 139 | aq_working_directory=command[0][:-len(ntpath.basename(command[0]))] 140 | self.command_line=command[0]+" "+" ".join(command[1:]) 141 | command=[c.encode(sys.getfilesystemencoding()) for c in command] 142 | aq_working_directory=aq_working_directory.encode(sys.getfilesystemencoding()) 143 | if aq_working_directory: 144 | log("AQ working directory:",aq_working_directory) 145 | self.process=subprocess.Popen(command,cwd=aq_working_directory, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 146 | else: 147 | self.process=subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 148 | self.size=0 149 | self.stderr_queue=Queue.Queue() 150 | self.stdout_queue=Queue.Queue() 151 | 152 | threading.Thread(target=self.consume_stderr).start() 153 | 154 | self.history=[] 155 | self.free_handicap_stones=[] 156 | 157 | def quick_evaluation(self,color): 158 | if color==2: 159 | self.play_white() 160 | else: 161 | self.play_black() 162 | all_moves=self.get_all_aq_moves() 163 | self.undo() 164 | 165 | txt="" 166 | try: 167 | if color==1: 168 | black_win_rate=str(all_moves[0][2])+"%" 169 | white_win_rate=opposite_rate(black_win_rate) 170 | else: 171 | white_win_rate=str(all_moves[0][2])+"%" 172 | black_win_rate=opposite_rate(white_win_rate) 173 | txt+= variation_data_formating["VNWR"]%(black_win_rate+'/'+white_win_rate) 174 | except: 175 | pass 176 | 177 | return txt 178 | 179 | def get_all_aq_moves(self): 180 | buff=[] 181 | 182 | sleep(.1) 183 | while not self.stderr_queue.empty(): 184 | while not self.stderr_queue.empty(): 185 | buff.append(self.stderr_queue.get()) 186 | sleep(.1) 187 | 188 | buff.reverse() 189 | 190 | answers=[] 191 | for err_line in buff: 192 | #log(err_line) 193 | if "total games=" in err_line: 194 | v1,v2=[int(v) for v in err_line.split("=")[-1].replace(")","").split("(")] 195 | if v2!=0: 196 | log("Computing requirement margin: "+str(int(100.*v1/v2)-100)+"%") 197 | if v1" in err_line) and ('|' in err_line): 207 | log(err_line) 208 | sequence=err_line.split("|")[-1].strip() 209 | sequence=sequence.replace(" ","") 210 | sequence=sequence.replace("\t","") 211 | sequence=sequence.replace("->"," ") 212 | 213 | err_line=err_line.replace("|"," ") 214 | err_line=err_line.strip().split() 215 | #print err_line.strip().split() 216 | one_answer=err_line[0] 217 | count=err_line[1] 218 | try: 219 | value=float(err_line[2]) 220 | except: 221 | value=0.0 222 | roll=err_line[3] 223 | prob=err_line[4] 224 | 225 | if sequence: 226 | answers=[[one_answer,int(count),value,float(roll),float(prob),sequence]]+answers 227 | 228 | return answers 229 | 230 | class AQSettings(BotProfiles): 231 | def __init__(self,parent,bot="AQ"): 232 | Frame.__init__(self,parent) 233 | self.parent=parent 234 | self.bot=bot 235 | self.profiles=get_bot_profiles(bot,False) 236 | profiles_frame=self 237 | 238 | self.listbox = Listbox(profiles_frame) 239 | self.listbox.grid(column=10,row=10,rowspan=10) 240 | self.update_listbox() 241 | 242 | row=10 243 | Label(profiles_frame,text=_("Profile")).grid(row=row,column=11,sticky=W) 244 | self.profile = StringVar() 245 | Entry(profiles_frame, textvariable=self.profile, width=30).grid(row=row,column=12) 246 | 247 | row+=1 248 | Label(profiles_frame,text=_("Command")).grid(row=row,column=11,sticky=W) 249 | self.command = StringVar() 250 | Entry(profiles_frame, textvariable=self.command, width=30).grid(row=row,column=12) 251 | 252 | row+=1 253 | Label(profiles_frame,text=_("Parameters")).grid(row=row,column=11,sticky=W) 254 | self.parameters = StringVar() 255 | Entry(profiles_frame, textvariable=self.parameters, width=30).grid(row=row,column=12) 256 | 257 | row+=10 258 | buttons_frame=Frame(profiles_frame) 259 | buttons_frame.grid(row=row,column=10,sticky=W,columnspan=3) 260 | Button(buttons_frame, text=_("Add profile"),command=self.add_profile).grid(row=row,column=1,sticky=W) 261 | Button(buttons_frame, text=_("Modify profile"),command=self.modify_profile).grid(row=row,column=2,sticky=W) 262 | Button(buttons_frame, text=_("Delete profile"),command=self.delete_profile).grid(row=row,column=3,sticky=W) 263 | Button(buttons_frame, text=_("Test"),command=lambda: self.parent.parent.test(self.bot_gtp,self.command,self.parameters)).grid(row=row,column=4,sticky=W) 264 | 265 | self.listbox.bind("", lambda e: self.after(100,self.change_selection)) 266 | 267 | row+=1 268 | Label(buttons_frame,text="").grid(row=row,column=1) 269 | row+=1 270 | Label(buttons_frame,text=_("See AQ parameters in \"aq_config.txt\"")).grid(row=row,column=1,columnspan=2,sticky=W) 271 | 272 | self.index=-1 273 | 274 | self.bot_gtp=AQ_gtp 275 | 276 | 277 | class AQOpenMove(BotOpenMove): 278 | def __init__(self,sgf_g,profile): 279 | BotOpenMove.__init__(self,sgf_g,profile) 280 | self.name='AQ' 281 | self.my_starting_procedure=aq_starting_procedure 282 | 283 | AQ={} 284 | AQ['name']="AQ" 285 | AQ['gtp_name']="AQ" 286 | AQ['analysis']=AQAnalysis 287 | AQ['openmove']=AQOpenMove 288 | AQ['settings']=AQSettings 289 | AQ['gtp']=AQ_gtp 290 | AQ['liveanalysis']=LiveAnalysis 291 | AQ['runanalysis']=RunAnalysis 292 | AQ['starting']=aq_starting_procedure 293 | 294 | if __name__ == "__main__": 295 | main(AQ) 296 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [General] 2 | language = 3 | sgffolder = 4 | rsgffolder = 5 | pngfolder = 6 | livefolder = live_analysis 7 | stonesound = 8 | 9 | [Analysis] 10 | maxvariations = 26 11 | savecommandline = False 12 | stopatfirstresign = False 13 | novariationifsamemove = False 14 | analyser = 15 | 16 | [Review] 17 | fuzzystoneplacement = .2 18 | realgamesequencedeepness = 5 19 | leftgobanratio = 0.4 20 | rightgobanratio = 0.4 21 | rightpanelratio = 0.2 22 | opengobanratio = 0.4 23 | maxvariations = 10 24 | variationscoloring = blue_for_winning 25 | variationslabel = letter 26 | invertedmousewheel = False 27 | lastgraph = 28 | yellowbar = #F39C12 29 | lastbot = 30 | lastmap = 31 | oneortwopanels = 1 32 | 33 | [Live] 34 | size = 19 35 | komi = 7.5 36 | handicap = 0 37 | nooverlap = False 38 | analyser = 39 | black = 40 | white = 41 | livegobanratio = 0.4 42 | thinkbeforeplaying = 0 43 | 44 | [Leela-0] 45 | profile = Slow 46 | command = 47 | parameters = --gtp --noponder 48 | timepermove = 15 49 | 50 | [Leela-1] 51 | profile = Fast 52 | command = 53 | parameters = --gtp --noponder 54 | timepermove = 5 55 | 56 | 57 | [GnuGo-0] 58 | profile = Slow 59 | command = 60 | parameters = --mode=gtp --level=12 61 | variations = 4 62 | deepness = 4 63 | 64 | [GnuGo-1] 65 | profile = Fast 66 | command = 67 | parameters = --mode=gtp --level=10 68 | variations = 4 69 | deepness = 4 70 | 71 | [Ray-0] 72 | profile = Slow 73 | command = 74 | parameters = --no-gpu --const-time 15 75 | 76 | [Ray-1] 77 | profile = Fast 78 | command = 79 | parameters = --no-gpu --const-time 5 80 | 81 | [AQ-0] 82 | profile = Slow 83 | command = 84 | parameters = --config=aq_config_slow.txt 85 | 86 | [AQ-1] 87 | profile = Fast 88 | command = 89 | parameters = --config=aq_config_fast.txt 90 | 91 | [LeelaZero-0] 92 | profile = Slow 93 | command = 94 | parameters = --gtp --noponder --weights weights.txt 95 | timepermove = 15 96 | 97 | [LeelaZero-1] 98 | profile = Fast 99 | command = 100 | parameters = --gtp --noponder --weights weights.txt 101 | timepermove = 5 102 | 103 | [Pachi-0] 104 | profile = Slow (19x19) 105 | command = 106 | parameters = reporting=json,dynkomi=linear:handicap_value=8%8:moves=150%150 --fuseki-time =4000 107 | timepermove = 15 108 | 109 | [Pachi-1] 110 | profile = Fast (19x19) 111 | command = 112 | parameters = reporting=json,dynkomi=linear:handicap_value=8%8:moves=150%150 --fuseki-time =4000 113 | timepermove = 5 114 | 115 | [Pachi-2] 116 | profile = Fast (13x13) 117 | command = 118 | parameters = reporting=json,dynkomi=linear:handicap_value=8%8:moves=50%50 119 | timepermove = 5 120 | 121 | [Pachi-3] 122 | profile = Fast (9x9) 123 | command = 124 | parameters = reporting=json,dynkomi=linear:handicap_value=8%8:moves=15%15 125 | timepermove = 5 126 | 127 | 128 | [PhoenixGo-windows-GPU-0] 129 | profile = Example-windows-GPU-notensorrt-Slow 130 | command = 131 | parameters = --gtp --config_path C:\users\yourusername\Downloads\PhoenixGo\etc\mcts_1gpu_notensorrt_grp.conf --logtostderr --v 1 132 | timepermove = 60 133 | 134 | [PhoenixGo-windows-CPU-0] 135 | profile = Example-windows-CPU-Slow 136 | command = 137 | parameters = --gtp --config_path C:\users\yourusername\Downloads\PhoenixGo\etc\mcts_cpu_grp.conf --logtostderr --v 1 138 | timepermove = 60 139 | 140 | [PhoenixGo-linux-GPU-A-0] 141 | profile = Example-linux-GPU-tensorrt-Slow 142 | command = 143 | parameters = --gtp --config_path=/home/yourusername/PhoenixGo/etc/mcts_1gpu_grp.conf --logtostderr --v=1 144 | timepermove = 60 145 | 146 | [PhoenixGo-linux-GPU-B-0] 147 | profile = Example-linux-GPU-notensorrt-Slow 148 | command = 149 | parameters = --gtp --config_path=/home/yourusername/PhoenixGo/etc/mcts_1gpu_notensorrt_grp.conf --logtostderr --v=1 150 | timepermove = 60 151 | 152 | [PhoenixGo-linux-CPU-0] 153 | profile = Example-linux-CPU-Slow 154 | command = 155 | parameters = --gtp --config_path=/home/yourusername/PhoenixGo/etc/mcts_cpu_grp.conf --logtostderr --v=1 156 | timepermove = 60 157 | -------------------------------------------------------------------------------- /file_association.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | NET SESSION 4 | IF %ERRORLEVEL% NEQ 0 GOTO ELEVATE 5 | GOTO ADMINTASKS 6 | 7 | :ELEVATE 8 | CD /d %~dp0 9 | MSHTA "javascript: var shell = new ActiveXObject('shell.application'); shell.ShellExecute('%~nx0', '', '', 'runas', 1);close();" 10 | EXIT 11 | 12 | :ADMINTASKS 13 | 14 | 15 | ::see https://superuser.com/questions/406985/programatically-associate-file-extensions-with-application-on-windows 16 | 17 | set grp_path=%~dp0% 18 | cd %~dp0% 19 | 20 | 21 | set filename=main.py 22 | if exist "%filename%" goto fromsources 23 | 24 | set filename=GoReviewPartner.exe 25 | if exist "%filename%" goto frompy2exe 26 | 27 | pause 28 | exit 29 | 30 | :fromsources 31 | echo GRP is running from the source 32 | 33 | 34 | 35 | for /f "delims=" %%i in ('Assoc .py') do set filetype=%%i 36 | set filetype=%filetype:~4% 37 | echo filetype for .py files: %filetype% 38 | 39 | 40 | for /f "delims=" %%i in ('Ftype %filetype%') do set pythonexe=%%i 41 | set pythonexe=%pythonexe:~12,-7% 42 | echo path to python interpreter: %pythonexe% 43 | 44 | set python_path="%grp_path%dual_view.py" 45 | echo path to GRP python file: %python_path% 46 | 47 | echo associating ".rsgf" extension with file type "rsgffile" 48 | assoc .rsgf=rsgffile 49 | 50 | echo setting the link between a rsgffile FileType and an GRP python file 51 | ftype rsgffile=%pythonexe% %python_path% %%1 52 | 53 | set ico_path="%grp_path%grp.ico" 54 | echo path to GRP icon: %ico_path% 55 | 56 | echo creating rsgffile entry in registry 57 | REG ADD HKEY_CLASSES_ROOT\rsgffile /f 58 | 59 | echo creating rsgffile\DefaultIcon key in registry 60 | REG ADD HKEY_CLASSES_ROOT\rsgffile\DefaultIcon /ve /T REG_EXPAND_SZ /f /d %ico_path% 61 | 62 | pause 63 | exit 64 | 65 | :frompy2exe 66 | 67 | echo GRP is running from the py2exe version 68 | echo not implemented yet 69 | pause 70 | exit 71 | 72 | rem GRP is running from the py2exe version 73 | rem let's associate *.rsgf file to GoReviewPartner.exe 74 | ::Assoc .rsgf=rsgffile 75 | ::Echo %~dpnx0 > %FOLDER% 76 | ::Set EXE=GoReviewPartner.exe 77 | ::Set FULLPATH=%FOLDER%%EXE% 78 | ::Ftype rsgffile=%FULLPATH% %1 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /gomill/README.txt: -------------------------------------------------------------------------------- 1 | Gomill 2 | ====== 3 | 4 | Gomill is a suite of tools, and a Python library, for use in developing and 5 | testing Go-playing programs. 6 | 7 | Updated versions of Gomill will be made available at 8 | http://mjw.woodcraft.me.uk/gomill/ 9 | 10 | The documentation is distributed separately in HTML form. It can be downloaded 11 | from the above web site, or viewed online at 12 | http://mjw.woodcraft.me.uk/gomill/doc/ 13 | 14 | A Git repository containing Gomill releases (but not detailed history) is 15 | available: 16 | git clone http://mjw.woodcraft.me.uk/gomill/git/ 17 | It has a web interface at http://mjw.woodcraft.me.uk/gitweb/gomill/ 18 | 19 | 20 | Contents 21 | -------- 22 | 23 | The contents of the distribution directory (the directory containing this 24 | README file) include: 25 | 26 | ringmaster -- Executable wrapper for the ringmaster program 27 | gomill -- Python source for the gomill package 28 | gomill_tests -- Test suite for the gomill package 29 | docs -- ReST sources for the HTML documentation 30 | examples -- Example scripts using the gomill library 31 | setup.py -- Installation script 32 | 33 | 34 | Requirements 35 | ------------ 36 | 37 | Gomill requires Python 2.5, 2.6, or 2.7. 38 | 39 | For Python 2.5 only, the --parallel feature requires the external 40 | `multiprocessing` package [1]. 41 | 42 | Gomill is intended to run on any modern Unix-like system. 43 | 44 | [1] http://pypi.python.org/pypi/multiprocessing 45 | 46 | 47 | Running the ringmaster 48 | ---------------------- 49 | 50 | The ringmaster executable in the distribution directory can be run directly 51 | without any further installation; it will use the copy of the gomill package 52 | in the distribution directory. 53 | 54 | A symbolic link to the ringmaster executable will also work, but if you move 55 | the executable elsewhere it will not be able to find the gomill package unless 56 | the package is installed. 57 | 58 | 59 | Installation 60 | ------------ 61 | 62 | Installing Gomill puts the gomill package onto the Python module search path, 63 | and the ringmaster executable onto the executable PATH. 64 | 65 | To install, first change to the distribution directory, then: 66 | 67 | - to install for the system as a whole, run (as a sufficiently privileged user) 68 | 69 | python setup.py install 70 | 71 | 72 | - to install for the current user only (Python 2.6 or 2.7), run 73 | 74 | python setup.py install --user 75 | 76 | (in this case the ringmaster executable will be placed in ~/.local/bin.) 77 | 78 | Pass --dry-run to see what these will do. 79 | See http://docs.python.org/2.7/install/ for more information. 80 | 81 | 82 | Uninstallation 83 | -------------- 84 | 85 | To remove an installed version of Gomill, run 86 | 87 | python setup.py uninstall 88 | 89 | (This uses the Python module search path and the executable PATH to find the 90 | files to remove; pass --dry-run to see what it will do.) 91 | 92 | 93 | Running the test suite 94 | ---------------------- 95 | 96 | To run the testsuite against the distributed gomill package, change to the 97 | distribution directory and run 98 | 99 | python -m gomill_tests.run_gomill_testsuite 100 | 101 | 102 | To run the testsuite against an installed gomill package, change to the 103 | distribution directory and run 104 | 105 | python test_installed_gomill.py 106 | 107 | 108 | With Python versions earlier than 2.7, the unittest2 library [1] is required 109 | to run the testsuite. 110 | 111 | [1] http://pypi.python.org/pypi/unittest2/ 112 | 113 | 114 | Running the example scripts 115 | --------------------------- 116 | 117 | To run the example scripts, it is simplest to install the gomill package 118 | first. 119 | 120 | If you do not wish to do so, you can run 121 | 122 | export PYTHONPATH= 123 | 124 | so that the example scripts will be able to find the gomill package. 125 | 126 | 127 | Building the HTML documentation 128 | ------------------------------- 129 | 130 | To build the HTML documentation, change to the distribution directory and run 131 | 132 | python setup.py build_sphinx 133 | 134 | The documentation will be generated in build/sphinx/html. 135 | 136 | Requirements: 137 | 138 | Sphinx [1] version 1.0 or later 139 | (at least 1.0.4 recommended; tested with 1.0 and 1.1) 140 | LaTeX [2] 141 | dvipng [3] 142 | 143 | [1] http://sphinx.pocoo.org/ 144 | [2] http://www.latex-project.org/ 145 | [3] http://www.nongnu.org/dvipng/ 146 | 147 | 148 | Licence 149 | ------- 150 | 151 | Gomill is copyright 2009-2012 Matthew Woodcraft 152 | 153 | Permission is hereby granted, free of charge, to any person obtaining a copy 154 | of this software and associated documentation files (the "Software"), to deal 155 | in the Software without restriction, including without limitation the rights 156 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 157 | copies of the Software, and to permit persons to whom the Software is 158 | furnished to do so, subject to the following conditions: 159 | 160 | The above copyright notice and this permission notice shall be included in all 161 | copies or substantial portions of the Software. 162 | 163 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 164 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 165 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 166 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 167 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 168 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 169 | SOFTWARE. 170 | 171 | 172 | Contact 173 | ------- 174 | 175 | Please send any bug reports, suggestions, patches, questions &c to 176 | 177 | Matthew Woodcraft 178 | matthew@woodcraft.me.uk 179 | 180 | I'm particularly interested in hearing about any GTP engines (even buggy ones) 181 | which don't work with the ringmaster. 182 | 183 | 184 | Changelog 185 | --------- 186 | 187 | See the 'Changes' page in the HTML documentation (docs/changes.rst). 188 | 189 | mjw 2012-08-26 190 | -------------------------------------------------------------------------------- /gomill/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.7.4" 2 | -------------------------------------------------------------------------------- /gomill/allplayalls.py: -------------------------------------------------------------------------------- 1 | """Competitions for all-play-all tournaments.""" 2 | 3 | from gomill import ascii_tables 4 | from gomill import game_jobs 5 | from gomill import competitions 6 | from gomill import tournaments 7 | from gomill import tournament_results 8 | from gomill.competitions import ( 9 | Competition, CompetitionError, ControlFileError) 10 | from gomill.settings import * 11 | from gomill.utils import format_float 12 | 13 | 14 | class Competitor_config(Quiet_config): 15 | """Competitor description for use in control files.""" 16 | # positional or keyword 17 | positional_arguments = ('player',) 18 | # keyword-only 19 | keyword_arguments = () 20 | 21 | class Competitor_spec(object): 22 | """Internal description of a competitor spec from the configuration file. 23 | 24 | Public attributes: 25 | player -- player code 26 | short_code -- eg 'A' or 'ZZ' 27 | 28 | """ 29 | 30 | 31 | class Allplayall(tournaments.Tournament): 32 | """A Tournament with matchups for all pairs of competitors. 33 | 34 | The game ids are like AvB_2, where A and B are the competitor short_codes 35 | and 2 is the game number between those two competitors. 36 | 37 | This tournament type doesn't permit ghost matchups. 38 | 39 | """ 40 | 41 | def control_file_globals(self): 42 | result = Competition.control_file_globals(self) 43 | result.update({ 44 | 'Competitor' : Competitor_config, 45 | }) 46 | return result 47 | 48 | 49 | special_settings = [ 50 | Setting('competitors', 51 | interpret_sequence_of_quiet_configs( 52 | Competitor_config, allow_simple_values=True)), 53 | ] 54 | 55 | def competitor_spec_from_config(self, i, competitor_config): 56 | """Make a Competitor_spec from a Competitor_config. 57 | 58 | i -- ordinal number of the competitor. 59 | 60 | Raises ControlFileError if there is an error in the configuration. 61 | 62 | Returns a Competitor_spec with all attributes set. 63 | 64 | """ 65 | arguments = competitor_config.resolve_arguments() 66 | cspec = Competitor_spec() 67 | 68 | if 'player' not in arguments: 69 | raise ValueError("player not specified") 70 | cspec.player = arguments['player'] 71 | if cspec.player not in self.players: 72 | raise ControlFileError("unknown player") 73 | 74 | def let(n): 75 | return chr(ord('A') + n) 76 | if i < 26: 77 | cspec.short_code = let(i) 78 | elif i < 26*27: 79 | n, m = divmod(i, 26) 80 | cspec.short_code = let(n-1) + let(m) 81 | else: 82 | raise ValueError("too many competitors") 83 | return cspec 84 | 85 | @staticmethod 86 | def _get_matchup_id(c1, c2): 87 | return "%sv%s" % (c1.short_code, c2.short_code) 88 | 89 | def initialise_from_control_file(self, config): 90 | Competition.initialise_from_control_file(self, config) 91 | 92 | matchup_settings = [ 93 | setting for setting in competitions.game_settings 94 | if setting.name not in ('handicap', 'handicap_style') 95 | ] + [ 96 | Setting('rounds', allow_none(interpret_int), default=None), 97 | ] 98 | try: 99 | matchup_parameters = load_settings(matchup_settings, config) 100 | except ValueError, e: 101 | raise ControlFileError(str(e)) 102 | matchup_parameters['alternating'] = True 103 | matchup_parameters['number_of_games'] = matchup_parameters.pop('rounds') 104 | 105 | try: 106 | specials = load_settings(self.special_settings, config) 107 | except ValueError, e: 108 | raise ControlFileError(str(e)) 109 | 110 | if not specials['competitors']: 111 | raise ControlFileError("competitors: empty list") 112 | # list of Competitor_specs 113 | self.competitors = [] 114 | seen_competitors = set() 115 | for i, competitor_spec in enumerate(specials['competitors']): 116 | try: 117 | cspec = self.competitor_spec_from_config(i, competitor_spec) 118 | except StandardError, e: 119 | code = competitor_spec.get_key() 120 | if code is None: 121 | code = i 122 | raise ControlFileError("competitor %s: %s" % (code, e)) 123 | if cspec.player in seen_competitors: 124 | raise ControlFileError("duplicate competitor: %s" 125 | % cspec.player) 126 | seen_competitors.add(cspec.player) 127 | self.competitors.append(cspec) 128 | 129 | # map matchup_id -> Matchup 130 | self.matchups = {} 131 | # Matchups in order of definition 132 | self.matchup_list = [] 133 | for c1_i, c1 in enumerate(self.competitors): 134 | for c2 in self.competitors[c1_i+1:]: 135 | try: 136 | m = self.make_matchup( 137 | self._get_matchup_id(c1, c2), 138 | c1.player, c2.player, 139 | matchup_parameters) 140 | except StandardError, e: 141 | raise ControlFileError("%s v %s: %s" % 142 | (c1.player, c2.player, e)) 143 | self.matchups[m.id] = m 144 | self.matchup_list.append(m) 145 | 146 | 147 | # Can bump this to prevent people loading incompatible .status files. 148 | status_format_version = 1 149 | 150 | def get_status(self): 151 | result = tournaments.Tournament.get_status(self) 152 | result['competitors'] = [c.player for c in self.competitors] 153 | return result 154 | 155 | def set_status(self, status): 156 | seen_competitors = status['competitors'] 157 | # This should mean that _check_results can never fail, but might as well 158 | # still let it run. 159 | if len(self.competitors) < len(seen_competitors): 160 | raise CompetitionError( 161 | "competitor has been removed from control file") 162 | if ([c.player for c in self.competitors[:len(seen_competitors)]] != 163 | seen_competitors): 164 | raise CompetitionError( 165 | "competitors have changed in the control file") 166 | tournaments.Tournament.set_status(self, status) 167 | 168 | 169 | def get_player_checks(self): 170 | result = [] 171 | matchup = self.matchup_list[0] 172 | for competitor in self.competitors: 173 | check = game_jobs.Player_check() 174 | check.player = self.players[competitor.player] 175 | check.board_size = matchup.board_size 176 | check.komi = matchup.komi 177 | result.append(check) 178 | return result 179 | 180 | 181 | def count_games_played(self): 182 | """Return the total number of games completed.""" 183 | return sum(len(l) for l in self.results.values()) 184 | 185 | def count_games_expected(self): 186 | """Return the total number of games required. 187 | 188 | Returns None if no limit has been set. 189 | 190 | """ 191 | rounds = self.matchup_list[0].number_of_games 192 | if rounds is None: 193 | return None 194 | n = len(self.competitors) 195 | return rounds * n * (n-1) // 2 196 | 197 | def write_screen_report(self, out): 198 | expected = self.count_games_expected() 199 | if expected is not None: 200 | print >>out, "%d/%d games played" % ( 201 | self.count_games_played(), expected) 202 | else: 203 | print >>out, "%d games played" % self.count_games_played() 204 | print >>out 205 | 206 | t = ascii_tables.Table(row_count=len(self.competitors)) 207 | t.add_heading("") # player short_code 208 | i = t.add_column(align='left') 209 | t.set_column_values(i, (c.short_code for c in self.competitors)) 210 | 211 | t.add_heading("") # player code 212 | i = t.add_column(align='left') 213 | t.set_column_values(i, (c.player for c in self.competitors)) 214 | 215 | for c2_i, c2 in enumerate(self.competitors): 216 | t.add_heading(" " + c2.short_code) 217 | i = t.add_column(align='left') 218 | column_values = [] 219 | for c1_i, c1 in enumerate(self.competitors): 220 | if c1_i == c2_i: 221 | column_values.append("") 222 | continue 223 | if c1_i < c2_i: 224 | matchup_id = self._get_matchup_id(c1, c2) 225 | matchup = self.matchups[matchup_id] 226 | player_x = matchup.player_1 227 | player_y = matchup.player_2 228 | else: 229 | matchup_id = self._get_matchup_id(c2, c1) 230 | matchup = self.matchups[matchup_id] 231 | player_x = matchup.player_2 232 | player_y = matchup.player_1 233 | ms = tournament_results.Matchup_stats( 234 | self.results[matchup.id], 235 | player_x, player_y) 236 | column_values.append( 237 | "%s-%s" % (format_float(ms.wins_1), 238 | format_float(ms.wins_2))) 239 | t.set_column_values(i, column_values) 240 | print >>out, "\n".join(t.render()) 241 | 242 | def write_short_report(self, out): 243 | def p(s): 244 | print >>out, s 245 | p("allplayall: %s" % self.competition_code) 246 | if self.description: 247 | p(self.description) 248 | p('') 249 | self.write_screen_report(out) 250 | p('') 251 | self.write_matchup_reports(out) 252 | p('') 253 | self.write_player_descriptions(out) 254 | p('') 255 | 256 | write_full_report = write_short_report 257 | 258 | -------------------------------------------------------------------------------- /gomill/ascii_boards.py: -------------------------------------------------------------------------------- 1 | """ASCII board representation.""" 2 | 3 | from gomill.common import * 4 | from gomill import boards 5 | from gomill.common import column_letters 6 | 7 | def render_grid(point_formatter, size): 8 | """Render a board-shaped grid as a list of strings. 9 | 10 | point_formatter -- function (row, col) -> string of length 2. 11 | 12 | Returns a list of strings. 13 | 14 | """ 15 | column_header_string = " ".join(column_letters[i] for i in range(size)) 16 | result = [] 17 | if size > 9: 18 | rowstart = "%2d " 19 | padding = " " 20 | else: 21 | rowstart = "%d " 22 | padding = "" 23 | for row in range(size-1, -1, -1): 24 | result.append(rowstart % (row+1) + 25 | " ".join(point_formatter(row, col) 26 | for col in range(size))) 27 | result.append(padding + " " + column_header_string) 28 | return result 29 | 30 | _point_strings = { 31 | None : " .", 32 | 'b' : " #", 33 | 'w' : " o", 34 | } 35 | 36 | def render_board(board): 37 | """Render a gomill Board in ascii. 38 | 39 | Returns a string without final newline. 40 | 41 | """ 42 | def format_pt(row, col): 43 | return _point_strings.get(board.get(row, col), " ?") 44 | return "\n".join(render_grid(format_pt, board.side)) 45 | 46 | def interpret_diagram(diagram, size, board=None): 47 | """Set up the position from a diagram. 48 | 49 | diagram -- board representation as from render_board() 50 | size -- int 51 | 52 | Returns a Board. 53 | 54 | If the optional 'board' parameter is provided, it must be an empty board of 55 | the right size; the same object will be returned. 56 | 57 | """ 58 | if board is None: 59 | board = boards.Board(size) 60 | else: 61 | if board.side != size: 62 | raise ValueError("wrong board size, must be %d" % size) 63 | if not board.is_empty(): 64 | raise ValueError("board not empty") 65 | lines = diagram.split("\n") 66 | colours = {'#' : 'b', 'o' : 'w', '.' : None} 67 | if size > 9: 68 | extra_offset = 1 69 | else: 70 | extra_offset = 0 71 | try: 72 | for (row, col) in board.board_points: 73 | colour = colours[lines[size-row-1][3*(col+1)+extra_offset]] 74 | if colour is not None: 75 | board.play(row, col, colour) 76 | except Exception: 77 | raise ValueError 78 | return board 79 | 80 | 81 | -------------------------------------------------------------------------------- /gomill/ascii_tables.py: -------------------------------------------------------------------------------- 1 | """Render tabular output. 2 | 3 | This is designed for screen or text-file output, using a fixed-width font. 4 | 5 | """ 6 | 7 | from collections import defaultdict 8 | 9 | class Column_spec(object): 10 | """Details of a table column. 11 | 12 | Public attributes: 13 | align -- 'left' or 'right' 14 | right_padding -- int 15 | 16 | """ 17 | def __init__(self, align='left', right_padding=1): 18 | self.align = align 19 | self.right_padding = right_padding 20 | 21 | def render(self, s, width): 22 | if self.align == 'left': 23 | s = s.ljust(width) 24 | elif self.align == 'right': 25 | s = s.rjust(width) 26 | return s + " " * self.right_padding 27 | 28 | class Table(object): 29 | """Render tabular output. 30 | 31 | Normal use: 32 | 33 | tbl = Table(row_count=3) 34 | tbl.add_heading('foo') 35 | i = tbl.add_column(align='left', right_padding=3) 36 | tbl.set_column_values(i, ['a', 'b']) 37 | [...] 38 | print '\n'.join(tbl.render()) 39 | 40 | """ 41 | def __init__(self, row_count=None): 42 | self.col_count = 0 43 | self.row_count = row_count 44 | self.headings = [] 45 | self.columns = [] 46 | self.cells = defaultdict(str) 47 | 48 | def set_row_count(self, row_count): 49 | """Change the table's row count.""" 50 | self.row_count = row_count 51 | 52 | def add_heading(self, heading, span=1): 53 | """Specify a column or column group heading. 54 | 55 | To leave a column with no heading, pass the empty string. 56 | 57 | To allow a heading to cover multiple columns, pass the 'span' parameter 58 | and don't add headings for the rest of the covered columns. 59 | 60 | """ 61 | self.headings.append((heading, span)) 62 | 63 | def add_column(self, **kwargs): 64 | """Add a column to the table. 65 | 66 | align -- 'left' (default) or 'right' 67 | right_padding -- int (default 1) 68 | 69 | Returns the column id 70 | 71 | Right padding is the number of spaces to leave between this column and 72 | the next. 73 | 74 | (The last column should have right padding 1, so that the heading can 75 | use the full width if necessary.) 76 | 77 | """ 78 | column = Column_spec(**kwargs) 79 | self.columns.append(column) 80 | column_id = self.col_count 81 | self.col_count += 1 82 | return column_id 83 | 84 | def get_column(self, column_id): 85 | """Retrieve a column object given its id. 86 | 87 | You can use this to change the column's attributes after adding it. 88 | 89 | """ 90 | return self.columns[column_id] 91 | 92 | def set_column_values(self, column_id, values): 93 | """Specify the values for a column. 94 | 95 | column_id -- as returned by add_column() 96 | values -- iterable 97 | 98 | str() is called on the values. 99 | 100 | If values are not supplied for all rows, the remaining rows are left 101 | blank. If too many values are supplied, the excess values are ignored. 102 | 103 | """ 104 | for row, value in enumerate(values): 105 | self.cells[row, column_id] = str(value) 106 | 107 | def render(self): 108 | """Render the table. 109 | 110 | Returns a list of strings. 111 | 112 | Each line has no trailing whitespace. 113 | 114 | Lines which would be wholly blank are omitted. 115 | 116 | """ 117 | def column_values(col): 118 | return [self.cells[row, col] for row in xrange(self.row_count)] 119 | 120 | result = [] 121 | 122 | cells = self.cells 123 | widths = [max(map(len, column_values(i))) 124 | for i in xrange(self.col_count)] 125 | col = 0 126 | heading_line = [] 127 | for heading, span in self.headings: 128 | # width available for the heading 129 | width = (sum(widths[col:col+span]) + 130 | sum(self.columns[i].right_padding 131 | for i in range(col, col+span)) - 1) 132 | shortfall = len(heading) - width 133 | if shortfall > 0: 134 | width += shortfall 135 | # Make the leftmost column in the span wider to fit the heading 136 | widths[col] += shortfall 137 | heading_line.append(heading.ljust(width)) 138 | col += span 139 | result.append(" ".join(heading_line).rstrip()) 140 | 141 | for row in xrange(self.row_count): 142 | l = [] 143 | for col, (column, width) in enumerate(zip(self.columns, widths)): 144 | l.append(column.render(cells[row, col], width)) 145 | line = "".join(l).rstrip() 146 | if line: 147 | result.append(line) 148 | return result 149 | 150 | -------------------------------------------------------------------------------- /gomill/boards.py: -------------------------------------------------------------------------------- 1 | """Go board representation.""" 2 | 3 | from gomill.common import * 4 | 5 | 6 | class _Group(object): 7 | """Represent a solidly-connected group. 8 | 9 | Public attributes: 10 | colour 11 | points 12 | is_surrounded 13 | 14 | Points are coordinate pairs (row, col). 15 | 16 | """ 17 | 18 | class _Region(object): 19 | """Represent an empty region. 20 | 21 | Public attributes: 22 | points 23 | neighbouring_colours 24 | 25 | Points are coordinate pairs (row, col). 26 | 27 | """ 28 | def __init__(self): 29 | self.points = set() 30 | self.neighbouring_colours = set() 31 | 32 | class Board(object): 33 | """A legal Go position. 34 | 35 | Supports playing stones with captures, and area scoring. 36 | 37 | Public attributes: 38 | side -- board size (eg 9) 39 | board_points -- list of coordinates of all points on the board 40 | 41 | Behaviour is unspecified if methods are passed out-of-range coordinates. 42 | 43 | """ 44 | def __init__(self, side): 45 | self.side = side 46 | self.board_points = [(_row, _col) for _row in range(side) 47 | for _col in range(side)] 48 | self.board = [] 49 | for row in range(side): 50 | self.board.append([None] * side) 51 | self._is_empty = True 52 | 53 | def copy(self): 54 | """Return an independent copy of this Board.""" 55 | b = Board(self.side) 56 | b.board = [self.board[i][:] for i in xrange(self.side)] 57 | b._is_empty = self._is_empty 58 | return b 59 | 60 | def _make_group(self, row, col, colour): 61 | points = set() 62 | is_surrounded = True 63 | to_handle = set() 64 | to_handle.add((row, col)) 65 | while to_handle: 66 | point = to_handle.pop() 67 | points.add(point) 68 | r, c = point 69 | for neighbour in [(r-1, c), (r+1, c), (r, c-1), (r, c+1)]: 70 | (r1, c1) = neighbour 71 | if not ((0 <= r1 < self.side) and (0 <= c1 < self.side)): 72 | continue 73 | neigh_colour = self.board[r1][c1] 74 | if neigh_colour is None: 75 | is_surrounded = False 76 | elif neigh_colour == colour: 77 | if neighbour not in points: 78 | to_handle.add(neighbour) 79 | group = _Group() 80 | group.colour = colour 81 | group.points = points 82 | group.is_surrounded = is_surrounded 83 | return group 84 | 85 | def _make_empty_region(self, row, col): 86 | points = set() 87 | neighbouring_colours = set() 88 | to_handle = set() 89 | to_handle.add((row, col)) 90 | while to_handle: 91 | point = to_handle.pop() 92 | points.add(point) 93 | r, c = point 94 | for neighbour in [(r-1, c), (r+1, c), (r, c-1), (r, c+1)]: 95 | (r1, c1) = neighbour 96 | if not ((0 <= r1 < self.side) and (0 <= c1 < self.side)): 97 | continue 98 | neigh_colour = self.board[r1][c1] 99 | if neigh_colour is None: 100 | if neighbour not in points: 101 | to_handle.add(neighbour) 102 | else: 103 | neighbouring_colours.add(neigh_colour) 104 | region = _Region() 105 | region.points = points 106 | region.neighbouring_colours = neighbouring_colours 107 | return region 108 | 109 | def _find_surrounded_groups(self): 110 | """Find solidly-connected groups with 0 liberties. 111 | 112 | Returns a list of _Groups. 113 | 114 | """ 115 | surrounded = [] 116 | handled = set() 117 | for (row, col) in self.board_points: 118 | colour = self.board[row][col] 119 | if colour is None: 120 | continue 121 | point = (row, col) 122 | if point in handled: 123 | continue 124 | group = self._make_group(row, col, colour) 125 | if group.is_surrounded: 126 | surrounded.append(group) 127 | handled.update(group.points) 128 | return surrounded 129 | 130 | def is_empty(self): 131 | """Say whether the board is empty.""" 132 | return self._is_empty 133 | 134 | def get(self, row, col): 135 | """Return the state of the specified point. 136 | 137 | Returns a colour, or None for an empty point. 138 | 139 | """ 140 | return self.board[row][col] 141 | 142 | def play(self, row, col, colour): 143 | """Play a move on the board. 144 | 145 | Raises ValueError if the specified point isn't empty. 146 | 147 | Performs any necessary captures. Allows self-captures. Doesn't enforce 148 | any ko rule. 149 | 150 | Returns the point forbidden by simple ko, or None 151 | 152 | """ 153 | if self.board[row][col] is not None: 154 | raise ValueError 155 | self.board[row][col] = colour 156 | self._is_empty = False 157 | surrounded = self._find_surrounded_groups() 158 | simple_ko_point = None 159 | if surrounded: 160 | if len(surrounded) == 1: 161 | to_capture = surrounded 162 | if len(to_capture[0].points) == self.side*self.side: 163 | self._is_empty = True 164 | else: 165 | to_capture = [group for group in surrounded 166 | if group.colour == opponent_of(colour)] 167 | if len(to_capture) == 1 and len(to_capture[0].points) == 1: 168 | self_capture = [group for group in surrounded 169 | if group.colour == colour] 170 | if len(self_capture[0].points) == 1: 171 | simple_ko_point = iter(to_capture[0].points).next() 172 | for group in to_capture: 173 | for r, c in group.points: 174 | self.board[r][c] = None 175 | return simple_ko_point 176 | 177 | def apply_setup(self, black_points, white_points, empty_points): 178 | """Add setup stones or removals to the position. 179 | 180 | This is intended to support SGF AB/AW/AE commands. 181 | 182 | Each parameter is an iterable of coordinate pairs (row, col). 183 | 184 | Applies all the setup specifications, then removes any groups with no 185 | liberties (so the resulting position is always legal). 186 | 187 | If the same point is specified in more than one list, the order in which 188 | they're applied is undefined. 189 | 190 | Returns a boolean saying whether the position was legal as specified. 191 | 192 | """ 193 | for (row, col) in black_points: 194 | self.board[row][col] = 'b' 195 | for (row, col) in white_points: 196 | self.board[row][col] = 'w' 197 | for (row, col) in empty_points: 198 | self.board[row][col] = None 199 | captured = self._find_surrounded_groups() 200 | for group in captured: 201 | for row, col in group.points: 202 | self.board[row][col] = None 203 | self._is_empty = True 204 | for (row, col) in self.board_points: 205 | if self.board[row][col] is not None: 206 | self._is_empty = False 207 | break 208 | return not(captured) 209 | 210 | def list_occupied_points(self): 211 | """List all nonempty points. 212 | 213 | Returns a list of pairs (colour, (row, col)) 214 | 215 | """ 216 | result = [] 217 | for (row, col) in self.board_points: 218 | colour = self.board[row][col] 219 | if colour is not None: 220 | result.append((colour, (row, col))) 221 | return result 222 | 223 | def area_score(self): 224 | """Calculate the area score of a position. 225 | 226 | Assumes all stones are alive. 227 | 228 | Returns black score minus white score. 229 | 230 | Doesn't take komi into account. 231 | 232 | """ 233 | scores = {'b' : 0, 'w' : 0} 234 | handled = set() 235 | for (row, col) in self.board_points: 236 | colour = self.board[row][col] 237 | if colour is not None: 238 | scores[colour] += 1 239 | continue 240 | point = (row, col) 241 | if point in handled: 242 | continue 243 | region = self._make_empty_region(row, col) 244 | region_size = len(region.points) 245 | for colour in ('b', 'w'): 246 | if colour in region.neighbouring_colours: 247 | scores[colour] += region_size 248 | handled.update(region.points) 249 | return scores['b'] - scores['w'] 250 | 251 | -------------------------------------------------------------------------------- /gomill/common.py: -------------------------------------------------------------------------------- 1 | """Domain-dependent utility functions for gomill. 2 | 3 | This module is designed to be used with 'from common import *'. 4 | 5 | This is for Go-specific utilities; see utils for generic utility functions. 6 | 7 | """ 8 | 9 | __all__ = ["opponent_of", "colour_name", "format_vertex", "format_vertex_list", 10 | "move_from_vertex"] 11 | 12 | _opponents = {"b":"w", "w":"b"} 13 | def opponent_of(colour): 14 | """Return the opponent colour. 15 | 16 | colour -- 'b' or 'w' 17 | 18 | Returns 'b' or 'w'. 19 | 20 | """ 21 | try: 22 | return _opponents[colour] 23 | except KeyError: 24 | raise ValueError 25 | 26 | def colour_name(colour): 27 | """Return the (lower-case) full name of a colour. 28 | 29 | colour -- 'b' or 'w' 30 | 31 | """ 32 | try: 33 | return {'b': 'black', 'w': 'white'}[colour] 34 | except KeyError: 35 | raise ValueError 36 | 37 | 38 | column_letters = "ABCDEFGHJKLMNOPQRSTUVWXYZ" 39 | 40 | def format_vertex(move): 41 | """Return coordinates as a string like 'A1', or 'pass'. 42 | 43 | move -- pair (row, col), or None for a pass 44 | 45 | The result is suitable for use directly in GTP responses. 46 | 47 | """ 48 | if move is None: 49 | return "pass" 50 | row, col = move 51 | if not 0 <= row < 25 or not 0 <= col < 25: 52 | raise ValueError 53 | return column_letters[col] + str(row+1) 54 | 55 | def format_vertex_list(moves): 56 | """Return a list of coordinates as a string like 'A1,B2'.""" 57 | return ",".join(map(format_vertex, moves)) 58 | 59 | def move_from_vertex(vertex, board_size): 60 | """Interpret a string representing a vertex, as specified by GTP. 61 | 62 | Returns a pair of coordinates (row, col) in range(0, board_size) 63 | 64 | Raises ValueError with an appropriate message if 'vertex' isn't a valid GTP 65 | vertex specification for a board of size 'board_size'. 66 | 67 | """ 68 | if not 0 < board_size <= 25: 69 | raise ValueError("board_size out of range") 70 | try: 71 | s = vertex.lower() 72 | except Exception: 73 | raise ValueError("invalid vertex") 74 | if s == "pass": 75 | return None 76 | try: 77 | col_c = s[0] 78 | if (not "a" <= col_c <= "z") or col_c == "i": 79 | raise ValueError 80 | if col_c > "i": 81 | col = ord(col_c) - ord("b") 82 | else: 83 | col = ord(col_c) - ord("a") 84 | row = int(s[1:]) - 1 85 | if row < 0: 86 | raise ValueError 87 | except (IndexError, ValueError): 88 | raise ValueError("invalid vertex: '%s'" % s) 89 | if not (col < board_size and row < board_size): 90 | raise ValueError("vertex is off board: '%s'" % s) 91 | return row, col 92 | 93 | -------------------------------------------------------------------------------- /gomill/compact_tracebacks.py: -------------------------------------------------------------------------------- 1 | """Compact formatting of tracebacks.""" 2 | 3 | import sys 4 | import traceback 5 | 6 | def log_traceback_from_info(exception_type, value, tb, dst=sys.stderr, skip=0): 7 | """Log a given exception nicely to 'dst', showing a traceback. 8 | 9 | dst -- writeable file-like object 10 | skip -- number of traceback entries to omit from the top of the list 11 | 12 | """ 13 | for line in traceback.format_exception_only(exception_type, value): 14 | dst.write(line) 15 | if (not isinstance(exception_type, str) and 16 | issubclass(exception_type, SyntaxError)): 17 | return 18 | print >>dst, 'traceback (most recent call last):' 19 | text = None 20 | for filename, lineno, fnname, text in traceback.extract_tb(tb)[skip:]: 21 | if fnname == "?": 22 | fn_s = "" 23 | else: 24 | fn_s = "(%s)" % fnname 25 | print >>dst, " %s:%s %s" % (filename, lineno, fn_s) 26 | if text is not None: 27 | print >>dst, "failing line:" 28 | print >>dst, text 29 | 30 | def format_traceback_from_info(exception_type, value, tb, skip=0): 31 | """Return a description of a given exception as a string. 32 | 33 | skip -- number of traceback entries to omit from the top of the list 34 | 35 | """ 36 | from cStringIO import StringIO 37 | log = StringIO() 38 | log_traceback_from_info(exception_type, value, tb, log, skip) 39 | return log.getvalue() 40 | 41 | def log_traceback(dst=sys.stderr, skip=0): 42 | """Log the current exception nicely to 'dst'. 43 | 44 | dst -- writeable file-like object 45 | skip -- number of traceback entries to omit from the top of the list 46 | 47 | """ 48 | exception_type, value, tb = sys.exc_info() 49 | log_traceback_from_info(exception_type, value, tb, dst, skip) 50 | 51 | def format_traceback(skip=0): 52 | """Return a description of the current exception as a string. 53 | 54 | skip -- number of traceback entries to omit from the top of the list 55 | 56 | """ 57 | exception_type, value, tb = sys.exc_info() 58 | return format_traceback_from_info(exception_type, value, tb, skip) 59 | 60 | 61 | def log_error_and_line_from_info(exception_type, value, tb, dst=sys.stderr): 62 | """Log a given exception briefly to 'dst', showing line number.""" 63 | if (not isinstance(exception_type, str) and 64 | issubclass(exception_type, SyntaxError)): 65 | for line in traceback.format_exception_only(exception_type, value): 66 | dst.write(line) 67 | else: 68 | try: 69 | filename, lineno, fnname, text = traceback.extract_tb(tb)[-1] 70 | except IndexError: 71 | pass 72 | else: 73 | print >>dst, "at line %s:" % lineno 74 | for line in traceback.format_exception_only(exception_type, value): 75 | dst.write(line) 76 | 77 | def format_error_and_line_from_info(exception_type, value, tb): 78 | """Return a brief description of a given exception as a string.""" 79 | from cStringIO import StringIO 80 | log = StringIO() 81 | log_error_and_line_from_info(exception_type, value, tb, log) 82 | return log.getvalue() 83 | 84 | def log_error_and_line(dst=sys.stderr): 85 | """Log the current exception briefly to 'dst'. 86 | 87 | dst -- writeable file-like object 88 | 89 | """ 90 | exception_type, value, tb = sys.exc_info() 91 | log_error_and_line_from_info(exception_type, value, tb, dst) 92 | 93 | def format_error_and_line(): 94 | """Return a brief description of the current exception as a string.""" 95 | exception_type, value, tb = sys.exc_info() 96 | return format_error_and_line_from_info(exception_type, value, tb) 97 | 98 | -------------------------------------------------------------------------------- /gomill/competition_schedulers.py: -------------------------------------------------------------------------------- 1 | """Schedule games in competitions. 2 | 3 | These schedulers are used to keep track of the ids of games which have been 4 | started, and which have reported their results. 5 | 6 | They provide a mechanism to reissue ids of games which were in progress when an 7 | unclean shutdown occurred. 8 | 9 | All scheduler classes are suitable for pickling. 10 | 11 | """ 12 | 13 | class Simple_scheduler(object): 14 | """Schedule a single sequence of games. 15 | 16 | The issued tokens are integers counting up from zero. 17 | 18 | Public attributes (treat as read-only): 19 | issued -- int 20 | fixed -- int 21 | 22 | """ 23 | def __init__(self): 24 | self.next_new = 0 25 | self.outstanding = set() 26 | self.to_reissue = set() 27 | self.issued = 0 28 | self.fixed = 0 29 | #self._check_consistent() 30 | 31 | def _check_consistent(self): 32 | assert self.issued == \ 33 | self.next_new - len(self.to_reissue) 34 | assert self.fixed == \ 35 | self.next_new - len(self.outstanding) - len(self.to_reissue) 36 | 37 | def __getstate__(self): 38 | return (self.next_new, self.outstanding, self.to_reissue) 39 | 40 | def __setstate__(self, state): 41 | (self.next_new, self.outstanding, self.to_reissue) = state 42 | self.issued = self.next_new - len(self.to_reissue) 43 | self.fixed = self.issued - len(self.outstanding) 44 | #self._check_consistent() 45 | 46 | def issue(self): 47 | """Choose the next game to start. 48 | 49 | Returns an integer 'token'. 50 | 51 | """ 52 | if self.to_reissue: 53 | result = min(self.to_reissue) 54 | self.to_reissue.discard(result) 55 | else: 56 | result = self.next_new 57 | self.next_new += 1 58 | self.outstanding.add(result) 59 | self.issued += 1 60 | #self._check_consistent() 61 | return result 62 | 63 | def fix(self, token): 64 | """Note that a game's result has been reliably stored.""" 65 | self.outstanding.remove(token) 66 | self.fixed += 1 67 | #self._check_consistent() 68 | 69 | def rollback(self): 70 | """Make issued-but-not-fixed tokens available again.""" 71 | self.issued -= len(self.outstanding) 72 | self.to_reissue.update(self.outstanding) 73 | self.outstanding = set() 74 | #self._check_consistent() 75 | 76 | 77 | class Group_scheduler(object): 78 | """Schedule multiple lists of games in parallel. 79 | 80 | This schedules for a number of _groups_, each of which may have a limit on 81 | the number of games to play. It schedules from the group (of those which 82 | haven't reached their limit) with the fewest issued games, with smallest 83 | group code breaking ties. 84 | 85 | group codes might be ints or short strings 86 | (any sortable, pickleable and hashable object should do). 87 | 88 | The issued tokens are pairs (group code, game number), with game numbers 89 | counting up from 0 independently for each group code. 90 | 91 | """ 92 | def __init__(self): 93 | self.allocators = {} 94 | self.limits = {} 95 | 96 | def __getstate__(self): 97 | return (self.allocators, self.limits) 98 | 99 | def __setstate__(self, state): 100 | (self.allocators, self.limits) = state 101 | 102 | def set_groups(self, group_specs): 103 | """Set the groups to be scheduled. 104 | 105 | group_specs -- iterable of pairs (group code, limit) 106 | limit -- int or None 107 | 108 | You can call this again after the first time. The limits will be set to 109 | the new values. Any existing groups not in the list are forgotten. 110 | 111 | """ 112 | new_allocators = {} 113 | new_limits = {} 114 | for group_code, limit in group_specs: 115 | if group_code in self.allocators: 116 | new_allocators[group_code] = self.allocators[group_code] 117 | else: 118 | new_allocators[group_code] = Simple_scheduler() 119 | new_limits[group_code] = limit 120 | self.allocators = new_allocators 121 | self.limits = new_limits 122 | 123 | def issue(self): 124 | """Choose the next game to start. 125 | 126 | Returns a pair (group code, game number) 127 | 128 | Returns (None, None) if all groups have reached their limit. 129 | 130 | """ 131 | groups = [ 132 | (group_code, allocator.issued, self.limits[group_code]) 133 | for (group_code, allocator) in self.allocators.iteritems() 134 | ] 135 | available = [ 136 | (issue_count, group_code) 137 | for (group_code, issue_count, limit) in groups 138 | if limit is None or issue_count < limit 139 | ] 140 | if not available: 141 | return None, None 142 | _, group_code = min(available) 143 | return group_code, self.allocators[group_code].issue() 144 | 145 | def fix(self, group_code, game_number): 146 | """Note that a game's result has been reliably stored.""" 147 | self.allocators[group_code].fix(game_number) 148 | 149 | def rollback(self): 150 | """Make issued-but-not-fixed tokens available again.""" 151 | for allocator in self.allocators.itervalues(): 152 | allocator.rollback() 153 | 154 | def nothing_issued_yet(self): 155 | """Say whether nothing has been issued yet.""" 156 | return all(allocator.issued == 0 157 | for allocator in self.allocators.itervalues()) 158 | 159 | def all_fixed(self): 160 | """Check whether all groups have reached their limits. 161 | 162 | This returns true if all groups have limits, and each group has as many 163 | _fixed_ tokens as its limit. 164 | 165 | """ 166 | return all(allocator.fixed >= self.limits[g] 167 | for (g, allocator) in self.allocators.iteritems()) 168 | -------------------------------------------------------------------------------- /gomill/gtp_proxy.py: -------------------------------------------------------------------------------- 1 | """Support for implementing proxy GTP engines. 2 | 3 | That is, engines which implement some or all of their commands by sending them 4 | on to another engine (the _back end_). 5 | 6 | """ 7 | 8 | from gomill import gtp_controller 9 | from gomill import gtp_engine 10 | from gomill.gtp_controller import ( 11 | BadGtpResponse, GtpChannelError, GtpChannelClosed) 12 | from gomill.gtp_engine import GtpError, GtpQuit, GtpFatalError 13 | 14 | 15 | class BackEndError(StandardError): 16 | """Difficulty communicating with the back end. 17 | 18 | Public attributes: 19 | cause -- Exception instance of an underlying exception (or None) 20 | 21 | """ 22 | def __init__(self, args, cause=None): 23 | StandardError.__init__(self, args) 24 | self.cause = cause 25 | 26 | class Gtp_proxy(object): 27 | """Manager for a GTP proxy engine. 28 | 29 | Public attributes: 30 | engine -- Gtp_engine_protocol 31 | controller -- Gtp_controller 32 | 33 | The 'engine' attribute is the proxy engine. Initially it supports all the 34 | commands reported by the back end's 'list_commands'. You can add commands to 35 | it in the usual way; new commands will override any commands with the same 36 | names in the back end. 37 | 38 | The proxy engine also supports the following commands: 39 | gomill-passthrough [args] ... 40 | Run a command on the back end (use this to get at overridden commands, 41 | or commands which don't appear in list_commands) 42 | 43 | If the proxy subprocess exits, this will be reported (as a transport error) 44 | when the next command is sent. If you're using handle_command, it will 45 | apropriately turn this into a fatal error. 46 | 47 | Sample use: 48 | proxy = gtp_proxy.Gtp_proxy() 49 | proxy.set_back_end_subprocess([, , ...]) 50 | proxy.engine.add_command(...) 51 | try: 52 | proxy.run() 53 | except KeyboardInterrupt: 54 | sys.exit(1) 55 | 56 | The default 'quit' handler passes 'quit' on the back end and raises 57 | GtpQuit. 58 | 59 | If you add a handler which you expect to cause the back end to exit (eg, by 60 | sending it 'quit'), you should have call expect_back_end_exit() (and usually 61 | also raise GtpQuit). 62 | 63 | If you want to hide one of the underlying commands, or don't want one of the 64 | additional commands, just use engine.remove_command(). 65 | 66 | """ 67 | def __init__(self): 68 | self.controller = None 69 | self.engine = None 70 | 71 | def _back_end_is_set(self): 72 | return self.controller is not None 73 | 74 | def _make_back_end_handlers(self): 75 | result = {} 76 | for command in self.back_end_commands: 77 | def handler(args, _command=command): 78 | return self.handle_command(_command, args) 79 | result[command] = handler 80 | return result 81 | 82 | def _make_engine(self): 83 | self.engine = gtp_engine.Gtp_engine_protocol() 84 | self.engine.add_commands(self._make_back_end_handlers()) 85 | self.engine.add_protocol_commands() 86 | self.engine.add_commands({ 87 | 'quit' : self.handle_quit, 88 | 'gomill-passthrough' : self.handle_passthrough, 89 | }) 90 | 91 | def set_back_end_controller(self, controller): 92 | """Specify the back end using a Gtp_controller. 93 | 94 | controller -- Gtp_controller 95 | 96 | Raises BackEndError if it can't communicate with the back end. 97 | 98 | By convention, the controller's channel name should be "back end". 99 | 100 | """ 101 | if self._back_end_is_set(): 102 | raise StandardError("back end already set") 103 | self.controller = controller 104 | try: 105 | self.back_end_commands = controller.list_commands() 106 | except (GtpChannelError, BadGtpResponse), e: 107 | raise BackEndError(str(e), cause=e) 108 | self._make_engine() 109 | 110 | def set_back_end_subprocess(self, command, **kwargs): 111 | """Specify the back end as a subprocess. 112 | 113 | command -- list of strings (as for subprocess.Popen) 114 | 115 | Additional keyword arguments are passed to the Subprocess_gtp_channel 116 | constructor. 117 | 118 | Raises BackEndError if it can't communicate with the back end. 119 | 120 | """ 121 | try: 122 | channel = gtp_controller.Subprocess_gtp_channel(command, **kwargs) 123 | except GtpChannelError, e: 124 | # Probably means exec failure 125 | raise BackEndError("can't launch back end command\n%s" % e, cause=e) 126 | controller = gtp_controller.Gtp_controller(channel, "back end") 127 | self.set_back_end_controller(controller) 128 | 129 | def close(self): 130 | """Close the channel to the back end. 131 | 132 | It's safe to call this at any time after set_back_end_... (including 133 | after receiving a BackEndError). 134 | 135 | It's not strictly necessary to call this if you're going to exit from 136 | the parent process anyway, as that will naturally close the command 137 | channel. But some engines don't behave well if you don't send 'quit', 138 | so it's safest to close the proxy explicitly. 139 | 140 | This will send 'quit' if low-level errors have not previously been seen 141 | on the channel, unless expect_back_end_exit() has been called. 142 | 143 | Errors (including failure responses to 'quit') are reported by raising 144 | BackEndError. 145 | 146 | """ 147 | if self.controller is None: 148 | return 149 | self.controller.safe_close() 150 | late_errors = self.controller.retrieve_error_messages() 151 | if late_errors: 152 | raise BackEndError("\n".join(late_errors)) 153 | 154 | def run(self): 155 | """Run a GTP session on stdin and stdout, using the proxy engine. 156 | 157 | This is provided for convenience; it's also ok to use the proxy engine 158 | directly. 159 | 160 | Returns either when EOF is seen on stdin, or when a handler (such as the 161 | default 'quit' handler) raises GtpQuit. 162 | 163 | Closes the channel to the back end before it returns. When it is 164 | meaningful (eg, for subprocess channels) this waits for the back end to 165 | exit. 166 | 167 | Propagates ControllerDisconnected if a pipe connected to stdout goes 168 | away. 169 | 170 | """ 171 | gtp_engine.run_interactive_gtp_session(self.engine) 172 | self.close() 173 | 174 | def pass_command(self, command, args): 175 | """Pass a command to the back end, and return its response. 176 | 177 | The response (or failure response) is unchanged, except for whitespace 178 | normalisation. 179 | 180 | This passes the command to the back end even if it isn't included in the 181 | back end's list_commands output; the back end will presumably return an 182 | 'unknown command' error. 183 | 184 | Failure responses from the back end are reported by raising 185 | BadGtpResponse. 186 | 187 | Low-level (ie, transport or protocol) errors are reported by raising 188 | BackEndError. 189 | 190 | """ 191 | if not self._back_end_is_set(): 192 | raise StandardError("back end isn't set") 193 | try: 194 | return self.controller.do_command(command, *args) 195 | except GtpChannelError, e: 196 | raise BackEndError(str(e), cause=e) 197 | 198 | def handle_command(self, command, args): 199 | """Run a command on the back end, from inside a GTP handler. 200 | 201 | This is a variant of pass_command, intended to be used directly in a 202 | command handler. 203 | 204 | Failure responses from the back end are reported by raising GtpError. 205 | 206 | Low-level (ie, transport or protocol) errors are reported by raising 207 | GtpFatalError. 208 | 209 | """ 210 | try: 211 | return self.pass_command(command, args) 212 | except BadGtpResponse, e: 213 | raise GtpError(e.gtp_error_message) 214 | except BackEndError, e: 215 | raise GtpFatalError(str(e)) 216 | 217 | def back_end_has_command(self, command): 218 | """Say whether the back end supports the specified command. 219 | 220 | This uses known_command, not list_commands. It caches the results. 221 | 222 | Low-level (ie, transport or protocol) errors are reported by raising 223 | BackEndError. 224 | 225 | """ 226 | if not self._back_end_is_set(): 227 | raise StandardError("back end isn't set") 228 | try: 229 | return self.controller.known_command(command) 230 | except GtpChannelError, e: 231 | raise BackEndError(str(e), cause=e) 232 | 233 | def expect_back_end_exit(self): 234 | """Mark that the back end is expected to have exited. 235 | 236 | Call this from any handler which you expect to cause the back end to 237 | exit (eg, by sending it 'quit'). 238 | 239 | """ 240 | self.controller.channel_is_bad = True 241 | 242 | def handle_quit(self, args): 243 | # Ignores GtpChannelClosed 244 | try: 245 | result = self.pass_command("quit", []) 246 | except BackEndError, e: 247 | if isinstance(e.cause, GtpChannelClosed): 248 | result = "" 249 | else: 250 | raise GtpFatalError(str(e)) 251 | except BadGtpResponse, e: 252 | self.expect_back_end_exit() 253 | raise GtpFatalError(e.gtp_error_message) 254 | self.expect_back_end_exit() 255 | raise GtpQuit(result) 256 | 257 | def handle_passthrough(self, args): 258 | try: 259 | command = args[0] 260 | except IndexError: 261 | gtp_engine.report_bad_arguments() 262 | return self.handle_command(command, args[1:]) 263 | -------------------------------------------------------------------------------- /gomill/handicap_layout.py: -------------------------------------------------------------------------------- 1 | """Standard layout of fixed handicap stones. 2 | 3 | This follows the rules from the GTP spec. 4 | 5 | """ 6 | 7 | def max_free_handicap_for_board_size(board_size): 8 | """Return the maximum number of stones for place_free_handicap command.""" 9 | return board_size * board_size - 1 10 | 11 | def max_fixed_handicap_for_board_size(board_size): 12 | """Return the maximum number of stones for fixed_handicap command.""" 13 | if board_size <= 7: 14 | return 0 15 | if board_size > 25: 16 | raise ValueError 17 | if board_size % 2 == 0 or board_size == 7: 18 | return 4 19 | else: 20 | return 9 21 | 22 | handicap_pattern = [ 23 | ['00', '22'], 24 | ['00', '22', '20'], 25 | ['00', '22', '20', '02'], 26 | ['00', '22', '20', '02', '11'], 27 | ['00', '22', '20', '02', '10', '12'], 28 | ['00', '22', '20', '02', '10', '12', '11'], 29 | ['00', '22', '20', '02', '10', '12', '01', '21'], 30 | ['00', '22', '20', '02', '10', '12', '01', '21', '11'], 31 | ] 32 | 33 | def handicap_points(number_of_stones, board_size): 34 | """Return the handicap points for a given number of stones and board size. 35 | 36 | Returns a list of pairs (row, col), length 'number_of_stones'. 37 | 38 | Raises ValueError if there isn't a placement pattern for the specified 39 | number of handicap stones and board size. 40 | 41 | """ 42 | if number_of_stones > max_fixed_handicap_for_board_size(board_size): 43 | raise ValueError 44 | if number_of_stones < 2: 45 | raise ValueError 46 | if board_size < 13: 47 | altitude = 2 48 | else: 49 | altitude = 3 50 | pos = {'0' : altitude, 51 | '1' : (board_size - 1) / 2, 52 | '2' : board_size - altitude - 1} 53 | return [(pos[s[0]], pos[s[1]]) 54 | for s in handicap_pattern[number_of_stones-2]] 55 | -------------------------------------------------------------------------------- /gomill/job_manager.py: -------------------------------------------------------------------------------- 1 | """Job system supporting multiprocessing.""" 2 | 3 | import sys 4 | 5 | from gomill import compact_tracebacks 6 | 7 | multiprocessing = None 8 | 9 | NoJobAvailable = object() 10 | 11 | class JobFailed(StandardError): 12 | """Error reported by a job.""" 13 | 14 | class JobSourceError(StandardError): 15 | """Error from a job source object.""" 16 | 17 | class JobError(object): 18 | """Error from a job.""" 19 | def __init__(self, job, msg): 20 | self.job = job 21 | self.msg = msg 22 | 23 | def _initialise_multiprocessing(): 24 | global multiprocessing 25 | if multiprocessing is not None: 26 | return 27 | try: 28 | import multiprocessing 29 | except ImportError: 30 | multiprocessing = None 31 | 32 | class Worker_finish_signal(object): 33 | pass 34 | worker_finish_signal = Worker_finish_signal() 35 | 36 | def worker_run_jobs(job_queue, response_queue, worker_id): 37 | try: 38 | #pid = os.getpid() 39 | #sys.stderr.write("worker %d starting\n" % pid) 40 | while True: 41 | job = job_queue.get() 42 | #sys.stderr.write("worker %d: %s\n" % (pid, repr(job))) 43 | if isinstance(job, Worker_finish_signal): 44 | break 45 | try: 46 | response = job.run(worker_id) 47 | except JobFailed, e: 48 | response = JobError(job, str(e)) 49 | sys.exc_clear() 50 | del e 51 | except Exception: 52 | response = JobError( 53 | job, compact_tracebacks.format_traceback(skip=1)) 54 | sys.exc_clear() 55 | response_queue.put(response) 56 | #sys.stderr.write("worker %d finishing\n" % pid) 57 | response_queue.cancel_join_thread() 58 | # Unfortunately, there will be places in the child that this doesn't cover. 59 | # But it will avoid the ugly traceback in most cases. 60 | except KeyboardInterrupt: 61 | sys.exit(3) 62 | 63 | class Job_manager(object): 64 | def __init__(self): 65 | self.passed_exceptions = [] 66 | 67 | def pass_exception(self, cls): 68 | self.passed_exceptions.append(cls) 69 | 70 | class Multiprocessing_job_manager(Job_manager): 71 | def __init__(self, number_of_workers): 72 | Job_manager.__init__(self) 73 | _initialise_multiprocessing() 74 | if multiprocessing is None: 75 | raise StandardError("multiprocessing not available") 76 | if not 1 <= number_of_workers < 1024: 77 | raise ValueError 78 | self.number_of_workers = number_of_workers 79 | 80 | def start_workers(self): 81 | self.job_queue = multiprocessing.Queue() 82 | self.response_queue = multiprocessing.Queue() 83 | self.workers = [] 84 | for i in range(self.number_of_workers): 85 | worker = multiprocessing.Process( 86 | target=worker_run_jobs, 87 | args=(self.job_queue, self.response_queue, i)) 88 | self.workers.append(worker) 89 | for worker in self.workers: 90 | worker.start() 91 | 92 | def run_jobs(self, job_source): 93 | active_jobs = 0 94 | while True: 95 | if active_jobs < self.number_of_workers: 96 | try: 97 | job = job_source.get_job() 98 | except Exception, e: 99 | for cls in self.passed_exceptions: 100 | if isinstance(e, cls): 101 | raise 102 | raise JobSourceError( 103 | "error from get_job()\n%s" % 104 | compact_tracebacks.format_traceback(skip=1)) 105 | if job is not NoJobAvailable: 106 | #sys.stderr.write("MGR: sending %s\n" % repr(job)) 107 | self.job_queue.put(job) 108 | active_jobs += 1 109 | continue 110 | if active_jobs == 0: 111 | break 112 | 113 | response = self.response_queue.get() 114 | if isinstance(response, JobError): 115 | try: 116 | job_source.process_error_response( 117 | response.job, response.msg) 118 | except Exception, e: 119 | for cls in self.passed_exceptions: 120 | if isinstance(e, cls): 121 | raise 122 | raise JobSourceError( 123 | "error from process_error_response()\n%s" % 124 | compact_tracebacks.format_traceback(skip=1)) 125 | else: 126 | try: 127 | job_source.process_response(response) 128 | except Exception, e: 129 | for cls in self.passed_exceptions: 130 | if isinstance(e, cls): 131 | raise 132 | raise JobSourceError( 133 | "error from process_response()\n%s" % 134 | compact_tracebacks.format_traceback(skip=1)) 135 | active_jobs -= 1 136 | #sys.stderr.write("MGR: received response %s\n" % repr(response)) 137 | 138 | def finish(self): 139 | for _ in range(self.number_of_workers): 140 | self.job_queue.put(worker_finish_signal) 141 | for worker in self.workers: 142 | worker.join() 143 | self.job_queue = None 144 | self.response_queue = None 145 | 146 | class In_process_job_manager(Job_manager): 147 | def start_workers(self): 148 | pass 149 | 150 | def run_jobs(self, job_source): 151 | while True: 152 | try: 153 | job = job_source.get_job() 154 | except Exception, e: 155 | for cls in self.passed_exceptions: 156 | if isinstance(e, cls): 157 | raise 158 | raise JobSourceError( 159 | "error from get_job()\n%s" % 160 | compact_tracebacks.format_traceback(skip=1)) 161 | if job is NoJobAvailable: 162 | break 163 | try: 164 | response = job.run(None) 165 | except Exception, e: 166 | if isinstance(e, JobFailed): 167 | msg = str(e) 168 | else: 169 | msg = compact_tracebacks.format_traceback(skip=1) 170 | try: 171 | job_source.process_error_response(job, msg) 172 | except Exception, e: 173 | for cls in self.passed_exceptions: 174 | if isinstance(e, cls): 175 | raise 176 | raise JobSourceError( 177 | "error from process_error_response()\n%s" % 178 | compact_tracebacks.format_traceback(skip=1)) 179 | else: 180 | try: 181 | job_source.process_response(response) 182 | except Exception, e: 183 | for cls in self.passed_exceptions: 184 | if isinstance(e, cls): 185 | raise 186 | raise JobSourceError( 187 | "error from process_response()\n%s" % 188 | compact_tracebacks.format_traceback(skip=1)) 189 | 190 | def finish(self): 191 | pass 192 | 193 | def run_jobs(job_source, max_workers=None, allow_mp=True, 194 | passed_exceptions=None): 195 | if allow_mp: 196 | _initialise_multiprocessing() 197 | if multiprocessing is None: 198 | allow_mp = False 199 | if allow_mp: 200 | if max_workers is None: 201 | max_workers = multiprocessing.cpu_count() 202 | job_manager = Multiprocessing_job_manager(max_workers) 203 | else: 204 | job_manager = In_process_job_manager() 205 | if passed_exceptions: 206 | for cls in passed_exceptions: 207 | job_manager.pass_exception(cls) 208 | job_manager.start_workers() 209 | try: 210 | job_manager.run_jobs(job_source) 211 | except Exception: 212 | try: 213 | job_manager.finish() 214 | except Exception, e2: 215 | print >>sys.stderr, "Error closing down workers:\n%s" % e2 216 | raise 217 | job_manager.finish() 218 | 219 | -------------------------------------------------------------------------------- /gomill/playoffs.py: -------------------------------------------------------------------------------- 1 | """Competitions made up of repeated matchups between specified players.""" 2 | 3 | from gomill import game_jobs 4 | from gomill import competitions 5 | from gomill import tournaments 6 | from gomill.competitions import (Competition, ControlFileError) 7 | from gomill.settings import * 8 | 9 | 10 | class Matchup_config(Quiet_config): 11 | """Matchup description for use in control files.""" 12 | # positional or keyword 13 | positional_arguments = ('player_1', 'player_2') 14 | # keyword-only 15 | keyword_arguments = ( 16 | ('id', 'name') + 17 | tuple(setting.name for setting in tournaments.matchup_settings)) 18 | 19 | 20 | class Playoff(tournaments.Tournament): 21 | """A Tournament with explicitly listed matchups. 22 | 23 | The game ids are like '0_2', where 0 is the matchup id and 2 is the game 24 | number within the matchup. 25 | 26 | """ 27 | 28 | def control_file_globals(self): 29 | result = Competition.control_file_globals(self) 30 | result.update({ 31 | 'Matchup' : Matchup_config, 32 | }) 33 | return result 34 | 35 | 36 | special_settings = [ 37 | Setting('matchups', 38 | interpret_sequence_of_quiet_configs(Matchup_config)), 39 | ] 40 | 41 | def matchup_from_config(self, matchup_number, 42 | matchup_config, matchup_defaults): 43 | """Make a Matchup from a Matchup_config. 44 | 45 | This does the following checks and fixups before calling make_matchup(): 46 | 47 | Checks that the player_1 and player_2 parameters exist, and that the 48 | player codes are present in self.players. 49 | 50 | Validates all the matchup_config arguments, and merges them with the 51 | defaults. 52 | 53 | If player_1 and player_2 are the same, takes the following actions: 54 | - sets player_2 to #2 55 | - if it doesn't already exist, creates #2 as a clone of 56 | player_1 and adds it to self.players 57 | 58 | """ 59 | matchup_id = str(matchup_number) 60 | try: 61 | arguments = matchup_config.resolve_arguments() 62 | if 'id' in arguments: 63 | try: 64 | matchup_id = interpret_identifier(arguments['id']) 65 | except ValueError, e: 66 | raise ValueError("'id': %s" % e) 67 | try: 68 | player_1 = arguments['player_1'] 69 | player_2 = arguments['player_2'] 70 | except KeyError: 71 | raise ControlFileError("not enough arguments") 72 | if player_1 not in self.players: 73 | raise ControlFileError("unknown player %s" % player_1) 74 | if player_2 not in self.players: 75 | raise ControlFileError("unknown player %s" % player_2) 76 | # If both players are the same, make a clone. 77 | if player_1 == player_2: 78 | player_2 += "#2" 79 | if player_2 not in self.players: 80 | self.players[player_2] = \ 81 | self.players[player_1].copy(player_2) 82 | interpreted = load_settings( 83 | tournaments.matchup_settings, arguments, 84 | apply_defaults=False, allow_missing=True) 85 | matchup_name = arguments.get('name') 86 | if matchup_name is not None: 87 | try: 88 | matchup_name = interpret_as_utf8(matchup_name) 89 | except ValueError, e: 90 | raise ValueError("'name': %s" % e) 91 | parameters = matchup_defaults.copy() 92 | parameters.update(interpreted) 93 | return self.make_matchup( 94 | matchup_id, player_1, player_2, 95 | parameters, matchup_name) 96 | except StandardError, e: 97 | raise ControlFileError("matchup %s: %s" % (matchup_id, e)) 98 | 99 | 100 | def initialise_from_control_file(self, config): 101 | Competition.initialise_from_control_file(self, config) 102 | 103 | try: 104 | matchup_defaults = load_settings( 105 | tournaments.matchup_settings, config, allow_missing=True) 106 | except ValueError, e: 107 | raise ControlFileError(str(e)) 108 | 109 | # Check default handicap settings when possible, for friendlier error 110 | # reporting (would be caught in the matchup anyway). 111 | if 'board_size' in matchup_defaults: 112 | try: 113 | competitions.validate_handicap( 114 | matchup_defaults['handicap'], 115 | matchup_defaults['handicap_style'], 116 | matchup_defaults['board_size']) 117 | except ControlFileError, e: 118 | raise ControlFileError("default %s" % e) 119 | 120 | try: 121 | specials = load_settings(self.special_settings, config) 122 | except ValueError, e: 123 | raise ControlFileError(str(e)) 124 | 125 | # map matchup_id -> Matchup 126 | self.matchups = {} 127 | # Matchups in order of definition 128 | self.matchup_list = [] 129 | if not specials['matchups']: 130 | raise ControlFileError("matchups: empty list") 131 | 132 | for i, matchup_config in enumerate(specials['matchups']): 133 | m = self.matchup_from_config(i, matchup_config, matchup_defaults) 134 | if m.id in self.matchups: 135 | raise ControlFileError("duplicate matchup id '%s'" % m.id) 136 | self.matchups[m.id] = m 137 | self.matchup_list.append(m) 138 | 139 | 140 | # Can bump this to prevent people loading incompatible .status files. 141 | status_format_version = 1 142 | 143 | def get_player_checks(self): 144 | # For board size and komi, we check the values from the first matchup 145 | # the player appears in. 146 | used_players = {} 147 | for m in reversed(self.matchup_list): 148 | if m.number_of_games == 0: 149 | continue 150 | used_players[m.player_1] = m 151 | used_players[m.player_2] = m 152 | result = [] 153 | for code, matchup in sorted(used_players.iteritems()): 154 | check = game_jobs.Player_check() 155 | check.player = self.players[code] 156 | check.board_size = matchup.board_size 157 | check.komi = matchup.komi 158 | result.append(check) 159 | return result 160 | 161 | 162 | def write_screen_report(self, out): 163 | self.write_matchup_reports(out) 164 | 165 | def write_short_report(self, out): 166 | def p(s): 167 | print >>out, s 168 | p("playoff: %s" % self.competition_code) 169 | if self.description: 170 | p(self.description) 171 | p('') 172 | self.write_screen_report(out) 173 | self.write_ghost_matchup_reports(out) 174 | p('') 175 | self.write_player_descriptions(out) 176 | p('') 177 | 178 | write_full_report = write_short_report 179 | 180 | -------------------------------------------------------------------------------- /gomill/ringmaster_command_line.py: -------------------------------------------------------------------------------- 1 | """Command-line interface to the ringmaster.""" 2 | 3 | import os 4 | import sys 5 | from optparse import OptionParser 6 | 7 | from gomill import compact_tracebacks 8 | from gomill.ringmasters import ( 9 | Ringmaster, RingmasterError, RingmasterInternalError) 10 | 11 | 12 | # Action functions return the desired exit status; implicit return is fine to 13 | # indicate a successful exit. 14 | 15 | def do_run(ringmaster, options): 16 | if not options.quiet: 17 | print "running startup checks on all players" 18 | if not ringmaster.check_players(discard_stderr=True): 19 | print "(use the 'check' command to see stderr output)" 20 | return 1 21 | if options.log_gtp: 22 | ringmaster.enable_gtp_logging() 23 | if options.quiet: 24 | ringmaster.set_display_mode('quiet') 25 | if ringmaster.status_file_exists(): 26 | ringmaster.load_status() 27 | else: 28 | ringmaster.set_clean_status() 29 | if options.parallel is not None: 30 | ringmaster.set_parallel_worker_count(options.parallel) 31 | ringmaster.run(options.max_games) 32 | ringmaster.report() 33 | 34 | def do_stop(ringmaster, options): 35 | ringmaster.write_command("stop") 36 | 37 | def do_show(ringmaster, options): 38 | if not ringmaster.status_file_exists(): 39 | raise RingmasterError("no status file") 40 | ringmaster.load_status() 41 | ringmaster.print_status_report() 42 | 43 | def do_report(ringmaster, options): 44 | if not ringmaster.status_file_exists(): 45 | raise RingmasterError("no status file") 46 | ringmaster.load_status() 47 | ringmaster.report() 48 | 49 | def do_reset(ringmaster, options): 50 | ringmaster.delete_state_and_output() 51 | 52 | def do_check(ringmaster, options): 53 | if not ringmaster.check_players(discard_stderr=False): 54 | return 1 55 | 56 | def do_debugstatus(ringmaster, options): 57 | ringmaster.print_status() 58 | 59 | _actions = { 60 | "run" : do_run, 61 | "stop" : do_stop, 62 | "show" : do_show, 63 | "report" : do_report, 64 | "reset" : do_reset, 65 | "check" : do_check, 66 | "debugstatus" : do_debugstatus, 67 | } 68 | 69 | 70 | def run(argv, ringmaster_class): 71 | usage = ("%prog [options] [command]\n\n" 72 | "commands: run (default), stop, show, report, reset, check") 73 | parser = OptionParser(usage=usage, prog="ringmaster", 74 | version=ringmaster_class.public_version) 75 | parser.add_option("--max-games", "-g", type="int", 76 | help="maximum number of games to play in this run") 77 | parser.add_option("--parallel", "-j", type="int", 78 | help="number of worker processes") 79 | parser.add_option("--quiet", "-q", action="store_true", 80 | help="be silent except for warnings and errors") 81 | parser.add_option("--log-gtp", action="store_true", 82 | help="write GTP logs") 83 | (options, args) = parser.parse_args(argv) 84 | if len(args) == 0: 85 | parser.error("no control file specified") 86 | if len(args) > 2: 87 | parser.error("too many arguments") 88 | if len(args) == 1: 89 | command = "run" 90 | else: 91 | command = args[1] 92 | try: 93 | action = _actions[command] 94 | except KeyError: 95 | parser.error("no such command: %s" % command) 96 | ctl_pathname = args[0] 97 | try: 98 | if not os.path.exists(ctl_pathname): 99 | raise RingmasterError("control file %s not found" % ctl_pathname) 100 | ringmaster = ringmaster_class(ctl_pathname) 101 | exit_status = action(ringmaster, options) 102 | except RingmasterError, e: 103 | print >>sys.stderr, "ringmaster:", e 104 | exit_status = 1 105 | except KeyboardInterrupt: 106 | exit_status = 3 107 | except RingmasterInternalError, e: 108 | print >>sys.stderr, "ringmaster: internal error" 109 | print >>sys.stderr, e 110 | exit_status = 4 111 | except: 112 | print >>sys.stderr, "ringmaster: internal error" 113 | compact_tracebacks.log_traceback() 114 | exit_status = 4 115 | sys.exit(exit_status) 116 | 117 | def main(): 118 | run(sys.argv[1:], Ringmaster) 119 | 120 | if __name__ == "__main__": 121 | main() 122 | 123 | -------------------------------------------------------------------------------- /gomill/ringmaster_presenters.py: -------------------------------------------------------------------------------- 1 | """Live display for ringmasters.""" 2 | 3 | import os 4 | import subprocess 5 | import sys 6 | from cStringIO import StringIO 7 | 8 | 9 | class Presenter(object): 10 | """Abstract base class for presenters. 11 | 12 | This accepts messages on four _channels_, with codes 13 | warnings 14 | status 15 | screen_report 16 | results 17 | 18 | Warnings are always displayed immediately. 19 | 20 | Some presenters will delay display of other channels until refresh() is 21 | called; some will display them immediately. 22 | 23 | """ 24 | 25 | # If this is true, ringmaster needn't bother doing the work to prepare most 26 | # of the display. 27 | shows_warnings_only = False 28 | 29 | def clear(self, channel): 30 | """Clear the contents of the specified channel.""" 31 | raise NotImplementedError 32 | 33 | def say(self, channel, s): 34 | """Add a message to the specified channel. 35 | 36 | channel -- channel code 37 | s -- string to display (no trailing newline) 38 | 39 | """ 40 | raise NotImplementedError 41 | 42 | def refresh(self): 43 | """Re-render the current screen. 44 | 45 | This typically displays the full status and screen_report, and the most 46 | recent warnings and results. 47 | 48 | """ 49 | raise NotImplementedError 50 | 51 | def get_stream(self, channel): 52 | """Return a file-like object wired up to the specified channel. 53 | 54 | When the object is closed, the text written it is sent to the channel 55 | (except that any trailing newline is removed). 56 | 57 | """ 58 | return _Channel_writer(self, channel) 59 | 60 | class _Channel_writer(object): 61 | """Support for get_stream() implementation.""" 62 | def __init__(self, parent, channel): 63 | self.parent = parent 64 | self.channel = channel 65 | self.stringio = StringIO() 66 | 67 | def write(self, s): 68 | self.stringio.write(s) 69 | 70 | def close(self): 71 | s = self.stringio.getvalue() 72 | if s.endswith("\n"): 73 | s = s[:-1] 74 | self.parent.say(self.channel, s) 75 | self.stringio.close() 76 | 77 | 78 | class Quiet_presenter(Presenter): 79 | """Presenter which shows only warnings. 80 | 81 | Warnings go to stderr. 82 | 83 | """ 84 | shows_warnings_only = True 85 | 86 | def clear(self, channel): 87 | pass 88 | 89 | def say(self, channel, s): 90 | if channel == 'warnings': 91 | print >>sys.stderr, s 92 | 93 | def refresh(self): 94 | pass 95 | 96 | 97 | class Box(object): 98 | """Description of screen layout for the clearing presenter.""" 99 | def __init__(self, name, heading, limit): 100 | self.name = name 101 | self.heading = heading 102 | self.limit = limit 103 | self.contents = [] 104 | 105 | def layout(self): 106 | return "\n".join(self.contents[-self.limit:]) 107 | 108 | class Clearing_presenter(Presenter): 109 | """Low-tech full-screen presenter. 110 | 111 | This shows all channels. 112 | 113 | """ 114 | shows_warnings_only = False 115 | 116 | # warnings has to be last, so we can add to it immediately 117 | box_specs = ( 118 | ('status', None, 999), 119 | ('screen_report', None, 999), 120 | ('results', "Results", 6), 121 | ('warnings', "Warnings", 4), 122 | ) 123 | 124 | def __init__(self): 125 | self.boxes = {} 126 | self.box_list = [] 127 | for t in self.box_specs: 128 | box = Box(*t) 129 | self.boxes[box.name] = box 130 | self.box_list.append(box) 131 | self.clear_method = None 132 | 133 | def clear(self, channel): 134 | self.boxes[channel].contents = [] 135 | 136 | def say(self, channel, s): 137 | self.boxes[channel].contents.append(s) 138 | # 'warnings' box heading might be missing, but never mind. 139 | if channel == 'warnings': 140 | print s 141 | 142 | def refresh(self): 143 | self.clear_screen() 144 | for box in self.box_list: 145 | if not box.contents: 146 | continue 147 | if box.heading: 148 | print "= %s = " % box.heading 149 | print box.layout() 150 | if box.name != 'warnings': 151 | print 152 | 153 | def screen_height(self): 154 | """Return the current terminal height, or best guess.""" 155 | return os.environ.get("LINES", 80) 156 | 157 | def clear_screen(self): 158 | """Try to clear the terminal screen (if stdout is a terminal).""" 159 | if self.clear_method is None: 160 | try: 161 | isatty = os.isatty(sys.stdout.fileno()) 162 | except Exception: 163 | isatty = False 164 | if isatty: 165 | self.clear_method = "clear" 166 | else: 167 | self.clear_method = "delimiter" 168 | 169 | if self.clear_method == "clear": 170 | try: 171 | retcode = subprocess.call("clear") 172 | except Exception: 173 | retcode = 1 174 | if retcode != 0: 175 | self.clear_method = "newlines" 176 | if self.clear_method == "newlines": 177 | print "\n" * (self.screen_height()+1) 178 | elif self.clear_method == "delimiter": 179 | print 78 * "-" 180 | 181 | -------------------------------------------------------------------------------- /gomill/sgf_moves.py: -------------------------------------------------------------------------------- 1 | """Higher-level processing of moves and positions from SGF games.""" 2 | 3 | from gomill import boards 4 | from gomill import sgf_properties 5 | 6 | 7 | def get_setup_and_moves(sgf_game, board=None): 8 | """Return the initial setup and the following moves from an Sgf_game. 9 | 10 | Returns a pair (board, plays) 11 | 12 | board -- boards.Board 13 | plays -- list of pairs (colour, move) 14 | moves are (row, col), or None for a pass. 15 | 16 | The board represents the position described by AB and/or AW properties 17 | in the root node. 18 | 19 | The moves are from the game's 'leftmost' variation. 20 | 21 | Raises ValueError if this position isn't legal. 22 | 23 | Raises ValueError if there are any AB/AW/AE properties after the root 24 | node. 25 | 26 | Doesn't check whether the moves are legal. 27 | 28 | If the optional 'board' parameter is provided, it must be an empty board of 29 | the right size; the same object will be returned. 30 | 31 | """ 32 | size = sgf_game.get_size() 33 | if board is None: 34 | board = boards.Board(size) 35 | else: 36 | if board.side != size: 37 | raise ValueError("wrong board size, must be %d" % size) 38 | if not board.is_empty(): 39 | raise ValueError("board not empty") 40 | root = sgf_game.get_root() 41 | nodes = sgf_game.main_sequence_iter() 42 | ab, aw, ae = root.get_setup_stones() 43 | if ab or aw: 44 | is_legal = board.apply_setup(ab, aw, ae) 45 | if not is_legal: 46 | raise ValueError("setup position not legal") 47 | colour, raw = root.get_raw_move() 48 | if colour is not None: 49 | raise ValueError("mixed setup and moves in root node") 50 | nodes.next() 51 | moves = [] 52 | for node in nodes: 53 | if node.has_setup_stones(): 54 | raise ValueError("setup properties after the root node") 55 | colour, raw = node.get_raw_move() 56 | if colour is not None: 57 | moves.append((colour, sgf_properties.interpret_go_point(raw, size))) 58 | return board, moves 59 | 60 | def set_initial_position(sgf_game, board): 61 | """Add setup stones to an Sgf_game reflecting a board position. 62 | 63 | sgf_game -- Sgf_game 64 | board -- boards.Board 65 | 66 | Replaces any existing setup stones in the Sgf_game's root node. 67 | 68 | """ 69 | stones = {'b' : set(), 'w' : set()} 70 | for (colour, point) in board.list_occupied_points(): 71 | stones[colour].add(point) 72 | sgf_game.get_root().set_setup_stones(stones['b'], stones['w']) 73 | 74 | def indicate_first_player(sgf_game): 75 | """Add a PL property to the root node if appropriate. 76 | 77 | Looks at the first child of the root to see who the first player is, and 78 | sets PL it isn't the expected player (ie, black normally, but white if 79 | there is a handicap), or if there are non-handicap setup stones. 80 | 81 | """ 82 | root = sgf_game.get_root() 83 | first_player, move = root[0].get_move() 84 | if first_player is None: 85 | return 86 | has_handicap = root.has_property("HA") 87 | if root.has_property("AW"): 88 | specify_pl = True 89 | elif root.has_property("AB") and not has_handicap: 90 | specify_pl = True 91 | elif not has_handicap and first_player == 'w': 92 | specify_pl = True 93 | elif has_handicap and first_player == 'b': 94 | specify_pl = True 95 | else: 96 | specify_pl = False 97 | if specify_pl: 98 | root.set('PL', first_player) 99 | 100 | -------------------------------------------------------------------------------- /gomill/terminal_input.py: -------------------------------------------------------------------------------- 1 | """Support for non-blocking terminal input.""" 2 | 3 | import os 4 | 5 | try: 6 | import termios 7 | except ImportError: 8 | termios = None 9 | 10 | class Terminal_reader(object): 11 | """Check for input on the controlling terminal.""" 12 | 13 | def __init__(self): 14 | self.enabled = True 15 | self.tty = None 16 | 17 | def is_enabled(self): 18 | return self.enabled 19 | 20 | def disable(self): 21 | self.enabled = False 22 | 23 | def initialise(self): 24 | if not self.enabled: 25 | return 26 | if termios is None: 27 | self.enabled = False 28 | return 29 | try: 30 | self.tty = open("/dev/tty", "w+") 31 | os.tcgetpgrp(self.tty.fileno()) 32 | self.clean_tcattr = termios.tcgetattr(self.tty) 33 | iflag, oflag, cflag, lflag, ispeed, ospeed, cc = self.clean_tcattr 34 | new_lflag = lflag & (0xffffffff ^ termios.ICANON) 35 | new_cc = cc[:] 36 | new_cc[termios.VMIN] = 0 37 | self.cbreak_tcattr = [ 38 | iflag, oflag, cflag, new_lflag, ispeed, ospeed, new_cc] 39 | except Exception: 40 | self.enabled = False 41 | return 42 | 43 | def close(self): 44 | if self.tty is not None: 45 | self.tty.close() 46 | self.tty = None 47 | 48 | def stop_was_requested(self): 49 | """Check whether a 'keyboard stop' instruction has been sent. 50 | 51 | Returns true if ^X has been sent on the controlling terminal. 52 | 53 | Consumes all available input on /dev/tty. 54 | 55 | """ 56 | if not self.enabled: 57 | return False 58 | # Don't try to read the terminal if we're in the background. 59 | # There's a race here, if we're backgrounded just after this check, but 60 | # I don't see a clean way to avoid it. 61 | if os.tcgetpgrp(self.tty.fileno()) != os.getpid(): 62 | return False 63 | try: 64 | termios.tcsetattr(self.tty, termios.TCSANOW, self.cbreak_tcattr) 65 | except EnvironmentError: 66 | return False 67 | try: 68 | seen_ctrl_x = False 69 | while True: 70 | c = os.read(self.tty.fileno(), 1) 71 | if not c: 72 | break 73 | if c == "\x18": 74 | seen_ctrl_x = True 75 | except EnvironmentError: 76 | seen_ctrl_x = False 77 | finally: 78 | termios.tcsetattr(self.tty, termios.TCSANOW, self.clean_tcattr) 79 | return seen_ctrl_x 80 | 81 | def acknowledge(self): 82 | """Leave an acknowledgement on the controlling terminal.""" 83 | self.tty.write("\rCtrl-X received; halting\n") 84 | -------------------------------------------------------------------------------- /gomill/tournament_results.py: -------------------------------------------------------------------------------- 1 | """Retrieving and reporting on tournament results.""" 2 | 3 | from __future__ import division 4 | 5 | from gomill import ascii_tables 6 | from gomill.utils import format_float, format_percent 7 | from gomill.common import colour_name 8 | 9 | class Matchup_description(object): 10 | """Description of a matchup (pairing of two players). 11 | 12 | Public attributes: 13 | id -- matchup id (very short string) 14 | player_1 -- player code (identifier-like string) 15 | player_2 -- player code (identifier-like string) 16 | name -- string (eg 'xxx v yyy') 17 | board_size -- int 18 | komi -- float 19 | alternating -- bool 20 | handicap -- int or None 21 | handicap_style -- 'fixed' or 'free' 22 | move_limit -- int 23 | scorer -- 'internal' or 'players' 24 | number_of_games -- int or None 25 | 26 | If alternating is False, player_1 plays black and player_2 plays white; 27 | otherwise they alternate. 28 | 29 | player_1 and player_2 are always different. 30 | 31 | """ 32 | def describe_details(self): 33 | """Return a text description of game settings. 34 | 35 | This covers the most important game settings which can't be observed 36 | in the results table (board size, handicap, and komi). 37 | 38 | """ 39 | s = "board size: %s " % self.board_size 40 | if self.handicap is not None: 41 | s += "handicap: %s (%s) " % ( 42 | self.handicap, self.handicap_style) 43 | s += "komi: %s" % self.komi 44 | return s 45 | 46 | 47 | class Tournament_results(object): 48 | """Provide access to results of a single tournament. 49 | 50 | The tournament results are catalogued in terms of 'matchups', with each 51 | matchup corresponding to a series of games which have the same players and 52 | settings. Each matchup has an id, which is a short string. 53 | 54 | """ 55 | def __init__(self, matchup_list, results): 56 | self.matchup_list = matchup_list 57 | self.results = results 58 | self.matchups = dict((m.id, m) for m in matchup_list) 59 | 60 | def get_matchup_ids(self): 61 | """Return a list of all matchup ids, in definition order.""" 62 | return [m.id for m in self.matchup_list] 63 | 64 | def get_matchup(self, matchup_id): 65 | """Describe the matchup with the specified id. 66 | 67 | Returns a Matchup_description (which should be treated as read-only). 68 | 69 | """ 70 | return self.matchups[matchup_id] 71 | 72 | def get_matchups(self): 73 | """Return a map matchup id -> Matchup_description.""" 74 | return self.matchups.copy() 75 | 76 | def get_matchup_results(self, matchup_id): 77 | """Return the results for the specified matchup. 78 | 79 | Returns a list of gtp_games.Game_results (in unspecified order). 80 | 81 | The Game_results all have game_id set. 82 | 83 | """ 84 | return self.results[matchup_id][:] 85 | 86 | def get_matchup_stats(self, matchup_id): 87 | """Return statistics for the specified matchup. 88 | 89 | Returns a Matchup_stats object. 90 | 91 | """ 92 | matchup = self.matchups[matchup_id] 93 | ms = Matchup_stats(self.results[matchup_id], 94 | matchup.player_1, matchup.player_2) 95 | ms.calculate_colour_breakdown() 96 | ms.calculate_time_stats() 97 | return ms 98 | 99 | 100 | class Matchup_stats(object): 101 | """Result statistics for games between a pair of players. 102 | 103 | Instantiate with 104 | results -- list of gtp_games.Game_results 105 | player_1 -- player code 106 | player_2 -- player code 107 | The game results should all be for games between player_1 and player_2. 108 | 109 | Public attributes: 110 | player_1 -- player code 111 | player_2 -- player code 112 | total -- int (number of games) 113 | wins_1 -- float (score) 114 | wins_2 -- float (score) 115 | forfeits_1 -- int (number of games) 116 | forfeits_2 -- int (number of games) 117 | unknown -- int (number of games) 118 | 119 | scores are multiples of 0.5 (as there may be jigos). 120 | 121 | """ 122 | def __init__(self, results, player_1, player_2): 123 | self._results = results 124 | self.player_1 = player_1 125 | self.player_2 = player_2 126 | 127 | self.total = len(results) 128 | 129 | js = self._jigo_score = 0.5 * sum(r.is_jigo for r in results) 130 | self.unknown = sum(r.winning_player is None and not r.is_jigo 131 | for r in results) 132 | 133 | self.wins_1 = sum(r.winning_player == player_1 for r in results) + js 134 | self.wins_2 = sum(r.winning_player == player_2 for r in results) + js 135 | 136 | self.forfeits_1 = sum(r.winning_player == player_2 and r.is_forfeit 137 | for r in results) 138 | self.forfeits_2 = sum(r.winning_player == player_1 and r.is_forfeit 139 | for r in results) 140 | 141 | def calculate_colour_breakdown(self): 142 | """Calculate futher statistics, broken down by colour played. 143 | 144 | Sets the following additional attributes: 145 | 146 | played_1b -- int (number of games) 147 | played_1w -- int (number of games) 148 | played_2b -- int (number of games) 149 | played_y2 -- int (number of games) 150 | alternating -- bool 151 | when alternating is true => 152 | wins_b -- float (score) 153 | wins_w -- float (score) 154 | wins_1b -- float (score) 155 | wins_1w -- float (score) 156 | wins_2b -- float (score) 157 | wins_2w -- float (score) 158 | else => 159 | colour_1 -- 'b' or 'w' 160 | colour_2 -- 'b' or 'w' 161 | 162 | """ 163 | results = self._results 164 | player_1 = self.player_1 165 | player_2 = self.player_2 166 | js = self._jigo_score 167 | 168 | self.played_1b = sum(r.player_b == player_1 for r in results) 169 | self.played_1w = sum(r.player_w == player_1 for r in results) 170 | self.played_2b = sum(r.player_b == player_2 for r in results) 171 | self.played_y2 = sum(r.player_w == player_2 for r in results) 172 | 173 | if self.played_1w == 0 and self.played_2b == 0: 174 | self.alternating = False 175 | self.colour_1 = 'b' 176 | self.colour_2 = 'w' 177 | elif self.played_1b == 0 and self.played_y2 == 0: 178 | self.alternating = False 179 | self.colour_1 = 'w' 180 | self.colour_2 = 'b' 181 | else: 182 | self.alternating = True 183 | self.wins_b = sum(r.winning_colour == 'b' for r in results) + js 184 | self.wins_w = sum(r.winning_colour == 'w' for r in results) + js 185 | self.wins_1b = sum( 186 | r.winning_player == player_1 and r.winning_colour == 'b' 187 | for r in results) + js 188 | self.wins_1w = sum( 189 | r.winning_player == player_1 and r.winning_colour == 'w' 190 | for r in results) + js 191 | self.wins_2b = sum( 192 | r.winning_player == player_2 and r.winning_colour == 'b' 193 | for r in results) + js 194 | self.wins_2w = sum( 195 | r.winning_player == player_2 and r.winning_colour == 'w' 196 | for r in results) + js 197 | 198 | def calculate_time_stats(self): 199 | """Calculate CPU time statistics. 200 | 201 | average_time_1 -- float or None 202 | average_time_2 -- float or None 203 | 204 | """ 205 | player_1 = self.player_1 206 | player_2 = self.player_2 207 | times_1 = [r.cpu_times[player_1] for r in self._results] 208 | known_times_1 = [t for t in times_1 if t is not None and t != '?'] 209 | times_2 = [r.cpu_times[player_2] for r in self._results] 210 | known_times_2 = [t for t in times_2 if t is not None and t != '?'] 211 | if known_times_1: 212 | self.average_time_1 = sum(known_times_1) / len(known_times_1) 213 | else: 214 | self.average_time_1 = None 215 | if known_times_2: 216 | self.average_time_2 = sum(known_times_2) / len(known_times_2) 217 | else: 218 | self.average_time_2 = None 219 | 220 | 221 | def make_matchup_stats_table(ms): 222 | """Produce an ascii table showing matchup statistics. 223 | 224 | ms -- Matchup_stats (with all statistics set) 225 | 226 | returns an ascii_tables.Table 227 | 228 | """ 229 | ff = format_float 230 | pct = format_percent 231 | 232 | t = ascii_tables.Table(row_count=3) 233 | t.add_heading("") # player name 234 | i = t.add_column(align='left', right_padding=3) 235 | t.set_column_values(i, [ms.player_1, ms.player_2]) 236 | 237 | t.add_heading("wins") 238 | i = t.add_column(align='right') 239 | t.set_column_values(i, [ff(ms.wins_1), ff(ms.wins_2)]) 240 | 241 | t.add_heading("") # overall pct 242 | i = t.add_column(align='right') 243 | t.set_column_values(i, [pct(ms.wins_1, ms.total), 244 | pct(ms.wins_2, ms.total)]) 245 | 246 | if ms.alternating: 247 | t.columns[i].right_padding = 7 248 | t.add_heading("black", span=2) 249 | i = t.add_column(align='left') 250 | t.set_column_values(i, [ff(ms.wins_1b), ff(ms.wins_2b), ff(ms.wins_b)]) 251 | i = t.add_column(align='right', right_padding=5) 252 | t.set_column_values(i, [pct(ms.wins_1b, ms.played_1b), 253 | pct(ms.wins_2b, ms.played_2b), 254 | pct(ms.wins_b, ms.total)]) 255 | 256 | t.add_heading("white", span=2) 257 | i = t.add_column(align='left') 258 | t.set_column_values(i, [ff(ms.wins_1w), ff(ms.wins_2w), ff(ms.wins_w)]) 259 | i = t.add_column(align='right', right_padding=3) 260 | t.set_column_values(i, [pct(ms.wins_1w, ms.played_1w), 261 | pct(ms.wins_2w, ms.played_y2), 262 | pct(ms.wins_w, ms.total)]) 263 | else: 264 | t.columns[i].right_padding = 3 265 | t.add_heading("") 266 | i = t.add_column(align='left') 267 | t.set_column_values(i, ["(%s)" % colour_name(ms.colour_1), 268 | "(%s)" % colour_name(ms.colour_2)]) 269 | 270 | if ms.forfeits_1 or ms.forfeits_2: 271 | t.add_heading("forfeits") 272 | i = t.add_column(align='right') 273 | t.set_column_values(i, [ms.forfeits_1, ms.forfeits_2]) 274 | 275 | if ms.average_time_1 or ms.average_time_2: 276 | if ms.average_time_1 is not None: 277 | avg_time_1_s = "%7.2f" % ms.average_time_1 278 | else: 279 | avg_time_1_s = " ----" 280 | if ms.average_time_2 is not None: 281 | avg_time_2_s = "%7.2f" % ms.average_time_2 282 | else: 283 | avg_time_2_s = " ----" 284 | t.add_heading("avg cpu") 285 | i = t.add_column(align='right', right_padding=2) 286 | t.set_column_values(i, [avg_time_1_s, avg_time_2_s]) 287 | 288 | return t 289 | 290 | def write_matchup_summary(out, matchup, ms): 291 | """Write a summary block for the specified matchup to 'out'. 292 | 293 | matchup -- Matchup_description 294 | ms -- Matchup_stats (with all statistics set) 295 | 296 | """ 297 | def p(s): 298 | print >>out, s 299 | 300 | if matchup.number_of_games is None: 301 | played_s = "%d" % ms.total 302 | else: 303 | played_s = "%d/%d" % (ms.total, matchup.number_of_games) 304 | p("%s (%s games)" % (matchup.name, played_s)) 305 | if ms.unknown > 0: 306 | p("unknown results: %d %s" % 307 | (ms.unknown, format_percent(ms.unknown, ms.total))) 308 | 309 | p(matchup.describe_details()) 310 | p("\n".join(make_matchup_stats_table(ms).render())) 311 | 312 | -------------------------------------------------------------------------------- /gomill/utils.py: -------------------------------------------------------------------------------- 1 | """Domain-independent utility functions for gomill. 2 | 3 | This module is designed to be used with 'from utils import *'. 4 | 5 | This is for generic utilities; see common for Go-specific utility functions. 6 | 7 | """ 8 | 9 | from __future__ import division 10 | 11 | __all__ = ["format_float", "format_percent", "sanitise_utf8", "isinf", "isnan"] 12 | 13 | def format_float(f): 14 | """Format a Python float in a friendly way. 15 | 16 | This is intended for values like komi or win counts, which will be either 17 | integers or half-integers. 18 | 19 | """ 20 | if f == int(f): 21 | return str(int(f)) 22 | else: 23 | return str(f) 24 | 25 | def format_percent(n, baseline): 26 | """Format a ratio as a percentage (showing two decimal places). 27 | 28 | Returns a string. 29 | 30 | Accepts baseline zero and returns '??' or '--'. 31 | 32 | """ 33 | if baseline == 0: 34 | if n == 0: 35 | return "--" 36 | else: 37 | return "??" 38 | return "%.2f%%" % (100 * n/baseline) 39 | 40 | 41 | def sanitise_utf8(s): 42 | """Ensure an 8-bit string is utf-8. 43 | 44 | s -- 8-bit string (or None) 45 | 46 | Returns the sanitised string. If the string was already valid utf-8, returns 47 | the same object. 48 | 49 | This replaces bad characters with ascii question marks (I don't want to use 50 | a unicode replacement character, because if this function is doing anything 51 | then it's likely that there's a non-unicode setup involved somewhere, so it 52 | probably wouldn't be helpful). 53 | 54 | """ 55 | if s is None: 56 | return None 57 | try: 58 | s.decode("utf-8") 59 | except UnicodeDecodeError: 60 | return (s.decode("utf-8", 'replace') 61 | .replace(u"\ufffd", u"?") 62 | .encode("utf-8")) 63 | else: 64 | return s 65 | 66 | try: 67 | from math import isinf, isnan 68 | except ImportError: 69 | # Python < 2.6 70 | def isinf(f): 71 | return (f == float("1e500") or f == float("-1e500")) 72 | def isnan(f): 73 | return (f != f) 74 | 75 | -------------------------------------------------------------------------------- /goreviewpartner.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Go Review Partner 3 | Comment=A tool to review SGFs using bots 4 | Path=/opt/goreviewpartner 5 | Exec=python2 "/opt/goreviewpartner/main.py" 6 | Terminal=false 7 | Type=Application 8 | Icon=goreviewpartner 9 | StartupWMClass=goreviewpartner 10 | Categories=Game; 11 | -------------------------------------------------------------------------------- /goreviewpartner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnprog/goreviewpartner/cbcc486cd4c51fb6fc3bc0a1eab61ff34298dadf/goreviewpartner.png -------------------------------------------------------------------------------- /gtp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import subprocess,sys 5 | import threading, Queue 6 | from time import sleep 7 | from toolbox import log,GRPException 8 | 9 | class gtp(): 10 | def __init__(self,command): 11 | self.c=1 12 | self.command_line=command[0]+" "+" ".join(command[1:]) 13 | command=[c.encode(sys.getfilesystemencoding()) for c in command] 14 | self.process=subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 15 | self.size=0 16 | self.stderr_queue=Queue.Queue() 17 | self.stdout_queue=Queue.Queue() 18 | threading.Thread(target=self.consume_stderr).start() 19 | self.free_handicap_stones=[] 20 | self.history=[] 21 | 22 | ####low level function#### 23 | def consume_stderr(self): 24 | while 1: 25 | try: 26 | err_line=self.process.stderr.readline().decode("utf-8") 27 | if err_line: 28 | log("#",err_line.strip()) 29 | self.stderr_queue.put(err_line) 30 | else: 31 | log("leaving consume_stderr thread") 32 | return 33 | except Exception,e: 34 | log("leaving consume_stderr thread due to exception") 35 | log(e) 36 | return 37 | 38 | def consume_stdout(self): 39 | while 1: 40 | try: 41 | line=self.process.stdout.readline().decode("utf-8") 42 | if line: 43 | self.stdout_queue.put(line) 44 | else: 45 | log("leaving consume_stdout thread") 46 | return 47 | except Exception, e: 48 | log("leaving consume_stdout thread due to exception") 49 | log(e) 50 | return 51 | 52 | def quick_evaluation(self,color): 53 | return "Feature not implemented" 54 | 55 | def write(self,txt): 56 | try: 57 | self.process.stdin.write(txt+"\n") 58 | except Exception, e: 59 | log("Error while writting to stdin\n"+unicode(e)) 60 | #self.process.stdin.write(str(self.c)+" "+txt+"\n") 61 | self.c+=1 62 | 63 | def readline(self): 64 | answer=self.process.stdout.readline().decode("utf-8") 65 | while answer in ("\n","\r\n","\r"): 66 | answer=self.process.stdout.readline().decode("utf-8") 67 | return answer 68 | 69 | ####hight level function#### 70 | def boardsize(self,size=19): 71 | self.size=size 72 | self.write("boardsize "+str(size)) 73 | answer=self.readline() 74 | if answer[0]=="=":return True 75 | else:return False 76 | 77 | def reset(self): 78 | self.write("clear_board") 79 | answer=self.readline() 80 | if answer[0]=="=":return True 81 | else:return False 82 | 83 | def komi(self,k): 84 | self.write("komi "+str(k)) 85 | answer=self.readline() 86 | if answer[0]=="=": 87 | self.komi_value=k 88 | return True 89 | else: 90 | self.komi_value=0 91 | return False 92 | 93 | def place_black(self,move): 94 | if move == "RESIGN": 95 | log("WARNING: trying to play RESIGN as GTP move") 96 | self.history.append(["b",move]) 97 | return True 98 | self.write("play black "+move) 99 | answer=self.readline() 100 | if answer[0]=="=": 101 | self.history.append(["b",move]) 102 | return True 103 | else:return False 104 | 105 | def place_white(self,move): 106 | if move == "RESIGN": 107 | log("WARNING: trying to play RESIGN as GTP move") 108 | self.history.append(["w",move]) 109 | return True 110 | self.write("play white "+move) 111 | answer=self.readline() 112 | if answer[0]=="=": 113 | self.history.append(["w",move]) 114 | return True 115 | else:return False 116 | 117 | 118 | def play_black(self): 119 | self.write("genmove black") 120 | answer=self.readline().strip() 121 | try: 122 | move=answer.split(" ")[1].upper() 123 | self.history.append(["b",move]) 124 | return move 125 | except Exception, e: 126 | raise GRPException("GRPException in genmove_black()\nanswer='"+answer+"'\n"+unicode(e)) 127 | 128 | 129 | def play_white(self): 130 | self.write("genmove white") 131 | answer=self.readline().strip() 132 | try: 133 | move=answer.split(" ")[1].upper() 134 | self.history.append(["w",move]) 135 | return move 136 | except Exception, e: 137 | raise GRPException("GRPException in genmove_white()\nanswer='"+answer+"'\n"+unicode(e)) 138 | 139 | 140 | def undo(self): 141 | self.reset() 142 | self.komi(self.komi_value) 143 | try: 144 | #adding handicap stones 145 | if len(self.free_handicap_stones)>0: 146 | self.set_free_handicap(self.free_handicap_stones) 147 | self.history.pop() 148 | history=self.history[:] 149 | self.history=[] 150 | for color,move in history: 151 | if color=="b": 152 | if not self.place_black(move): 153 | return False 154 | else: 155 | if not self.place_white(move): 156 | return False 157 | return True 158 | except Exception, e: 159 | raise GRPException("GRPException in undo()\n"+unicode(e)) 160 | 161 | def place(self,move,color): 162 | if color==1: 163 | return self.place_black(move) 164 | else: 165 | return self.place_white(move) 166 | 167 | def name(self): 168 | self.write("name") 169 | answer=self.readline().strip() 170 | try: 171 | return " ".join(answer.split(" ")[1:]) 172 | except Exception, e: 173 | raise GRPException("GRPException in name()\nanswer='"+answer+"'\n"+unicode(e)) 174 | 175 | def version(self): 176 | self.write("version") 177 | answer=self.readline().strip() 178 | try: 179 | return answer.split(" ")[1] 180 | except Exception,e: 181 | raise GRPException("GRPException in version()\nanswer='"+answer+"'\n"+unicode(e)) 182 | 183 | 184 | def set_free_handicap(self,positions): 185 | self.free_handicap_stones=positions[:] 186 | stones="" 187 | for p in positions: 188 | stones+=p+" " 189 | self.write("set_free_handicap "+stones.strip()) 190 | answer=self.readline().strip() 191 | try: 192 | if answer[0]=="=": 193 | return True 194 | else: 195 | return False 196 | except Exception, e: 197 | raise GRPException("GRPException in set_free_handicap()\nanswer='"+answer+"'\n"+unicode(e)) 198 | 199 | def undo_standard(self): 200 | self.write("undo") 201 | answer=self.readline() 202 | try: 203 | if answer[0]=="=": 204 | return True 205 | else: 206 | return False 207 | except Exception, e: 208 | raise GRPException("GRPException in undo()\nanswer='"+answer+"'\n"+unicode(e)) 209 | 210 | def countlib(self,move): 211 | self.write("countlib "+move) 212 | answer=self.readline() 213 | return " ".join(answer.split(" ")[1:]) 214 | 215 | #is that needed? 216 | def final_score(self): 217 | self.write("final_score") 218 | answer=self.readline() 219 | return " ".join(answer.split(" ")[1:]).strip() 220 | 221 | #is that needed? 222 | def final_status(self,move): 223 | self.write("final_status "+move) 224 | answer=self.readline() 225 | answer=answer.strip() 226 | return " ".join(answer.split(" ")[1:]) 227 | 228 | def set_time(self,main_time=30,byo_yomi_time=30,byo_yomi_stones=1): 229 | self.write("time_settings "+str(main_time)+" "+str(byo_yomi_time)+" "+str(byo_yomi_stones)) 230 | answer=self.readline() 231 | try: 232 | if answer[0]=="=":return True 233 | else:return False 234 | except Exception, e: 235 | raise GRPException("GRPException in set_time()\nanswer='"+answer+"'\n"+unicode(e)) 236 | 237 | def quit(self): 238 | self.write("quit") 239 | answer=self.readline() 240 | if answer[0]=="=":return True 241 | else:return False 242 | 243 | def terminate(self): 244 | t=10 245 | while 1: 246 | self.quitting_thread.join(0.0) 247 | if not self.quitting_thread.is_alive(): 248 | log("The bot has quitted properly") 249 | break 250 | elif t==0: 251 | log("The bot is still running...") 252 | log("Forcefully closing it now!") 253 | break 254 | t-=1 255 | log("Waiting for the bot to close",t,"s") 256 | sleep(1) 257 | 258 | try: self.process.kill() 259 | except: pass 260 | try: self.process.stdin.close() 261 | except: pass 262 | 263 | def close(self): 264 | log("Now closing") 265 | self.quitting_thread=threading.Thread(target=self.quit) 266 | self.quitting_thread.start() 267 | threading.Thread(target=self.terminate).start() 268 | -------------------------------------------------------------------------------- /gtp_bot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from gtp import gtp 5 | from Tkinter import * 6 | from toolbox import * 7 | from toolbox import _ 8 | 9 | 10 | def gtpbot_starting_procedure(sgf_g,profile,silentfail=False): 11 | return bot_starting_procedure("GtpBot","GtpBot",GtpBot_gtp,sgf_g,profile,silentfail) 12 | 13 | 14 | class GtpBot_gtp(gtp): 15 | pass 16 | 17 | 18 | class GtpBotSettings(BotProfiles): 19 | def __init__(self,parent,bot="GTP bot"): 20 | Frame.__init__(self,parent) 21 | self.parent=parent 22 | self.bot=bot 23 | self.profiles=get_bot_profiles(bot,False) 24 | profiles_frame=self 25 | 26 | self.listbox = Listbox(profiles_frame) 27 | self.listbox.grid(column=10,row=10,rowspan=10) 28 | self.update_listbox() 29 | 30 | row=10 31 | Label(profiles_frame,text=_("Profile")).grid(row=row,column=11,sticky=W) 32 | self.profile = StringVar() 33 | Entry(profiles_frame, textvariable=self.profile, width=30).grid(row=row,column=12) 34 | 35 | row+=1 36 | Label(profiles_frame,text=_("Command")).grid(row=row,column=11,sticky=W) 37 | self.command = StringVar() 38 | Entry(profiles_frame, textvariable=self.command, width=30).grid(row=row,column=12) 39 | 40 | row+=1 41 | Label(profiles_frame,text=_("Parameters")).grid(row=row,column=11,sticky=W) 42 | self.parameters = StringVar() 43 | Entry(profiles_frame, textvariable=self.parameters, width=30).grid(row=row,column=12) 44 | 45 | row+=10 46 | buttons_frame=Frame(profiles_frame) 47 | buttons_frame.grid(row=row,column=10,sticky=W,columnspan=3) 48 | Button(buttons_frame, text=_("Add profile"),command=self.add_profile).grid(row=row,column=1,sticky=W) 49 | Button(buttons_frame, text=_("Modify profile"),command=self.modify_profile).grid(row=row,column=2,sticky=W) 50 | Button(buttons_frame, text=_("Delete profile"),command=self.delete_profile).grid(row=row,column=3,sticky=W) 51 | Button(buttons_frame, text=_("Test"),command=lambda: self.parent.parent.test(self.bot_gtp,self.command,self.parameters)).grid(row=row,column=4,sticky=W) 52 | self.listbox.bind("", lambda e: self.after(100,self.change_selection)) 53 | 54 | self.index=-1 55 | 56 | self.bot_gtp=GtpBot_gtp 57 | 58 | 59 | def clear_selection(self): 60 | self.index=-1 61 | self.profile.set("") 62 | self.command.set("") 63 | self.parameters.set("") 64 | 65 | def change_selection(self): 66 | try: 67 | index=int(self.listbox.curselection()[0]) 68 | self.index=index 69 | except: 70 | log("No selection") 71 | self.clear_selection() 72 | return 73 | data=self.profiles[index] 74 | self.profile.set(data["profile"]) 75 | self.command.set(data["command"]) 76 | self.parameters.set(data["parameters"]) 77 | 78 | def add_profile(self): 79 | profiles=self.profiles 80 | if self.profile.get()=="": 81 | return 82 | data={"bot":self.bot} 83 | data["profile"]=self.profile.get() 84 | data["command"]=self.command.get() 85 | data["parameters"]=self.parameters.get() 86 | 87 | self.empty_profiles() 88 | profiles.append(data) 89 | self.create_profiles() 90 | self.clear_selection() 91 | 92 | def modify_profile(self): 93 | profiles=self.profiles 94 | if self.profile.get()=="": 95 | return 96 | 97 | if self.index<0: 98 | log("No selection") 99 | return 100 | index=self.index 101 | 102 | profiles[index]["profile"]=self.profile.get() 103 | profiles[index]["command"]=self.command.get() 104 | profiles[index]["parameters"]=self.parameters.get() 105 | 106 | self.empty_profiles() 107 | self.create_profiles() 108 | self.clear_selection() 109 | 110 | 111 | class GtpBotOpenMove(BotOpenMove): 112 | def __init__(self,sgf_g,profile): 113 | BotOpenMove.__init__(self,sgf_g,profile) 114 | self.name='GtpBot' 115 | self.my_starting_procedure=gtpbot_starting_procedure 116 | 117 | 118 | GtpBot={} 119 | GtpBot['name']="GTP bot" 120 | GtpBot['gtp_name']="GtpBot" 121 | GtpBot['openmove']=GtpBotOpenMove 122 | GtpBot['settings']=GtpBotSettings 123 | GtpBot['gtp']=GtpBot_gtp 124 | GtpBot['starting']=gtpbot_starting_procedure 125 | -------------------------------------------------------------------------------- /gtp_terminal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from Tkinter import * 5 | import threading 6 | from toolbox import * 7 | from toolbox import _ 8 | 9 | class Terminal(Toplevel): 10 | def __init__(self,parent,bot_gtp,bot_command_line): 11 | Toplevel.__init__(self,parent) 12 | self.parent=parent 13 | self.protocol("WM_DELETE_WINDOW", self.close) 14 | try: 15 | self.bot=bot_gtp(bot_command_line) 16 | except Exception, e: 17 | self.close() 18 | show_error(_("Could not run the program:")+"\n"+unicode(e),self.parent) 19 | return 20 | 21 | threading.Thread(target=self.bot.consume_stdout).start() 22 | 23 | stdin_frame=Frame(self) 24 | stdin_frame.grid(row=1,column=1) 25 | 26 | self.gtp_command = StringVar() 27 | self.gtp_command.set("genmove black") 28 | entry=Entry(stdin_frame,textvariable=self.gtp_command) 29 | entry.grid(row=1,column=1,sticky=W) 30 | entry.bind("", self.send_gtp_command) 31 | 32 | Button(stdin_frame,text=_("Send GTP command"),command=self.send_gtp_command).grid(row=1,column=2,sticky=W) 33 | 34 | stdout_frame=Frame(self) 35 | stdout_frame.grid(row=2,column=1,sticky=N+S+E+W) 36 | self.stdout=Text(stdout_frame,width=60,height=10,bg="black",fg="white") 37 | self.stdout.pack(side="left", fill="both", expand=True) 38 | 39 | stderr_frame=Frame(self) 40 | stderr_frame.grid(row=3,column=1,sticky=N+S+E+W) 41 | self.stderr=Text(stderr_frame,width=60,height=20,bg="black",fg="white") 42 | self.stderr.pack(side="left", fill="both", expand=True) 43 | 44 | self.grid_columnconfigure(1, weight=1) 45 | self.grid_rowconfigure(2, weight=1) 46 | self.grid_rowconfigure(3, weight=1) 47 | 48 | self.follow_stdout() 49 | self.follow_stderr() 50 | 51 | 52 | def send_gtp_command(self,event=None): 53 | log("STDIN:",self.gtp_command.get()) 54 | self.bot.write(self.gtp_command.get()) 55 | 56 | def follow_stdout(self): 57 | try: 58 | msg=self.bot.stdout_queue.get(False) 59 | log("STDOUT:",msg) 60 | self.stdout.insert("end",msg) 61 | self.stdout.see("end") 62 | self.parent.after(10,self.follow_stdout) 63 | except: 64 | self.parent.after(500,self.follow_stdout) 65 | 66 | def follow_stderr(self): 67 | try: 68 | msg=self.bot.stderr_queue.get(False) 69 | log("STDERR:",msg) 70 | self.stderr.insert("end",msg) 71 | self.stderr.see("end") 72 | self.parent.after(10,self.follow_stderr) 73 | except: 74 | self.parent.after(500,self.follow_stderr) 75 | 76 | def close(self): 77 | log("closing popup") 78 | try: 79 | self.bot.close() 80 | except: 81 | pass 82 | try: 83 | self.destroy() 84 | self.parent.remove_popup(self) 85 | except: 86 | pass 87 | log("done") 88 | 89 | if __name__ == "__main__": 90 | from gnugo_analysis import * 91 | 92 | command=["gnugo","--mode=gtp"] 93 | 94 | top = Application() 95 | popup=Terminal(top,GnuGo_gtp,command) 96 | top.add_popup(popup) 97 | top.mainloop() 98 | 99 | -------------------------------------------------------------------------------- /icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnprog/goreviewpartner/cbcc486cd4c51fb6fc3bc0a1eab61ff34298dadf/icon.gif -------------------------------------------------------------------------------- /live_analysis/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import sys, os 5 | 6 | if (sys.version_info > (3, 0)): 7 | print("GoReviewPartner needs Python version 2 (e.g. 2.7.9) to run.") 8 | print("GoReviewPartner cannot work with verions 3 of Python at the moment.") 9 | print("Sorry about that :/") 10 | input() 11 | exit() 12 | 13 | print("STDIN encoding:",sys.stdin.encoding) 14 | print("STDOUT encoding:",sys.stdout.encoding) 15 | print("STDERR encoding:",sys.stderr.encoding) 16 | print("File system encoding:",sys.getfilesystemencoding()) 17 | 18 | try: 19 | from Tkinter import * 20 | except Exception: 21 | print("Could not import the Tkinter librairy, please double check it is installed.") 22 | raw_input() 23 | sys.exit() 24 | 25 | 26 | import dual_view 27 | import settings 28 | from toolbox import * 29 | from toolbox import _ 30 | from live_analysis import LiveAnalysisLauncher 31 | from r2sgf import rsgf2sgf 32 | from r2csv import rsgf2csv 33 | 34 | class Main(Toplevel): 35 | def __init__(self,parent): 36 | Toplevel.__init__(self,parent) 37 | self.parent=parent 38 | 39 | bg=self.cget("background") 40 | 41 | 42 | self.popups=[] 43 | 44 | self.control_frame = Frame(self) 45 | 46 | label = Label(self.control_frame, text=_("This is GoReviewPartner"), font="-weight bold") 47 | label.pack(padx=5, pady=5) 48 | 49 | self.analysis_bouton=Button(self.control_frame, text=_("Run a SGF file analysis"), command=self.launch_analysis) 50 | self.analysis_bouton.pack(fill=X,padx=5, pady=5) 51 | 52 | self.download_bouton=Button(self.control_frame, text=_("Download a SGF file for analysis"), command=self.download_sgf_for_review) 53 | self.download_bouton.pack(fill=X,padx=5, pady=5) 54 | 55 | self.live_bouton=Button(self.control_frame, text=_("Run a live analysis"), command=self.launch_live_analysis) 56 | self.live_bouton.pack(fill=X,padx=5, pady=5) 57 | 58 | review_bouton=Button(self.control_frame, text=_("Open a RSGF file for review"), command=self.launch_review) 59 | review_bouton.pack(fill=X,padx=5, pady=5) 60 | 61 | r2sgf_bouton=Button(self.control_frame, text=_("Convert RSGF file to SGF file"), command=self.r2sgf) 62 | r2sgf_bouton.pack(fill=X,padx=5, pady=5) 63 | 64 | r2csv_bouton=Button(self.control_frame, text=_("Convert RSGF file to CSV file"), command=self.r2csv) 65 | r2csv_bouton.pack(fill=X,padx=5, pady=5) 66 | 67 | bouton=Button(self.control_frame, text=_("Settings"), command=self.launch_settings) 68 | bouton.pack(fill=X,padx=5, pady=5) 69 | 70 | self.control_frame.pack(fill=X, side=BOTTOM) 71 | 72 | self.logo_frame = Frame(self) 73 | 74 | logo = Canvas(self.logo_frame,bg=bg,width=5,height=5) 75 | logo.pack(fill=BOTH, expand=1) 76 | logo.bind("",lambda e: draw_logo(logo,e)) 77 | self.logo_frame.pack(fill=BOTH, side=BOTTOM, expand=1) 78 | 79 | self.protocol("WM_DELETE_WINDOW", self.close) 80 | 81 | def r2sgf(self): 82 | filename = open_rsgf_file(parent=self.parent) 83 | if not filename: 84 | return 85 | rsgf2sgf(filename) 86 | show_info(_("The file %s has been converted to %s")%(os.path.basename(filename),os.path.basename(filename)+".sgf"),parent=self.parent) 87 | 88 | def r2csv(self): 89 | filename = open_rsgf_file(parent=self.parent) 90 | if not filename: 91 | return 92 | rsgf2csv(filename) 93 | show_info(_("The file %s has been converted to %s")%(os.path.basename(filename),os.path.basename(filename)+".csv"),parent=self.parent) 94 | 95 | def close(self): 96 | for popup in self.popups[:]: 97 | popup.close() 98 | log("closing Main") 99 | self.destroy() 100 | self.parent.remove_popup(self) 101 | 102 | def launch_analysis(self): 103 | filename = open_sgf_file(parent=self) 104 | if not filename: 105 | return 106 | 107 | log("filename:",filename) 108 | 109 | new_popup=RangeSelector(self.parent,filename,bots=get_available()) 110 | 111 | self.parent.add_popup(new_popup) 112 | 113 | def download_sgf_for_review(self): 114 | new_popup=DownloadFromURL(self.parent,bots=get_available()) 115 | self.parent.add_popup(new_popup) 116 | 117 | def launch_live_analysis(self): 118 | new_popup=LiveAnalysisLauncher(self.parent) 119 | self.parent.add_popup(new_popup) 120 | 121 | def launch_review(self): 122 | filename = open_rsgf_file(parent=self.parent) 123 | if not filename: 124 | return 125 | 126 | new_popup=dual_view.DualView(self.parent,filename) 127 | 128 | self.parent.add_popup(new_popup) 129 | 130 | def launch_settings(self): 131 | new_popup=settings.OpenSettings(self.parent,refresh=self.refresh) 132 | self.parent.add_popup(new_popup) 133 | 134 | def refresh(self): 135 | log("refreshing") 136 | if len(get_available())==0: 137 | self.analysis_bouton.config(state='disabled') 138 | self.download_bouton.config(state='disabled') 139 | self.live_bouton.config(state='disabled') 140 | else: 141 | self.analysis_bouton.config(state='normal') 142 | self.download_bouton.config(state='normal') 143 | self.live_bouton.config(state='normal') 144 | 145 | if len(get_available())==0: 146 | self.live_bouton.config(state='disabled') 147 | else: 148 | self.live_bouton.config(state='normal') 149 | 150 | 151 | if __name__ == "__main__": 152 | app = Application() 153 | if len(sys.argv)==1: 154 | popup=Main(app) 155 | popup.refresh() 156 | app.add_popup(popup) 157 | app.mainloop() 158 | 159 | else: 160 | 161 | opened=0 162 | for filename in argv[1:]: 163 | log(filename,"???") 164 | extension=filename[-4:].lower() 165 | log("extension",extension) 166 | if extension=="rsgf": 167 | opened+=1 168 | log("Opening",filename) 169 | popup=dual_view.DualView(app,filename) 170 | app.add_popup(popup) 171 | elif extension==".sgf": 172 | log("Opening",filename) 173 | popup=RangeSelector(app,filename,bots=get_available()) 174 | app.add_popup(popup) 175 | opened+=1 176 | 177 | if not opened: 178 | popup=Main(app) 179 | popup.refresh() 180 | app.add_popup(popup) 181 | 182 | 183 | app.mainloop() 184 | 185 | -------------------------------------------------------------------------------- /mss/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | An ultra fast cross-platform multiple screenshots module in pure python 4 | using ctypes. 5 | 6 | This module is maintained by Mickaël Schoentgen . 7 | 8 | You can always get the latest version of this module at: 9 | https://github.com/BoboTiG/python-mss 10 | If that URL should fail, try contacting the author. 11 | """ 12 | 13 | from .exception import ScreenShotError 14 | from .factory import mss 15 | 16 | __version__ = '3.1.2' 17 | __author__ = "Mickaël 'Tiger-222' Schoentgen" 18 | __copyright__ = """ 19 | Copyright (c) 2013-2018, Mickaël 'Tiger-222' Schoentgen 20 | 21 | Permission to use, copy, modify, and distribute this software and its 22 | documentation for any purpose and without fee or royalty is hereby 23 | granted, provided that the above copyright notice appear in all copies 24 | and that both that copyright notice and this permission notice appear 25 | in supporting documentation or portions thereof, including 26 | modifications, that you make. 27 | """ 28 | __all__ = ('ScreenShotError', 'mss') 29 | -------------------------------------------------------------------------------- /mss/__main__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | This is part of the MSS Python's module. 4 | Source: https://github.com/BoboTiG/python-mss 5 | """ 6 | 7 | from __future__ import print_function 8 | 9 | import os.path 10 | import sys 11 | from argparse import ArgumentParser 12 | 13 | from . import __version__ 14 | from .exception import ScreenShotError 15 | from .factory import mss 16 | from .tools import to_png 17 | 18 | 19 | def main(args=None): 20 | # type: (Optional[List[str]]) -> int 21 | """ Main logic. """ 22 | 23 | cli_args = ArgumentParser() 24 | cli_args.add_argument('-c', '--coordinates', default='', type=str, 25 | help='the part of the screen to capture:' 26 | ' top, left, width, height') 27 | cli_args.add_argument('-m', '--monitor', default=0, type=int, 28 | help='the monitor to screen shot') 29 | cli_args.add_argument('-o', '--output', default='monitor-{mon}.png', 30 | help='the output file name') 31 | cli_args.add_argument('-q', '--quiet', default=False, action='store_true', 32 | help='do not print created files') 33 | cli_args.add_argument('-v', '--version', action='version', 34 | version=__version__) 35 | 36 | options = cli_args.parse_args(args) 37 | kwargs = { 38 | 'mon': options.monitor, 39 | 'output': options.output, 40 | } 41 | if options.coordinates: 42 | try: 43 | top, left, width, height = options.coordinates.split(',') 44 | except ValueError: 45 | print('Coordinates syntax: top, left, width, height') 46 | return 2 47 | 48 | kwargs['mon'] = { 49 | 'top': int(top), 50 | 'left': int(left), 51 | 'width': int(width), 52 | 'height': int(height), 53 | } 54 | if options.output == 'monitor-{mon}.png': 55 | kwargs['output'] = 'sct-{top}x{left}_{width}x{height}.png' 56 | 57 | try: 58 | with mss() as sct: 59 | if options.coordinates: 60 | output = kwargs['output'].format(**kwargs['mon']) 61 | sct_img = sct.grab(kwargs['mon']) 62 | to_png(sct_img.rgb, sct_img.size, output) 63 | if not options.quiet: 64 | print(os.path.realpath(output)) 65 | else: 66 | for file_name in sct.save(**kwargs): 67 | if not options.quiet: 68 | print(os.path.realpath(file_name)) 69 | return 0 70 | except ScreenShotError: 71 | return 1 72 | 73 | 74 | if __name__ == '__main__': 75 | exit(main(sys.argv[1:])) 76 | -------------------------------------------------------------------------------- /mss/base.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | This is part of the MSS Python's module. 4 | Source: https://github.com/BoboTiG/python-mss 5 | """ 6 | 7 | from datetime import datetime 8 | 9 | from .exception import ScreenShotError 10 | from .screenshot import ScreenShot 11 | from .tools import to_png 12 | 13 | 14 | class MSSBase(object): 15 | """ This class will be overloaded by a system specific one. """ 16 | 17 | cls_image = ScreenShot # type: object 18 | _monitors = [] # type: List[Dict[str, int]] 19 | 20 | def __enter__(self): 21 | # type: () -> MSSBase 22 | """ For the cool call `with MSS() as mss:`. """ 23 | 24 | return self 25 | 26 | def __exit__(self, *_): 27 | # type: (*str) -> None 28 | """ For the cool call `with MSS() as mss:`. """ 29 | 30 | def grab(self, monitor): 31 | # type: (Dict[str, int]) -> ScreenShot 32 | """ 33 | Retrieve screen pixels for a given monitor. 34 | 35 | :param monitor: The coordinates and size of the box to capture. 36 | See :meth:`monitors ` for object details. 37 | :return :class:`ScreenShot `. 38 | """ 39 | 40 | raise NotImplementedError('Subclasses need to implement this!') 41 | 42 | @property 43 | def monitors(self): 44 | # type: () -> List[Dict[str, int]] 45 | """ 46 | Get positions of all monitors. 47 | If the monitor has rotation, you have to deal with it 48 | inside this method. 49 | 50 | This method has to fill self._monitors with all informations 51 | and use it as a cache: 52 | self._monitors[0] is a dict of all monitors together 53 | self._monitors[N] is a dict of the monitor N (with N > 0) 54 | 55 | Each monitor is a dict with: 56 | { 57 | 'left': the x-coordinate of the upper-left corner, 58 | 'top': the y-coordinate of the upper-left corner, 59 | 'width': the width, 60 | 'height': the height 61 | } 62 | 63 | Note: monitor can be a tuple like PIL.Image.grab() accepts, 64 | it must be converted to the appropriate dict. 65 | """ 66 | 67 | raise NotImplementedError('Subclasses need to implement this!') 68 | 69 | def save(self, mon=0, output='monitor-{mon}.png', callback=None): 70 | # type: (int, str, Callable[[str], None]) -> Iterator[str] 71 | """ 72 | Grab a screen shot and save it to a file. 73 | 74 | :param int mon: The monitor to screen shot (default=0). 75 | -1: grab one screen shot of all monitors 76 | 0: grab one screen shot by monitor 77 | N: grab the screen shot of the monitor N 78 | 79 | :param str output: The output filename. 80 | 81 | It can take several keywords to customize the filename: 82 | - `{mon}`: the monitor number 83 | - `{top}`: the screen shot y-coordinate of the upper-left corner 84 | - `{left}`: the screen shot x-coordinate of the upper-left corner 85 | - `{width}`: the screen shot's width 86 | - `{height}`: the screen shot's height 87 | - `{date}`: the current date using the default formatter 88 | 89 | As it is using the `format()` function, you can specify 90 | formatting options like `{date:%Y-%m-%s}`. 91 | 92 | :param callable callback: Callback called before saving the 93 | screen shot to a file. Take the `output` argument as parameter. 94 | 95 | :return generator: Created file(s). 96 | """ 97 | 98 | monitors = self.monitors 99 | if not monitors: 100 | raise ScreenShotError('No monitor found.') 101 | 102 | if mon == 0: 103 | # One screen shot by monitor 104 | for idx, monitor in enumerate(monitors[1:], 1): 105 | fname = output.format(mon=idx, date=datetime.now(), **monitor) 106 | if callable(callback): 107 | callback(fname) 108 | sct = self.grab(monitor) 109 | to_png(sct.rgb, sct.size, fname) 110 | yield fname 111 | else: 112 | # A screen shot of all monitors together or 113 | # a screen shot of the monitor N. 114 | mon = 0 if mon == -1 else mon 115 | try: 116 | monitor = monitors[mon] 117 | except IndexError: 118 | raise ScreenShotError('Monitor does not exist.', locals()) 119 | 120 | output = output.format(mon=mon, date=datetime.now(), **monitor) 121 | if callable(callback): 122 | callback(output) 123 | sct = self.grab(monitor) 124 | to_png(sct.rgb, sct.size, output) 125 | yield output 126 | 127 | def shot(self, **kwargs): 128 | """ 129 | Helper to save the screen shot of the 1st monitor, by default. 130 | You can pass the same arguments as for ``save``. 131 | """ 132 | 133 | kwargs['mon'] = kwargs.get('mon', 1) 134 | return next(self.save(**kwargs)) 135 | -------------------------------------------------------------------------------- /mss/darwin.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | This is part of the MSS Python's module. 4 | Source: https://github.com/BoboTiG/python-mss 5 | """ 6 | 7 | # pylint: disable=import-error, too-many-locals 8 | 9 | from __future__ import division 10 | 11 | import ctypes 12 | import ctypes.util 13 | import sys 14 | 15 | from .base import MSSBase 16 | from .exception import ScreenShotError 17 | from .screenshot import Size 18 | 19 | __all__ = ('MSS',) 20 | 21 | 22 | def cgfloat(): 23 | # type: () -> Any 24 | """ Get the appropriate value for a float. """ 25 | 26 | return ctypes.c_double if sys.maxsize > 2 ** 32 else ctypes.c_float 27 | 28 | 29 | class CGPoint(ctypes.Structure): 30 | """ Structure that contains coordinates of a rectangle. """ 31 | 32 | _fields_ = [('x', cgfloat()), ('y', cgfloat())] 33 | 34 | def __repr__(self): 35 | return '{0}(left={1} top={2})'.format( 36 | type(self).__name__, self.x, self.y) 37 | 38 | 39 | class CGSize(ctypes.Structure): 40 | """ Structure that contains dimensions of an rectangle. """ 41 | 42 | _fields_ = [('width', cgfloat()), ('height', cgfloat())] 43 | 44 | def __repr__(self): 45 | return '{0}(width={1} height={2})'.format( 46 | type(self).__name__, self.width, self.height) 47 | 48 | 49 | class CGRect(ctypes.Structure): 50 | """ Structure that contains informations about a rectangle. """ 51 | 52 | _fields_ = [('origin', CGPoint), ('size', CGSize)] 53 | 54 | def __repr__(self): 55 | return '{0}<{1} {2}>'.format( 56 | type(self).__name__, self.origin, self.size) 57 | 58 | 59 | class MSS(MSSBase): 60 | """ 61 | Multiple ScreenShots implementation for macOS. 62 | It uses intensively the CoreGraphics library. 63 | """ 64 | 65 | max_displays = 32 # type: int 66 | 67 | def __init__(self): 68 | # type: () -> None 69 | """ macOS initialisations. """ 70 | 71 | coregraphics = ctypes.util.find_library('CoreGraphics') 72 | if not coregraphics: 73 | raise ScreenShotError('No CoreGraphics library found.', locals()) 74 | self.core = ctypes.cdll.LoadLibrary(coregraphics) 75 | 76 | self._set_argtypes() 77 | self._set_restypes() 78 | 79 | def _set_argtypes(self): 80 | # type: () -> None 81 | """ Functions arguments. """ 82 | 83 | self.core.CGGetActiveDisplayList.argtypes = [ 84 | ctypes.c_uint32, 85 | ctypes.POINTER(ctypes.c_uint32), 86 | ctypes.POINTER(ctypes.c_uint32)] 87 | self.core.CGDisplayBounds.argtypes = [ctypes.c_uint32] 88 | self.core.CGRectStandardize.argtypes = [CGRect] 89 | self.core.CGRectUnion.argtypes = [CGRect, CGRect] 90 | self.core.CGDisplayRotation.argtypes = [ctypes.c_uint32] 91 | self.core.CGWindowListCreateImage.argtypes = [ 92 | CGRect, 93 | ctypes.c_uint32, 94 | ctypes.c_uint32, 95 | ctypes.c_uint32] 96 | self.core.CGImageGetWidth.argtypes = [ctypes.c_void_p] 97 | self.core.CGImageGetHeight.argtypes = [ctypes.c_void_p] 98 | self.core.CGImageGetDataProvider.argtypes = [ctypes.c_void_p] 99 | self.core.CGDataProviderCopyData.argtypes = [ctypes.c_void_p] 100 | self.core.CFDataGetBytePtr.argtypes = [ctypes.c_void_p] 101 | self.core.CFDataGetLength.argtypes = [ctypes.c_void_p] 102 | self.core.CGImageGetBytesPerRow.argtypes = [ctypes.c_void_p] 103 | self.core.CGImageGetBitsPerPixel.argtypes = [ctypes.c_void_p] 104 | self.core.CGDataProviderRelease.argtypes = [ctypes.c_void_p] 105 | self.core.CFRelease.argtypes = [ctypes.c_void_p] 106 | 107 | def _set_restypes(self): 108 | # type: () -> None 109 | """ Functions return type. """ 110 | 111 | self.core.CGGetActiveDisplayList.restype = ctypes.c_int32 112 | self.core.CGDisplayBounds.restype = CGRect 113 | self.core.CGRectStandardize.restype = CGRect 114 | self.core.CGRectUnion.restype = CGRect 115 | self.core.CGDisplayRotation.restype = ctypes.c_float 116 | self.core.CGWindowListCreateImage.restype = ctypes.c_void_p 117 | self.core.CGImageGetWidth.restype = ctypes.c_size_t 118 | self.core.CGImageGetHeight.restype = ctypes.c_size_t 119 | self.core.CGImageGetDataProvider.restype = ctypes.c_void_p 120 | self.core.CGDataProviderCopyData.restype = ctypes.c_void_p 121 | self.core.CFDataGetBytePtr.restype = ctypes.c_void_p 122 | self.core.CFDataGetLength.restype = ctypes.c_uint64 123 | self.core.CGImageGetBytesPerRow.restype = ctypes.c_size_t 124 | self.core.CGImageGetBitsPerPixel.restype = ctypes.c_size_t 125 | self.core.CGDataProviderRelease.restype = ctypes.c_void_p 126 | self.core.CFRelease.restype = ctypes.c_void_p 127 | 128 | @property 129 | def monitors(self): 130 | # type: () -> List[Dict[str, int]] 131 | """ Get positions of monitors (see parent class). """ 132 | 133 | if not self._monitors: 134 | # All monitors 135 | # We need to update the value with every single monitor found 136 | # using CGRectUnion. Else we will end with infinite values. 137 | all_monitors = CGRect() 138 | self._monitors.append({}) 139 | 140 | # Each monitors 141 | display_count = ctypes.c_uint32(0) 142 | active_displays = (ctypes.c_uint32 * self.max_displays)() 143 | self.core.CGGetActiveDisplayList(self.max_displays, 144 | active_displays, 145 | ctypes.byref(display_count)) 146 | rotations = {0.0: 'normal', 90.0: 'right', -90.0: 'left'} 147 | for idx in range(display_count.value): 148 | display = active_displays[idx] 149 | rect = self.core.CGDisplayBounds(display) 150 | rect = self.core.CGRectStandardize(rect) 151 | width, height = rect.size.width, rect.size.height 152 | rot = self.core.CGDisplayRotation(display) 153 | if rotations[rot] in ['left', 'right']: 154 | width, height = height, width 155 | self._monitors.append({ 156 | 'left': int(rect.origin.x), 157 | 'top': int(rect.origin.y), 158 | 'width': int(width), 159 | 'height': int(height), 160 | }) 161 | 162 | # Update AiO monitor's values 163 | all_monitors = self.core.CGRectUnion(all_monitors, rect) 164 | 165 | # Set the AiO monitor's values 166 | self._monitors[0] = { 167 | 'left': int(all_monitors.origin.x), 168 | 'top': int(all_monitors.origin.y), 169 | 'width': int(all_monitors.size.width), 170 | 'height': int(all_monitors.size.height), 171 | } 172 | 173 | return self._monitors 174 | 175 | def grab(self, monitor): 176 | # type: (Dict[str, int]) -> ScreenShot 177 | """ 178 | See :meth:`MSSBase.grab ` for full details. 179 | """ 180 | 181 | # Convert PIL bbox style 182 | if isinstance(monitor, tuple): 183 | monitor = { 184 | 'left': monitor[0], 185 | 'top': monitor[1], 186 | 'width': monitor[2] - monitor[0], 187 | 'height': monitor[3] - monitor[1], 188 | } 189 | 190 | rect = CGRect((monitor['left'], monitor['top']), 191 | (monitor['width'], monitor['height'])) 192 | 193 | image_ref = self.core.CGWindowListCreateImage(rect, 1, 0, 0) 194 | if not image_ref: 195 | raise ScreenShotError( 196 | 'CoreGraphics.CGWindowListCreateImage() failed.', locals()) 197 | 198 | width = int(self.core.CGImageGetWidth(image_ref)) 199 | height = int(self.core.CGImageGetHeight(image_ref)) 200 | prov = copy_data = None 201 | try: 202 | prov = self.core.CGImageGetDataProvider(image_ref) 203 | copy_data = self.core.CGDataProviderCopyData(prov) 204 | data_ref = self.core.CFDataGetBytePtr(copy_data) 205 | buf_len = self.core.CFDataGetLength(copy_data) 206 | raw = ctypes.cast( 207 | data_ref, ctypes.POINTER(ctypes.c_ubyte * buf_len)) 208 | data = bytearray(raw.contents) 209 | 210 | # Remove padding per row 211 | bytes_per_row = int(self.core.CGImageGetBytesPerRow(image_ref)) 212 | bytes_per_pixel = int(self.core.CGImageGetBitsPerPixel(image_ref)) 213 | bytes_per_pixel = (bytes_per_pixel + 7) // 8 214 | 215 | if bytes_per_pixel * width != bytes_per_row: 216 | cropped = bytearray() 217 | for row in range(height): 218 | start = row * bytes_per_row 219 | end = start + width * bytes_per_pixel 220 | cropped.extend(data[start:end]) 221 | data = cropped 222 | finally: 223 | if prov: 224 | self.core.CGDataProviderRelease(prov) 225 | if copy_data: 226 | self.core.CFRelease(copy_data) 227 | 228 | return self.cls_image(data, monitor, size=Size(width, height)) 229 | -------------------------------------------------------------------------------- /mss/exception.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | This is part of the MSS Python's module. 4 | Source: https://github.com/BoboTiG/python-mss 5 | """ 6 | 7 | 8 | class ScreenShotError(Exception): 9 | """ Error handling class. """ 10 | -------------------------------------------------------------------------------- /mss/factory.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | This is part of the MSS Python's module. 4 | Source: https://github.com/BoboTiG/python-mss 5 | """ 6 | 7 | import platform 8 | 9 | from .exception import ScreenShotError 10 | 11 | 12 | def mss(**kwargs): 13 | # type: (**str) -> MSS 14 | """ Factory returning a proper MSS class instance. 15 | 16 | It detects the plateform we are running on 17 | and choose the most adapted mss_class to take 18 | screenshots. 19 | 20 | It then proxies its arguments to the class for 21 | instantiation. 22 | """ 23 | 24 | operating_system = platform.system().lower() 25 | if operating_system == 'darwin': 26 | from .darwin import MSS 27 | elif operating_system == 'linux': 28 | from .linux import MSS 29 | elif operating_system == 'windows': 30 | from .windows import MSS 31 | else: 32 | raise ScreenShotError('System not (yet?) implemented.', locals()) 33 | 34 | return MSS(**kwargs) 35 | -------------------------------------------------------------------------------- /mss/screenshot.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | This is part of the MSS Python's module. 4 | Source: https://github.com/BoboTiG/python-mss 5 | """ 6 | 7 | import collections 8 | 9 | from .exception import ScreenShotError 10 | 11 | 12 | Pos = collections.namedtuple('Pos', 'left, top') 13 | Size = collections.namedtuple('Size', 'width, height') 14 | 15 | 16 | class ScreenShot(object): 17 | """ 18 | Screen shot object. 19 | 20 | .. note:: 21 | 22 | A better name would have been *Image*, but to prevent collisions 23 | with PIL.Image, it has been decided to use *ScreenShot*. 24 | """ 25 | 26 | __pixels = None # type: List[Tuple[int, int, int]] 27 | __rgb = None # type: bytes 28 | 29 | def __init__(self, data, monitor, size=None): 30 | # type: (bytearray, Dict[str, int], Any) -> None 31 | #: Bytearray of the raw BGRA pixels retrieved by ctype 32 | #: OS independent implementations. 33 | self.raw = bytearray(data) # type: bytearray 34 | 35 | #: NamedTuple of the screen shot coordinates. 36 | self.pos = Pos(monitor['left'], monitor['top']) # type: Pos 37 | 38 | if size is not None: 39 | #: NamedTuple of the screen shot size. 40 | self.size = size # type: Size 41 | else: 42 | self.size = Size(monitor['width'], monitor['height']) # type: Size 43 | 44 | def __repr__(self): 45 | # type: () -> str 46 | return ('<{!s}' 47 | ' pos={cls.left},{cls.top}' 48 | ' size={cls.width}x{cls.height}' 49 | '>').format(type(self).__name__, cls=self) 50 | 51 | @property 52 | def __array_interface__(self): 53 | # type: () -> Dict[str, Any] 54 | """ 55 | Numpy array interface support. 56 | It uses raw data in BGRA form. 57 | 58 | See https://docs.scipy.org/doc/numpy/reference/arrays.interface.html 59 | """ 60 | 61 | return { 62 | 'version': 3, 63 | 'shape': (self.height, self.width, 4), 64 | 'typestr': '|u1', 65 | 'data': self.raw, 66 | } 67 | 68 | @classmethod 69 | def from_size(cls, data, width, height): 70 | # type: (bytearray, int, int) -> ScreenShot 71 | """ Instantiate a new class given only screen shot's data and size. """ 72 | 73 | monitor = {'left': 0, 'top': 0, 'width': width, 'height': height} 74 | return cls(data, monitor) 75 | 76 | @property 77 | def top(self): 78 | # type: () -> int 79 | """ Convenient accessor to the top position. """ 80 | return self.pos.top 81 | 82 | @property 83 | def left(self): 84 | # type: () -> int 85 | """ Convenient accessor to the left position. """ 86 | return self.pos.left 87 | 88 | @property 89 | def width(self): 90 | # type: () -> int 91 | """ Convenient accessor to the width size. """ 92 | return self.size.width 93 | 94 | @property 95 | def height(self): 96 | # type: () -> int 97 | """ Convenient accessor to the height size. """ 98 | return self.size.height 99 | 100 | @property 101 | def pixels(self): 102 | # type: () -> List[Tuple[int, int, int]] 103 | """ 104 | :return list: RGB tuples. 105 | """ 106 | 107 | if not self.__pixels: 108 | rgb_tuples = zip(self.raw[2::4], self.raw[1::4], self.raw[0::4]) 109 | self.__pixels = list(zip(*[iter(rgb_tuples)] * self.width)) 110 | 111 | return self.__pixels 112 | 113 | def pixel(self, coord_x, coord_y): 114 | # type: (int, int) -> Tuple[int, int, int] 115 | """ 116 | Returns the pixel value at a given position. 117 | 118 | :param int coord_x: The x coordinate. 119 | :param int coord_y: The y coordinate. 120 | :return tuple: The pixel value as (R, G, B). 121 | """ 122 | 123 | try: 124 | return self.pixels[coord_y][coord_x] 125 | except IndexError: 126 | raise ScreenShotError('Pixel location out of range.', locals()) 127 | 128 | @property 129 | def rgb(self): 130 | # type: () -> bytes 131 | """ 132 | Compute RGB values from the BGRA raw pixels. 133 | 134 | :return bytes: RGB pixels. 135 | """ 136 | 137 | if not self.__rgb: 138 | rgb = bytearray(self.height * self.width * 3) 139 | rgb[0::3], rgb[1::3], rgb[2::3] = \ 140 | self.raw[2::4], self.raw[1::4], self.raw[0::4] 141 | self.__rgb = bytes(rgb) 142 | 143 | return self.__rgb 144 | -------------------------------------------------------------------------------- /mss/tools.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | This is part of the MSS Python's module. 4 | Source: https://github.com/BoboTiG/python-mss 5 | """ 6 | 7 | import struct 8 | import zlib 9 | 10 | 11 | def to_png(data, size, output=None): 12 | # type: (bytes, Tuple[int, int], Optional[str]) -> Union[None, bytes] 13 | """ 14 | Dump data to a PNG file. If `output` is `None`, create no file but return 15 | the whole PNG data. 16 | 17 | :param bytes data: RGBRGB...RGB data. 18 | :param tuple size: The (width, height) pair. 19 | :param str output: Output file name. 20 | """ 21 | 22 | width, height = size 23 | line = width * 3 24 | png_filter = struct.pack('>B', 0) 25 | scanlines = b''.join( 26 | [png_filter + data[y * line:y * line + line] 27 | for y in range(height)]) 28 | 29 | magic = struct.pack('>8B', 137, 80, 78, 71, 13, 10, 26, 10) 30 | 31 | # Header: size, marker, data, CRC32 32 | ihdr = [b'', b'IHDR', b'', b''] 33 | ihdr[2] = struct.pack('>2I5B', width, height, 8, 2, 0, 0, 0) 34 | ihdr[3] = struct.pack('>I', zlib.crc32(b''.join(ihdr[1:3])) & 0xffffffff) 35 | ihdr[0] = struct.pack('>I', len(ihdr[2])) 36 | 37 | # Data: size, marker, data, CRC32 38 | idat = [b'', b'IDAT', zlib.compress(scanlines), b''] 39 | idat[3] = struct.pack('>I', zlib.crc32(b''.join(idat[1:3])) & 0xffffffff) 40 | idat[0] = struct.pack('>I', len(idat[2])) 41 | 42 | # Footer: size, marker, None, CRC32 43 | iend = [b'', b'IEND', b'', b''] 44 | iend[3] = struct.pack('>I', zlib.crc32(iend[1]) & 0xffffffff) 45 | iend[0] = struct.pack('>I', len(iend[2])) 46 | 47 | if not output: 48 | # Returns raw bytes of the whole PNG data 49 | return magic + b''.join(ihdr + idat + iend) 50 | 51 | with open(output, 'wb') as fileh: 52 | fileh.write(magic) 53 | fileh.write(b''.join(ihdr)) 54 | fileh.write(b''.join(idat)) 55 | fileh.write(b''.join(iend)) 56 | 57 | return None 58 | -------------------------------------------------------------------------------- /mss/windows.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | This is part of the MSS Python's module. 4 | Source: https://github.com/BoboTiG/python-mss 5 | """ 6 | 7 | from __future__ import division 8 | 9 | import ctypes 10 | import ctypes.wintypes 11 | 12 | from .base import MSSBase 13 | from .exception import ScreenShotError 14 | 15 | __all__ = ('MSS',) 16 | 17 | 18 | CAPTUREBLT = 0x40000000 19 | DIB_RGB_COLORS = 0 20 | SRCCOPY = 0x00CC0020 21 | 22 | 23 | class BITMAPINFOHEADER(ctypes.Structure): 24 | """ Information about the dimensions and color format of a DIB. """ 25 | 26 | _fields_ = [('biSize', ctypes.wintypes.DWORD), 27 | ('biWidth', ctypes.wintypes.LONG), 28 | ('biHeight', ctypes.wintypes.LONG), 29 | ('biPlanes', ctypes.wintypes.WORD), 30 | ('biBitCount', ctypes.wintypes.WORD), 31 | ('biCompression', ctypes.wintypes.DWORD), 32 | ('biSizeImage', ctypes.wintypes.DWORD), 33 | ('biXPelsPerMeter', ctypes.wintypes.LONG), 34 | ('biYPelsPerMeter', ctypes.wintypes.LONG), 35 | ('biClrUsed', ctypes.wintypes.DWORD), 36 | ('biClrImportant', ctypes.wintypes.DWORD)] 37 | 38 | 39 | class BITMAPINFO(ctypes.Structure): 40 | """ 41 | Structure that defines the dimensions and color information for a DIB. 42 | """ 43 | 44 | _fields_ = [('bmiHeader', BITMAPINFOHEADER), 45 | ('bmiColors', ctypes.wintypes.DWORD * 3)] 46 | 47 | 48 | class MSS(MSSBase): 49 | """ Multiple ScreenShots implementation for Microsoft Windows. """ 50 | 51 | __scale_factor = None # type: float 52 | 53 | def __init__(self): 54 | # type: () -> None 55 | """ Windows initialisations. """ 56 | 57 | self.monitorenumproc = ctypes.WINFUNCTYPE( 58 | ctypes.wintypes.INT, 59 | ctypes.wintypes.DWORD, 60 | ctypes.wintypes.DWORD, 61 | ctypes.POINTER(ctypes.wintypes.RECT), 62 | ctypes.wintypes.DOUBLE 63 | ) 64 | set_argtypes(self.monitorenumproc) 65 | set_restypes() 66 | 67 | @property 68 | def scale_factor(self): 69 | """ Compute the scale factor. """ 70 | 71 | if not self.__scale_factor: 72 | display = None 73 | try: 74 | display = ctypes.windll.user32.GetWindowDC(0) 75 | width = ctypes.windll.gdi32.GetDeviceCaps(display, 8) 76 | width_orig = ctypes.windll.gdi32.GetDeviceCaps(display, 118) 77 | scale = (100 + 100 - width * 100 // width_orig) / 100 78 | self.__scale_factor = round(scale * 4) / 4 79 | finally: 80 | if display: 81 | ctypes.windll.gdi32.DeleteObject(display) 82 | 83 | return self.__scale_factor 84 | 85 | def scale(self, value): 86 | # type: (float) -> int 87 | """ Compute a monitor value at scale, rounded to 2. """ 88 | 89 | if self.scale_factor == 1.0: 90 | return int(value) 91 | return int(value * self.scale_factor * 2 + 0.5) // 2 92 | 93 | @property 94 | def monitors(self): 95 | # type: () -> List[Dict[str, int]] 96 | """ Get positions of monitors (see parent class). """ 97 | 98 | if not self._monitors: 99 | # All monitors 100 | sm_xvirtualscreen, sm_yvirtualscreen = 76, 77 101 | sm_cxvirtualscreen, sm_cyvirtualscreen = 78, 79 102 | left = ctypes.windll.user32.GetSystemMetrics(sm_xvirtualscreen) 103 | right = ctypes.windll.user32.GetSystemMetrics(sm_cxvirtualscreen) 104 | top = ctypes.windll.user32.GetSystemMetrics(sm_yvirtualscreen) 105 | bottom = ctypes.windll.user32.GetSystemMetrics(sm_cyvirtualscreen) 106 | self._monitors.append({ 107 | 'left': self.scale(left), 108 | 'top': self.scale(top), 109 | 'width': self.scale(right - left), 110 | 'height': self.scale(bottom - top), 111 | 'scale': self.scale_factor, 112 | }) 113 | 114 | # Each monitors 115 | def _callback(monitor, data, rect, dc_): 116 | # type: (Any, Any, Any, float) -> int 117 | """ 118 | Callback for monitorenumproc() function, it will return 119 | a RECT with appropriate values. 120 | """ 121 | 122 | del monitor, data, dc_ 123 | rct = rect.contents 124 | self._monitors.append({ 125 | 'left': self.scale(rct.left), 126 | 'top': self.scale(rct.top), 127 | 'width': self.scale(rct.right - rct.left), 128 | 'height': self.scale(rct.bottom - rct.top), 129 | 'scale': self.scale_factor, 130 | }) 131 | return 1 132 | 133 | callback = self.monitorenumproc(_callback) 134 | ctypes.windll.user32.EnumDisplayMonitors(0, 0, callback, 0) 135 | 136 | return self._monitors 137 | 138 | def grab(self, monitor): 139 | # type: (Dict[str, int]) -> ScreenShot 140 | """ Retrieve all pixels from a monitor. Pixels have to be RGB. 141 | 142 | In the code, there are few interesting things: 143 | 144 | [1] bmi.bmiHeader.biHeight = -height 145 | 146 | A bottom-up DIB is specified by setting the height to a 147 | positive number, while a top-down DIB is specified by 148 | setting the height to a negative number. 149 | https://msdn.microsoft.com/en-us/library/ms787796.aspx 150 | https://msdn.microsoft.com/en-us/library/dd144879%28v=vs.85%29.aspx 151 | 152 | 153 | [2] bmi.bmiHeader.biBitCount = 32 154 | image_data = create_string_buffer(height * width * 4) 155 | 156 | We grab the image in RGBX mode, so that each word is 32bit 157 | and we have no striding, then we transform to RGB. 158 | Inspired by https://github.com/zoofIO/flexx 159 | 160 | 161 | [3] bmi.bmiHeader.biClrUsed = 0 162 | bmi.bmiHeader.biClrImportant = 0 163 | 164 | When biClrUsed and biClrImportant are set to zero, there 165 | is "no" color table, so we can read the pixels of the bitmap 166 | retrieved by gdi32.GetDIBits() as a sequence of RGB values. 167 | Thanks to http://stackoverflow.com/a/3688682 168 | """ 169 | 170 | # Convert PIL bbox style 171 | if isinstance(monitor, tuple): 172 | monitor = { 173 | 'left': monitor[0], 174 | 'top': monitor[1], 175 | 'width': monitor[2] - monitor[0], 176 | 'height': monitor[3] - monitor[1], 177 | } 178 | 179 | srcdc = memdc = bmp = None 180 | try: 181 | bmi = BITMAPINFO() 182 | bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER) 183 | bmi.bmiHeader.biWidth = monitor['width'] 184 | bmi.bmiHeader.biHeight = -monitor['height'] # Why minus? See [1] 185 | bmi.bmiHeader.biPlanes = 1 # Always 1 186 | bmi.bmiHeader.biBitCount = 32 # See [2] 187 | bmi.bmiHeader.biCompression = 0 # 0 = BI_RGB (no compression) 188 | bmi.bmiHeader.biClrUsed = 0 # See [3] 189 | bmi.bmiHeader.biClrImportant = 0 # See [3] 190 | 191 | buf_len = monitor['width'] * monitor['height'] * 4 # See [2] 192 | data = ctypes.create_string_buffer(buf_len) 193 | srcdc = ctypes.windll.user32.GetWindowDC(0) 194 | 195 | memdc = ctypes.windll.gdi32.CreateCompatibleDC(srcdc) 196 | bmp = ctypes.windll.gdi32.CreateCompatibleBitmap( 197 | srcdc, monitor['width'], monitor['height']) 198 | 199 | ctypes.windll.gdi32.SelectObject(memdc, bmp) 200 | ctypes.windll.gdi32.BitBlt(memdc, 0, 0, 201 | monitor['width'], monitor['height'], 202 | srcdc, 203 | monitor['left'], monitor['top'], 204 | SRCCOPY | CAPTUREBLT) 205 | 206 | bits = ctypes.windll.gdi32.GetDIBits( 207 | memdc, bmp, 0, monitor['height'], data, bmi, DIB_RGB_COLORS) 208 | if bits != monitor['height']: 209 | del data 210 | raise ScreenShotError('gdi32.GetDIBits() failed.', locals()) 211 | finally: 212 | # Clean up 213 | if srcdc: 214 | ctypes.windll.gdi32.DeleteObject(srcdc) 215 | if memdc: 216 | ctypes.windll.gdi32.DeleteObject(memdc) 217 | if bmp: 218 | ctypes.windll.gdi32.DeleteObject(bmp) 219 | 220 | return self.cls_image(data, monitor) 221 | 222 | 223 | def set_argtypes(callback): 224 | # type: (Callable[[int, Any, Any, Any, float], int]) -> None 225 | """ Functions arguments. """ 226 | 227 | ctypes.windll.user32.GetSystemMetrics.argtypes = [ctypes.wintypes.INT] 228 | ctypes.windll.user32.EnumDisplayMonitors.argtypes = [ 229 | ctypes.wintypes.HDC, 230 | ctypes.c_void_p, 231 | callback, 232 | ctypes.wintypes.LPARAM] 233 | ctypes.windll.user32.GetWindowDC.argtypes = [ctypes.wintypes.HWND] 234 | ctypes.windll.gdi32.GetDeviceCaps.argtypes = [ctypes.wintypes.HWND, 235 | ctypes.wintypes.INT] 236 | ctypes.windll.gdi32.CreateCompatibleDC.argtypes = [ctypes.wintypes.HDC] 237 | ctypes.windll.gdi32.CreateCompatibleBitmap.argtypes = [ 238 | ctypes.wintypes.HDC, 239 | ctypes.wintypes.INT, 240 | ctypes.wintypes.INT] 241 | ctypes.windll.gdi32.SelectObject.argtypes = [ctypes.wintypes.HDC, 242 | ctypes.wintypes.HGDIOBJ] 243 | ctypes.windll.gdi32.BitBlt.argtypes = [ 244 | ctypes.wintypes.HDC, 245 | ctypes.wintypes.INT, 246 | ctypes.wintypes.INT, 247 | ctypes.wintypes.INT, 248 | ctypes.wintypes.INT, 249 | ctypes.wintypes.HDC, 250 | ctypes.wintypes.INT, 251 | ctypes.wintypes.INT, 252 | ctypes.wintypes.DWORD] 253 | ctypes.windll.gdi32.DeleteObject.argtypes = [ctypes.wintypes.HGDIOBJ] 254 | ctypes.windll.gdi32.GetDIBits.argtypes = [ 255 | ctypes.wintypes.HDC, 256 | ctypes.wintypes.HBITMAP, 257 | ctypes.wintypes.UINT, 258 | ctypes.wintypes.UINT, 259 | ctypes.c_void_p, 260 | ctypes.POINTER(BITMAPINFO), 261 | ctypes.wintypes.UINT] 262 | 263 | 264 | def set_restypes(): 265 | # type: () -> None 266 | """ Functions return type. """ 267 | 268 | ctypes.windll.user32.GetSystemMetrics.restype = ctypes.wintypes.INT 269 | ctypes.windll.user32.EnumDisplayMonitors.restype = ctypes.wintypes.BOOL 270 | ctypes.windll.user32.GetWindowDC.restype = ctypes.wintypes.HDC 271 | ctypes.windll.gdi32.GetDeviceCaps.restype = ctypes.wintypes.INT 272 | ctypes.windll.gdi32.CreateCompatibleDC.restype = ctypes.wintypes.HDC 273 | ctypes.windll.gdi32.CreateCompatibleBitmap.restype = \ 274 | ctypes.wintypes.HBITMAP 275 | ctypes.windll.gdi32.SelectObject.restype = ctypes.wintypes.HGDIOBJ 276 | ctypes.windll.gdi32.BitBlt.restype = ctypes.wintypes.BOOL 277 | ctypes.windll.gdi32.GetDIBits.restype = ctypes.wintypes.INT 278 | ctypes.windll.gdi32.DeleteObject.restype = ctypes.wintypes.BOOL 279 | -------------------------------------------------------------------------------- /playsound.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2016 Taylor Marks 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | class PlaysoundException(Exception): 24 | pass 25 | 26 | def _playsoundWin(sound, block = True): 27 | ''' 28 | Utilizes windll.winmm. Tested and known to work with MP3 and WAVE on 29 | Windows 7 with Python 2.7. Probably works with more file formats. 30 | Probably works on Windows XP thru Windows 10. Probably works with all 31 | versions of Python. 32 | 33 | Inspired by (but not copied from) Michael Gundlach 's mp3play: 34 | https://github.com/michaelgundlach/mp3play 35 | 36 | I never would have tried using windll.winmm without seeing his code. 37 | ''' 38 | from ctypes import c_buffer, windll 39 | from random import random 40 | from time import sleep 41 | from sys import getfilesystemencoding 42 | 43 | def winCommand(*command): 44 | buf = c_buffer(255) 45 | command = ' '.join(command).encode(getfilesystemencoding()) 46 | errorCode = int(windll.winmm.mciSendStringA(command, buf, 254, 0)) 47 | if errorCode: 48 | errorBuffer = c_buffer(255) 49 | windll.winmm.mciGetErrorStringA(errorCode, errorBuffer, 254) 50 | exceptionMessage = ('\n Error ' + str(errorCode) + ' for command:' 51 | '\n ' + command.decode() + 52 | '\n ' + errorBuffer.value.decode()) 53 | raise PlaysoundException(exceptionMessage) 54 | return buf.value 55 | 56 | alias = 'playsound_' + str(random()) 57 | winCommand('open "' + sound + '" alias', alias) 58 | winCommand('set', alias, 'time format milliseconds') 59 | durationInMS = winCommand('status', alias, 'length') 60 | winCommand('play', alias, 'from 0 to', durationInMS.decode()) 61 | 62 | if block: 63 | sleep(float(durationInMS) / 1000.0) 64 | 65 | def _playsoundOSX(sound, block = True): 66 | ''' 67 | Utilizes AppKit.NSSound. Tested and known to work with MP3 and WAVE on 68 | OS X 10.11 with Python 2.7. Probably works with anything QuickTime supports. 69 | Probably works on OS X 10.5 and newer. Probably works with all versions of 70 | Python. 71 | 72 | Inspired by (but not copied from) Aaron's Stack Overflow answer here: 73 | http://stackoverflow.com/a/34568298/901641 74 | 75 | I never would have tried using AppKit.NSSound without seeing his code. 76 | ''' 77 | from AppKit import NSSound 78 | from Foundation import NSURL 79 | from time import sleep 80 | 81 | if '://' not in sound: 82 | if not sound.startswith('/'): 83 | from os import getcwd 84 | sound = getcwd() + '/' + sound 85 | sound = 'file://' + sound 86 | url = NSURL.URLWithString_(sound) 87 | nssound = NSSound.alloc().initWithContentsOfURL_byReference_(url, True) 88 | if not nssound: 89 | raise IOError('Unable to load sound named: ' + sound) 90 | nssound.play() 91 | 92 | if block: 93 | sleep(nssound.duration()) 94 | 95 | def _playsoundNix(sound, block=True): 96 | """Play a sound using GStreamer. 97 | 98 | Inspired by this: 99 | https://gstreamer.freedesktop.org/documentation/tutorials/playback/playbin-usage.html 100 | """ 101 | if not block: 102 | raise NotImplementedError( 103 | "block=False cannot be used on this platform yet") 104 | 105 | # pathname2url escapes non-URL-safe characters 106 | import os 107 | try: 108 | from urllib.request import pathname2url 109 | except ImportError: 110 | # python 2 111 | from urllib import pathname2url 112 | 113 | import gi 114 | gi.require_version('Gst', '1.0') 115 | from gi.repository import Gst 116 | 117 | Gst.init(None) 118 | 119 | playbin = Gst.ElementFactory.make('playbin', 'playbin') 120 | if sound.startswith(('http://', 'https://')): 121 | playbin.props.uri = sound 122 | else: 123 | playbin.props.uri = 'file://' + pathname2url(os.path.abspath(sound)) 124 | 125 | set_result = playbin.set_state(Gst.State.PLAYING) 126 | if set_result != Gst.StateChangeReturn.ASYNC: 127 | raise PlaysoundException( 128 | "playbin.set_state returned " + repr(set_result)) 129 | 130 | # FIXME: use some other bus method than poll() with block=False 131 | # https://lazka.github.io/pgi-docs/#Gst-1.0/classes/Bus.html 132 | bus = playbin.get_bus() 133 | bus.poll(Gst.MessageType.EOS, Gst.CLOCK_TIME_NONE) 134 | playbin.set_state(Gst.State.NULL) 135 | 136 | 137 | from platform import system 138 | system = system() 139 | 140 | if system == 'Windows': 141 | playsound = _playsoundWin 142 | elif system == 'Darwin': 143 | playsound = _playsoundOSX 144 | else: 145 | playsound = _playsoundNix 146 | 147 | del system 148 | -------------------------------------------------------------------------------- /r2csv.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from gomill import sgf_moves 5 | from toolbox import * 6 | 7 | def rsgf2csv(filename): 8 | game=open_sgf(filename) 9 | sgf_moves.indicate_first_player(game) 10 | gameroot=game.get_root() 11 | 12 | max_move=get_moves_number(gameroot) 13 | 14 | csv=open(filename+".csv","w") 15 | 16 | black="" 17 | if gameroot.has_property("PB"): 18 | black=gameroot.get("PB") 19 | new_line="black: "+black 20 | csv.write(new_line+"\n") 21 | 22 | 23 | white="" 24 | if gameroot.has_property("PW"): 25 | white=gameroot.get("PW") 26 | new_line="white: "+white 27 | csv.write(new_line+"\n") 28 | 29 | date="" 30 | if gameroot.has_property("DT"): 31 | date=gameroot.get("DT") 32 | new_line="date: "+date 33 | csv.write(new_line+"\n") 34 | 35 | event="" 36 | if gameroot.has_property("EV"): 37 | event=gameroot.get("EV") 38 | new_line="event: "+event 39 | csv.write(new_line+"\n") 40 | 41 | new_line="" 42 | csv.write(new_line+"\n") 43 | 44 | headers_left=["Move number", "Move color"] 45 | headers_game=["Move", "Win rate", "Value Network", "Monte Carlo", "Evaluation", "Rave", "Score Estimation", "Policy Network", "Simulations", "Follow up", "Variations"] 46 | headers_bot=headers_game[:] 47 | 48 | header_first_line=[""]*len(headers_left)+["Game move"]*len(headers_game)+["Bot move"]*len(headers_bot) 49 | 50 | table=[headers_left[:]+headers_game[:]+headers_bot[:]] 51 | 52 | nb_max_variations=1 53 | 54 | columns_sgf_properties=["CBM","BWWR","VNWR","MCWR","EVAL","RAVE","ES","PNV","PLYO","nothing_here","nothing_here"] 55 | 56 | for m in range(1,max_move): 57 | one_move=get_node(gameroot,m) 58 | table.append(["" for i in range(len(table[0]))]) 59 | #-- Move number 60 | table[m][0]=m 61 | 62 | #-- colour 63 | color=guess_color_to_play(gameroot,m) 64 | table[m][1]=color.upper() 65 | 66 | #-- game move 67 | one_move=get_node(gameroot,m) 68 | c=len(headers_left) 69 | for header, sgf_property in zip(headers_bot,columns_sgf_properties): 70 | if header=="Follow up": 71 | break 72 | try: 73 | next_move=one_move[0] #taking data from the following move 74 | if sgf_property =="PLYO": 75 | next_move=one_move[1] 76 | if sgf_property =="PNV": 77 | for variation in one_move.parent[1:]: 78 | if node_get(variation,color.upper())==node_get(one_move,color.upper()): 79 | next_move=variation 80 | break 81 | 82 | if sgf_property=="CBM": 83 | if color=="w": 84 | sgf_property="B" 85 | else: 86 | sgf_property="W" 87 | 88 | if node_has(next_move,sgf_property): 89 | if sgf_property=="B" or sgf_property=="W": 90 | value=node_get(one_move,color.upper()) 91 | value=ij2gtp(value) 92 | else: 93 | value=node_get(next_move,sgf_property) 94 | 95 | if "%/" in value: 96 | if color=="b": 97 | value=float(value.split("%/")[0]) 98 | value=round(value,2) 99 | value=str(value)+"%" 100 | else: 101 | value=float(value.split("/")[1][:-1]) 102 | value=round(value,2) 103 | value=str(value)+"%" 104 | table[m][c]=value 105 | except: 106 | pass 107 | c+=1 108 | 109 | #-- variations 110 | 111 | if len(one_move.parent[1:])>nb_max_variations: 112 | nb_max_variations=len(one_move.parent[1:]) 113 | headers_bot=headers_game[:]*nb_max_variations 114 | table[0]=headers_left[:]+headers_game[:]+headers_bot[:]*nb_max_variations 115 | header_first_line=[""]*len(headers_left)+["Game move"]*len(headers_game)+["Bot move"]*len(headers_game) 116 | for i in range(2,nb_max_variations+1): 117 | header_first_line+=["Bot move "+str(i)]*len(headers_game) 118 | 119 | c=len(headers_left+headers_game) 120 | if len(one_move.parent[1:]): 121 | table[m][c-1]=len(one_move.parent[1:]) 122 | nbv=1 123 | for one_variation in one_move.parent[1:]: 124 | nbv+=1 125 | for header, sgf_property in zip(headers_bot,columns_sgf_properties): 126 | #if header=="Follow up": 127 | # break 128 | try: 129 | #one_move=get_node(gameroot,m) 130 | #if sgf_property in ("PNV","PLYO"): 131 | # one_move=one_variation 132 | one_move=one_variation 133 | 134 | if sgf_property=="CBM": 135 | if color=="w": 136 | sgf_property="W" 137 | else: 138 | sgf_property="B" 139 | 140 | if node_has(one_move,sgf_property): 141 | if sgf_property=="B" or sgf_property=="W": 142 | value=node_get(one_move,color.upper()) 143 | value=ij2gtp(value) 144 | else: 145 | value=node_get(one_move,sgf_property) 146 | if "%/" in value: 147 | if color=="b": 148 | value=float(value.split("%/")[0]) 149 | value=round(value,2) 150 | value=str(value)+"%" 151 | else: 152 | value=float(value.split("/")[1][:-1]) 153 | value=round(value,2) 154 | value=str(value)+"%" 155 | table[m][c]=value 156 | 157 | except: 158 | pass 159 | c+=1 160 | 161 | 162 | #scanning for empty columns 163 | for c in range(len(table[0])): #checking column after column 164 | found=False 165 | for m in range(len(table[1:])): #for each columns checking for at least one value 166 | try: 167 | if table[1+m][c]!="": #one value was found, so let's keept that column 168 | found=True 169 | break 170 | except: 171 | #no data (probably no variation data, so keep searching) 172 | pass 173 | 174 | if not found: 175 | table[0][c]="" #no value found in the entire column, so let's remove the header to remeber to skip that column 176 | 177 | c=len(headers_left+headers_game)-1 178 | header_first_line[c]="" 179 | c=0 180 | for header in table[0]: 181 | if header!="": 182 | csv.write(header_first_line[c]+",") 183 | c+=1 184 | 185 | csv.write("\n") 186 | 187 | for m in table: 188 | line="" 189 | for value, header in zip(m,table[0]): 190 | if header!="": 191 | line+=str(value).strip()+"," 192 | #print line 193 | csv.write(line+"\n") 194 | 195 | 196 | log("saving") 197 | csv.close() 198 | 199 | if __name__ == "__main__": 200 | from sys import argv 201 | 202 | if len(argv)==1: 203 | temp_root = Tk() 204 | filename = open_rsgf_file(parent=temp_root) 205 | temp_root.destroy() 206 | log(filename) 207 | if not filename: 208 | sys.exit() 209 | rsgf2csv(filename) 210 | 211 | else: 212 | for filename in argv[1:]: 213 | rsgf2csv(filename) 214 | -------------------------------------------------------------------------------- /r2sgf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from toolbox import * 5 | 6 | def rsgf2sgf(rsgf_file): 7 | #log("Convertion of",rsgf_file,"into",rsgf_file+".sgf") 8 | g=open_sgf(rsgf_file) 9 | sgf_moves.indicate_first_player(g) 10 | gameroot=g.get_root() 11 | 12 | max_move=get_moves_number(gameroot) 13 | 14 | current_move=1 15 | while current_move<=max_move: 16 | comments=get_position_comments(current_move,gameroot) 17 | node_set(get_node(gameroot,current_move),"C",comments) 18 | parent=get_node(gameroot,current_move-1) 19 | 20 | for a in range(1,len(parent)): 21 | one_alternative=parent[a] 22 | comments=get_variation_comments(one_alternative) 23 | node_set(one_alternative,"C",comments) 24 | current_move+=1 25 | 26 | write_sgf(rsgf_file+".sgf",g) 27 | 28 | if __name__ == "__main__": 29 | from sys import argv 30 | 31 | if len(argv)==1: 32 | temp_root = Tk() 33 | filename = open_rsgf_file(parent=temp_root) 34 | temp_root.destroy() 35 | log(filename) 36 | if not filename: 37 | sys.exit() 38 | rsgf2sgf(filename) 39 | 40 | else: 41 | for filename in argv[1:]: 42 | rsgf2sgf(filename) 43 | -------------------------------------------------------------------------------- /ray_analysis.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from gtp import gtp 5 | import sys 6 | from Tkinter import * 7 | from toolbox import * 8 | from toolbox import _ 9 | from time import time 10 | 11 | class RayAnalysis(): 12 | 13 | def run_analysis(self,current_move): 14 | 15 | one_move=go_to_move(self.move_zero,current_move) 16 | player_color=guess_color_to_play(self.move_zero,current_move) 17 | ray=self.ray 18 | 19 | log() 20 | log("==============") 21 | log("move",current_move) 22 | 23 | #additional_comments="" 24 | if player_color in ('w',"W"): 25 | log("ray play white") 26 | answer=ray.get_ray_stat("white") 27 | else: 28 | log("ray play black") 29 | answer=ray.get_ray_stat("black") 30 | 31 | if current_move>2: 32 | es=ray.final_score() 33 | node_set(one_move,"ES",es) 34 | 35 | log(len(answer),"sequences") 36 | 37 | if len(answer)>0: 38 | best_move=True 39 | for sequence_first_move,count,simulation,policy,value,win,one_sequence in answer[:self.maxvariations]: 40 | log("Adding sequence starting from",sequence_first_move) 41 | if best_move: 42 | best_answer=sequence_first_move 43 | node_set(one_move,"CBM",best_answer) 44 | 45 | previous_move=one_move.parent 46 | current_color=player_color 47 | 48 | one_sequence=player_color+' '+sequence_first_move+' '+one_sequence 49 | one_sequence=one_sequence.replace("b ",',b') 50 | one_sequence=one_sequence.replace("w ",',w') 51 | one_sequence=one_sequence.replace(" ",'') 52 | #log("one_sequence=",one_sequence[1:]) 53 | first_variation_move=True 54 | for one_deep_move in one_sequence.split(',')[1:]: 55 | if one_deep_move in ["PASS","RESIGN"]: 56 | log("Leaving the variation when encountering",one_deep_move) 57 | break 58 | current_color=one_deep_move[0] 59 | one_deep_move=one_deep_move[1:].strip() 60 | if one_deep_move!="PASS": 61 | i,j=gtp2ij(one_deep_move) 62 | new_child=previous_move.new_child() 63 | node_set(new_child,current_color,(i,j)) 64 | if first_variation_move: 65 | first_variation_move=False 66 | if win: 67 | if current_color=='b': 68 | winrate=str(float(win))+'%/'+str(100-float(win))+'%' 69 | else: 70 | winrate=str(100-float(win))+'%/'+str(win)+'%' 71 | node_set(new_child,"BWWR",winrate) 72 | if best_move: 73 | node_set(one_move,"BWWR",winrate) 74 | 75 | if count: 76 | node_set(new_child,"PLYO",count) 77 | 78 | if simulation: 79 | simulation+="%" 80 | if current_color=='b': 81 | black_value=simulation 82 | white_value=opposite_rate(black_value) 83 | else: 84 | white_value=simulation 85 | black_value=opposite_rate(white_value) 86 | 87 | node_set(new_child,"MCWR",black_value+'/'+white_value) 88 | if best_move: 89 | node_set(one_move,"MCWR",black_value+'/'+white_value) 90 | 91 | 92 | if policy: 93 | node_set(new_child,"PNV",policy+"%") 94 | 95 | if value: 96 | if player_color=='b': 97 | black_value=value+"%" 98 | white_value=opposite_rate(black_value) 99 | else: 100 | white_value=value+"%" 101 | black_value=opposite_rate(white_value) 102 | node_set(new_child,"VNWR",black_value+'/'+white_value) 103 | if best_move: 104 | node_set(one_move,"VNWR",black_value+'/'+white_value) 105 | 106 | if best_move: 107 | best_move=False 108 | 109 | previous_move=new_child 110 | else: 111 | break 112 | 113 | log("==== no more sequences =====") 114 | 115 | #one_move.add_comment_text(additional_comments) 116 | return best_answer 117 | 118 | 119 | def initialize_bot(self): 120 | ray=ray_starting_procedure(self.g,self.profile) 121 | self.ray=ray 122 | self.time_per_move=0 123 | return ray 124 | 125 | def ray_starting_procedure(sgf_g,profile,silentfail=False): 126 | return bot_starting_procedure("Ray","RLO",Ray_gtp,sgf_g,profile,silentfail) 127 | 128 | 129 | class RunAnalysis(RayAnalysis,RunAnalysisBase): 130 | def __init__(self,parent,filename,move_range,intervals,variation,komi,profile,existing_variations="remove_everything"): 131 | RunAnalysisBase.__init__(self,parent,filename,move_range,intervals,variation,komi,profile,existing_variations) 132 | 133 | class LiveAnalysis(RayAnalysis,LiveAnalysisBase): 134 | def __init__(self,g,filename,profile): 135 | LiveAnalysisBase.__init__(self,g,filename,profile) 136 | 137 | import subprocess 138 | import Queue 139 | 140 | class Ray_gtp(gtp): 141 | 142 | def __init__(self,command): 143 | self.c=1 144 | self.command_line=command[0]+" "+" ".join(command[1:]) 145 | command=[c.encode(sys.getfilesystemencoding()) for c in command] 146 | self.process=subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 147 | self.size=0 148 | 149 | self.stderr_starting_queue=Queue.Queue(maxsize=100) 150 | self.stderr_queue=Queue.Queue() 151 | self.stdout_queue=Queue.Queue() 152 | threading.Thread(target=self.consume_stderr).start() 153 | 154 | log("Checking Ray stderr to check for OpenCL SGEMM tuner running") 155 | delay=60 156 | while 1: 157 | try: 158 | err_line=self.stderr_starting_queue.get(True,delay) 159 | delay=10 160 | if "Started OpenCL SGEMM tuner." in err_line: 161 | log("OpenCL SGEMM tuner is running") 162 | show_info(_("Ray is currently running the OpenCL SGEMM tuner. It may take several minutes until Ray is ready.")) 163 | break 164 | elif "Loaded existing SGEMM tuning.\n" in err_line: 165 | log("OpenCL SGEMM tuner has already been runned") 166 | break 167 | elif "BLAS Core:" in err_line: 168 | log("Could not find out, abandoning") 169 | break 170 | elif "Could not open weights file" in err_line: 171 | show_info(err_line.strip()) 172 | break 173 | elif "Weights file is the wrong version." in err_line: 174 | show_info(err_line.strip()) 175 | break 176 | 177 | except: 178 | log("Could not find out, abandoning") 179 | break 180 | 181 | 182 | self.free_handicap_stones=[] 183 | self.history=[] 184 | 185 | def consume_stderr(self): 186 | while 1: 187 | try: 188 | err_line=self.process.stderr.readline() 189 | if err_line: 190 | self.stderr_queue.put(err_line) 191 | try: 192 | self.stderr_starting_queue.put(err_line,block=False) 193 | except: 194 | #no need to keep all those log in memory, so there is a limit at 100 lines 195 | pass 196 | else: 197 | log("leaving consume_stderr thread") 198 | return 199 | except Exception, e: 200 | log("leaving consume_stderr thread due to exception:") 201 | log(e) 202 | return 203 | 204 | def quick_evaluation(self,color): 205 | 206 | if color==2: 207 | answer=self.get_ray_stat("white") 208 | else: 209 | answer=self.get_ray_stat("black") 210 | 211 | unused,unused,unused,unused,unused,win,unused=answer[0] 212 | 213 | txt="" 214 | if win: 215 | if color==1: 216 | winrate=str(float(win))+'%/'+str(100-float(win))+'%' 217 | else: 218 | winrate=str(100-float(win))+'%/'+str(win)+'%' 219 | txt+= variation_data_formating["BWWR"]%winrate 220 | 221 | return txt 222 | 223 | def get_ray_stat(self,color): 224 | t0=time() 225 | self.write("ray-stat "+color) 226 | header_line=self.readline() 227 | log(">>>>>>>>>>>>",time()-t0) 228 | log("HEADER:",header_line) 229 | sequences=[] 230 | 231 | for i in range(10): 232 | one_line=self.process.stdout.readline().strip() 233 | if one_line.strip()=="": 234 | break 235 | log(one_line) 236 | #log("\t",[s.strip() for s in one_line.split("|")[1:]]) 237 | sequences.append([s.strip() for s in one_line.split("|")[1:]]) 238 | 239 | if sequences[0][5]=="": 240 | log("===================================================================") 241 | log("=== WARNING: Ray thinking time is too short for proper analysis ===") 242 | log("===================================================================") 243 | log("\a") #let's make this annoying enough :) 244 | return sequences 245 | 246 | 247 | class RaySettings(BotProfiles): 248 | def __init__(self,parent,bot="Ray"): 249 | BotProfiles.__init__(self,parent,bot) 250 | self.bot_gtp=Ray_gtp 251 | 252 | 253 | class RayOpenMove(BotOpenMove): 254 | def __init__(self,sgf_g,profile): 255 | BotOpenMove.__init__(self,sgf_g,profile) 256 | self.name='Ray' 257 | self.my_starting_procedure=ray_starting_procedure 258 | 259 | 260 | Ray={} 261 | Ray['name']="Ray" 262 | Ray['gtp_name']="RLO" 263 | Ray['analysis']=RayAnalysis 264 | Ray['openmove']=RayOpenMove 265 | Ray['settings']=RaySettings 266 | Ray['gtp']=Ray_gtp 267 | Ray['liveanalysis']=LiveAnalysis 268 | Ray['runanalysis']=RunAnalysis 269 | Ray['starting']=ray_starting_procedure 270 | 271 | if __name__ == "__main__": 272 | main(Ray) 273 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from Tkinter import * 5 | from gnugo_analysis import GnuGoSettings 6 | from ray_analysis import RaySettings 7 | from leela_analysis import LeelaSettings 8 | from aq_analysis import AQSettings 9 | from leela_zero_analysis import LeelaZeroSettings 10 | from pachi_analysis import PachiSettings 11 | from phoenixgo_analysis import PhoenixGoSettings 12 | from gtp_bot import GtpBotSettings 13 | from toolbox import * 14 | from toolbox import _ 15 | 16 | class OpenSettings(Toplevel): 17 | def display_settings(self): 18 | if self.setting_frame: 19 | self.setting_frame.pack_forget() 20 | 21 | settings_dict={"GRP":self.display_GRP_settings, "AQ":AQSettings, "GnuGo":GnuGoSettings, "Leela":LeelaSettings, "Ray":RaySettings, "Leela Zero":LeelaZeroSettings, "Pachi":PachiSettings, "PhoenixGo":PhoenixGoSettings, "GtpBot": GtpBotSettings} 22 | 23 | self.setting_frame=Frame(self.right_column) 24 | self.setting_frame.parent=self 25 | key=self.setting_mode.get() 26 | new_settings=settings_dict[key](self.setting_frame) 27 | new_settings.grid(row=0,column=0, padx=5, pady=5) 28 | self.current_settings=new_settings 29 | 30 | self.setting_frame.pack(fill=BOTH, expand=1) 31 | self.focus() 32 | 33 | 34 | def display_GRP_settings(self,top_setting_frame): 35 | 36 | log("Initializing GRP setting interface") 37 | 38 | setting_frame=Frame(top_setting_frame) 39 | 40 | row=0 41 | Label(setting_frame,text=_("%s settings")%"Go Review Partner", font="-weight bold").grid(row=row,column=1,sticky=W) 42 | row+=1 43 | Label(setting_frame,text="").grid(row=row,column=1) 44 | 45 | row+=1 46 | Label(setting_frame,text=_("General parameters")).grid(row=row,column=1,sticky=W) 47 | 48 | row+=1 49 | Label(setting_frame,text=_("Language")).grid(row=row,column=1,sticky=W) 50 | Language = StringVar() 51 | Language.set(available_translations[lang]) 52 | OptionMenu(setting_frame,Language,*tuple(available_translations.values())).grid(row=row,column=2,sticky=W) 53 | 54 | row+=1 55 | Label(setting_frame,text="").grid(row=row,column=1) 56 | row+=1 57 | Label(setting_frame,text=_("Parameters for the analysis")).grid(row=row,column=1,sticky=W) 58 | 59 | row+=1 60 | Label(setting_frame,text=_("Maximum number of variations to record during analysis")).grid(row=row,column=1,sticky=W) 61 | MaxVariationsToRecord = StringVar() 62 | MaxVariationsToRecord.set(grp_config.get("Analysis","MaxVariations")) 63 | Entry(setting_frame, textvariable=MaxVariationsToRecord, width=30).grid(row=row,column=2) 64 | 65 | row+=1 66 | Label(setting_frame,text=_("Only keep variations where game move and bot move differ")).grid(row=row,column=1,sticky=W) 67 | NoVariationIfSameMove = BooleanVar(value=grp_config.getboolean("Analysis","NoVariationIfSameMove")) 68 | NoVariationIfSameMoveCheckbutton=Checkbutton(setting_frame, text="", variable=NoVariationIfSameMove,onvalue=True,offvalue=False) 69 | NoVariationIfSameMoveCheckbutton.grid(row=row,column=2,sticky=W) 70 | NoVariationIfSameMoveCheckbutton.var=NoVariationIfSameMove 71 | 72 | row+=1 73 | Label(setting_frame,text=_("Save bot command line into RSGF file")).grid(row=row,column=1,sticky=W) 74 | SaveCommandLine = BooleanVar(value=grp_config.getboolean('Analysis', 'SaveCommandLine')) 75 | SaveCommandLineCheckbutton=Checkbutton(setting_frame, text="", variable=SaveCommandLine,onvalue=True,offvalue=False) 76 | SaveCommandLineCheckbutton.grid(row=row,column=2,sticky=W) 77 | SaveCommandLineCheckbutton.var=SaveCommandLine 78 | row+=1 79 | Label(setting_frame,text=_("Stop the analysis if the bot resigns")).grid(row=row,column=1,sticky=W) 80 | StopAtFirstResign = BooleanVar(value=grp_config.getboolean('Analysis', 'StopAtFirstResign')) 81 | StopAtFirstResignCheckbutton=Checkbutton(setting_frame, text="", variable=StopAtFirstResign,onvalue=True,offvalue=False) 82 | StopAtFirstResignCheckbutton.grid(row=row,column=2,sticky=W) 83 | StopAtFirstResignCheckbutton.var=StopAtFirstResign 84 | 85 | row+=1 86 | Label(setting_frame,text="").grid(row=row,column=1) 87 | row+=1 88 | Label(setting_frame,text=_("Parameters for the review")).grid(row=row,column=1,sticky=W) 89 | 90 | row+=1 91 | Label(setting_frame,text=_("Natural stone placement")).grid(row=row,column=1,sticky=W) 92 | FuzzyStonePlacement = StringVar() 93 | FuzzyStonePlacement.set(grp_config.get("Review","FuzzyStonePlacement")) 94 | Entry(setting_frame, textvariable=FuzzyStonePlacement, width=30).grid(row=row,column=2) 95 | row+=1 96 | 97 | Label(setting_frame,text=_("Real game sequence deepness")).grid(row=row,column=1,sticky=W) 98 | RealGameSequenceDeepness = StringVar() 99 | RealGameSequenceDeepness.set(grp_config.get("Review","RealGameSequenceDeepness")) 100 | Entry(setting_frame, textvariable=RealGameSequenceDeepness, width=30).grid(row=row,column=2) 101 | row+=1 102 | 103 | Label(setting_frame,text=_("Maximum number of variations to display during review")).grid(row=row,column=1,sticky=W) 104 | MaxVariationsToDisplay = StringVar() 105 | MaxVariationsToDisplay.set(grp_config.get("Review","MaxVariations")) 106 | Entry(setting_frame, textvariable=MaxVariationsToDisplay, width=30).grid(row=row,column=2) 107 | row+=1 108 | 109 | Label(setting_frame,text=_("Blue/red coloring of the variations")).grid(row=row,column=1,sticky=W) 110 | VariationsColoring = StringVar() 111 | coloring={"blue_for_winning":_("Win rate > 50% in blue"),"blue_for_best":_("The best variation in blue"),"blue_for_better":_("Variations better than actual game move in blue")} 112 | VariationsColoring.set(coloring[grp_config.get("Review","VariationsColoring")]) 113 | OptionMenu(setting_frame,VariationsColoring,*tuple(coloring.values())).grid(row=row,column=2,sticky=W) 114 | 115 | row+=1 116 | Label(setting_frame,text=_("Labels for the variations")).grid(row=row,column=1,sticky=W) 117 | values={"letter":_("Letters"),"rate":_("Percentages")} 118 | VariationsLabel = StringVar() 119 | VariationsLabel.set(values[grp_config.get("Review","VariationsLabel")]) 120 | OptionMenu(setting_frame,VariationsLabel,*tuple(values.values())).grid(row=row,column=2,sticky=W) 121 | 122 | row+=1 123 | Label(setting_frame,text=_("Inverted mouse wheel")).grid(row=row,column=1,sticky=W) 124 | InvertedMouseWheel = BooleanVar(value=grp_config.getboolean('Review', 'InvertedMouseWheel')) 125 | InvertedMouseWheelCheckbutton=Checkbutton(setting_frame, text="", variable=InvertedMouseWheel,onvalue=True,offvalue=False) 126 | InvertedMouseWheelCheckbutton.grid(row=row,column=2,sticky=W) 127 | InvertedMouseWheelCheckbutton.var=InvertedMouseWheel 128 | 129 | Button(self.setting_frame,text=_("Save settings"),command=self.save).grid(row=1,column=0, padx=5, pady=5,sticky=W) 130 | 131 | self.Language=Language 132 | self.FuzzyStonePlacement=FuzzyStonePlacement 133 | self.RealGameSequenceDeepness=RealGameSequenceDeepness 134 | #self.GobanScreenRatio=GobanScreenRatio 135 | self.MaxVariationsToRecord=MaxVariationsToRecord 136 | self.SaveCommandLine=SaveCommandLine 137 | self.StopAtFirstResign=StopAtFirstResign 138 | self.MaxVariationsToDisplay=MaxVariationsToDisplay 139 | self.VariationsColoring=VariationsColoring 140 | self.InvertedMouseWheel=InvertedMouseWheel 141 | self.NoVariationIfSameMove=NoVariationIfSameMove 142 | self.VariationsColoring=VariationsColoring 143 | self.VariationsLabel=VariationsLabel 144 | 145 | setting_frame.save=self.save 146 | 147 | return setting_frame 148 | 149 | def close(self): 150 | log("closing popup") 151 | self.destroy() 152 | self.parent.remove_popup(self) 153 | log("done") 154 | 155 | def __init__(self,parent,refresh=None): 156 | Toplevel.__init__(self) 157 | self.parent=parent 158 | 159 | self.refresh=refresh 160 | 161 | self.title('GoReviewPartner') 162 | 163 | left_column=Frame(self, padx=5, pady=5, height=2, bd=1, relief=SUNKEN) 164 | left_column.pack(side=LEFT, fill=Y) 165 | 166 | right_column=Frame(self, padx=5, pady=5, height=2, bd=1, relief=SUNKEN) 167 | right_column.pack(side=LEFT, fill=BOTH, expand=1) 168 | 169 | self.setting_mode=StringVar() 170 | self.setting_mode.set("GRP") # initialize 171 | Radiobutton(left_column, text="Go Review Partner",command=self.display_settings,variable=self.setting_mode, value="GRP",indicatoron=0).pack(side=TOP, fill=X) 172 | Radiobutton(left_column, text="AQ",command=self.display_settings,variable=self.setting_mode, value="AQ",indicatoron=0).pack(side=TOP, fill=X) 173 | Radiobutton(left_column, text="GnuGo",command=self.display_settings,variable=self.setting_mode, value="GnuGo",indicatoron=0).pack(side=TOP, fill=X) 174 | Radiobutton(left_column, text="Leela",command=self.display_settings,variable=self.setting_mode, value="Leela",indicatoron=0).pack(side=TOP, fill=X) 175 | Radiobutton(left_column, text="Ray",command=self.display_settings,variable=self.setting_mode, value="Ray",indicatoron=0).pack(side=TOP, fill=X) 176 | Radiobutton(left_column, text="Leela Zero",command=self.display_settings,variable=self.setting_mode, value="Leela Zero",indicatoron=0).pack(side=TOP, fill=X) 177 | Radiobutton(left_column, text="Pachi",command=self.display_settings,variable=self.setting_mode, value="Pachi",indicatoron=0).pack(side=TOP, fill=X) 178 | Radiobutton(left_column, text="PhoenixGo",command=self.display_settings,variable=self.setting_mode, value="PhoenixGo",indicatoron=0).pack(side=TOP, fill=X) 179 | Radiobutton(left_column, text="GTP bots",command=self.display_settings,variable=self.setting_mode, value="GtpBot",indicatoron=0).pack(side=TOP, fill=X) 180 | 181 | self.right_column=right_column 182 | self.setting_frame=None 183 | self.display_settings() 184 | self.protocol("WM_DELETE_WINDOW", self.close) 185 | 186 | def save(self): 187 | global lang, translations 188 | log("Saving GRP settings") 189 | for lang2, language in available_translations.iteritems(): 190 | if language==self.Language.get(): 191 | if lang!=lang2: 192 | grp_config.set("General","Language",lang2) 193 | break 194 | grp_config.set("Review","FuzzyStonePlacement",self.FuzzyStonePlacement.get()) 195 | grp_config.set("Review","RealGameSequenceDeepness",self.RealGameSequenceDeepness.get()) 196 | #grp_config.set("Review","GobanScreenRatio",self.GobanScreenRatio.get()) 197 | grp_config.set("Analysis","MaxVariations",self.MaxVariationsToRecord.get()) 198 | grp_config.set("Analysis","SaveCommandLine",self.SaveCommandLine.get()) 199 | grp_config.set("Analysis","StopAtFirstResign",self.StopAtFirstResign.get()) 200 | grp_config.set("Review","MaxVariations",self.MaxVariationsToDisplay.get()) 201 | coloring={_("Win rate > 50% in blue"):"blue_for_winning",_("The best variation in blue"):"blue_for_best",_("Variations better than actual game move in blue"):"blue_for_better"} 202 | grp_config.set("Review","VariationsColoring",coloring[self.VariationsColoring.get()]) 203 | grp_config.set("Review","InvertedMouseWheel",self.InvertedMouseWheel.get()) 204 | grp_config.set("Analysis","NoVariationIfSameMove",self.NoVariationIfSameMove.get()) 205 | labeling={_("Letters"):"letter",_("Percentages"):"rate"} 206 | grp_config.set("Review","VariationsLabel",labeling[self.VariationsLabel.get()]) 207 | 208 | 209 | 210 | if self.refresh!=None: 211 | self.refresh() 212 | 213 | def test(self,gtp_bot,command,parameters): 214 | from gtp_terminal import Terminal 215 | 216 | command=command.get() 217 | parameters=parameters.get().split() 218 | 219 | if not command: 220 | log("Empty command line!") 221 | return 222 | 223 | popup=Terminal(self.parent,gtp_bot,[command]+parameters) 224 | self.parent.add_popup(popup) 225 | 226 | if __name__ == "__main__": 227 | app = Application() 228 | popup=OpenSettings(app) 229 | app.add_popup(popup) 230 | app.mainloop() 231 | -------------------------------------------------------------------------------- /translations/check.py: -------------------------------------------------------------------------------- 1 | 2 | import subprocess 3 | 4 | #updating the list of english entries 5 | python_files=["main.py", "toolbox.py", "settings.py", "gnugo_analysis.py", "leela_analysis.py", "leela_zero_analysis.py", "aq_analysis.py", "ray_analysis.py", "dual_view.py", "live_analysis.py", "gtp_terminal.py", "r2sgf.py", "r2csv.py", "tabbed.py"] 6 | 7 | 8 | cmd="xgettext --no-wrap -E --keyword=_ --language=python -o en.po --sort-by-file " 9 | output = subprocess.check_output(cmd.split()+["../"+f for f in python_files]) 10 | 11 | def get_sentences_old(lang,check_for_double=False): 12 | 13 | data_file_url=lang+".po" 14 | print "Loading translation file:",data_file_url 15 | 16 | data_file = open(data_file_url,"r") 17 | translation_data=data_file.read() 18 | data_file.close() 19 | 20 | sentences=[] 21 | translated_sentences=[] 22 | for line in translation_data.split('\n'): 23 | key="msgid" 24 | if line[:len(key)+2]==key+' "': 25 | entry=line[len(key)+2:-1] 26 | if check_for_double: 27 | if entry in sentences: 28 | print "[Warning] double entries for:",entry 29 | sentences.append(entry) 30 | 31 | key="msgstr" 32 | if line[:len(key)+2]==key+' "': 33 | translation=line[len(key)+2:-1] 34 | translation=translation.replace("\\\"","\"") 35 | translated_sentences.append(translation) 36 | 37 | return sentences,translated_sentences 38 | 39 | def get_translations(lang,check_for_double=False): 40 | translations={} 41 | 42 | data_file_url=lang+".po" 43 | print "Loading translation file:",data_file_url 44 | 45 | data_file = open(data_file_url,"r") 46 | translation_data=data_file.read() 47 | data_file.close() 48 | 49 | entry="" 50 | translation="" 51 | 52 | for line in translation_data.split('\n'): 53 | 54 | key="msgid" 55 | if line[:len(key)+2]==key+' "': 56 | entry=line[len(key)+2:-1] 57 | translation="" 58 | 59 | key="msgstr" 60 | if line[:len(key)+2]==key+' "': 61 | translation=line[len(key)+2:-1] 62 | translation=translation.replace("\\\"","\"") 63 | if len(entry)>0: 64 | if check_for_double: 65 | if entry in translations: 66 | print "[Warning] double entries for:",entry 67 | translations[entry]=translation 68 | 69 | 70 | entry="" 71 | translation="" 72 | return translations 73 | 74 | print 75 | print "================================================================" 76 | print "================================================================" 77 | print "================================================================" 78 | print 79 | 80 | english=get_translations("en",True) 81 | 82 | available_translations=["fr","de","kr","zh","pl","new_translation"] 83 | 84 | from sys import argv 85 | if len(argv)==2: 86 | if argv[1] in available_translations: 87 | print "Selection of lang=",argv[1] 88 | available_translations=[argv[1]] 89 | 90 | statistics={} 91 | 92 | for lang in available_translations: 93 | print 94 | print "============= Checking language="+lang,"=============" 95 | translations=get_translations(lang) 96 | statistics[lang]={"total":len(translations.keys()),"missing":0,"empty":0,"extra":0} 97 | print 98 | print "==== English sentences missing in",lang+".po ====" 99 | found=False 100 | for sentence in english.keys(): 101 | if sentence not in translations.keys(): 102 | print 'msgid "'+sentence+'"' 103 | print 'msgstr ""' 104 | print 105 | found=True 106 | statistics[lang]["missing"]+=1 107 | if not found: 108 | print "(none)" 109 | 110 | if lang!="new_translation": 111 | print 112 | print "==== Empty translations in",lang+".po ====" 113 | found=False 114 | for sentence,translation in translations.iteritems(): 115 | if not translation: 116 | print 'msgid "'+sentence+'"' 117 | print 'msgstr ""' 118 | print 119 | found=True 120 | statistics[lang]["empty"]+=1 121 | if not found: 122 | print "(none)" 123 | 124 | 125 | print 126 | print "==== Extra sentences in",lang+".po ====" 127 | found=False 128 | for sentence,translation in translations.iteritems(): 129 | if sentence not in english.keys(): 130 | print "msgid", '"'+sentence+'"' 131 | print "msgstr", '"'+translation+'"' 132 | print 133 | found=True 134 | statistics[lang]["extra"]+=1 135 | if not found: 136 | print "(none)" 137 | 138 | 139 | print 140 | print 141 | print "========================" 142 | for lang in statistics.keys(): 143 | print "language: "+lang+":" 144 | print "\ttotal:",statistics[lang]["total"] 145 | print "\tmissing:",statistics[lang]["missing"] 146 | print "\tempty:",statistics[lang]["empty"] 147 | print "\textra:",statistics[lang]["extra"] 148 | print 149 | -------------------------------------------------------------------------------- /translations/translation_help.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnprog/goreviewpartner/cbcc486cd4c51fb6fc3bc0a1eab61ff34298dadf/translations/translation_help.odg -------------------------------------------------------------------------------- /translations/translation_help.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnprog/goreviewpartner/cbcc486cd4c51fb6fc3bc0a1eab61ff34298dadf/translations/translation_help.pdf --------------------------------------------------------------------------------