SgfParser 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +5 -0
- data/.gitignore +25 -0
- data/LICENSE +20 -0
- data/README.rdoc +28 -0
- data/Rakefile +59 -0
- data/SgfParser.gemspec +66 -0
- data/VERSION +1 -0
- data/lib/sgf/parser/node.rb +67 -0
- data/lib/sgf/parser/properties.rb +97 -0
- data/lib/sgf/parser/tree.rb +124 -0
- data/lib/sgf/parser/tree_parse.rb +111 -0
- data/lib/sgf/sgfindent.rb +118 -0
- data/lib/sgf_parser.rb +8 -0
- data/sample_sgf/ff4_ex.sgf +165 -0
- data/sample_sgf/ff4_ex_saved.sgf +45 -0
- data/sample_sgf/redrose-tartrate.sgf +1068 -0
- data/sample_usage/parsing_files.rb +19 -0
- data/spec/node_spec.rb +34 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/tree_spec.rb +23 -0
- metadata +87 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
module SgfParser
|
2
|
+
class Tree
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
# This function parses a SGF string into a linked list, or tree.
|
7
|
+
def parse
|
8
|
+
@sgf.gsub! "\\\\n\\\\r", ""
|
9
|
+
@sgf.gsub! "\\\\r\\\\n", ""
|
10
|
+
@sgf.gsub! "\\\\r", ""
|
11
|
+
@sgf.gsub! "\\\\n", ""
|
12
|
+
#@sgf.gsub! "\n", ""
|
13
|
+
branches = [] # This stores where new branches are open
|
14
|
+
current_node = @root # Let's start at the beginning, shall we?
|
15
|
+
identprop = false # We are not in the middle of an identprop value.
|
16
|
+
# An identprop is an identity property - a value.
|
17
|
+
content = Hash.new # Hash holding all the properties
|
18
|
+
param, property = "", "" # Variables holding the idents and props
|
19
|
+
end_of_a_series = false # To keep track of params with multiple properties
|
20
|
+
|
21
|
+
sgf_array = @sgf.split(//)
|
22
|
+
iterator = 0
|
23
|
+
array_length = sgf_array.size
|
24
|
+
|
25
|
+
while iterator < array_length
|
26
|
+
char = sgf_array[iterator]
|
27
|
+
case char
|
28
|
+
=begin
|
29
|
+
Basically, if we're inside an identprop, it's a normal character (or a closing
|
30
|
+
character).
|
31
|
+
If we're not, it's either a property or a special SGF character so we have
|
32
|
+
to handle that properly.
|
33
|
+
=end
|
34
|
+
when '(' # Opening a new branch
|
35
|
+
if identprop
|
36
|
+
property << char
|
37
|
+
else
|
38
|
+
branches.unshift current_node
|
39
|
+
end
|
40
|
+
when ')' # Closing a branch
|
41
|
+
if identprop
|
42
|
+
property << char
|
43
|
+
else
|
44
|
+
current_node = branches.shift
|
45
|
+
param, property = "", ""
|
46
|
+
content.clear
|
47
|
+
end
|
48
|
+
when ';' # Opening a new node
|
49
|
+
if identprop
|
50
|
+
property << char
|
51
|
+
else
|
52
|
+
# Make the current node the old node, make new node, store data
|
53
|
+
parent = current_node
|
54
|
+
current_node = Node.new :parent => parent
|
55
|
+
parent.add_properties content
|
56
|
+
parent.add_children current_node
|
57
|
+
param, property = "", ""
|
58
|
+
content.clear
|
59
|
+
end
|
60
|
+
when '[' # Open identprop?
|
61
|
+
if identprop
|
62
|
+
property << char
|
63
|
+
else # If we're not inside an identprop, then now we are.
|
64
|
+
identprop = true
|
65
|
+
end_of_a_series = false
|
66
|
+
end
|
67
|
+
when ']' # Close identprop
|
68
|
+
# Cleverness : checking for this first, then for the backslash.
|
69
|
+
# This means that if we encounter this, it must be closing an identprop.
|
70
|
+
# Because the "\\" code handles the logic to see if we're inside an identprop,
|
71
|
+
# And for skipping the bracket if necessary.
|
72
|
+
end_of_a_series = true # Maybe end of a series of identprop.
|
73
|
+
identprop = false # That's our cue to close an identprop.
|
74
|
+
content[param] = property
|
75
|
+
property = ""
|
76
|
+
when "\\"
|
77
|
+
# If we're inside a comment, then maybe we're about to escape a ].
|
78
|
+
# This is the whole reason we need this ugly loop.
|
79
|
+
if identprop
|
80
|
+
# If the next character is a closing bracket, then it's just
|
81
|
+
# escaped and the identprop continues.
|
82
|
+
if sgf_array[iterator + 1] == "]"
|
83
|
+
property << "]"
|
84
|
+
# On the next pass through, we will skip that bracket.
|
85
|
+
iterator += 1
|
86
|
+
else
|
87
|
+
property << "\\"
|
88
|
+
end
|
89
|
+
else
|
90
|
+
#This should never happen - a backslash outside an identprop ?!
|
91
|
+
#But let's not have it be told that I'm not prepared.
|
92
|
+
param << "\\"
|
93
|
+
end
|
94
|
+
when "\n"
|
95
|
+
property << "\n" if identprop
|
96
|
+
|
97
|
+
else
|
98
|
+
# Well, I guess it's "just" a character after all.
|
99
|
+
if end_of_a_series
|
100
|
+
end_of_a_series = false
|
101
|
+
param, property = "", ""
|
102
|
+
end
|
103
|
+
identprop ? (property << char) : param << char
|
104
|
+
end
|
105
|
+
iterator += 1
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module SgfParser
|
2
|
+
|
3
|
+
# This indents an SGF file to make it more readable. It outputs to the screen
|
4
|
+
# by default, but can be given a file as output.
|
5
|
+
# Usage: SgfParser::Indenter.new infile, outfile
|
6
|
+
class Indenter
|
7
|
+
|
8
|
+
def initialize file, out=$stdout
|
9
|
+
sgf = ""
|
10
|
+
File.open(file) { |f| sgf = f.read }
|
11
|
+
@new_string = ""
|
12
|
+
sgf.gsub! "\\\\n\\\\r", ""
|
13
|
+
sgf.gsub! "\\\\r\\\\n", ""
|
14
|
+
sgf.gsub! "\\\\r", ""
|
15
|
+
sgf.gsub! "\\\\n", ""
|
16
|
+
#sgf.gsub! "\n", ""
|
17
|
+
|
18
|
+
end_of_a_series = false
|
19
|
+
identprop = false # We are not in the middle of an identprop value.
|
20
|
+
# An identprop is an identity property - a value.
|
21
|
+
|
22
|
+
sgf_array = sgf.split(//)
|
23
|
+
iterator = 0
|
24
|
+
array_length = sgf_array.size
|
25
|
+
indent = 0
|
26
|
+
|
27
|
+
while iterator < array_length - 1
|
28
|
+
char = sgf_array[iterator]
|
29
|
+
|
30
|
+
case char
|
31
|
+
when '(' # Opening a new branch
|
32
|
+
if !identprop
|
33
|
+
@new_string << "\n"
|
34
|
+
indent += 2
|
35
|
+
#tabulate indent
|
36
|
+
@new_string << " " * indent
|
37
|
+
end
|
38
|
+
@new_string << char
|
39
|
+
|
40
|
+
when ')' # Closing a branch
|
41
|
+
@new_string << char
|
42
|
+
if !identprop
|
43
|
+
@new_string << "\n"
|
44
|
+
indent -= 2
|
45
|
+
@new_string << " " * indent
|
46
|
+
#tabulate indent
|
47
|
+
end
|
48
|
+
|
49
|
+
when ';' # Opening a new node
|
50
|
+
if !identprop
|
51
|
+
@new_string << "\n"
|
52
|
+
@new_string << " " * indent
|
53
|
+
#tabulate indent
|
54
|
+
end
|
55
|
+
@new_string << char
|
56
|
+
|
57
|
+
when '[' # Open comment?
|
58
|
+
if !identprop #If we're not inside a comment, then now we are.
|
59
|
+
identprop = true
|
60
|
+
end_of_a_series = false
|
61
|
+
end
|
62
|
+
@new_string << char
|
63
|
+
|
64
|
+
when ']' # Close comment
|
65
|
+
end_of_a_series = true # Maybe end of a series of comments.
|
66
|
+
identprop = false # That's our cue to close a comment.
|
67
|
+
@new_string << char
|
68
|
+
|
69
|
+
when "\\" # If we're inside a comment, then maybe we're about to escape a ].
|
70
|
+
# This is the whole reason we need this ugly charade of a loop.
|
71
|
+
if identprop
|
72
|
+
if sgf_array[iterator + 1] == "]"
|
73
|
+
@new_string << "\\]"
|
74
|
+
iterator += 1
|
75
|
+
else
|
76
|
+
@new_string << "\\"
|
77
|
+
end
|
78
|
+
else
|
79
|
+
#This should never happen - a backslash outside a comment ?!
|
80
|
+
#But let's not have it be told that I'm not prepared.
|
81
|
+
@new_string << "\\"
|
82
|
+
end
|
83
|
+
|
84
|
+
when "\n"
|
85
|
+
@new_string << "\n"
|
86
|
+
@new_string << " " * indent
|
87
|
+
#tabulate indent
|
88
|
+
|
89
|
+
else
|
90
|
+
# Well, I guess it's "just" a character after all.
|
91
|
+
if end_of_a_series
|
92
|
+
end_of_a_series = false
|
93
|
+
end
|
94
|
+
@new_string << char
|
95
|
+
end
|
96
|
+
|
97
|
+
iterator += 1
|
98
|
+
end
|
99
|
+
|
100
|
+
if out == $stdout
|
101
|
+
$stdout << @new_string
|
102
|
+
else
|
103
|
+
File.open(out, 'w') { |file| file << @new_string }
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def tabulate indent
|
111
|
+
indent.times { print " " }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
|
118
|
+
|
data/lib/sgf_parser.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
(;FF[4]AP[Primiview:3.1]GM[1]SZ[19]GN[Gametree 1: properties]US[Arno Hollosi]
|
2
|
+
|
3
|
+
(;B[pd]N[Moves, comments, annotations]
|
4
|
+
C[Nodename set to: "Moves, comments, annotations"];W[dp]GW[1]
|
5
|
+
C[Marked as "Good for White"];B[pp]GB[2]
|
6
|
+
C[Marked as "Very good for Black"];W[dc]GW[2]
|
7
|
+
C[Marked as "Very good for White"];B[pj]DM[1]
|
8
|
+
C[Marked as "Even position"];W[ci]UC[1]
|
9
|
+
C[Marked as "Unclear position"];B[jd]TE[1]
|
10
|
+
C[Marked as "Tesuji" or "Good move"];W[jp]BM[2]
|
11
|
+
C[Marked as "Very bad move"];B[gd]DO[]
|
12
|
+
C[Marked as "Doubtful move"];W[de]IT[]
|
13
|
+
C[Marked as "Interesting move"];B[jj];
|
14
|
+
C[White "Pass" move]W[];
|
15
|
+
C[Black "Pass" move]B[tt])
|
16
|
+
|
17
|
+
(;AB[dd][de][df][dg][do:gq]
|
18
|
+
AW[jd][je][jf][jg][kn:lq][pn:pq]
|
19
|
+
N[Setup]C[Black & white stones at the top are added as single stones.
|
20
|
+
|
21
|
+
Black & white stones at the bottom are added using compressed point lists.]
|
22
|
+
;AE[ep][fp][kn][lo][lq][pn:pq]
|
23
|
+
C[AddEmpty
|
24
|
+
|
25
|
+
Black stones & stones of left white group are erased in FF[3\] way.
|
26
|
+
|
27
|
+
White stones at bottom right were erased using compressed point list.]
|
28
|
+
;AB[pd]AW[pp]PL[B]C[Added two stones.
|
29
|
+
|
30
|
+
Node marked with "Black to play".];PL[W]
|
31
|
+
C[Node marked with "White to play"])
|
32
|
+
|
33
|
+
(;AB[dd][de][df][dg][dh][di][dj][nj][ni][nh][nf][ne][nd][ij][ii][ih][hq]
|
34
|
+
[gq][fq][eq][dr][ds][dq][dp][cp][bp][ap][iq][ir][is][bo][bn][an][ms][mr]
|
35
|
+
AW[pd][pe][pf][pg][ph][pi][pj][fd][fe][ff][fh][fi][fj][kh][ki][kj][os][or]
|
36
|
+
[oq][op][pp][qp][rp][sp][ro][rn][sn][nq][mq][lq][kq][kr][ks][fs][gs][gr]
|
37
|
+
[er]N[Markup]C[Position set up without compressed point lists.]
|
38
|
+
|
39
|
+
;TR[dd][de][df][ed][ee][ef][fd:ff]
|
40
|
+
MA[dh][di][dj][ej][ei][eh][fh:fj]
|
41
|
+
CR[nd][ne][nf][od][oe][of][pd:pf]
|
42
|
+
SQ[nh][ni][nj][oh][oi][oj][ph:pj]
|
43
|
+
SL[ih][ii][ij][jj][ji][jh][kh:kj]
|
44
|
+
TW[pq:ss][so][lr:ns]
|
45
|
+
TB[aq:cs][er:hs][ao]
|
46
|
+
C[Markup at top partially using compressed point lists (for markup on white stones); listed clockwise, starting at upper left:
|
47
|
+
- TR (triangle)
|
48
|
+
- CR (circle)
|
49
|
+
- SQ (square)
|
50
|
+
- SL (selected points)
|
51
|
+
- MA ('X')
|
52
|
+
|
53
|
+
Markup at bottom: black & white territory (using compressed point lists)]
|
54
|
+
;LB[dc:1][fc:2][nc:3][pc:4][dj:a][fj:b][nj:c]
|
55
|
+
[pj:d][gs:ABCDEFGH][gr:ABCDEFG][gq:ABCDEF][gp:ABCDE][go:ABCD][gn:ABC][gm:AB]
|
56
|
+
[mm:12][mn:123][mo:1234][mp:12345][mq:123456][mr:1234567][ms:12345678]
|
57
|
+
C[Label (LB property)
|
58
|
+
|
59
|
+
Top: 8 single char labels (1-4, a-d)
|
60
|
+
|
61
|
+
Bottom: Labels up to 8 char length.]
|
62
|
+
|
63
|
+
;DD[kq:os][dq:hs]
|
64
|
+
AR[aa:sc][sa:ac][aa:sa][aa:ac][cd:cj]
|
65
|
+
[gd:md][fh:ij][kj:nh]
|
66
|
+
LN[pj:pd][nf:ff][ih:fj][kh:nj]
|
67
|
+
C[Arrows, lines and dimmed points.])
|
68
|
+
|
69
|
+
(;B[qd]N[Style & text type]
|
70
|
+
C[There are hard linebreaks & soft linebreaks.
|
71
|
+
Soft linebreaks are linebreaks preceeded by '\\' like this one >o\
|
72
|
+
k<. Hard line breaks are all other linebreaks.
|
73
|
+
Soft linebreaks are converted to >nothing<, i.e. removed.
|
74
|
+
|
75
|
+
Note that linebreaks are coded differently on different systems.
|
76
|
+
|
77
|
+
Examples (>ok< shouldn't be split):
|
78
|
+
|
79
|
+
linebreak 1 "\\n": >o\
|
80
|
+
k<
|
81
|
+
linebreak 2 "\\n\\r": >o\
|
82
|
+
|
83
|
+
linebreak 3 "\\r\\n": >o\
|
84
|
+
k<
|
85
|
+
linebreak 4 "\\r": >o\
|
86
|
+
|
87
|
+
(;W[dd]N[W d16]C[Variation C is better.](;B[pp]N[B q4])
|
88
|
+
(;B[dp]N[B d4])
|
89
|
+
(;B[pq]N[B q3])
|
90
|
+
(;B[oq]N[B p3])
|
91
|
+
)
|
92
|
+
(;W[dp]N[W d4])
|
93
|
+
(;W[pp]N[W q4])
|
94
|
+
(;W[cc]N[W c17])
|
95
|
+
(;W[cq]N[W c3])
|
96
|
+
(;W[qq]N[W r3])
|
97
|
+
)
|
98
|
+
|
99
|
+
(;B[qr]N[Time limits, captures & move numbers]
|
100
|
+
BL[120.0]C[Black time left: 120 sec];W[rr]
|
101
|
+
WL[300]C[White time left: 300 sec];B[rq]
|
102
|
+
BL[105.6]OB[10]C[Black time left: 105.6 sec
|
103
|
+
Black stones left (in this byo-yomi period): 10];W[qq]
|
104
|
+
WL[200]OW[2]C[White time left: 200 sec
|
105
|
+
White stones left: 2];B[sr]
|
106
|
+
BL[87.00]OB[9]C[Black time left: 87 sec
|
107
|
+
Black stones left: 9];W[qs]
|
108
|
+
WL[13.20]OW[1]C[White time left: 13.2 sec
|
109
|
+
White stones left: 1];B[rs]
|
110
|
+
C[One white stone at s2 captured];W[ps];B[pr];W[or]
|
111
|
+
MN[2]C[Set move number to 2];B[os]
|
112
|
+
C[Two white stones captured
|
113
|
+
(at q1 & r1)]
|
114
|
+
;MN[112]W[pq]C[Set move number to 112];B[sq];W[rp];B[ps]
|
115
|
+
;W[ns];B[ss];W[nr]
|
116
|
+
;B[rr];W[sp];B[qs]C[Suicide move
|
117
|
+
(all B stones get captured)])
|
118
|
+
)
|
119
|
+
|
120
|
+
(;FF[4]AP[Primiview:3.1]GM[1]SZ[19]C[Gametree 2: game-info
|
121
|
+
|
122
|
+
Game-info properties are usually stored in the root node.
|
123
|
+
If games are merged into a single game-tree, they are stored in the node\
|
124
|
+
where the game first becomes distinguishable from all other games in\
|
125
|
+
the tree.]
|
126
|
+
;B[pd]
|
127
|
+
(;PW[W. Hite]WR[6d]RO[2]RE[W+3.5]
|
128
|
+
PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[dp]
|
129
|
+
C[Game-info:
|
130
|
+
Black: B. Lack, 5d
|
131
|
+
White: W. Hite, 6d
|
132
|
+
Place: London
|
133
|
+
Event: Go Congress
|
134
|
+
Round: 2
|
135
|
+
Result: White wins by 3.5])
|
136
|
+
(;PW[T. Suji]WR[7d]RO[1]RE[W+Resign]
|
137
|
+
PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[cp]
|
138
|
+
C[Game-info:
|
139
|
+
Black: B. Lack, 5d
|
140
|
+
White: T. Suji, 7d
|
141
|
+
Place: London
|
142
|
+
Event: Go Congress
|
143
|
+
Round: 1
|
144
|
+
Result: White wins by resignation])
|
145
|
+
(;W[ep];B[pp]
|
146
|
+
(;PW[S. Abaki]WR[1d]RO[3]RE[B+63.5]
|
147
|
+
PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[ed]
|
148
|
+
C[Game-info:
|
149
|
+
Black: B. Lack, 5d
|
150
|
+
White: S. Abaki, 1d
|
151
|
+
Place: London
|
152
|
+
Event: Go Congress
|
153
|
+
Round: 3
|
154
|
+
Result: Balck wins by 63.5])
|
155
|
+
(;PW[A. Tari]WR[12k]KM[-59.5]RO[4]RE[B+R]
|
156
|
+
PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[cd]
|
157
|
+
C[Game-info:
|
158
|
+
Black: B. Lack, 5d
|
159
|
+
White: A. Tari, 12k
|
160
|
+
Place: London
|
161
|
+
Event: Go Congress
|
162
|
+
Round: 4
|
163
|
+
Komi: -59.5 points
|
164
|
+
Result: Black wins by resignation])
|
165
|
+
))
|
@@ -0,0 +1,45 @@
|
|
1
|
+
(;FF[4]AP[Primiview:3.1]GM[1]SZ[19]GN[Gametree 1: properties]US[Arno Hollosi](;B[pd]N[Moves, comments, annotations]C[Nodename set to: "Moves, comments, annotations"];W[dp]C[Marked as "Good for White"]GW[1];B[pp]C[Marked as "Very good for Black"]GB[2];W[dc]C[Marked as "Very good for White"]GW[2];B[pj]C[Marked as "Even position"]DM[1];W[ci]UC[1]C[Marked as "Unclear position"];B[jd]C[Marked as "Tesuji" or "Good move"]TE[1];BM[2]W[jp]C[Marked as "Very bad move"];DO[]B[gd]C[Marked as "Doubtful move"];IT[]W[de]C[Marked as "Interesting move"];B[jj];W[]C[White "Pass" move];)(;AB[do:gq]N[Setup]C[Black & white stones at the top are added as single stones.
|
2
|
+
|
3
|
+
Black & white stones at the bottom are added using compressed point lists.] AW[pn:pq];C[AddEmpty
|
4
|
+
|
5
|
+
Black stones & stones of left white group are erased in FF[3\] way.
|
6
|
+
|
7
|
+
White stones at bottom right were erased using compressed point list.]AE[pn:pq];AW[pp]AB[pd]C[Added two stones.
|
8
|
+
|
9
|
+
Node marked with "Black to play".]PL[B];)(;AW[er]AB[mr]N[Markup]C[Position set up without compressed point lists.]; SL[kh:kj] TW[lr:ns] TB[ao]C[Markup at top partially using compressed point lists (for markup on white stones); listed clockwise, starting at upper left:
|
10
|
+
- TR (triangle)
|
11
|
+
- CR (circle)
|
12
|
+
- SQ (square)
|
13
|
+
- SL (selected points)
|
14
|
+
- MA ('X')
|
15
|
+
|
16
|
+
Markup at bottom: black & white territory (using compressed point lists)] CR[pd:pf]TR[fd:ff] SQ[ph:pj] MA[fh:fj];LB[ms:12345678]C[Label (LB property)
|
17
|
+
|
18
|
+
Top: 8 single char labels (1-4, a-d)
|
19
|
+
|
20
|
+
Bottom: Labels up to 8 char length.];)(;B[qd]N[Style & text type]C[There are hard linebreaks & soft linebreaks.
|
21
|
+
Soft linebreaks are linebreaks preceeded by '\\' like this one >o\
|
22
|
+
k<. Hard line breaks are all other linebreaks.
|
23
|
+
Soft linebreaks are converted to >nothing<, i.e. removed.
|
24
|
+
|
25
|
+
Note that linebreaks are coded differently on different systems.
|
26
|
+
|
27
|
+
Examples (>ok< shouldn't be split):
|
28
|
+
|
29
|
+
linebreak 1 "": >o\
|
30
|
+
k<
|
31
|
+
linebreak 2 "": >o\
|
32
|
+
|
33
|
+
linebreak 3 "": >o\
|
34
|
+
k<
|
35
|
+
linebreak 4 "": >o\
|
36
|
+
Black stones left (in this byo-yomi period): 10]OB[10]BL[105.6];W[qq]C[White time left: 200 sec
|
37
|
+
White stones left: 2]OW[2]WL[200];B[sr]C[Black time left: 87 sec
|
38
|
+
Black stones left: 9]OB[9]BL[87.00];W[qs]C[White time left: 13.2 sec
|
39
|
+
White stones left: 1]OW[1]WL[13.20];B[rs]C[One white stone at s2 captured];W[ps];B[pr];W[or]MN[2]C[Set move number to 2];B[os]C[Two white stones captured
|
40
|
+
(at q1 & r1)];MN[112]W[pq]C[Set move number to 112];B[sq];W[rp];B[ps];W[ns];B[ss];W[nr];B[rr];W[sp];);FF[4]C[Gametree 2: game-info
|
41
|
+
|
42
|
+
Game-info properties are usually stored in the root node.
|
43
|
+
If games are merged into a single game-tree, they are stored in the node\
|
44
|
+
where the game first becomes distinguishable from all other games in\
|
45
|
+
the tree.]AP[Primiview:3.1]GM[1]SZ[19];B[pd](;)(;)(;W[ep];B[pp](;)(;))
|