├── .gitignore ├── sgftools ├── __init__.py ├── progressbar.py ├── annotations.py ├── gotools.py ├── leela.py ├── typelib.py └── sgflib.py ├── LICENSE ├── README.md ├── sgfanalyze.py └── gpl.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /sgftools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This program is free software: you can redistribute it and/or modify 2 | it under the terms of the GNU General Public License as published by 3 | the Free Software Foundation, either version 3 of the License, or 4 | (at your option) any later version. 5 | 6 | This program is distributed in the hope that it will be useful, 7 | but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | GNU General Public License for more details. 10 | 11 | You should have received a copy of the GNU General Public License 12 | along with this program. If not, see . 13 | -------------------------------------------------------------------------------- /sgftools/progressbar.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import datetime 3 | 4 | class ProgressBar(object): 5 | 6 | def __init__(self, min_value = 0, max_value = 100, width = 50, frequency=1, stream=sys.stderr): 7 | self.max_value = max_value 8 | self.min_value = min_value 9 | self.value = min_value 10 | 11 | self.message = None 12 | self.width = width 13 | self.stream = stream 14 | self.update_cnt = 0 15 | self.frequency=frequency 16 | 17 | def start(self): 18 | self.start_time = datetime.datetime.now() 19 | self.stream.write( "\n" ) 20 | self.update(0,self.max_value) 21 | 22 | def estimate_time(self, percent): 23 | if percent == 0: 24 | return "Est..." 25 | 26 | n = datetime.datetime.now() 27 | delta = n - self.start_time 28 | ts = delta.total_seconds() 29 | tt = ts / percent 30 | tr = tt - ts 31 | 32 | H = int(tr / 3600) 33 | tr -= H * 3600 34 | M = int(tr / 60) 35 | tr -= M * 60 36 | S = int(tr) 37 | 38 | time_remaining = "%d:%02d:%02d" % ( H, M, S ) 39 | return time_remaining 40 | 41 | def elapsed_time(self): 42 | 43 | n = datetime.datetime.now() 44 | delta = n - self.start_time 45 | 46 | ts = delta.total_seconds() 47 | 48 | H = int(ts / 3600) 49 | ts -= H * 3600 50 | M = int(ts / 60) 51 | ts -= M * 60 52 | S = int(ts) 53 | 54 | time_elapsed = "%d:%02d:%02d" % ( H, M, S ) 55 | return time_elapsed 56 | 57 | def update(self, value, max_value): 58 | self.value = value 59 | self.max_value = max_value 60 | 61 | D = float(self.max_value - self.min_value) 62 | if D == 0: 63 | percent = 1.0 64 | else: 65 | percent = float(self.value - self.min_value) / D 66 | bar_cnt = int( self.width * percent ) 67 | 68 | bar_str = "=" * bar_cnt 69 | bar_str += " " * (self.width - bar_cnt) 70 | 71 | percent_str = "%0.2f" % (100.0 * percent) 72 | time_remaining = self.estimate_time(percent) 73 | 74 | if self.update_cnt == 0: 75 | self.stream.write( "|%s| %6s%% | %s | %s / %s\n" % (bar_str, "done", "Est...", "done", "total") ) 76 | 77 | if self.update_cnt % self.frequency == 0: 78 | if self.message is None: 79 | self.stream.write( "|%s| %6s%% | %s | %d / %d\n" % (bar_str, percent_str, time_remaining, value, self.max_value) ) 80 | else: 81 | self.stream.write( "|%s| %6s%% | %s | %d / %d | %s\n" % (bar_str, percent_str, time_remaining, value, self.max_value, self.message) ) 82 | self.update_cnt += 1 83 | 84 | def set_message(self, message): 85 | self.message = message 86 | 87 | def finish(self): 88 | self.update(self.max_value, self.max_value) 89 | bar_str = "=" * self.width 90 | time_remaining = self.elapsed_time() 91 | self.stream.write( "\r|%s| 100.00%% | Done. | Elapsed Time: %s \n" % (bar_str, time_remaining) ) 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Leela Analysis Script 2 | 3 | This script is a modified version of scripts originally from: 4 | https://bitbucket.org/mkmatlock/sgftools 5 | 6 | Currently, it's designed to work with Leela 0.10.0, or 0.11.0, no guarantees about compatibility with any past or future versions. It runs on python 2.7. 7 | 8 | Added features and modifications from the original scripts: 9 | 10 | * Leela directly finds your game mistakes on its own and gives you alternate variations. 11 | * Supports SGFs with handicap and komi. (NOTE: Leela only uses Chinese rules, so if your game was really in Japanese rules, 12 | in very close endgames or with certain kinds of sekis the analysis may not be correct). 13 | * A variety of minor tweaks to the script interface and the information output to the SGF. 14 | * Cache takes into account the search limits (Leela will not use its past results if they were done with less search time). 15 | * Removed dependence on fcntl library - scripts now work on Windows! 16 | 17 | WARNING: It is not uncommon for Leela to mess up on tactical situations and give poor suggestions, particularly when it hasn't 18 | realized the life or death of a group yet that is actually already alive or dead. Like all MC bots, it also has a somewhat different 19 | notion than humans of how to wrap up a won game and what moves (despite still being winning) are "mistakes". Take the analysis with 20 | many grains of salt. 21 | 22 | ### How to Use 23 | First, download and install the "engine only"/"commandline"/"GTP" version of Leela 0.10.0 from: 24 | https://sjeng.org/leela.html 25 | 26 | Clone this repository to a local directory: 27 | 28 | git clone https://github.com/lightvector/leela-analysis 29 | cd leela-analysis 30 | 31 | Then simply run the script to analyze a game, providing the command to run to the leela executable, such as ./Leela0100GTP.exe or ./leela_0100_linux_x64. 32 | 33 | sgfanalyze.py my_game.sgf --leela /PATH/TO/LEELA.exe > my_game_analyzed.sgf 34 | 35 | By default, Leela will go through every position in the provided game and find what it considers to be all the mistakes by both players, 36 | producing an SGF file where it highlights those mistakes and provides alternative variations it would have expected. It will probably take 37 | an hour or two to run. 38 | 39 | Run the script with --help to see other options you can configure. You can change the amount of time Leela will analyze for, change how 40 | much effort it puts in to making variations versus just analyzing the main game, or select just a subrange of the game to analyze. 41 | 42 | ### Troubleshooting 43 | 44 | If you get an "OSError: [Errno 2] No such file or directory" error or you get an "OSError: [Errno 8] Exec format error" originating from "subprocess.py", 45 | check to make sure the command you provided for running Leela is correct. The former usually happens if you provided the wrong path, the latter if 46 | you provided the wrong Leela executable for your OS. 47 | 48 | If get an error like "WARNING: analysis stats missing data" that causes the analysis to consistently fail at a particular spot in a given sgf file and only 49 | output partial results, there is probably a bug in the script that causes it not to be able to parse a particular output by Leela in that position. Feel 50 | free to open an issue and provide the SGF file that causes the failure. You can also run with "-v 3" to enable super-verbose output and see exactly what 51 | Leela is outputting on that position. 52 | -------------------------------------------------------------------------------- /sgftools/annotations.py: -------------------------------------------------------------------------------- 1 | from sgftools import sgflib 2 | 3 | def insert_sequence(cursor, seq, data=None, callback=None): 4 | if data is None: 5 | data = [0]*len(seq) 6 | for (color, mv), elem in zip(seq, data): 7 | nnode = sgflib.Node() 8 | assert color in ['white', 'black'] 9 | color = 'W' if color =='white' else 'B' 10 | nnode.addProperty( nnode.makeProperty(color, [mv]) ) 11 | cursor.appendNode( nnode ) 12 | cursor.next( len(cursor.children) - 1 ) 13 | 14 | if callback is not None: 15 | if type(elem) in [list, tuple]: 16 | elem = list(elem) 17 | else: 18 | elem = [elem] 19 | callback( *tuple([cursor] + elem) ) 20 | 21 | for i in xrange(len(seq)): 22 | cursor.previous() 23 | 24 | def format_variation(cursor, seq): 25 | mv_seq = [(color, mv) for color, mv, _stats, _mv_list in seq] 26 | mv_data = [('black' if color == 'white' else 'white', stats, mv_list) for color, _mv, stats, mv_list in seq] 27 | insert_sequence(cursor, mv_seq, mv_data, format_analysis) 28 | 29 | def pos_is_pass(pos): 30 | if pos == "" or pos == "tt": 31 | return True 32 | return False 33 | 34 | def format_pos(pos,board_size): 35 | #In an sgf file, passes are the empty string or tt 36 | if pos_is_pass(pos): 37 | return "pass" 38 | if len(pos) != 2: 39 | return pos 40 | return "ABCDEFGHJKLMNOPQRSTUVXYZ"[ord(pos[0]) - ord('a')] + str(board_size - (ord(pos[1]) - ord('a'))) 41 | 42 | def format_winrate(stats,move_list,board_size,next_game_move): 43 | comment = "" 44 | if'winrate' in stats: 45 | comment += "Overall black win%%: %.2f%%\n" % (stats['winrate'] * 100) 46 | else: 47 | comment += "Overall black win%: not computed (Leela still in opening book)\n" 48 | 49 | if len(move_list) > 0 and move_list[0]['pos'] != next_game_move: 50 | comment += "Leela's preferred next move: %s\n" % format_pos(move_list[0]['pos'],board_size) 51 | else: 52 | comment += "\n" 53 | 54 | return comment 55 | 56 | def format_delta_info(delta, transdelta, stats, this_move, board_size): 57 | comment = "" 58 | LB_values = [] 59 | if(transdelta <= -0.200): 60 | comment += "==========================\n" 61 | comment += "Big Mistake? (%s) (delta %.2f%%)\n" % (format_pos(this_move,board_size), delta * 100) 62 | comment += "==========================\n" 63 | if not pos_is_pass(this_move): 64 | LB_values.append("%s:%s" % (this_move,"?")) 65 | elif(transdelta <= -0.075): 66 | comment += "==========================\n" 67 | comment += "Mistake? (%s) (delta %.2f%%)\n" % (format_pos(this_move,board_size), delta * 100) 68 | comment += "==========================\n" 69 | if not pos_is_pass(this_move): 70 | LB_values.append("%s:%s" % (this_move,"?")) 71 | elif(transdelta <= -0.040): 72 | comment += "==========================\n" 73 | comment += "Inaccuracy? (%s) (delta %.2f%%)\n" % (format_pos(this_move,board_size), delta * 100) 74 | comment += "==========================\n" 75 | if not pos_is_pass(this_move): 76 | LB_values.append("%s:%s" % (this_move,"?")) 77 | elif(transdelta <= -0.005): 78 | comment += "Leela slightly dislikes %s (delta %.2f%%).\n" % (format_pos(this_move,board_size), delta * 100) 79 | 80 | comment += "\n" 81 | return (comment,LB_values) 82 | 83 | def format_analysis(stats, move_list, this_move): 84 | abet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 85 | comment = "" 86 | if 'bookmoves' in stats: 87 | comment += "==========================\n" 88 | comment += "Considered %d/%d bookmoves\n" % (stats['bookmoves'], stats['positions']) 89 | else: 90 | comment += "==========================\n" 91 | comment += "Visited %d nodes\n" % (stats['visits']) 92 | comment += "\n" 93 | 94 | for L, mv in zip(abet, move_list): 95 | comment += "%s -> Win%%: %.2f%% (%d visits) \n" % (L, mv['winrate'] * 100, mv['visits']) 96 | 97 | #Check for pos being "" or "tt", values which indicate passes, and don't attempt to display markers for them 98 | LB_values = ["%s:%s" % (mv['pos'],L) for L, mv in zip(abet, move_list) if mv['pos'] != "" and mv['pos'] != "tt"] 99 | mvs = [mv['pos'] for mv in move_list] 100 | TR_values = [this_move] if this_move not in mvs and this_move is not None and not pos_is_pass(this_move) else [] 101 | return (comment,LB_values,TR_values) 102 | 103 | def label_key(label): 104 | return label.split(':')[0] 105 | 106 | def label_keys(labels): 107 | return [label_key(l) for l in labels] 108 | 109 | def annotate_sgf(cursor, comment, LB_values, TR_values): 110 | cnode = cursor.node 111 | 112 | LB_existing = [] 113 | TR_existing = [] 114 | set_existing = set() 115 | # ensure there are no duplicates 116 | if cnode.has_key("LB"): 117 | LB_existing = cnode["LB"] 118 | set_existing |= set(label_keys(LB_existing)) 119 | 120 | if cnode.has_key("TR"): 121 | TR_existing = cnode["TR"] 122 | set_existing |= set(TR_existing) 123 | 124 | LB_values = [x for x in LB_values if label_key(x) not in set_existing] 125 | TR_values = [x for x in TR_values if x not in set_existing] 126 | 127 | if cnode.has_key('C'): 128 | cnode['C'].data[0] += comment 129 | else: 130 | cnode.addProperty( cnode.makeProperty( 'C', [comment] ) ) 131 | 132 | if len(LB_values) > 0: 133 | LB_prop = cnode.makeProperty( 'LB', LB_values + LB_existing) 134 | if (cnode.has_key("LB")): 135 | cnode["LB"] = LB_prop 136 | else: 137 | cnode.addProperty( LB_prop ) 138 | if len(TR_values) > 0: 139 | TR_prop = cnode.makeProperty( 'TR', TR_values + TR_existing) 140 | if (cnode.has_key("TR")): 141 | cnode["TR"] = TR_prop 142 | else: 143 | cnode.addProperty( TR_prop ) 144 | -------------------------------------------------------------------------------- /sgftools/gotools.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import sgflib 3 | #import numpy as np 4 | 5 | class Pattern(object): 6 | def __init__( self, boardstate, area=None ): 7 | mapping = {None: 0, 'b': 1, 'w': 2} 8 | self.seedstate = np.array([[mapping[item] for item in col] for col in boardstate]) 9 | self.SZ = self.seedstate.shape[0] 10 | 11 | if area is None: 12 | self.seedarea = np.ones(self.seedstate.shape) 13 | else: 14 | x1,y1 = self.get_coords(area[0:2]) 15 | x2,y2 = self.get_coords(area[2:4]) 16 | 17 | self.seedarea = \ 18 | np.array( [[1 if (x1 <= j <= x2) and (y1 <= i <= y2) else 0 \ 19 | for i in xrange(self.SZ)] \ 20 | for j in xrange(self.SZ)] ) 21 | 22 | self._states = [self.seedstate, 23 | np.rot90( self.seedstate, 1), 24 | np.rot90( self.seedstate, 2), 25 | np.rot90( self.seedstate, 3), 26 | np.flipud( self.seedstate ), 27 | np.flipud( np.rot90( self.seedstate, 1 ) ), 28 | np.flipud( np.rot90( self.seedstate, 2 ) ), 29 | np.flipud( np.rot90( self.seedstate, 3 ) )] 30 | self._areas = [self.seedarea, 31 | np.rot90( self.seedarea, 1), 32 | np.rot90( self.seedarea, 2), 33 | np.rot90( self.seedarea, 3), 34 | np.flipud( self.seedarea ), 35 | np.flipud( np.rot90( self.seedarea, 1 ) ), 36 | np.flipud( np.rot90( self.seedarea, 2 ) ), 37 | np.flipud( np.rot90( self.seedarea, 3 ) )] 38 | 39 | def assert_matches_seed_state(self, goban): 40 | if self.SZ != goban.SZ: 41 | raise Exception("Incompatible pattern sizes: %d != %d" (self.SZ, goban.SZ)) 42 | 43 | S, A = self.seedstate, self.seedarea 44 | S2 = goban.pattern() 45 | sel_area = np.logical_not( A == 1 ) 46 | match = S == S2 47 | 48 | if not np.all(np.logical_or( match, sel_area )): 49 | raise AssertionError("Seed state not matched:\n" + str(self) + "\n" + str(goban)) 50 | 51 | def get_coords( self, pos ): 52 | x = ord(pos[0])-97 53 | y = ord(pos[1])-97 54 | 55 | return x,y 56 | 57 | def print_pattern(self, states, area): 58 | state_map = {0: ".", 1: "b", 2: "w"} 59 | p_rep = "+" + "-" * (2*self.SZ+1) + "+\n" 60 | for j in xrange(0, self.SZ): 61 | p_rep += "|" 62 | for i in xrange(0, self.SZ): 63 | state = states[i, j] 64 | use = area[i, j] 65 | p_rep += " " + (state_map[state] if use == 1 else "*") 66 | p_rep += " |\n" 67 | p_rep += "+" + "-" * (2*self.SZ+1) + "+" 68 | return p_rep 69 | 70 | def __str__(self): 71 | return self.print_pattern( self.seedstate, self.seedarea ) 72 | 73 | def __repr__(self): 74 | return self.print_pattern( self.seedstate, self.seedarea ) 75 | # p_rep = "" 76 | # for state, area in zip(self._states, self._areas): 77 | # p_rep += self.print_pattern( state, area ) + "\n" 78 | # return p_rep 79 | 80 | def __eq__(self, goban): 81 | if self.SZ != goban.SZ: 82 | raise Exception("Incompatible pattern sizes: %d != %d" (self.SZ, goban.SZ)) 83 | 84 | S2 = goban.pattern() 85 | for S, A in zip(self._states, self._areas): 86 | sel_area = np.logical_not( A == 1 ) 87 | match = S == S2 88 | 89 | if np.all(np.logical_or( match, sel_area )): 90 | return True 91 | return False 92 | 93 | def align(self, goban): 94 | if self.SZ != goban.SZ: 95 | raise Exception("Incompatible pattern sizes: %d != %d" (self.SZ, goban.SZ)) 96 | 97 | S2 = goban.pattern() 98 | for i, S1, A1 in zip(range(8), self._states, self._areas): 99 | sel_area = np.logical_not( A1 == 1 ) 100 | match = S1 == S2 101 | 102 | if np.all(np.logical_or( match, sel_area )): 103 | alignment = i 104 | # print >>sys.stderr, alignment 105 | # print >>sys.stderr, self.print_pattern(S1, A1) 106 | # print >>sys.stderr, goban 107 | break 108 | 109 | if alignment < 4: 110 | return ['rot90'] * alignment 111 | else: 112 | return ['fliplr'] + ['rot90'] * (alignment-4) 113 | 114 | class Goban(object): 115 | def __init__( self, sgf ): 116 | self.sgf = sgf 117 | self.init_board_state() 118 | 119 | def init_board_state( self ): 120 | c = self.sgf.cursor() 121 | 122 | self.SZ = 19 123 | 124 | for k in c.node.keys(): 125 | v = c.node[k] 126 | if v.name == 'SZ': 127 | self.SZ = int( v[0] ) 128 | 129 | self.boardstate = [ ] 130 | for i in xrange(0, self.SZ): 131 | self.boardstate.append( list() ) 132 | for j in xrange(0, self.SZ): 133 | self.boardstate[i].append( None ) 134 | 135 | def area_occupied(self, x1, y1, x2, y2): 136 | return any([self.boardstate[i][j] is not None \ 137 | for i in xrange(x1, x2) \ 138 | for j in xrange(y1, y2)]) 139 | 140 | def pattern( self ): 141 | mapping = {None: 0, 'b': 1, 'w': 2} 142 | return np.array([[mapping[item] for item in col] for col in self.boardstate]) 143 | 144 | def __repr__(self): 145 | state_map = {None: ".", 'b': "b", 'w': "w"} 146 | p_rep = "+" + "-" * (2*self.SZ+1) + "+\n" 147 | for j in xrange(0, self.SZ): 148 | p_rep += "|" 149 | for i in xrange(0, self.SZ): 150 | state = self.boardstate[i][j] 151 | p_rep += " " + (state_map[state]) 152 | p_rep += " |\n" 153 | p_rep += "+" + "-" * (2*self.SZ+1) + "+" 154 | return p_rep 155 | 156 | def copy( self ): 157 | g = Goban( self.sgf ) 158 | for i in xrange(0, self.SZ): 159 | for j in xrange(0, self.SZ): 160 | g.boardstate[i][j] = self.boardstate[i][j] 161 | return g 162 | 163 | def __str__( self ): 164 | return self.__repr__() 165 | 166 | def node_has_move( self, node ): 167 | for k in node.keys(): 168 | v = node[k] 169 | if v.name in ['W','B']: 170 | return True 171 | return False 172 | 173 | def perform( self, node ): 174 | 175 | move = None 176 | color = None 177 | for k in node.keys(): 178 | v = node[k] 179 | 180 | if v.name == 'AB': 181 | for p in v: 182 | x,y = self.get_coords( p ) 183 | self.boardstate[x][y] = 'b' 184 | 185 | if v.name == 'AW': 186 | for p in v: 187 | x,y = self.get_coords( p ) 188 | self.boardstate[x][y] = 'w' 189 | 190 | if v.name == 'B': 191 | if not is_pass( v[0] ) and not is_tenuki( v[0] ): 192 | x,y = self.get_coords( v[0] ) 193 | self.boardstate[x][y] = 'b' 194 | move = x,y 195 | color = 'b' 196 | else: 197 | move = None 198 | color = 'b' 199 | 200 | if v.name == 'W': 201 | if not is_pass( v[0] ) and not is_tenuki( v[0] ): 202 | x,y = self.get_coords( v[0] ) 203 | self.boardstate[x][y] = 'w' 204 | move = x,y 205 | color = 'w' 206 | else: 207 | move = None 208 | color = 'w' 209 | 210 | killed = 0 211 | if move is not None: 212 | killed = self.process_dead_stones( move, color ) 213 | 214 | return killed 215 | 216 | def get_adjacent( self, x, y ): 217 | positions = [] 218 | if x > 0: 219 | positions.append( (x-1, y) ) 220 | if x+1 < self.SZ: 221 | positions.append( (x+1, y) ) 222 | if y > 0: 223 | positions.append( (x, y-1) ) 224 | if y+1 < self.SZ: 225 | positions.append( (x, y+1) ) 226 | 227 | return positions 228 | 229 | 230 | def process_dead_stones( self, lastMove, color ): 231 | x, y = lastMove 232 | killed = 0 233 | opposing = 'w' 234 | if opposing == color: 235 | opposing = 'b' 236 | 237 | for pos in self.get_adjacent( x, y ): 238 | grp, c = self.get_group( pos ) 239 | 240 | if c == opposing and self.get_liberties( grp ) == 0: 241 | self.kill_group( grp ) 242 | killed += len( grp ) 243 | 244 | return killed 245 | 246 | def get_liberties( self, grp ): 247 | liberties = 0 248 | for x, y, c in grp: 249 | for pos in self.get_adjacent( x, y ): 250 | i, j = pos 251 | if self.boardstate[i][j] == None: 252 | liberties+=1 253 | return liberties 254 | 255 | def kill_group( self, grp ): 256 | for x, y, c in grp: 257 | self.boardstate[x][y] = None 258 | 259 | 260 | def get_group( self, pos, g=None, color=None, visited=None ): 261 | x, y = pos 262 | if color == None: 263 | color = self.boardstate[x][y] 264 | if visited == None: 265 | visited = set() 266 | if g == None: 267 | g = list() 268 | 269 | if color is None: 270 | return [], None 271 | 272 | visited.add( pos ) 273 | g.append((x, y, color)) 274 | 275 | for adj in self.get_adjacent( x, y ): 276 | if adj not in visited and self.boardstate[adj[0]][adj[1]] == color: 277 | self.get_group( adj, g, color, visited ) 278 | 279 | return g, color 280 | 281 | 282 | def get_coords( self, pos ): 283 | x = ord(pos[0])-97 284 | y = ord(pos[1])-97 285 | 286 | if x < 0 or x >= self.SZ or y < 0 or y >= self.SZ: 287 | raise ValueError("Invalid board coordinate: ('%s': %d, %d)" % (pos, x, y)) 288 | 289 | return x, y 290 | 291 | 292 | def split_continuations( sgf ): 293 | c = sgf.cursor() 294 | goban = Goban( sgf ) 295 | navigate_splits( c, goban ) 296 | 297 | def navigate_splits( c, goban ): 298 | k = goban.perform( c.node ) 299 | pos, color = get_capture_move( c ) 300 | 301 | if k > 0: 302 | player = "Black" 303 | if color == 'w': player = "White" 304 | 305 | n = sgflib.Node([ sgflib.Property( 'LB', ['%s:A' % ( pos )] ), 306 | sgflib.Property( 'C', ['%s captures at A, see variation for continuation' % ( player )] ) ]) 307 | c.pushNode( n ) 308 | 309 | i = 0 310 | while i < len( c.children ): 311 | c.next( i ) 312 | navigate_splits( c, goban.copy() ) 313 | c.previous() 314 | i += 1 315 | 316 | def get_capture_move( c ): 317 | pos, color = None, None 318 | for k in c.node.keys(): 319 | v = c.node[k] 320 | if v.name == 'B': 321 | pos = v[0] 322 | color = 'b' 323 | if v.name == 'W': 324 | pos = v[0] 325 | color = 'w' 326 | 327 | return pos, color 328 | 329 | def add_numberings( sgf ): 330 | c = sgf.cursor() 331 | number_endpoints( c, {} ) 332 | 333 | def is_pass( p ): 334 | return p == '' or p == '``' 335 | 336 | def is_tenuki( p ): 337 | return p == 'tt' 338 | 339 | def clean_sgf( sgf ): 340 | c = sgf.cursor() 341 | clean_node( c ) 342 | 343 | def clean_node( cursor ): 344 | pass_repl = "" 345 | for k in cursor.node.keys(): 346 | v = cursor.node[k] 347 | if v.name == 'B': 348 | p = v[0] 349 | if is_pass(p): 350 | pass_repl = 'B' 351 | if v.name == 'W': 352 | p = v[0] 353 | if is_pass(p): 354 | pass_repl = 'W' 355 | 356 | if pass_repl != "": 357 | p = get_property( cursor.node, pass_repl ) 358 | p[0] = '' 359 | 360 | for i in xrange(0, len( cursor.children )): 361 | cursor.next( i ) 362 | clean_node( cursor ) 363 | cursor.previous() 364 | 365 | def number_endpoints( cursor, moves, num=1 ): 366 | hasMove = False 367 | for k in cursor.node.keys(): 368 | v = cursor.node[k] 369 | if v.name == 'B': 370 | p = v[0] 371 | if not is_pass(p): 372 | moves[p] = num 373 | hasMove = True 374 | if v.name == 'W': 375 | p = v[0] 376 | if not is_pass(p): 377 | moves[p] = num 378 | hasMove = True 379 | 380 | for i in xrange(0, len( cursor.children )): 381 | cursor.next( i ) 382 | number_endpoints( cursor, moves.copy(), num+1 if hasMove else num ) 383 | cursor.previous() 384 | 385 | if len( cursor.children ) == 0: 386 | for pos in moves: 387 | add_label( cursor.node, pos, moves[pos] ) 388 | 389 | def get_property( node, tag ): 390 | for prop in node: 391 | if prop.name == tag: 392 | return prop 393 | 394 | def add_or_extend_property( node, tag, vals ): 395 | prop = get_property(node, tag) 396 | if prop != None: 397 | for v in vals: 398 | prop.append( v ) 399 | else: 400 | prop = sgflib.Property(tag, vals) 401 | node.addProperty( prop ) 402 | 403 | def add_label( node, pos, lbl, overwrite=False ): 404 | prop = get_property( node, 'LB' ) 405 | 406 | lbl_template = "%s:%s" % (pos, str( lbl )) 407 | 408 | if prop == None: 409 | prop = sgflib.Property( 'LB', [ lbl_template ]) 410 | node.addProperty( prop ) 411 | else: 412 | remove = [] 413 | exists = False 414 | for i in xrange(0, len(prop)): 415 | v = prop[i] 416 | if v[:2] == pos and overwrite: 417 | prop[i] = lbl_template 418 | exists = True 419 | elif v[:2] == pos: 420 | exists = True 421 | # print "Not overwriting: " + v + " with: " + lbl_template 422 | 423 | if not exists: 424 | prop.append( lbl_template ) 425 | 426 | def get_crop( sgf ): 427 | positions = [] 428 | 429 | positions += collect_positions( sgf.cursor() ) 430 | 431 | x = [] 432 | y = [] 433 | for pos in positions: 434 | pos = pos.lower().strip() 435 | if pos == '``' or pos == '': 436 | continue 437 | x.append( ord( pos[0] ) - 96 ) 438 | y.append( ord( pos[1] ) - 96 ) 439 | 440 | minx, maxx = process_limits(x) 441 | miny, maxy = process_limits(y) 442 | 443 | return minx + miny + maxx + maxy 444 | 445 | 446 | def process_limits(x): 447 | if min(x) <= 10 and max(x) <= 10: 448 | minx = 'a' 449 | maxx = 'j' 450 | elif min(x) >= 10 and max(x) >= 10: 451 | minx = 'j' 452 | maxx = 's' 453 | elif min(x) >= 4 and max(x) <= 16: 454 | minx = 'd' 455 | maxx = 'p' 456 | else: 457 | minx = 'a' 458 | maxx = 's' 459 | return minx, maxx 460 | 461 | 462 | def collect_positions( cursor ): 463 | positions = [] 464 | 465 | for k in cursor.node.keys(): 466 | v = cursor.node[k] 467 | 468 | if v.name in ['AB', 'W', 'B', 'AW', 'SQ', 'TR', 'CR']: 469 | positions += [ p for p in v ] 470 | if v.name in ['LB']: 471 | positions += [ p.split(":")[0] for p in v ] 472 | 473 | for i in xrange(0, len( cursor.children )): 474 | cursor.next( i ) 475 | positions += collect_positions( cursor ) 476 | cursor.previous() 477 | 478 | return positions 479 | 480 | 481 | def import_sgf( filename ): 482 | data = "" 483 | with open(filename, 'r') as ifile: 484 | for line in ifile: 485 | data += line 486 | 487 | parser = sgflib.SGFParser( data ) 488 | return parser.parse() 489 | -------------------------------------------------------------------------------- /sgftools/leela.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import re 4 | import time 5 | import hashlib 6 | from Queue import Queue, Empty 7 | from threading import Thread 8 | from subprocess import Popen, PIPE, STDOUT 9 | 10 | update_regex = r'Nodes: ([0-9]+), Win: ([0-9]+\.[0-9]+)\% \(MC:[0-9]+\.[0-9]+\%\/VN:[0-9]+\.[0-9]+\%\), PV:(( [A-Z][0-9]+)+)' 11 | update_regex_no_vn = r'Nodes: ([0-9]+), Win: ([0-9]+\.[0-9]+)\%, PV:(( [A-Z][0-9]+)+)' 12 | 13 | status_regex = r'MC winrate=([0-9]+\.[0-9]+), NN eval=([0-9]+\.[0-9]+), score=([BW]\+[0-9]+\.[0-9]+)' 14 | status_regex_no_vn = r'MC winrate=([0-9]+\.[0-9]+), score=([BW]\+[0-9]+\.[0-9]+)' 15 | 16 | move_regex = r'^([A-Z][0-9]+) -> +([0-9]+) \(W: +(\-?[0-9]+\.[0-9]+)\%\) \(U: +(\-?[0-9]+\.[0-9]+)\%\) \(V: +([0-9]+\.[0-9]+)\%: +([0-9]+)\) \(N: +([0-9]+\.[0-9]+)\%\) PV: (.*)$' 17 | move_regex_no_vn = r'^([A-Z][0-9]+) -> +([0-9]+) \(U: +(\-?[0-9]+\.[0-9]+)\%\) \(R: +([0-9]+\.[0-9]+)\%: +([0-9]+)\) \(N: +([0-9]+\.[0-9]+)\%\) PV: (.*)$' 18 | 19 | best_regex = r'([0-9]+) visits, score (\-? ?[0-9]+\.[0-9]+)\% \(from \-? ?[0-9]+\.[0-9]+\%\) PV: (.*)' 20 | stats_regex = r'([0-9]+) visits, ([0-9]+) nodes(?:, ([0-9]+) playouts)(?:, ([0-9]+) p/s)' 21 | bookmove_regex = r'([0-9]+) book moves, ([0-9]+) total positions' 22 | finished_regex = r'= ([A-Z][0-9]+|resign|pass)' 23 | 24 | #Start a thread that perpetually reads from the given file descriptor 25 | #and pushes the result on to a queue, to simulate non-blocking io. We 26 | #could just use fcntl and make the file descriptor non-blocking, but 27 | #fcntl isn't available on windows so we do this horrible hack. 28 | class ReaderThread: 29 | def __init__(self,fd): 30 | self.queue = Queue() 31 | self.fd = fd 32 | self.stopped = False 33 | def stop(self): 34 | #No lock since this is just a simple bool that only ever changes one way 35 | self.stopped = True 36 | def loop(self): 37 | while not self.stopped and not self.fd.closed: 38 | line = None 39 | #fd.readline() should return due to eof once the process is closed 40 | #at which point 41 | try: 42 | line = self.fd.readline() 43 | except IOError: 44 | time.sleep(0.2) 45 | pass 46 | if line is not None and len(line) > 0: 47 | self.queue.put(line) 48 | 49 | def readline(self): 50 | try: 51 | line = self.queue.get_nowait() 52 | except Empty: 53 | return "" 54 | return line 55 | 56 | def read_all_lines(self): 57 | lines = [] 58 | while True: 59 | try: 60 | line = self.queue.get_nowait() 61 | except Empty: 62 | break 63 | lines.append(line) 64 | return lines 65 | 66 | 67 | def start_reader_thread(fd): 68 | rt = ReaderThread(fd) 69 | def begin_loop(): 70 | rt.loop() 71 | 72 | t = Thread(target=begin_loop) 73 | t.start() 74 | return rt 75 | 76 | class CLI(object): 77 | def __init__(self, board_size, executable, is_handicap_game, komi, seconds_per_search, verbosity): 78 | self.history=[] 79 | self.executable = executable 80 | self.verbosity = verbosity 81 | self.board_size = board_size 82 | self.is_handicap_game = is_handicap_game 83 | self.komi = komi 84 | self.seconds_per_search = seconds_per_search + 1 #add one to account for lag time 85 | self.p = None 86 | 87 | def convert_position(self, pos): 88 | abet = 'abcdefghijklmnopqrstuvwxyz' 89 | mapped = 'abcdefghjklmnopqrstuvwxyz' 90 | pos = '%s%d' % (mapped[abet.index(pos[0])], self.board_size-abet.index(pos[1])) 91 | return pos 92 | 93 | def parse_position(self, pos): 94 | #Pass moves are the empty string in sgf files 95 | if pos == "pass": 96 | return "" 97 | 98 | abet = 'abcdefghijklmnopqrstuvwxyz' 99 | mapped = 'abcdefghjklmnopqrstuvwxyz' 100 | 101 | X = mapped.index(pos[0].lower()) 102 | Y = self.board_size-int(pos[1:]) 103 | 104 | return "%s%s" % (abet[X], abet[Y]) 105 | 106 | def history_hash(self): 107 | H = hashlib.md5() 108 | for cmd in self.history: 109 | _, c, p = cmd.split() 110 | H.update(c[0] + p) 111 | return H.hexdigest() 112 | 113 | def add_move(self, color, pos): 114 | if pos == '' or pos =='tt': 115 | pos = 'pass' 116 | else: 117 | pos = self.convert_position(pos) 118 | cmd = "play %s %s" % (color, pos) 119 | self.history.append(cmd) 120 | 121 | def pop_move(self): 122 | self.history.pop() 123 | 124 | def clear_history(self): 125 | self.history = [] 126 | 127 | def whoseturn(self): 128 | if len(self.history) == 0: 129 | if self.is_handicap_game: 130 | return "white" 131 | else: 132 | return "black" 133 | elif 'white' in self.history[-1]: 134 | return 'black' 135 | else: 136 | return 'white' 137 | 138 | def parse_status_update(self, message): 139 | M = re.match(update_regex, message) 140 | if M is None: 141 | M = re.match(update_regex_no_vn, message) 142 | 143 | if M is not None: 144 | visits = int(M.group(1)) 145 | winrate = self.to_fraction(M.group(2)) 146 | seq = M.group(3) 147 | seq = [self.parse_position(p) for p in seq.split()] 148 | 149 | return {'visits': visits, 'winrate': winrate, 'seq': seq} 150 | return {} 151 | 152 | # Drain all remaining stdout and stderr current contents 153 | def drain(self): 154 | so = self.stdout_thread.read_all_lines() 155 | se = self.stderr_thread.read_all_lines() 156 | return (so,se) 157 | 158 | # Send command and wait for ack 159 | def send_command(self, cmd, expected_success_count=1, drain=True, timeout=20): 160 | self.p.stdin.write(cmd + "\n") 161 | sleep_per_try = 0.1 162 | tries = 0 163 | success_count = 0 164 | while tries * sleep_per_try <= timeout and self.p is not None: 165 | time.sleep(sleep_per_try) 166 | tries += 1 167 | # Readline loop 168 | while True: 169 | s = self.stdout_thread.readline() 170 | # Leela follows GTP and prints a line starting with "=" upon success. 171 | if s.strip() == '=': 172 | success_count += 1 173 | if success_count >= expected_success_count: 174 | if drain: 175 | self.drain() 176 | return 177 | # No output, so break readline loop and sleep and wait for more 178 | if s == "": 179 | break 180 | raise Exception("Failed to send command '%s' to Leela" % (cmd)) 181 | 182 | def start(self): 183 | xargs = [] 184 | 185 | if self.verbosity > 0: 186 | print >>sys.stderr, "Starting leela..." 187 | 188 | p = Popen([self.executable, '--gtp', '--noponder'] + xargs, stdout=PIPE, stdin=PIPE, stderr=PIPE) 189 | self.p = p 190 | self.stdout_thread = start_reader_thread(p.stdout) 191 | self.stderr_thread = start_reader_thread(p.stderr) 192 | 193 | time.sleep(2) 194 | if self.verbosity > 0: 195 | print >>sys.stderr, "Setting board size %d and komi %f to Leela" % (self.board_size, self.komi) 196 | self.send_command('boardsize %d' % (self.board_size)) 197 | self.send_command('komi %f' % (self.komi)) 198 | self.send_command('time_settings 0 %d 1' % (self.seconds_per_search)) 199 | 200 | def stop(self): 201 | if self.verbosity > 0: 202 | print >>sys.stderr, "Stopping leela..." 203 | 204 | if self.p is not None: 205 | p = self.p 206 | stdout_thread = self.stdout_thread 207 | stderr_thread = self.stderr_thread 208 | self.p = None 209 | self.stdout_thread = None 210 | self.stderr_thread = None 211 | stdout_thread.stop() 212 | stderr_thread.stop() 213 | try: 214 | p.stdin.write('exit\n') 215 | except IOError: 216 | pass 217 | time.sleep(0.1) 218 | try: 219 | p.terminate() 220 | except OSError: 221 | pass 222 | 223 | def playmove(self, pos): 224 | color = self.whoseturn() 225 | self.send_command('play %s %s' % (color, pos)) 226 | self.history.append(cmd) 227 | 228 | def reset(self): 229 | self.send_command('clear_board') 230 | 231 | def boardstate(self): 232 | self.send_command("showboard",drain=False) 233 | (so,se) = self.drain() 234 | return "".join(se) 235 | 236 | def goto_position(self): 237 | count = len(self.history) 238 | cmd = "\n".join(self.history) 239 | self.send_command(cmd,expected_success_count=count) 240 | 241 | def analyze(self): 242 | p = self.p 243 | if self.verbosity > 1: 244 | print >>sys.stderr, "Analyzing state:" 245 | print >>sys.stderr, self.whoseturn(), "to play" 246 | print >>sys.stderr, self.boardstate() 247 | 248 | self.send_command('time_left black %d 1\n' % (self.seconds_per_search)) 249 | self.send_command('time_left white %d 1\n' % (self.seconds_per_search)) 250 | 251 | cmd = "genmove %s\n" % (self.whoseturn()) 252 | p.stdin.write(cmd) 253 | 254 | updated = 0 255 | stderr = [] 256 | stdout = [] 257 | 258 | while updated < 20 + self.seconds_per_search * 2 and self.p is not None: 259 | O,L = self.drain() 260 | stdout.extend(O) 261 | stderr.extend(L) 262 | 263 | D = self.parse_status_update("".join(L)) 264 | if 'visits' in D: 265 | if self.verbosity > 0: 266 | print >>sys.stderr, "Visited %d positions" % (D['visits']) 267 | updated = 0 268 | updated += 1 269 | if re.search(finished_regex, ''.join(stdout)) is not None: 270 | if re.search(stats_regex, ''.join(stderr)) is not None or re.search(bookmove_regex, ''.join(stderr)) is not None: 271 | break 272 | time.sleep(1) 273 | 274 | p.stdin.write("\n") 275 | time.sleep(1) 276 | O,L = self.drain() 277 | stdout.extend(O) 278 | stderr.extend(L) 279 | 280 | stats, move_list = self.parse(stdout, stderr) 281 | if self.verbosity > 0: 282 | print >>sys.stderr, "Chosen move: %s" % (stats['chosen']) 283 | if 'best' in stats: 284 | print >>sys.stderr, "Best move: %s" % (stats['best']) 285 | print >>sys.stderr, "Winrate: %f" % (stats['winrate']) 286 | print >>sys.stderr, "Visits: %d" % (stats['visits']) 287 | 288 | return stats, move_list 289 | 290 | def to_fraction(self, v): 291 | v = v.strip() 292 | return 0.01 * float(v) 293 | 294 | def parse(self, stdout, stderr): 295 | if self.verbosity > 2: 296 | print >>sys.stderr, "LEELA STDOUT" 297 | print >>sys.stderr, "".join(stdout) 298 | print >>sys.stderr, "END OF LEELA STDOUT" 299 | print >>sys.stderr, "LEELA STDERR" 300 | print >>sys.stderr, "".join(stderr) 301 | print >>sys.stderr, "END OF LEELA STDERR" 302 | 303 | stats = {} 304 | move_list = [] 305 | 306 | flip_winrate = self.whoseturn() == "white" 307 | def maybe_flip(winrate): 308 | return ((1.0 - winrate) if flip_winrate else winrate) 309 | 310 | finished=False 311 | summarized=False 312 | for line in stderr: 313 | line = line.strip() 314 | if line.startswith('================'): 315 | finished=True 316 | 317 | M = re.match(bookmove_regex, line) 318 | if M is not None: 319 | stats['bookmoves'] = int(M.group(1)) 320 | stats['positions'] = int(M.group(2)) 321 | 322 | M = re.match(status_regex, line) 323 | if M is not None: 324 | stats['mc_winrate'] = maybe_flip(float(M.group(1))) 325 | stats['nn_winrate'] = maybe_flip(float(M.group(2))) 326 | stats['margin'] = M.group(3) 327 | 328 | M = re.match(status_regex_no_vn, line) 329 | if M is not None: 330 | stats['mc_winrate'] = maybe_flip(float(M.group(1))) 331 | stats['margin'] = M.group(2) 332 | 333 | M = re.match(move_regex, line) 334 | if M is not None: 335 | pos = self.parse_position(M.group(1)) 336 | visits = int(M.group(2)) 337 | W = maybe_flip(self.to_fraction(M.group(3))) 338 | U = maybe_flip(self.to_fraction(M.group(4))) 339 | Vp = maybe_flip(self.to_fraction(M.group(5))) 340 | Vn = int(M.group(6)) 341 | N = self.to_fraction(M.group(7)) 342 | seq = M.group(8) 343 | seq = [self.parse_position(p) for p in seq.split()] 344 | 345 | info = { 346 | 'pos': pos, 347 | 'visits': visits, 348 | 'winrate': W, 'mc_winrate': U, 'nn_winrate': Vp, 'nn_count': Vn, 349 | 'policy_prob': N, 'pv': seq 350 | } 351 | move_list.append(info) 352 | 353 | M = re.match(move_regex_no_vn, line) 354 | if M is not None: 355 | pos = self.parse_position(M.group(1)) 356 | visits = int(M.group(2)) 357 | U = maybe_flip(self.to_fraction(M.group(3))) 358 | R = maybe_flip(self.to_fraction(M.group(4))) 359 | Rn = int(M.group(5)) 360 | N = self.to_fraction(M.group(6)) 361 | seq = M.group(7) 362 | seq = [self.parse_position(p) for p in seq.split()] 363 | 364 | info = { 365 | 'pos': pos, 366 | 'visits': visits, 367 | 'winrate': U, 'mc_winrate': U, 'r_winrate': R, 'r_count': Rn, 368 | 'policy_prob': N, 'pv': seq 369 | } 370 | move_list.append(info) 371 | 372 | if finished and not summarized: 373 | M = re.match(best_regex, line) 374 | if M is not None: 375 | stats['best'] = self.parse_position(M.group(3).split()[0]) 376 | stats['winrate'] = maybe_flip(self.to_fraction(M.group(2))) 377 | 378 | M = re.match(stats_regex, line) 379 | if M is not None: 380 | stats['visits'] = int(M.group(1)) 381 | summarized=True 382 | 383 | M = re.search(finished_regex, "".join(stdout)) 384 | if M is not None: 385 | if M.group(1) == "resign": 386 | stats['chosen'] = "resign" 387 | else: 388 | stats['chosen'] = self.parse_position(M.group(1)) 389 | 390 | if 'bookmoves' in stats and len(move_list)==0: 391 | move_list.append({'pos': stats['chosen'], 'is_book': True}) 392 | else: 393 | required_keys = ['mc_winrate', 'margin', 'best', 'winrate', 'visits'] 394 | for k in required_keys: 395 | if k not in stats: 396 | print >>sys.stderr, "WARNING: analysis stats missing data %s" % (k) 397 | 398 | move_list = sorted(move_list, key = (lambda info: 1000000000000000 if info['pos'] == stats['best'] else info['visits']), reverse=True) 399 | move_list = [info for (i,info) in enumerate(move_list) if i == 0 or info['visits'] > 0] 400 | 401 | #In the case where leela resigns, rather than resigning, just replace with the move Leela did think was best 402 | if stats['chosen'] == "resign": 403 | stats['chosen'] = stats['best'] 404 | 405 | return stats, move_list 406 | -------------------------------------------------------------------------------- /sgftools/typelib.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | # typelib.py (Type Class Library) 4 | # Copyright (c) 2000 David John Goodger 5 | # 6 | # This software is provided "as-is", without any express or implied warranty. 7 | # In no event will the authors be held liable for any damages arising from the 8 | # use of this software. 9 | # 10 | # Permission is granted to anyone to use this software for any purpose, 11 | # including commercial applications, and to alter it and redistribute it 12 | # freely, subject to the following restrictions: 13 | # 14 | # 1. The origin of this software must not be misrepresented; you must not 15 | # claim that you wrote the original software. If you use this software in a 16 | # product, an acknowledgment in the product documentation would be appreciated 17 | # but is not required. 18 | # 19 | # 2. Altered source versions must be plainly marked as such, and must not be 20 | # misrepresented as being the original software. 21 | # 22 | # 3. This notice may not be removed or altered from any source distribution. 23 | 24 | """ 25 | ================================ 26 | Type Class Library: typelib.py 27 | ================================ 28 | version 1.0 (2000-03-27) 29 | 30 | Homepage: [[http://gotools.sourceforge.net/]] (see sgflib.py) 31 | 32 | Copyright (C) 2000 David John Goodger ([[mailto:dgoodger@bigfoot.com]]). 33 | typelib.py comes with ABSOLUTELY NO WARRANTY. This is free software, and you are 34 | welcome to redistribute it and/or modify it under certain conditions; see the 35 | source code for details. 36 | 37 | Description 38 | =========== 39 | This library implements abstract superclasses to emulate Python's built-in data 40 | types. This is useful when you want a class which acts like a built-in type, but 41 | with added/modified behaviour (methods) and/or data (attributes). 42 | 43 | Implemented types are: 'String', 'Tuple', 'List', 'Dictionary', 'Integer', 44 | 'Long', 'Float', 'Complex' (along with their abstract superclasses). 45 | 46 | All methods, including special overloading methods, are implemented for each 47 | type-emulation class. Instance data is stored internally in the 'data' attribute 48 | (i.e., 'self.data'). The type the class is emulating is stored in the class 49 | attribute 'self.TYPE' (as given by the built-in 'type(class)'). The 50 | 'SuperClass.__init__()' method uses two class-specific methods to instantiate 51 | objects: '_reset()' and '_convert()'. 52 | 53 | See "sgflib.py" (at module's homepage, see above) for examples of use. The Node 54 | class is of particular interest: a modified 'Dictionary' which is ordered and 55 | allows for offset-indexed retrieval.""" 56 | 57 | 58 | # Revision History 59 | # 60 | # 1.0 (2000-03-27): First public release. 61 | # - Implemented Integer, Long, Float, and Complex. 62 | # - Cleaned up a few loose ends. 63 | # - Completed docstring documentatation. 64 | # 65 | # 0.1 (2000-01-27): 66 | # - Implemented String, Tuple, List, and Dictionary emulation. 67 | # 68 | # To do: 69 | # - Implement Function? File? (Have to come up with a good reason first ;-) 70 | 71 | 72 | class SuperType: 73 | """ Superclass of all type classes. Implements methods common to all types. 74 | Concrete (as opposed to abstract) subclasses must define a class 75 | attribute 'self.TYPE' ('=type(Class)'), and methods '_reset(self)' and 76 | '_convert(self, data)'.""" 77 | 78 | def __init__(self, data=None): 79 | """ 80 | On 'Class()', initialize 'self.data'. Argument: 81 | - 'data' : optional, default 'None' -- 82 | - If the type of 'data' is identical to the Class' 'TYPE', 83 | 'data' will be shared (relevant for mutable types only). 84 | - If 'data' is given (and not false), it will be converted by 85 | the Class-specific method 'self._convert(data)'. Incompatible 86 | data types will raise an exception. 87 | - If 'data' is 'None', false, or not given, a Class-specific method 88 | 'self._reset()' is called to initialize an empty instance.""" 89 | if data: 90 | if type(data) is self.TYPE: 91 | self.data = data 92 | else: 93 | self.data = self._convert(data) 94 | else: 95 | self._reset() 96 | 97 | def __str__(self): 98 | """ On 'str(self)' and 'print self'. Returns string representation.""" 99 | return str(self.data) 100 | 101 | def __cmp__(self, x): 102 | """ On 'self>x', 'self==x', 'cmp(self,x)', etc. Catches all 103 | comparisons: returns -1, 0, or 1 for less, equal, or greater.""" 104 | return cmp(self.data, x) 105 | 106 | def __rcmp__(self, x): 107 | """ On 'x>self', 'x==self', 'cmp(x,self)', etc. Catches all 108 | comparisons: returns -1, 0, or 1 for less, equal, or greater.""" 109 | return cmp(x, self.data) 110 | 111 | def __hash__(self): 112 | """ On 'dictionary[self]', 'hash(self)'. Returns a unique and unchanging 113 | integer hash-key.""" 114 | return hash(self.data) 115 | 116 | 117 | class AddMulMixin: 118 | """ Addition & multiplication for numbers, concatenation & repetition for 119 | sequences.""" 120 | 121 | def __add__(self, other): 122 | """ On 'self+other'. Numeric addition, or sequence concatenation.""" 123 | return self.data + other 124 | 125 | def __radd__(self, other): 126 | """ On 'other+self'. Numeric addition, or sequence concatenation.""" 127 | return other + self.data 128 | 129 | def __mul__(self, other): 130 | """ On 'self*other'. Numeric multiplication, or sequence repetition.""" 131 | return self.data * other 132 | 133 | def __rmul__(self, other): 134 | """ On 'other*self'. Numeric multiplication, or sequence repetition.""" 135 | return other * self.data 136 | 137 | 138 | class MutableMixin: 139 | """ Assignment to and deletion of collection component.""" 140 | 141 | def __setitem__(self, key, x): 142 | """ On 'self[key]=x'.""" 143 | self.data[key] = x 144 | 145 | def __delitem__(self, key): 146 | """ On 'del self[key]'.""" 147 | del self.data[key] 148 | 149 | 150 | class ModMixin: 151 | """ Modulo remainder and string formatting.""" 152 | 153 | def __mod__(self, other): 154 | """ On 'self%other'.""" 155 | return self.data % other 156 | 157 | def __rmod__(self, other): 158 | """ On 'other%self'.""" 159 | return other % self.data 160 | 161 | 162 | class Number(SuperType, AddMulMixin, ModMixin): 163 | """ Superclass for numeric emulation types.""" 164 | 165 | def __sub__(self, other): 166 | """ On 'self-other'.""" 167 | return self.data - other 168 | 169 | def __rsub__(self, other): 170 | """ On 'other-self'.""" 171 | return other - self.data 172 | 173 | def __div__(self, other): 174 | """ On 'self/other'.""" 175 | return self.data / other 176 | 177 | def __rdiv__(self, other): 178 | """ On 'other/self'.""" 179 | return other / self.data 180 | 181 | def __divmod__(self, other): 182 | """ On 'divmod(self,other)'.""" 183 | return divmod(self.data, other) 184 | 185 | def __rdivmod__(self, other): 186 | """ On 'divmod(other,self)'.""" 187 | return divmod(other, self.data) 188 | 189 | def __pow__(self, other, mod=None): 190 | """ On 'pow(self,other[,mod])', 'self**other'.""" 191 | if mod is None: 192 | return self.data ** other 193 | else: 194 | return pow(self.data, other, mod) 195 | 196 | def __rpow__(self, other): 197 | """ On 'pow(other,self)', 'other**self'.""" 198 | return other ** self.data 199 | 200 | def __neg__(self): 201 | """ On '-self'.""" 202 | return -self.data 203 | 204 | def __pos__(self): 205 | """ On '+self'.""" 206 | return +self.data 207 | 208 | def __abs__(self): 209 | """ On 'abs(self)'.""" 210 | return abs(self.data) 211 | 212 | def __int__(self): 213 | """ On 'int(self)'.""" 214 | return int(self.data) 215 | 216 | def __long__(self): 217 | """ On 'long(self)'.""" 218 | return long(self.data) 219 | 220 | def __float__(self): 221 | """ On 'float(self)'.""" 222 | return float(self.data) 223 | 224 | def __complex__(self): 225 | """ On 'complex(self)'.""" 226 | return complex(self.data) 227 | 228 | def __nonzero__(self): 229 | """ On truth-value (or uses '__len__()' if defined).""" 230 | return self.data != 0 231 | 232 | def __coerce__(self, other): 233 | """ On mixed-type expression, 'coerce()'. Returns tuple of '(self, other)' 234 | converted to a common type.""" 235 | return coerce(self.data, other) 236 | 237 | 238 | class Integer(Number): 239 | """ Emulates a Python integer.""" 240 | 241 | TYPE = type(1) 242 | 243 | def _reset(self): 244 | """ Initialize an integer.""" 245 | self.data = 0 246 | 247 | def _convert(self, data): 248 | """ Convert data into an integer.""" 249 | return int(data) 250 | 251 | def __lshift__(self, other): 252 | """ On 'self<>other'.""" 261 | return self.data >> other 262 | 263 | def __rrshift__(self, other): 264 | """ On 'other>>self'.""" 265 | return other >> self.data 266 | 267 | def __and__(self, other): 268 | """ On 'self&other'.""" 269 | return self.data & other 270 | 271 | def __rand__(self, other): 272 | """ On 'other&self'.""" 273 | return other & self.data 274 | 275 | def __or__(self, other): 276 | """ On 'self|other'.""" 277 | return self.data | other 278 | 279 | def __ror__(self, other): 280 | """ On 'other|self'.""" 281 | return other | self.data 282 | 283 | def __xor__(self, other): 284 | """ On 'self^other'.""" 285 | return self.data ^ other 286 | 287 | def __rxor__(self, other): 288 | """ On 'other%self'.""" 289 | return other % self.data 290 | 291 | def __invert__(self): 292 | """ On '~self'.""" 293 | return ~self.data 294 | 295 | def __oct__(self): 296 | """ On 'oct(self)'. Returns octal string representation.""" 297 | return oct(self.data) 298 | 299 | def __hex__(self): 300 | """ On 'hex(self)'. Returns hexidecimal string representation.""" 301 | return hex(self.data) 302 | 303 | 304 | class Long(Integer): 305 | """ Emulates a Python long integer.""" 306 | 307 | TYPE = type(1L) 308 | 309 | def _reset(self): 310 | """ Initialize an integer.""" 311 | self.data = 0L 312 | 313 | def _convert(self, data): 314 | """ Convert data into an integer.""" 315 | return long(data) 316 | 317 | 318 | class Float(Number): 319 | """ Emulates a Python floating-point number.""" 320 | 321 | TYPE = type(0.1) 322 | 323 | def _reset(self): 324 | """ Initialize a float.""" 325 | self.data = 0.0 326 | 327 | def _convert(self, data): 328 | """ Convert data into a float.""" 329 | return float(data) 330 | 331 | 332 | class Complex(Number): 333 | """ Emulates a Python complex number.""" 334 | 335 | TYPE = type(0+0j) 336 | 337 | def _reset(self): 338 | """ Initialize an integer.""" 339 | self.data = 0+0j 340 | 341 | def _convert(self, data): 342 | """ Convert data into an integer.""" 343 | return complex(data) 344 | 345 | def __getattr__(self, name): 346 | """ On 'self.real' & 'self.imag'.""" 347 | if name == "real": 348 | return self.data.real 349 | elif name == "imag": 350 | return self.data.imag 351 | else: 352 | raise AttributeError(name) 353 | 354 | def conjugate(self): 355 | """ On 'self.conjugate()'.""" 356 | return self.data.conjugate() 357 | 358 | 359 | class Container(SuperType): 360 | """ Superclass for countable, indexable collection types ('Sequence', 'Mapping').""" 361 | 362 | def __len__(self): 363 | """ On 'len(self)', truth-value tests. Returns sequence or mapping 364 | collection size. Zero means false.""" 365 | return len(self.data) 366 | 367 | def __getitem__(self, key): 368 | """ On 'self[key]', 'x in self', 'for x in self'. Implements all 369 | indexing-related operations. Membership and iteration ('in', 'for') 370 | repeatedly index from 0 until 'IndexError'.""" 371 | return self.data[key] 372 | 373 | 374 | class Sequence(Container, AddMulMixin): 375 | """ Superclass for classes which emulate sequences ('List', 'Tuple', 'String').""" 376 | 377 | def __getslice__(self, low, high): 378 | """ On 'self[low:high]'.""" 379 | return self.data[low:high] 380 | 381 | 382 | class String(Sequence, ModMixin): 383 | """ Emulates a Python string.""" 384 | 385 | TYPE = type("") 386 | 387 | def _reset(self): 388 | """ Initialize an empty string.""" 389 | self.data = "" 390 | 391 | def _convert(self, data): 392 | """ Convert data into a string.""" 393 | return str(data) 394 | 395 | 396 | class Tuple(Sequence): 397 | """ Emulates a Python tuple.""" 398 | 399 | TYPE = type(()) 400 | 401 | def _reset(self): 402 | """ Initialize an empty tuple.""" 403 | self.data = () 404 | 405 | def _convert(self, data): 406 | """ Non-tuples cannot be converted. Raise an exception.""" 407 | raise TypeError("Non-tuples cannot be converted to a tuple.") 408 | 409 | 410 | class MutableSequence(Sequence, MutableMixin): 411 | """ Superclass for classes which emulate mutable (modifyable in-place) 412 | sequences ('List').""" 413 | 414 | def __setslice__(self, low, high, seq): 415 | """ On 'self[low:high]=seq'.""" 416 | self.data[low:high] = seq 417 | 418 | def __delslice__(self, low, high): 419 | """ On 'del self[low:high]'.""" 420 | del self.data[low:high] 421 | 422 | def append(self, x): 423 | """ Inserts object 'x' at the end of 'self.data' in-place.""" 424 | self.data.append(x) 425 | 426 | def count(self, x): 427 | """ Returns the number of occurrences of 'x' in 'self.data'.""" 428 | return self.data.count(x) 429 | 430 | def extend(self, x): 431 | """ Concatenates sequence 'x' to the end of 'self' in-place 432 | (like 'self=self+x').""" 433 | self.data.extend(x) 434 | 435 | def index(self, x): 436 | """ Returns the offset of the first occurrence of object 'x' in 437 | 'self.data'; raises an exception if not found.""" 438 | return self.data.index(x) 439 | 440 | def insert(self, i, x): 441 | """ Inserts object 'x' into 'self.data' at offset 'i' 442 | (like 'self[i:i]=[x]').""" 443 | self.data.insert(i, x) 444 | 445 | def pop(self, i=-1): 446 | """ Returns and deletes the last item of 'self.data' (or item 447 | 'self.data[i]' if 'i' given).""" 448 | return self.data.pop(i) 449 | 450 | def remove(self, x): 451 | """ Deletes the first occurrence of object 'x' from 'self.data'; 452 | raise an exception if not found.""" 453 | self.data.remove(x) 454 | 455 | def reverse(self): 456 | """ Reverses items in 'self.data' in-place.""" 457 | self.data.reverse() 458 | 459 | def sort(self, func=None): 460 | """ 461 | Sorts 'self.data' in-place. Argument: 462 | - func : optional, default 'None' -- 463 | - If 'func' not given, sorting will be in ascending 464 | order. 465 | - If 'func' given, it will determine the sort order. 466 | 'func' must be a two-argument comparison function 467 | which returns -1, 0, or 1, to mean before, same, 468 | or after ordering.""" 469 | if func: 470 | self.data.sort(func) 471 | else: 472 | self.data.sort() 473 | 474 | 475 | class List(MutableSequence): 476 | """ Emulates a Python list. When instantiating an object with data 477 | ('List(data)'), you can force a copy with 'List(list(data))'.""" 478 | 479 | TYPE = type([]) 480 | 481 | def _reset(self): 482 | """ Initialize an empty list.""" 483 | self.data = [] 484 | 485 | def _convert(self, data): 486 | """ Convert data into a list.""" 487 | return list(data) 488 | 489 | 490 | class Mapping(Container): 491 | """ Superclass for classes which emulate mappings/hashes ('Dictionary').""" 492 | 493 | def has_key(self, key): 494 | """ Returns 1 (true) if 'self.data' has a key 'key', or 0 otherwise.""" 495 | return self.data.has_key(key) 496 | 497 | def keys(self): 498 | """ Returns a new list holding all keys from 'self.data'.""" 499 | return self.data.keys() 500 | 501 | def values(self): 502 | """ Returns a new list holding all values from 'self.data'.""" 503 | return self.data.values() 504 | 505 | def items(self): 506 | """ Returns a new list of tuple pairs '(key, value)', one for each entry 507 | in 'self.data'.""" 508 | return self.data.items() 509 | 510 | def clear(self): 511 | """ Removes all items from 'self.data'.""" 512 | self.data.clear() 513 | 514 | def get(self, key, default=None): 515 | """ Similar to 'self[key]', but returns 'default' (or 'None') instead of 516 | raising an exception when 'key' is not found in 'self.data'.""" 517 | return self.data.get(key, default) 518 | 519 | def copy(self): 520 | """ Returns a shallow (top-level) copy of 'self.data'.""" 521 | return self.data.copy() 522 | 523 | def update(self, dict): 524 | """ Merges 'dict' into 'self.data' 525 | (i.e., 'for (k,v) in dict.items(): self.data[k]=v').""" 526 | self.data.update(dict) 527 | 528 | 529 | class Dictionary(Mapping, MutableMixin): 530 | """ Emulates a Python dictionary, a mutable mapping. When instantiating an 531 | object with data ('Dictionary(data)'), you can force a (shallow) copy 532 | with 'Dictionary(data.copy())'.""" 533 | 534 | TYPE = type({}) 535 | 536 | def _reset(self): 537 | """ Initialize an empty dictionary.""" 538 | self.data = {} 539 | 540 | def _convert(self, data): 541 | """ Non-dictionaries cannot be converted. Raise an exception.""" 542 | raise TypeError("Non-dictionaries cannot be converted to a dictionary.") 543 | 544 | 545 | if __name__ == "__main__": 546 | print __doc__ # show module's documentation string 547 | -------------------------------------------------------------------------------- /sgfanalyze.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | import os, sys 3 | import argparse 4 | import hashlib 5 | import pickle 6 | import traceback 7 | import math 8 | from sgftools import gotools, leela, annotations, progressbar, sgflib 9 | 10 | # Stdev of bell curve whose cdf we take to be the "real" probability given Leela's winrate 11 | DEFAULT_STDEV = 0.22 12 | 13 | RESTART_COUNT=1 14 | 15 | def graph_winrates(winrates, color, outp_fn): 16 | import matplotlib as mpl 17 | mpl.use('Agg') 18 | import matplotlib.pyplot as plt 19 | 20 | X = [] 21 | Y = [] 22 | for move_num in sorted(winrates.keys()): 23 | pl, wr = winrates[move_num] 24 | X.append(move_num) 25 | Y.append(wr) 26 | 27 | plt.figure(1, figsize=(7,3.6)) 28 | plt.xlim(0, max(winrates.keys())) 29 | plt.ylim(0, 1) 30 | plt.xlabel("Move number", fontsize=12) 31 | plt.ylabel("Black's win rate", fontsize=12) 32 | plt.yticks([yc/10.0 for yc in range(0,10+1)], fontsize=6) 33 | plt.xticks(range(0, max(winrates.keys()), 10), fontsize=6) 34 | 35 | for yc in range(0, 10): 36 | plt.axhline(yc/10.0, 0, max(winrates.keys()), linewidth=0.4, color='0.7') 37 | for yc in range(0, 40): 38 | plt.axhline(yc/40.0, 0, max(winrates.keys()), linewidth=0.1, color='0.7') 39 | for xc in range(0, max(winrates.keys()), 50): 40 | plt.axvline(xc, 0, 1, linewidth=0.4, color='0.7') 41 | for xc in range(0, max(winrates.keys()), 5): 42 | plt.axvline(xc, 0, 1, linewidth=0.1, color='0.7') 43 | 44 | plt.plot(X, Y, color='k', marker='.', linewidth=0.2, markersize=3) 45 | 46 | plt.savefig(outp_fn, dpi=200, format='pdf', bbox_inches='tight') 47 | 48 | #Also returns the move played, if any, else None 49 | def add_moves_to_leela(C,leela): 50 | this_move = None 51 | if 'W' in C.node.keys(): 52 | this_move = C.node['W'].data[0] 53 | leela.add_move('white', this_move) 54 | if 'B' in C.node.keys(): 55 | this_move = C.node['B'].data[0] 56 | leela.add_move('black', this_move) 57 | # SGF commands to add black or white stones, often used for setting up handicap and such 58 | if 'AB' in C.node.keys(): 59 | for move in C.node['AB'].data: 60 | leela.add_move('black', move) 61 | if 'AW' in C.node.keys(): 62 | for move in C.node['AW'].data: 63 | leela.add_move('white', move) 64 | return this_move 65 | 66 | # Make a function that applies a transform to the winrate that stretches out the middle range and squashes the extreme ranges, 67 | # to make it a more linear function and suppress Leela's suggestions in won/lost games. 68 | # Currently, the CDF of the probability distribution from 0 to 1 given by x^k * (1-x)^k, where k is set to be the value such that 69 | # the stdev of the distribution is stdev. 70 | def winrate_transformer(stdev, verbosity): 71 | def logfactorial(x): 72 | return math.lgamma(x+1) 73 | # Variance of the distribution = 74 | # = The integral from 0 to 1 of (x-0.5)^2 x^k (1-x)^k dx 75 | # = (via integration by parts) (k+2)!k! / (2k+3)! - (k+1)!k! / (2k+2)! + (1/4) * k!^2 / (2k+1)! 76 | # 77 | # Normalize probability by dividing by the integral from 0 to 1 of x^k (1-x)^k dx : 78 | # k!^2 / (2k+1)! 79 | # And we get: 80 | # (k+1)(k+2) / (2k+2) / (2k+3) - (k+1) / (2k+2) + (1/4) 81 | def variance(k): 82 | k = float(k) 83 | return (k+1) * (k+2) / (2*k+2) / (2*k+3) - (k+1) / (2*k+2) + 0.25 84 | # Perform binary search to find the appropriate k 85 | def find_k(lower,upper): 86 | while True: 87 | mid = 0.5 * (lower + upper) 88 | if mid == lower or mid == upper or lower >= upper: 89 | return mid 90 | var = variance(mid) 91 | if var < stdev * stdev: 92 | upper = mid 93 | else: 94 | lower = mid 95 | 96 | if stdev * stdev <= 1e-10: 97 | raise ValueError("Stdev too small, please choose a more reasonable value") 98 | 99 | # Repeated doubling to find an upper bound big enough 100 | upper = 1 101 | while variance(upper) > stdev * stdev: 102 | upper = upper * 2 103 | 104 | k = find_k(0,upper) 105 | 106 | if verbosity > 2: 107 | print >>sys.stderr, "Using k=%f, stdev=%f" % (k,math.sqrt(variance(k))) 108 | 109 | def unnormpdf(x): 110 | if x <= 0 or x >= 1 or 1-x <= 0: 111 | return 0 112 | a = math.log(x) 113 | b = math.log(1-x) 114 | logprob = a * k + b * k 115 | # Constant scaling so we don't overflow floats with crazy values 116 | logprob = logprob - 2 * k * math.log(0.5) 117 | return math.exp(logprob) 118 | 119 | #Precompute a big array to approximate the CDF 120 | n = 100000 121 | lookup = [ unnormpdf(float(x)/float(n)) for x in range(n+1) ] 122 | cum = 0 123 | for i in range(n+1): 124 | cum += lookup[i] 125 | lookup[i] = cum 126 | for i in range(n+1): 127 | lookup[i] = lookup[i] / lookup[n] 128 | 129 | def cdf(x): 130 | i = int(math.floor(x * n)) 131 | if i >= n or i < 0: 132 | return x 133 | excess = x * n - i 134 | return lookup[i] + excess * (lookup[i+1] - lookup[i]) 135 | 136 | return (lambda x: cdf(x)) 137 | 138 | 139 | def retry_analysis(fn): 140 | global RESTART_COUNT 141 | def wrapped(*args, **kwargs): 142 | for i in xrange(RESTART_COUNT+1): 143 | try: 144 | return fn(*args, **kwargs) 145 | except Exception as e: 146 | if i+1 == RESTART_COUNT+1: 147 | raise 148 | print >>sys.stderr, "Error in leela, retrying analysis..." 149 | return wrapped 150 | 151 | @retry_analysis 152 | def do_analyze(leela, base_dir, verbosity): 153 | ckpt_hash = 'analyze_' + leela.history_hash() + "_" + str(leela.seconds_per_search) + "sec" 154 | ckpt_fn = os.path.join(base_dir, ckpt_hash) 155 | if verbosity > 2: 156 | print >>sys.stderr, "Looking for checkpoint file:", ckpt_fn 157 | 158 | if os.path.exists(ckpt_fn): 159 | if verbosity > 1: 160 | print >>sys.stderr, "Loading checkpoint file:", ckpt_fn 161 | with open(ckpt_fn, 'r') as ckpt_file: 162 | stats, move_list = pickle.load(ckpt_file) 163 | else: 164 | leela.reset() 165 | leela.goto_position() 166 | stats, move_list = leela.analyze() 167 | with open(ckpt_fn, 'w') as ckpt_file: 168 | pickle.dump((stats, move_list), ckpt_file) 169 | 170 | return stats, move_list 171 | 172 | # move_list is from a call to do_analyze 173 | # Iteratively expands a tree of moves by expanding on the leaf with the highest "probability of reaching". 174 | def do_variations(C, leela, stats, move_list, nodes_per_variation, board_size, game_move, base_dir, verbosity): 175 | if 'bookmoves' in stats or len(move_list) <= 0: 176 | return 177 | 178 | rootcolor = leela.whoseturn() 179 | leaves = [] 180 | tree = { "children": [], "is_root": True, "history": [], "explored": False, "prob": 1.0, "stats": stats, "move_list": move_list, "color": rootcolor } 181 | 182 | def expand(node, stats, move_list): 183 | assert node["color"] in ['white', 'black'] 184 | def child_prob_raw(i,move): 185 | # possible for book moves 186 | if "is_book" in move: 187 | return 1.0 188 | elif node["color"] == rootcolor: 189 | return move["visits"] ** 1.0 190 | else: 191 | return (move["policy_prob"] + move["visits"]) / 2.0 192 | probsum = 0.0 193 | for (i,move) in enumerate(move_list): 194 | probsum += child_prob_raw(i,move) 195 | def child_prob(i,move): 196 | return child_prob_raw(i,move) / probsum 197 | 198 | for (i,move) in enumerate(move_list): 199 | #Don't expand on the actual game line as a variation! 200 | if node["is_root"] and move["pos"] == game_move: 201 | node["children"].append(None) 202 | continue 203 | subhistory = node["history"][:] 204 | subhistory.append(move["pos"]) 205 | prob = node["prob"] * child_prob(i,move) 206 | clr = "white" if node["color"] == "black" else "black" 207 | child = { "children": [], "is_root": False, "history": subhistory, "explored": False, "prob": prob, "stats": {}, "move_list": [], "color": clr } 208 | node["children"].append(child) 209 | leaves.append(child) 210 | 211 | node["stats"] = stats 212 | node["move_list"] = move_list 213 | node["explored"] = True 214 | 215 | for i in range(len(leaves)): 216 | if leaves[i] is node: 217 | del leaves[i] 218 | break 219 | 220 | def search(node): 221 | for mv in node["history"]: 222 | leela.add_move(leela.whoseturn(),mv) 223 | stats, move_list = do_analyze(leela,base_dir,verbosity) 224 | expand(node,stats,move_list) 225 | 226 | for mv in node["history"]: 227 | leela.pop_move() 228 | 229 | expand(tree,stats,move_list) 230 | for i in range(nodes_per_variation): 231 | if len(leaves) > 0: 232 | node = max(leaves,key=(lambda n: n["prob"])) 233 | search(node) 234 | 235 | def advance(C, color, mv): 236 | foundChildIdx = None 237 | clr = 'W' if color =='white' else 'B' 238 | for j in range(len(C.children)): 239 | if clr in C.children[j].keys() and C.children[j][clr].data[0] == mv: 240 | foundChildIdx = j 241 | if foundChildIdx is not None: 242 | C.next(foundChildIdx) 243 | else: 244 | nnode = sgflib.Node() 245 | nnode.addProperty(nnode.makeProperty(clr,[mv])) 246 | C.appendNode(nnode) 247 | C.next(len(C.children)-1) 248 | 249 | def record(node): 250 | if not node["is_root"]: 251 | annotations.annotate_sgf(C, annotations.format_winrate(node["stats"],node["move_list"],board_size,None), [], []) 252 | move_list_to_display = [] 253 | # Only display info for the principal variation or for lines that have been explored. 254 | for i in range(len(node["children"])): 255 | child = node["children"][i] 256 | if child is not None and (i == 0 or child["explored"]): 257 | move_list_to_display.append(node["move_list"][i]) 258 | (analysis_comment, lb_values, tr_values) = annotations.format_analysis(node["stats"],move_list_to_display,None) 259 | annotations.annotate_sgf(C, analysis_comment, lb_values, tr_values) 260 | 261 | for i in range(len(node["children"])): 262 | child = node["children"][i] 263 | if child is not None: 264 | if child["explored"]: 265 | advance(C, node["color"], child["history"][-1]) 266 | record(child) 267 | C.previous() 268 | # Only show variations for the principal line, to prevent info overload 269 | elif i == 0: 270 | pv = node["move_list"][i]["pv"] 271 | c = node["color"] 272 | num_to_show = min(len(pv), max(1, len(pv) * 2 / 3 - 1)) 273 | for k in range(num_to_show): 274 | advance(C, c, pv[k]) 275 | c = 'black' if c =='white' else 'white' 276 | for k in range(num_to_show): 277 | C.previous() 278 | 279 | record(tree) 280 | 281 | 282 | def calculate_tasks_left(sgf, start_m, end_n, comment_requests_analyze, comment_requests_variations): 283 | C = sgf.cursor() 284 | move_num = 0 285 | analyze_tasks = 0 286 | variations_tasks = 0 287 | while not C.atEnd: 288 | C.next() 289 | 290 | analysis_mode = None 291 | if move_num >= args.analyze_start and move_num <= args.analyze_end: 292 | analysis_mode='analyze' 293 | if move_num in comment_requests_analyze or (move_num-1) in comment_requests_analyze or (move_num-1) in comment_requests_variations: 294 | analysis_mode='analyze' 295 | if move_num in comment_requests_variations: 296 | analysis_mode='variations' 297 | 298 | if analysis_mode=='analyze': 299 | analyze_tasks += 1 300 | elif analysis_mode=='variations': 301 | analyze_tasks += 1 302 | variations_tasks += 1 303 | 304 | move_num += 1 305 | return (analyze_tasks,variations_tasks) 306 | 307 | default_analyze_thresh = 0.030 308 | default_var_thresh = 0.030 309 | 310 | if __name__=='__main__': 311 | parser = argparse.ArgumentParser() 312 | required = parser.add_argument_group('required named arguments') 313 | parser.add_argument('--start', dest='analyze_start', default=0, type=int, metavar="MOVENUM", 314 | help="Analyze game starting at this move (default=0)") 315 | parser.add_argument('--stop', dest='analyze_end', default=1000, type=int, metavar="MOVENUM", 316 | help="Analyze game stopping at this move (default=1000)") 317 | 318 | parser.add_argument('--analyze-thresh', dest='analyze_threshold', default=default_analyze_thresh, type=float, metavar="T", 319 | help="Display analysis on moves losing approx at least this much win rate when the game is close (default=0.03)") 320 | parser.add_argument('--var-thresh', dest='variations_threshold', default=default_var_thresh, type=float, metavar="T", 321 | help="Explore variations on moves losing approx at least this much win rate when the game is close (default=0.03)") 322 | 323 | parser.add_argument('--secs-per-search', dest='seconds_per_search', default=10, type=float, metavar="S", 324 | help="How many seconds to use per search (default=10)") 325 | parser.add_argument('--nodes-per-var', dest='nodes_per_variation', default=8, type=int, metavar="N", 326 | help="How many nodes to explore with leela in each variation tree (default=8)") 327 | parser.add_argument('--win-graph', dest='win_graph', metavar="PDF", 328 | help="Output pdf graph of win rate to this file, must have matplotlib installed") 329 | parser.add_argument('-v','--verbosity', default=0, type=int, metavar="V", 330 | help="Set the verbosity level, 0: progress only, 1: progress+status, 2: progress+status+state") 331 | required.add_argument('--leela', dest='executable', required=True, metavar="CMD", 332 | help="Command to run Leela executable") 333 | parser.add_argument('--cache', dest='ckpt_dir', metavar="DIR", 334 | default=os.path.expanduser('~/.leela_checkpoints'), 335 | help="Set a directory to cache partially complete analyses, default ~/.leela_checkpoints") 336 | parser.add_argument('--restarts', default=2, type=int, metavar="N", 337 | help="If leela crashes, retry the analysis step this many times before reporting a failure") 338 | parser.add_argument('--wipe-comments', dest='wipe_comments', action='store_true', 339 | help="Remove existing comments from the main line of the SGF file") 340 | parser.add_argument('--skip-white', dest='skip_white', action='store_true', 341 | help="Do not display analysis or explore variations for white mistakes") 342 | parser.add_argument('--skip-black', dest='skip_black', action='store_true', 343 | help="Do not display analysis or explore variations for black mistakes") 344 | parser.add_argument('--mark-next-move', dest='mark_next', action='store_true', 345 | help="Add a marker for the next move that is played on the main line") 346 | parser.add_argument('--mark-leela-suggestion', dest='mark_leela', action='store_true', 347 | help="Add a marker for the move that Leela thinks is best") 348 | parser.add_argument("SGF_FILE", help="SGF file to analyze") 349 | 350 | args = parser.parse_args() 351 | sgf_fn = args.SGF_FILE 352 | if not os.path.exists(sgf_fn): 353 | parser.error("No such file: %s" % (sgf_fn)) 354 | sgf = gotools.import_sgf(sgf_fn) 355 | 356 | RESTART_COUNT = args.restarts 357 | 358 | if not os.path.exists( args.ckpt_dir ): 359 | os.mkdir( args.ckpt_dir ) 360 | base_hash = hashlib.md5( os.path.abspath(sgf_fn) ).hexdigest() 361 | base_dir = os.path.join(args.ckpt_dir, base_hash) 362 | if not os.path.exists( base_dir ): 363 | os.mkdir( base_dir ) 364 | if args.verbosity > 1: 365 | print >>sys.stderr, "Checkpoint dir:", base_dir 366 | 367 | comment_requests_analyze = {} 368 | comment_requests_variations = {} 369 | 370 | C = sgf.cursor() 371 | if 'SZ' in C.node.keys(): 372 | board_size = int(C.node['SZ'].data[0]) 373 | else: 374 | board_size = 19 375 | 376 | if board_size != 19: 377 | print >>sys.stderr, "Warning: board size is not 19 so Leela could be much weaker and less accurate" 378 | if args.analyze_threshold == default_analyze_thresh or args.variations_threshold == default_var_thresh: 379 | print >>sys.stderr, "Warning: Consider also setting --analyze-thresh and --var-thresh higher" 380 | 381 | move_num = -1 382 | C = sgf.cursor() 383 | while not C.atEnd: 384 | C.next() 385 | move_num += 1 386 | if 'C' in C.node.keys(): 387 | if 'analyze' in C.node['C'].data[0]: 388 | comment_requests_analyze[move_num] = True 389 | if 'variations' in C.node['C'].data[0]: 390 | comment_requests_variations[move_num] = True 391 | 392 | if args.wipe_comments: 393 | C = sgf.cursor() 394 | cnode = C.node 395 | if cnode.has_key('C'): 396 | cnode['C'].data[0] = "" 397 | while not C.atEnd: 398 | C.next() 399 | cnode = C.node 400 | if cnode.has_key('C'): 401 | cnode['C'].data[0] = "" 402 | 403 | C = sgf.cursor() 404 | is_handicap_game = False 405 | handicap_stone_count = 0 406 | if 'HA' in C.node.keys() and int(C.node['HA'].data[0]) > 1: 407 | is_handicap_game = True 408 | handicap_stone_count = int(C.node['HA'].data[0]) 409 | 410 | is_japanese_rules = False 411 | if 'RU' in C.node.keys(): 412 | rules = C.node['RU'].data[0].lower() 413 | is_japanese_rules = (rules == 'jp' or rules == 'japanese' or rules == 'japan') 414 | 415 | komi = 7.5 416 | if 'KM' in C.node.keys(): 417 | komi = float(C.node['KM'].data[0]) 418 | if is_handicap_game and is_japanese_rules: 419 | old_komi = komi 420 | komi = old_komi + handicap_stone_count 421 | print >>sys.stderr, "Adjusting komi from %f to %f in converting Japanese rules with %d handicap to Chinese rules" % (old_komi,komi,handicap_stone_count) 422 | 423 | else: 424 | if is_handicap_game: 425 | komi = 0.5 426 | print >>sys.stderr, "Warning: Komi not specified, assuming %f" % (komi) 427 | 428 | (analyze_tasks_initial,variations_tasks_initial) = calculate_tasks_left(sgf, args.analyze_start, args.analyze_end, comment_requests_analyze, comment_requests_variations) 429 | variations_task_probability = 1.0 / (1.0 + args.variations_threshold * 100.0) 430 | analyze_tasks_initial_done = 0 431 | variations_tasks = variations_tasks_initial 432 | variations_tasks_done = 0 433 | def approx_tasks_done(): 434 | return ( 435 | analyze_tasks_initial_done + 436 | (variations_tasks_done * args.nodes_per_variation) 437 | ) 438 | def approx_tasks_max(): 439 | return ( 440 | (analyze_tasks_initial - analyze_tasks_initial_done) * 441 | (1 + variations_task_probability * args.nodes_per_variation) + 442 | analyze_tasks_initial_done + 443 | (variations_tasks * args.nodes_per_variation) 444 | ) 445 | 446 | transform_winrate = winrate_transformer(DEFAULT_STDEV, args.verbosity) 447 | analyze_threshold = transform_winrate(0.5 + 0.5 * args.analyze_threshold) - transform_winrate(0.5 - 0.5 * args.analyze_threshold) 448 | variations_threshold = transform_winrate(0.5 + 0.5 * args.variations_threshold) - transform_winrate(0.5 - 0.5 * args.variations_threshold) 449 | print >>sys.stderr, "Executing approx %.0f analysis steps" % (approx_tasks_max()) 450 | 451 | pb = progressbar.ProgressBar(max_value=approx_tasks_max()) 452 | pb.start() 453 | def refresh_pb(): 454 | pb.update(approx_tasks_done(), approx_tasks_max()) 455 | 456 | leela = leela.CLI(board_size=board_size, 457 | executable=args.executable, 458 | is_handicap_game=is_handicap_game, 459 | komi=komi, 460 | seconds_per_search=args.seconds_per_search, 461 | verbosity=args.verbosity) 462 | 463 | collected_winrates = {} 464 | collected_best_moves = {} 465 | collected_best_move_winrates = {} 466 | needs_variations = {} 467 | 468 | try: 469 | move_num = -1 470 | C = sgf.cursor() 471 | prev_stats = {} 472 | prev_move_list = [] 473 | has_prev = False 474 | 475 | leela.start() 476 | add_moves_to_leela(C,leela) 477 | while not C.atEnd: 478 | C.next() 479 | move_num += 1 480 | this_move = add_moves_to_leela(C,leela) 481 | current_player = leela.whoseturn() 482 | prev_player = "white" if current_player == "black" else "black" 483 | if ((move_num >= args.analyze_start and move_num <= args.analyze_end) or 484 | (move_num in comment_requests_analyze) or 485 | ((move_num-1) in comment_requests_analyze) or 486 | (move_num in comment_requests_variations) or 487 | ((move_num-1) in comment_requests_variations)): 488 | stats, move_list = do_analyze(leela,base_dir,args.verbosity) 489 | 490 | if 'winrate' in stats and stats['visits'] > 100: 491 | collected_winrates[move_num] = (current_player, stats['winrate']) 492 | if len(move_list) > 0 and 'winrate' in move_list[0]: 493 | collected_best_moves[move_num] = move_list[0]['pos'] 494 | collected_best_move_winrates[move_num] = move_list[0]['winrate'] 495 | 496 | delta = 0.0 497 | transdelta = 0.0 498 | if 'winrate' in stats and (move_num-1) in collected_best_moves: 499 | if(this_move != collected_best_moves[move_num-1]): 500 | delta = stats['winrate'] - collected_best_move_winrates[move_num-1] 501 | delta = min(0.0, (-delta if leela.whoseturn() == "black" else delta)) 502 | transdelta = transform_winrate(stats['winrate']) - transform_winrate(collected_best_move_winrates[move_num-1]) 503 | transdelta = min(0.0, (-transdelta if leela.whoseturn() == "black" else transdelta)) 504 | 505 | if transdelta <= -analyze_threshold: 506 | (delta_comment,delta_lb_values) = annotations.format_delta_info(delta,transdelta,stats,this_move,board_size) 507 | annotations.annotate_sgf(C, delta_comment, delta_lb_values, []) 508 | 509 | if has_prev and (transdelta <= -variations_threshold or (move_num-1) in comment_requests_variations): 510 | if not (args.skip_white and prev_player == "white") and not (args.skip_black and prev_player == "black"): 511 | needs_variations[move_num-1] = (prev_stats,prev_move_list) 512 | if (move_num-1) not in comment_requests_variations: 513 | variations_tasks += 1 514 | next_game_move = None 515 | if not C.atEnd: 516 | C.next() 517 | if 'W' in C.node.keys(): 518 | next_game_move = C.node['W'].data[0] 519 | if 'B' in C.node.keys(): 520 | next_game_move = C.node['B'].data[0] 521 | C.previous() 522 | 523 | # add triangle marker for next move and "A" label for bot move 524 | LB_values = [] 525 | TR_values = [] 526 | if args.mark_next and next_game_move != None and not annotations.pos_is_pass(next_game_move): 527 | TR_values.append(next_game_move) 528 | 529 | if len(move_list) > 0: 530 | leela_move = move_list[0]['pos'] 531 | if args.mark_leela and leela_move != next_game_move and not annotations.pos_is_pass(leela_move): 532 | LB_values.append("%s:%s" % (leela_move, "A")) 533 | 534 | annotations.annotate_sgf(C, annotations.format_winrate(stats,move_list,board_size,next_game_move), LB_values, TR_values) 535 | 536 | # add analysis when a bad move was made 537 | if has_prev and ((move_num-1) in comment_requests_analyze or (move_num-1) in comment_requests_variations or transdelta <= -analyze_threshold): 538 | if not (args.skip_white and prev_player == "white") and not (args.skip_black and prev_player == "black"): 539 | (analysis_comment, lb_values, tr_values) = annotations.format_analysis(prev_stats, prev_move_list, this_move) 540 | C.previous() 541 | annotations.annotate_sgf(C, analysis_comment, lb_values, tr_values) 542 | C.next() 543 | 544 | prev_stats = stats 545 | prev_move_list = move_list 546 | has_prev = True 547 | 548 | analyze_tasks_initial_done += 1 549 | refresh_pb() 550 | else: 551 | prev_stats = {} 552 | prev_move_list = [] 553 | has_prev = False 554 | 555 | leela.stop() 556 | leela.clear_history() 557 | 558 | # Now fill in variations for everything we need 559 | move_num = -1 560 | C = sgf.cursor() 561 | leela.start() 562 | add_moves_to_leela(C,leela) 563 | while not C.atEnd: 564 | C.next() 565 | move_num += 1 566 | add_moves_to_leela(C,leela) 567 | 568 | if move_num not in needs_variations: 569 | continue 570 | stats,move_list = needs_variations[move_num] 571 | next_game_move = None 572 | if not C.atEnd: 573 | C.next() 574 | if 'W' in C.node.keys(): 575 | next_game_move = C.node['W'].data[0] 576 | if 'B' in C.node.keys(): 577 | next_game_move = C.node['B'].data[0] 578 | C.previous() 579 | 580 | do_variations(C, leela, stats, move_list, args.nodes_per_variation, board_size, next_game_move, base_dir, args.verbosity) 581 | variations_tasks_done += 1 582 | refresh_pb() 583 | 584 | except: 585 | traceback.print_exc() 586 | print >>sys.stderr, "Failure, reporting partial results...\n" 587 | finally: 588 | leela.stop() 589 | 590 | if args.win_graph: 591 | graph_winrates(collected_winrates, "black", args.win_graph) 592 | 593 | pb.finish() 594 | print sgf 595 | -------------------------------------------------------------------------------- /sgftools/sgflib.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | # sgflib.py (Smart Game Format Parser Library) 4 | # Copyright (C) 2000 David John Goodger (dgoodger@bigfoot.com) 5 | # 6 | # This library is free software; you can redistribute it and/or modify it 7 | # under the terms of the GNU Lesser General Public License as published by the 8 | # Free Software Foundation; either version 2 of the License, or (at your 9 | # option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, but WITHOUT 12 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License 14 | # for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public License 17 | # (lgpl.txt) along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | # The license is currently available on the Internet at: 20 | # http://www.gnu.org/copyleft/lesser.html 21 | 22 | """ 23 | ============================================= 24 | Smart Game Format Parser Library: sgflib.py 25 | ============================================= 26 | version 1.0 (2000-03-27) 27 | 28 | Homepage: [[http://gotools.sourceforge.net]] 29 | 30 | Copyright (C) 2000 David John Goodger ([[mailto:dgoodger@bigfoot.com]]; davidg 31 | on NNGS, IGS, goclub.org). sgflib.py comes with ABSOLUTELY NO WARRANTY. This is 32 | free software, and you are welcome to redistribute it and/or modify it under the 33 | terms of the GNU Lesser General Public License; see the source code for details. 34 | 35 | Description 36 | =========== 37 | This library contains a parser and classes for SGF, the Smart Game Format. SGF 38 | is a text only, tree based file format designed to store game records of board 39 | games for two players, most commonly for the game of go. (See the official SGF 40 | specification at [[http://www.POBoxes.com/sgf/]]). 41 | 42 | Given a string containing a complete SGF data instance, the 'SGFParser' class 43 | will create a 'Collection' object consisting of one or more 'GameTree''s (one 44 | 'GameTree' per game in the SGF file), each containing a sequence of 'Node''s and 45 | (potentially) two or more variation 'GameTree''s (branches). Each 'Node' 46 | contains an ordered dictionary of 'Property' ID/value pairs (note that values 47 | are lists, and can have multiple entries). 48 | 49 | Tree traversal methods are provided through the 'Cursor' class. 50 | 51 | The default representation (using 'str()' or 'print') of each class of SGF 52 | objects is the Smart Game Format itself.""" 53 | 54 | 55 | # Revision History: 56 | # 57 | # 1.0 (2000-03-27): First public release. 58 | # - Ready for prime time. 59 | # 60 | # 0.1 (2000-01-16): 61 | # - Initial idea & started coding. 62 | 63 | 64 | import string, re 65 | from typelib import List, Dictionary 66 | 67 | 68 | # Parsing Exceptions 69 | 70 | class EndOfDataParseError(Exception): 71 | """ Raised by 'SGFParser.parseVariations()', 'SGFParser.parseNode()'.""" 72 | pass 73 | 74 | class GameTreeParseError(Exception): 75 | """ Raised by 'SGFParser.parseGameTree()'.""" 76 | pass 77 | 78 | class NodePropertyParseError(Exception): 79 | """ Raised by 'SGFParser.parseNode()'.""" 80 | pass 81 | 82 | class PropertyValueParseError(Exception): 83 | """ Raised by 'SGFParser.parsePropertyValue()'.""" 84 | pass 85 | 86 | # Tree Construction Exceptions 87 | 88 | class DirectAccessError(Exception): 89 | """ Raised by 'Node.__setitem__()', 'Node.update()'.""" 90 | pass 91 | 92 | class DuplicatePropertyError(Exception): 93 | """ Raised by 'Node.addProperty()'.""" 94 | pass 95 | 96 | # Tree Navigation Exceptions 97 | class GameTreeNavigationError(Exception): 98 | """ Raised by 'Cursor.next()'.""" 99 | pass 100 | 101 | class GameTreeEndError(Exception): 102 | """ Raised by 'Cursor.next()', 'Cursor.previous()'.""" 103 | pass 104 | 105 | 106 | # for type checking 107 | INT_TYPE = type(0) # constant 108 | 109 | # miscellaneous constants 110 | MAX_LINE_LEN = 76 # constant; for line breaks 111 | 112 | 113 | class SGFParser: 114 | """ 115 | Parser for SGF data. Creates a tree structure based on the SGF standard 116 | itself. 'SGFParser.parse()' will return a 'Collection' object for the entire 117 | data. 118 | 119 | Instance Attributes: 120 | - self.data : string -- The complete SGF data instance. 121 | - self.datalen : integer -- Length of 'self.data'. 122 | - self.index : integer -- Current parsing position in 'self.data'. 123 | 124 | Class Attributes: 125 | - re* : re.RegexObject -- Regular expression text matching patterns. 126 | - ctrltrans: string[256] -- Control character translation table for 127 | string.translate(), used to remove all control characters from Property 128 | values. May be overridden (preferably in instances).""" 129 | 130 | # text matching patterns 131 | reGameTreeStart = re.compile(r'\s*\(') 132 | reGameTreeEnd = re.compile(r'\s*\)') 133 | reGameTreeNext = re.compile(r'\s*(;|\(|\))') 134 | reNodeContents = re.compile(r'\s*([A-Za-z]+(?=\s*\[))') 135 | rePropertyStart = re.compile(r'\s*\[') 136 | rePropertyEnd = re.compile(r'\]') 137 | reEscape = re.compile(r'\\') 138 | reLineBreak = re.compile(r'\r\n?|\n\r?') # CR, LF, CR/LF, LF/CR 139 | 140 | 141 | # character translation tables 142 | # for control characters (except LF \012 & CR \015): convert to spaces 143 | ctrltrans = string.maketrans("\000\001\002\003\004\005\006\007" + 144 | "\010\011\013\014\016\017\020\021\022\023\024\025\026\027" + 145 | "\030\031\032\033\034\035\036\037", " "*30) 146 | 147 | def __init__(self, data): 148 | """ Initialize the instance attributes. See the class itself for info.""" 149 | self.data = data 150 | self.datalen = len(data) 151 | self.index = 0 152 | 153 | def parse(self): 154 | """ Parses the SGF data stored in 'self.data', and returns a 'Collection'.""" 155 | c = Collection() 156 | while self.index < self.datalen: 157 | g = self.parseOneGame() 158 | if g: 159 | c.append(g) 160 | else: 161 | break 162 | return c 163 | 164 | def parseOneGame(self): 165 | """ Parses one game from 'self.data'. Returns a 'GameTree' containing 166 | one game, or 'None' if the end of 'self.data' has been reached.""" 167 | if self.index < self.datalen: 168 | match = self.reGameTreeStart.match(self.data, self.index) 169 | if match: 170 | self.index = match.end() 171 | return self.parseGameTree() 172 | return None 173 | 174 | def parseGameTree(self): 175 | """ Called when "(" encountered, ends when a matching ")" encountered. 176 | Parses and returns one 'GameTree' from 'self.data'. Raises 177 | 'GameTreeParseError' if a problem is encountered.""" 178 | g = GameTree() 179 | while self.index < self.datalen: 180 | match = self.reGameTreeNext.match(self.data, self.index) 181 | if match: 182 | self.index = match.end() 183 | if match.group(1) == ";": # found start of node 184 | if g.variations: 185 | raise GameTreeParseError( 186 | "A node was encountered after a variation.") 187 | g.append(g.makeNode(self.parseNode())) 188 | elif match.group(1) == "(": # found start of variation 189 | g.variations = self.parseVariations() 190 | else: # found end of GameTree ")" 191 | return g 192 | else: # error 193 | raise GameTreeParseError 194 | return g 195 | 196 | def parseVariations(self): 197 | """ Called when "(" encountered inside a 'GameTree', ends when a 198 | non-matching ")" encountered. Returns a list of variation 199 | 'GameTree''s. Raises 'EndOfDataParseError' if the end of 'self.data' 200 | is reached before the end of the enclosing 'GameTree'.""" 201 | v = [] 202 | while self.index < self.datalen: 203 | # check for ")" at end of GameTree, but don't consume it 204 | match = self.reGameTreeEnd.match(self.data, self.index) 205 | if match: 206 | return v 207 | g = self.parseGameTree() 208 | if g: 209 | v.append(g) 210 | # check for next variation, and consume "(" 211 | match = self.reGameTreeStart.match(self.data, self.index) 212 | if match: 213 | self.index = match.end() 214 | raise EndOfDataParseError 215 | 216 | def parseNode(self): 217 | """ Called when ";" encountered (& is consumed). Parses and returns one 218 | 'Node', which can be empty. Raises 'NodePropertyParseError' if no 219 | property values are extracted. Raises 'EndOfDataParseError' if the 220 | end of 'self.data' is reached before the end of the node (i.e., the 221 | start of the next node, the start of a variation, or the end of the 222 | enclosing game tree).""" 223 | n = Node() 224 | while self.index < self.datalen: 225 | match = self.reNodeContents.match(self.data, self.index) 226 | if match: 227 | self.index = match.end() 228 | pvlist = self.parsePropertyValue() 229 | if pvlist: 230 | n.addProperty(n.makeProperty(match.group(1), pvlist)) 231 | else: 232 | raise NodePropertyParseError 233 | else: # reached end of Node 234 | return n 235 | 236 | raise EndOfDataParseError 237 | 238 | def parsePropertyValue(self): 239 | """ Called when "[" encountered (but not consumed), ends when the next 240 | property, node, or variation encountered. Parses and returns a list 241 | of property values. Raises 'PropertyValueParseError' if there is a 242 | problem.""" 243 | pvlist = [] 244 | while self.index < self.datalen: 245 | match = self.rePropertyStart.match(self.data, self.index) 246 | if match: 247 | self.index = match.end() 248 | v = "" # value 249 | # scan for escaped characters (using '\'), unescape them (remove linebreaks) 250 | mend = self.rePropertyEnd.search(self.data, self.index) 251 | mesc = self.reEscape.search(self.data, self.index) 252 | while mesc and mend and (mesc.end() < mend.end()): 253 | # copy up to '\', but remove '\' 254 | v = v + self.data[self.index:mesc.start()] 255 | mbreak = self.reLineBreak.match(self.data, mesc.end()) 256 | if mbreak: 257 | self.index = mbreak.end() # remove linebreak 258 | else: 259 | v = v + self.data[mesc.end()] # copy escaped character 260 | self.index = mesc.end() + 1 # move to point after escaped char 261 | mend = self.rePropertyEnd.search(self.data, self.index) 262 | mesc = self.reEscape.search(self.data, self.index) 263 | if mend: 264 | v = v + self.data[self.index:mend.start()] 265 | self.index = mend.end() 266 | pvlist.append(self._convertControlChars(v)) 267 | else: 268 | raise PropertyValueParseError 269 | else: # reached end of Property 270 | break 271 | if len(pvlist) >= 1: 272 | return pvlist 273 | else: 274 | raise PropertyValueParseError 275 | 276 | def _convertControlChars(self, text): 277 | """ Converts control characters in 'text' to spaces, using the 278 | 'self.ctrltrans' translation table. Override for variant 279 | behaviour.""" 280 | return string.translate(text, self.ctrltrans) 281 | 282 | 283 | class RootNodeSGFParser(SGFParser): 284 | """ For parsing only the first 'GameTree''s root Node of an SGF file.""" 285 | 286 | def parseNode(self): 287 | """ Calls 'SGFParser.parseNode()', sets 'self.index' to point to the end 288 | of the data (effectively ending the 'GameTree' and 'Collection'), 289 | and returns the single (root) 'Node' parsed.""" 290 | n = SGFParser.parseNode(self) # process one Node as usual 291 | self.index = self.datalen # set end of data 292 | return n # we're only interested in the root node 293 | 294 | 295 | class Collection(List): 296 | """ 297 | An SGF collection: multiple 'GameTree''s. Instance atributes: 298 | - self[.data] : list of 'GameTree' -- One 'GameTree' per game.""" 299 | 300 | def __str__(self): 301 | """ SGF representation. Separates game trees with a blank line.""" 302 | return string.join(map(str, self.data), "\n"*2) 303 | 304 | def cursor(self, gamenum=0): 305 | """ Returns a 'Cursor' object for navigation of the given 'GameTree'.""" 306 | return Cursor(self[gamenum]) 307 | 308 | 309 | class GameTree(List): 310 | """ 311 | An SGF game tree: a game or variation. Instance attributes: 312 | - self[.data] : list of 'Node' -- game tree 'trunk'. 313 | - self.variations : list of 'GameTree' -- 0 or 2+ variations. 314 | 'self.variations[0]' contains the main branch (sequence actually played).""" 315 | 316 | def __init__(self, nodelist=None, variations=None): 317 | """ 318 | Initialize the 'GameTree'. Arguments: 319 | - nodelist : 'GameTree' or list of 'Node' -- Stored in 'self.data'. 320 | - variations : list of 'GameTree' -- Stored in 'self.variations'.""" 321 | List.__init__(self, nodelist) 322 | self.variations = variations or [] 323 | 324 | def __str__(self): 325 | """ SGF representation, with proper line breaks between nodes.""" 326 | if len(self): 327 | s = "(" + str(self[0]) # append the first Node automatically 328 | l = len(string.split(s, "\n")[-1]) # accounts for line breaks within Nodes 329 | for n in map(str, self[1:]): 330 | if l + len(string.split(n, "\n")[0]) > MAX_LINE_LEN: 331 | s = s + "\n" 332 | l = 0 333 | s = s + n 334 | l = len(string.split(s, "\n")[-1]) 335 | return s + string.join(map(str, [""] + self.variations), "\n") + ")" 336 | else: 337 | return "" # empty GameTree illegal; "()" illegal 338 | 339 | def mainline(self): 340 | """ Returns the main line of the game (variation A) as a 'GameTree'.""" 341 | if self.variations: 342 | return GameTree(self.data + self.variations[0].mainline()) 343 | else: 344 | return self 345 | 346 | def makeNode(self, plist): 347 | """ 348 | Create a new 'Node' containing the properties contained in 'plist'. 349 | Override/extend to create 'Node' subclass instances (move, setup). 350 | Argument: 351 | - plist : 'Node' or list of 'Property'""" 352 | return Node(plist) 353 | 354 | def cursor(self): 355 | """ Returns a 'Cursor' object for navigation of this 'GameTree'.""" 356 | return Cursor(self) 357 | 358 | def appendTree(self, ntree, index): 359 | if index + 1 < len( self.data ): 360 | subtree = GameTree( self.data[index+1:], self.variations ) 361 | self.data = self.data[:index+1] 362 | self.variations = [ subtree, ntree ] 363 | else: 364 | self.variations.append( ntree ) 365 | 366 | def pushTree(self, ntree, index): 367 | if index + 1 < len( self.data ): 368 | subtree = GameTree( self.data[index+1:], self.variations ) 369 | self.data = self.data[:index+1] 370 | self.variations = [ ntree, subtree ] 371 | else: 372 | self.variations = [ ntree ] + self.variations 373 | 374 | def appendNode(self, node): 375 | self.data.append(node) 376 | 377 | def propertySearch(self, pid, getall=0): 378 | """ 379 | Searches this 'GameTree' for nodes containing matching properties. 380 | Returns a 'GameTree' containing the matched node(s). Arguments: 381 | - pid : string -- ID of properties to search for. 382 | - getall : boolean -- Set to true (1) to return all 'Node''s that 383 | match, or to false (0) to return only the first match.""" 384 | matches = [] 385 | for n in self: 386 | if n.has_key(pid): 387 | matches.append(n) 388 | if not getall: 389 | break 390 | else: # getall or not matches: 391 | for v in self.variations: 392 | matches = matches + v.propertySearch(pid, getall) 393 | if not getall and matches: 394 | break 395 | return GameTree(matches) 396 | 397 | 398 | class Node(Dictionary): 399 | """ 400 | An SGF node. Instance Attributes: 401 | - self[.data] : ordered dictionary -- '{Property.id:Property}' mapping. 402 | (Ordered dictionary: allows offset-indexed retrieval). Properties *must* 403 | be added using 'self.addProperty()'. 404 | 405 | Example: Let 'n' be a 'Node' parsed from ';B[aa]BL[250]C[comment]': 406 | - 'str(n["BL"])' => '"BL[250]"' 407 | - 'str(n[0])' => '"B[aa]"' 408 | - 'map(str, n)' => '["B[aa]","BL[250]","C[comment]"]'""" 409 | 410 | def __init__(self, plist=[]): 411 | """ 412 | Initializer. Argument: 413 | - plist: Node or list of 'Property'.""" 414 | Dictionary.__init__(self) 415 | self.order = [] 416 | for p in plist: 417 | self.addProperty(p) 418 | 419 | def copy(self): 420 | plist = [] 421 | for prop in self.order: 422 | plist.append( prop.copy() ) 423 | return Node(plist) 424 | 425 | def __getitem__(self, key): 426 | """ On 'self[key]', 'x in self', 'for x in self'. Implements all 427 | indexing-related operations. Allows both key- and offset-indexed 428 | retrieval. Membership and iteration ('in', 'for') repeatedly index 429 | from 0 until 'IndexError'.""" 430 | if type(key) is INT_TYPE: 431 | return self.order[key] 432 | else: 433 | return self.data[key] 434 | 435 | def __setitem__(self, key, x): 436 | """ On 'self[key]=x'. Allows assignment to existing items only. Raises 437 | 'DirectAccessError' on new item assignment.""" 438 | if self.has_key(key): 439 | self.order[self.order.index(self[key])] = x 440 | Dictionary.__setitem__(self, key, x) 441 | else: 442 | raise DirectAccessError( 443 | "Properties may not be added directly; use addProperty() instead.") 444 | 445 | def __delitem__(self, key): 446 | """ On 'del self[key]'. Updates 'self.order' to maintain consistency.""" 447 | self.order.remove(self[key]) 448 | Dictionary.__delitem__(self, key) 449 | 450 | def __getslice__(self, low, high): 451 | """ On 'self[low:high]'.""" 452 | return self.order[low:high] 453 | 454 | def __str__(self): 455 | """ SGF representation, with proper line breaks between properties.""" 456 | if len(self): 457 | s = ";" + str(self[0]) 458 | l = len(string.split(s, "\n")[-1]) # accounts for line breaks within Properties 459 | for p in map(str, self[1:]): 460 | if l + len(string.split(p, "\n")[0]) > MAX_LINE_LEN: 461 | s = s + "\n" 462 | l = 0 463 | s = s + p 464 | l = len(string.split(s, "\n")[-1]) 465 | return s 466 | else: 467 | return ";" 468 | 469 | def update(self, dict): 470 | """ 'Dictionary' method not applicable to 'Node'. Raises 471 | 'DirectAccessError'.""" 472 | raise DirectAccessError( 473 | "The update() method is not supported by Node; use addProperty() instead.") 474 | 475 | def addProperty(self, property): 476 | """ 477 | Adds a 'Property' to this 'Node'. Checks for duplicate properties 478 | (illegal), and maintains the property order. Argument: 479 | - property : 'Property'""" 480 | if self.has_key(property.id): 481 | self.appendData(property.id, property[:]) 482 | raise DuplicatePropertyError 483 | else: 484 | self.data[property.id] = property 485 | self.order.append(property) 486 | 487 | def makeProperty(self, id, valuelist): 488 | """ 489 | Create a new 'Property'. Override/extend to create 'Property' 490 | subclass instances (move, setup, game-info, etc.). Arguments: 491 | - id : string 492 | - valuelist : 'Property' or list of values""" 493 | return Property(id, valuelist) 494 | 495 | def appendData(self, id, values): 496 | newProp = Property( id, self.data[id][:] + values ) 497 | self.data[id] = newProp 498 | for i in xrange(0, len(self.order)): 499 | if self.order[i].id == id: 500 | self.order[i] = newProp 501 | 502 | class Property(List): 503 | """ 504 | An SGF property: a set of label and value(s). Instance attributes: 505 | - self[.data] : list -- property values. 506 | - self.id : string -- SGF standard property label. 507 | - self.name : string -- actual label used in the SGF data. For example, the 508 | property 'CoPyright[...]' has name 'CoPyright' and id 'CP'.""" 509 | 510 | def __init__(self, id, values, name=None): 511 | """ 512 | Initialize the 'Property'. Arguments: 513 | - id : string 514 | - name : string (optional) -- If not given, 'self.name' 515 | - nodelist : 'GameTree' or list of 'Node' -- Stored in 'self.data'. 516 | - variations : list of 'GameTree' -- Stored in 'self.variations'.""" 517 | List.__init__(self, values) # XXX will _convert work here? 518 | self.id = id 519 | self.name = name or id 520 | 521 | def __str__(self): 522 | return self.name + "[" + string.join(map(_escapeText, self), "][") + "]" 523 | 524 | def copy(self): 525 | nvalues = [v for v in self] 526 | return Property(self.id, nvalues, self.name) 527 | 528 | class Cursor: 529 | """ 530 | 'GameTree' navigation tool. Instance attributes: 531 | - self.game : 'GameTree' -- The root 'GameTree'. 532 | - self.gametree : 'GameTree' -- The current 'GameTree'. 533 | - self.node : 'Node' -- The current Node. 534 | - self.nodenum : integer -- The offset of 'self.node' from the root of 535 | 'self.game'. The nodenum of the root node is 0. 536 | - self.index : integer -- The offset of 'self.node' within 'self.gametree'. 537 | - self.stack : list of 'GameTree' -- A record of 'GameTree''s traversed. 538 | - self.children : list of 'Node' -- All child nodes of the current node. 539 | - self.atEnd : boolean -- Flags if we are at the end of a branch. 540 | - self.atStart : boolean -- Flags if we are at the start of the game.""" 541 | 542 | def __init__(self, gametree): 543 | """ Initialize root 'GameTree' and instance variables.""" 544 | self.game = gametree # root GameTree 545 | self.reset() 546 | 547 | def reset(self): 548 | """ Set 'Cursor' to point to the start of the root 'GameTree', 'self.game'.""" 549 | self.gametree = self.game 550 | self.nodenum = 0 551 | self.index = 0 552 | self.stack = [] 553 | self.node = self.gametree[self.index] 554 | self._setChildren() 555 | self._setFlags() 556 | 557 | def next(self, varnum=0): 558 | """ 559 | Moves the 'Cursor' to & returns the next 'Node'. Raises 560 | 'GameTreeEndError' if the end of a branch is exceeded. Raises 561 | 'GameTreeNavigationError' if a non-existent variation is accessed. 562 | Argument: 563 | - varnum : integer, default 0 -- Variation number. Non-zero only 564 | valid at a branching, where variations exist.""" 565 | if self.index + 1 < len(self.gametree): # more main line? 566 | if varnum != 0: 567 | raise GameTreeNavigationError("Nonexistent variation.") 568 | self.index = self.index + 1 569 | elif self.gametree.variations: # variations exist? 570 | if varnum < len(self.gametree.variations): 571 | self.stack.append(self.gametree) 572 | self.gametree = self.gametree.variations[varnum] 573 | self.index = 0 574 | else: 575 | raise GameTreeNavigationError("Nonexistent variation.") 576 | else: 577 | raise GameTreeEndError 578 | self.node = self.gametree[self.index] 579 | self.nodenum = self.nodenum + 1 580 | self._setChildren() 581 | self._setFlags() 582 | return self.node 583 | 584 | def previous(self): 585 | """ Moves the 'Cursor' to & returns the previous 'Node'. Raises 586 | 'GameTreeEndError' if the start of a branch is exceeded.""" 587 | if self.index - 1 >= 0: # more main line? 588 | self.index = self.index - 1 589 | elif self.stack: # were we in a variation? 590 | self.gametree = self.stack.pop() 591 | self.index = len(self.gametree) - 1 592 | else: 593 | raise GameTreeEndError 594 | self.node = self.gametree[self.index] 595 | self.nodenum = self.nodenum - 1 596 | self._setChildren() 597 | self._setFlags() 598 | return self.node 599 | 600 | def pushNode(self, node): 601 | var = GameTree([node]) 602 | self.gametree.pushTree( var, self.index ) 603 | self._setChildren() 604 | self._setFlags() 605 | 606 | def appendNode(self, node): 607 | if self.index + 1 < len(self.gametree) or self.gametree.variations: 608 | var = GameTree([node]) 609 | self.gametree.appendTree( var, self.index ) 610 | self._setChildren() 611 | self._setFlags() 612 | else: 613 | self.gametree.appendNode(node) 614 | self._setChildren() 615 | self._setFlags() 616 | 617 | def _setChildren(self): 618 | """ Sets up 'self.children'.""" 619 | if self.index + 1 < len(self.gametree): 620 | self.children = [self.gametree[self.index+1]] 621 | else: 622 | self.children = map(lambda list: list[0], self.gametree.variations) 623 | 624 | def _setFlags(self): 625 | """ Sets up the flags 'self.atEnd' and 'self.atStart'.""" 626 | self.atEnd = not self.gametree.variations and (self.index + 1 == len(self.gametree)) 627 | self.atStart = not self.stack and (self.index == 0) 628 | 629 | 630 | reCharsToEscape = re.compile(r'\]|\\') # characters that need to be \escaped 631 | 632 | def _escapeText(text): 633 | """ Adds backslash-escapes to property value characters that need them.""" 634 | output = "" 635 | index = 0 636 | match = reCharsToEscape.search(text, index) 637 | while match: 638 | output = output + text[index:match.start()] + '\\' + text[match.start()] 639 | index = match.end() 640 | match = reCharsToEscape.search(text, index) 641 | output = output + text[index:] 642 | return output 643 | 644 | 645 | def selfTest1(onConsole=0): 646 | """ Canned data test case""" 647 | sgfdata = r""" (;GM [1]US[someone]CoPyright[\ 648 | Permission to reproduce this game is given.]GN[a-b]EV[None]RE[B+Resign] 649 | PW[a]WR[2k*]PB[b]BR[4k*]PC[somewhere]DT[2000-01-16]SZ[19]TM[300]KM[4.5] 650 | HA[3]AB[pd][dp][dd];W[pp];B[nq];W[oq]C[ x started observation. 651 | ](;B[qc]C[ [b\]: \\ hi x! ;-) \\];W[kc])(;B[hc];W[oe])) """ 652 | print "\n\n********** Self-Test 1 **********\n" 653 | print "Input data:\n" 654 | print sgfdata 655 | print "\n\nParsed data: " 656 | col = SGFParser(sgfdata).parse() 657 | print "done\n" 658 | cstr = str(col) 659 | print cstr, "\n" 660 | print "Mainline:\n" 661 | m = col[0].mainline() 662 | print m, "\n" 663 | ##print "as GameTree:\n" 664 | ##print GameTree(m), "\n" 665 | print "Tree traversal (forward):\n" 666 | c = col.cursor() 667 | while 1: 668 | print "nodenum: %s; index: %s; children: %s; node: %s" % (c.nodenum, c.index, len(c.children), c.node) 669 | if c.atEnd: break 670 | c.next() 671 | print "\nTree traversal (backward):\n" 672 | while 1: 673 | print "nodenum: %s; index: %s; children: %s; node: %s" % (c.nodenum, c.index, len(c.children), c.node) 674 | if c.atStart: break 675 | c.previous() 676 | print "\nSearch for property 'B':" 677 | print col[0].propertySearch("B", 1) 678 | print "\nSearch for property 'C':" 679 | print col[0].propertySearch("C", 1) 680 | pass 681 | 682 | def selfTest2(onConsole=0): 683 | """ Macintosh-based SGF file test""" 684 | import macfs 685 | print "\n\n********** Self-Test 2 (Mac) **********\n" 686 | thefile = macfs.PromptGetFile("Please choose an SGF file:") 687 | if not thefile[1]: 688 | return 689 | srcpath = thefile[0].as_pathname() 690 | src = open(srcpath, 'r') 691 | sgfdata = src.read() 692 | print "Input data:\n" 693 | print sgfdata 694 | print "\n\nParsed data:" 695 | col = SGFParser(sgfdata).parse() 696 | print "done\n" 697 | print str(col) 698 | 699 | 700 | if __name__ == '__main__': 701 | print __doc__ # show module's documentation string 702 | selfTest1() 703 | import os 704 | if os.name == 'mac': 705 | selfTest2() 706 | -------------------------------------------------------------------------------- /gpl.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------