SgfParser 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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](;)(;))
|