├── .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 |
--------------------------------------------------------------------------------