kantan-sgf 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +21 -0
- data/README +54 -0
- data/Rakefile +0 -0
- data/data/game-01.sgf +31 -0
- data/data/stoic-bojo.sgf +250 -0
- data/examples/kantan_example.rb +17 -0
- data/lib/grammar/sgf-grammar.rb +427 -0
- data/lib/grammar/sgf-grammar.tt +68 -0
- data/lib/kantan-sgf/sgf.rb +132 -0
- data/lib/kantan-sgf/version.rb +9 -0
- data/lib/kantan-sgf.rb +4 -0
- data/test/sgf-grammar_test.rb +25 -0
- metadata +65 -0
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2009 Brian Jones
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
~~ ABOUT ~~~~~*
|
2
|
+
|
3
|
+
KANTAN means "simple" or "easy" in
|
4
|
+
that wacky Japanese language.
|
5
|
+
|
6
|
+
There doesn't seem to be any such
|
7
|
+
thing as a dedicated Ruby SGF project.
|
8
|
+
I know because I've been looking for
|
9
|
+
a few years.
|
10
|
+
|
11
|
+
If I missed one, sorry.
|
12
|
+
|
13
|
+
Anyways, I got sick of searching around,
|
14
|
+
and an SGF parser applies to the
|
15
|
+
project I am working on, so I broke down
|
16
|
+
and wrote this.
|
17
|
+
|
18
|
+
It doesn't do node parsing (yet).
|
19
|
+
I just upgraded it using Treetop grammar
|
20
|
+
to parse the file, so a good majority of
|
21
|
+
the data is pulled out now. Some of it
|
22
|
+
will get refined with more internal handling,
|
23
|
+
but for now enjoy!
|
24
|
+
|
25
|
+
~~ Requirements ~~~~~*
|
26
|
+
|
27
|
+
Requires the Treetop gem to run.
|
28
|
+
|
29
|
+
$ gem install treetop
|
30
|
+
|
31
|
+
~~ USAGE ~~~~~*
|
32
|
+
|
33
|
+
You pretty much run it like so:
|
34
|
+
|
35
|
+
# Load and parse
|
36
|
+
sgf = KantanSgf::Sgf.new('data/stoic-bojo.sgf')
|
37
|
+
sgf.parse
|
38
|
+
|
39
|
+
# Pull back properties
|
40
|
+
puts sgf.player_black
|
41
|
+
puts sgf.player_white
|
42
|
+
puts sgf.komi
|
43
|
+
puts sgf.result
|
44
|
+
|
45
|
+
# do some magic with the move hash
|
46
|
+
for move in sgf.move_list
|
47
|
+
puts "%s: (%i, %i)" % [move[:color], move[:x], move[:y]]
|
48
|
+
end
|
49
|
+
|
50
|
+
Note that the move data is stored as:
|
51
|
+
* color: 'B' or 'W'
|
52
|
+
* x, y: Integer value from 0..board_size - 1
|
53
|
+
* time: Clock time left
|
54
|
+
* ot_stones: Overtime stones
|
data/Rakefile
ADDED
File without changes
|
data/data/game-01.sgf
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
(
|
2
|
+
;
|
3
|
+
FF[4]
|
4
|
+
EV[Female Guksu,13,Korea,]
|
5
|
+
RO[1]
|
6
|
+
PB[Lee MinJin]
|
7
|
+
BR[5p]
|
8
|
+
PW[Park JiEun]
|
9
|
+
WR[9p]
|
10
|
+
KM[6.5]
|
11
|
+
DT[2008-03-11]
|
12
|
+
PC[Korea]
|
13
|
+
RE[B+R]
|
14
|
+
SO[http://gobase.org/games/korea/female/guksu/13/game-01.sgf]
|
15
|
+
AP[sgf2misc:3.1.9]
|
16
|
+
;B[qd];W[dd];B[pp];W[dp];B[fc];W[cf];B[db];W[od];B[oc];W[pd]
|
17
|
+
;B[pc];W[qe];B[qc];W[nd];B[mc];W[qn];B[nq];W[pf];B[pk];W[qp]
|
18
|
+
;B[qq];W[rq];B[qo];W[rp];B[po];W[ro];B[pn];W[qm];B[qr];W[pm]
|
19
|
+
;B[om];W[ol];B[pl];W[nm];B[on];W[rk];B[pi];W[iq];B[cn];W[fp]
|
20
|
+
;B[bp];W[cm];B[dm];W[cl];B[cq];W[bn];B[bo];W[dn];B[co];W[en]
|
21
|
+
;B[er];W[ge];B[mj];W[hj];B[hl];W[gl];B[hh];W[hk];B[fg];W[fd]
|
22
|
+
;B[gc];W[hf];B[hg];W[jg];B[dg];W[cg];B[di];W[ch];B[jh];W[kh]
|
23
|
+
;B[ji];W[ki];B[kj];W[jj];B[kk];W[nc];B[nb];W[ih];B[ii];W[hi]
|
24
|
+
;B[ig];W[ij];B[jf];W[ih];B[kg];W[lg];B[jh];W[ji];B[md];W[me]
|
25
|
+
;B[kd];W[cc];B[hd];W[km];B[nl];W[ml];B[nk];W[dh];B[eh];W[ei]
|
26
|
+
;B[fi];W[ej];B[kq];W[kp];B[lp];W[jp];B[gq];W[lo];B[lq];W[fq]
|
27
|
+
;B[fr];W[gp];B[mf];W[mb];B[lb];W[le];B[nf];W[rd];B[rc];W[ne]
|
28
|
+
;B[lf];W[ke];B[je];W[rf];B[sd];W[rj];B[kf];W[qh];B[jl];W[cb]
|
29
|
+
;B[jm];W[hm];B[hn];W[io];B[gm];W[il];B[im];W[hl];B[fj];W[fk]
|
30
|
+
;B[in];W[gn];B[go];W[fn];B[hp];W[ho];B[jo];W[ip];B[ko]
|
31
|
+
)
|
data/data/stoic-bojo.sgf
ADDED
@@ -0,0 +1,250 @@
|
|
1
|
+
(;GM[1]FF[4]CA[UTF-8]AP[CGoban:3]ST[2]
|
2
|
+
RU[Japanese]SZ[19]KM[0.50]TM[1800]OT[5x30 byo-yomi]
|
3
|
+
PW[stoic]PB[bojo]WR[3k]BR[4k]DT[2008-11-30]PC[The KGS Go Server at http://www.gokgs.com/]C[bojo [4k\]: hi
|
4
|
+
stoic [3k\]: hi!
|
5
|
+
]RE[B+2.50]
|
6
|
+
;B[pc]BL[1792.458]
|
7
|
+
;W[pp]WL[1794.464]
|
8
|
+
;B[cd]BL[1786.722]
|
9
|
+
;W[qe]WL[1787.185]
|
10
|
+
;B[dq]BL[1775.793]
|
11
|
+
;W[od]WL[1768.345]
|
12
|
+
;B[oc]BL[1747.164]
|
13
|
+
;W[nd]WL[1758.788]
|
14
|
+
;B[nc]BL[1743.67]
|
15
|
+
;W[qj]WL[1738.284]
|
16
|
+
;B[pd]BL[1726.526]
|
17
|
+
;W[of]WL[1681.06]
|
18
|
+
;B[jp]BL[1639.907]
|
19
|
+
;W[do]WL[1663.982]
|
20
|
+
;B[fp]BL[1631.746]
|
21
|
+
;W[cq]WL[1654.511]
|
22
|
+
;B[dp]BL[1626.011]
|
23
|
+
;W[cp]WL[1646.782]
|
24
|
+
;B[eo]BL[1623.1]
|
25
|
+
;W[dn]WL[1643.852]
|
26
|
+
;B[cr]BL[1620.583]
|
27
|
+
;W[br]WL[1641.17]
|
28
|
+
;B[dr]BL[1616.752]
|
29
|
+
;W[cj]WL[1630.233]
|
30
|
+
;B[ed]BL[1584.282]
|
31
|
+
;W[md]WL[1611.942]
|
32
|
+
;B[mc]BL[1580.724]
|
33
|
+
;W[hc]WL[1603.668]
|
34
|
+
;B[kc]BL[1560.629]
|
35
|
+
;W[he]WL[1590.915]
|
36
|
+
;B[ke]BL[1543.078]
|
37
|
+
;W[en]WL[1587.257]
|
38
|
+
;B[fn]BL[1538.112]
|
39
|
+
;W[fm]WL[1576.929]
|
40
|
+
;B[gm]BL[1528.599]
|
41
|
+
;W[gl]WL[1558.097]
|
42
|
+
;B[fl]BL[1523.783]
|
43
|
+
;W[em]WL[1547.426]
|
44
|
+
;B[gn]BL[1508.898]
|
45
|
+
;W[gk]WL[1541.47]
|
46
|
+
;B[ej]BL[1504.11]
|
47
|
+
;W[dh]WL[1533.93]
|
48
|
+
;B[cg]BL[1489.237]
|
49
|
+
;W[fh]WL[1513.371]
|
50
|
+
;B[ci]BL[1476.168]
|
51
|
+
;W[di]WL[1511.16]
|
52
|
+
;B[ch]BL[1449.339]
|
53
|
+
;W[dg]WL[1439.636]
|
54
|
+
;B[cf]BL[1441.658]
|
55
|
+
;W[pq]WL[1399.774]
|
56
|
+
;B[ql]BL[1402.308]
|
57
|
+
;W[qn]WL[1371.667]
|
58
|
+
;B[qg]BL[1395.566]
|
59
|
+
;W[pe]WL[1290.265]
|
60
|
+
;B[pj]BL[1372.501]
|
61
|
+
;W[pi]WL[1198.698]
|
62
|
+
;B[oj]BL[1318.808]
|
63
|
+
;W[qi]WL[1169.751]
|
64
|
+
;B[ol]BL[1314.671]
|
65
|
+
;W[qk]WL[1160.616]
|
66
|
+
;B[pl]BL[1299.257]
|
67
|
+
;W[lp]WL[1132.469]
|
68
|
+
;B[lq]BL[1281.075]
|
69
|
+
;W[mq]WL[1106.595]
|
70
|
+
;B[kq]BL[1277.244]
|
71
|
+
;W[ln]WL[1088.387]
|
72
|
+
;B[ll]BL[1245.779]
|
73
|
+
;W[km]WL[1069.595]
|
74
|
+
;B[nh]BL[1215.502]
|
75
|
+
;W[kg]WL[990.928]
|
76
|
+
;B[lh]BL[1196.657]
|
77
|
+
;W[kh]WL[906.956]
|
78
|
+
;B[lg]BL[1191.135]
|
79
|
+
;W[kf]WL[886.502]
|
80
|
+
;B[lf]BL[1188.459]
|
81
|
+
;W[je]WL[880.939]
|
82
|
+
;B[ld]BL[1182.558]
|
83
|
+
;W[oi]WL[832.87]
|
84
|
+
;B[ni]BL[1177.604]
|
85
|
+
;W[lj]WL[785.912]
|
86
|
+
;B[mk]BL[1161.47]
|
87
|
+
;W[kl]WL[750.526]
|
88
|
+
;B[oh]BL[1156.4]
|
89
|
+
;W[ph]WL[741.468]
|
90
|
+
;B[pg]BL[1153.321]
|
91
|
+
;W[og]WL[738.531]
|
92
|
+
;B[rg]BL[1150.763]
|
93
|
+
;W[rh]WL[701.128]
|
94
|
+
;B[rk]BL[1145.239]
|
95
|
+
;W[rj]WL[689.609]
|
96
|
+
;B[rl]BL[1141.614]
|
97
|
+
;W[rf]WL[685.214]
|
98
|
+
;B[sh]BL[1063.241]
|
99
|
+
;W[si]WL[659.907]
|
100
|
+
;B[sg]BL[1059.723]
|
101
|
+
;W[qh]WL[650.385]
|
102
|
+
;B[sk]BL[1054.989]
|
103
|
+
;W[sf]WL[632.39]
|
104
|
+
;B[sj]BL[1049.175]
|
105
|
+
;W[qf]WL[615.379]
|
106
|
+
;B[ri]BL[1044.751]
|
107
|
+
;W[go]WL[576.126]
|
108
|
+
;B[gp]BL[1036.254]
|
109
|
+
;W[si]WL[573.861]
|
110
|
+
;B[co]BL[1024.166]
|
111
|
+
;W[pf]WL[554.652]
|
112
|
+
;B[bo]BL[1020.167]
|
113
|
+
;W[bm]WL[515.418]
|
114
|
+
;B[ck]BL[925.688]
|
115
|
+
;W[bj]WL[429.279]
|
116
|
+
;B[dj]BL[919.363]
|
117
|
+
;W[bk]WL[381.195]
|
118
|
+
;B[cn]BL[907.87]
|
119
|
+
;W[cm]WL[366.769]
|
120
|
+
;B[el]BL[904.885]
|
121
|
+
;W[dm]WL[365.266]
|
122
|
+
;B[bq]BL[901.476]
|
123
|
+
;W[dk]WL[340.13]
|
124
|
+
;B[im]BL[892.108]
|
125
|
+
;W[jn]WL[324.538]
|
126
|
+
;B[io]BL[888.293]
|
127
|
+
;W[hm]WL[300.362]
|
128
|
+
;B[hn]BL[881.802]
|
129
|
+
;W[hl]WL[291.957]
|
130
|
+
;B[in]BL[878.121]
|
131
|
+
;W[il]WL[277.59]
|
132
|
+
;B[ff]BL[873.851]
|
133
|
+
;W[gg]WL[271.55]
|
134
|
+
;B[gd]BL[870.995]
|
135
|
+
;W[fb]WL[264.46]
|
136
|
+
;B[fc]BL[864.486]
|
137
|
+
;W[eb]WL[256.088]
|
138
|
+
;B[gb]BL[857.77]
|
139
|
+
;W[gc]WL[229.165]
|
140
|
+
;B[hb]BL[847.356]
|
141
|
+
;W[ib]WL[198.916]
|
142
|
+
;B[ic]BL[843.043]
|
143
|
+
;W[hd]WL[191.117]
|
144
|
+
;B[jb]BL[841.065]
|
145
|
+
;W[ec]WL[175.606]
|
146
|
+
;B[fd]BL[837.8]
|
147
|
+
;W[bc]WL[174.381]
|
148
|
+
;B[cc]BL[790.076]
|
149
|
+
;W[cb]WL[160.963]
|
150
|
+
;B[bb]BL[766.773]
|
151
|
+
;W[ab]WL[129.073]
|
152
|
+
;B[ba]BL[736.205]
|
153
|
+
;W[ca]WL[104.992]
|
154
|
+
;B[db]BL[708.763]
|
155
|
+
;W[aa]WL[99.323]
|
156
|
+
;B[dc]BL[705.328]
|
157
|
+
;W[bd]WL[97.071]
|
158
|
+
;B[be]BL[691.701]
|
159
|
+
;W[ad]WL[95.193]
|
160
|
+
;B[da]BL[686.344]
|
161
|
+
;W[nm]WL[76.997]
|
162
|
+
;B[pn]BL[681.103]
|
163
|
+
;W[po]WL[62.894]
|
164
|
+
;B[on]BL[678.213]
|
165
|
+
;W[nl]WL[39.376]
|
166
|
+
;B[nk]BL[674.879]
|
167
|
+
;W[no]WL[28.175]
|
168
|
+
;B[lk]BL[642.766]
|
169
|
+
;W[kk]WL[22.144]
|
170
|
+
;B[li]BL[639.436]
|
171
|
+
;W[ki]WL[17.945]
|
172
|
+
;B[gf]BL[636.261]
|
173
|
+
;W[hf]WL[15.471]
|
174
|
+
;B[rd]BL[631.134]
|
175
|
+
;W[qd]WL[12.752]
|
176
|
+
;B[qc]BL[628.157]
|
177
|
+
;W[jd]WL[7.529]
|
178
|
+
;B[jc]BL[613.093]
|
179
|
+
;W[kd]WL[5.897]
|
180
|
+
;B[le]BL[610.468]
|
181
|
+
;W[mr]WL[4.232]
|
182
|
+
;B[kr]BL[602.191]
|
183
|
+
;W[df]WL[30]OW[5]
|
184
|
+
;B[de]BL[598.237]
|
185
|
+
;W[ag]WL[30]OW[5]
|
186
|
+
;B[bg]BL[588.912]
|
187
|
+
;W[ah]WL[30]OW[5]
|
188
|
+
;B[af]BL[584.436]
|
189
|
+
;W[bi]WL[30]OW[5]
|
190
|
+
;B[so]BL[574.598]
|
191
|
+
;W[ro]WL[30]OW[5]
|
192
|
+
;B[sm]BL[562.012]
|
193
|
+
;W[sp]WL[30]OW[5]
|
194
|
+
;B[sn]BL[559.893]
|
195
|
+
;W[rp]WL[30]OW[5]
|
196
|
+
;B[nn]BL[545.779]
|
197
|
+
;W[mm]WL[30]OW[5]
|
198
|
+
;B[mn]BL[542.459]
|
199
|
+
;W[lm]WL[30]OW[5]
|
200
|
+
;B[mo]BL[539.489]
|
201
|
+
;W[np]WL[30]OW[5]
|
202
|
+
;B[mp]BL[534.382]
|
203
|
+
;W[lo]WL[30]OW[5]
|
204
|
+
;B[oo]BL[528.951]
|
205
|
+
;W[op]WL[30]OW[5]
|
206
|
+
;B[mj]BL[524.453]
|
207
|
+
;W[pk]WL[30]OW[5]
|
208
|
+
;B[ok]BL[521.145]
|
209
|
+
;W[kj]WL[30]OW[5]
|
210
|
+
;B[ef]BL[515.481]
|
211
|
+
;W[fg]WL[30]OW[5]
|
212
|
+
;B[jm]BL[508.218]
|
213
|
+
;W[jl]WL[30]OW[5]
|
214
|
+
;B[jo]BL[505.946]
|
215
|
+
;W[kn]WL[30]OW[5]
|
216
|
+
;B[eg]BL[478.246]
|
217
|
+
;W[eh]WL[30]OW[5]
|
218
|
+
;B[ae]BL[466.784]
|
219
|
+
;W[re]WL[30]OW[5]
|
220
|
+
;B[rc]BL[463.459]
|
221
|
+
;W[rm]WL[30]OW[5]
|
222
|
+
;B[ng]BL[436.579]
|
223
|
+
;W[nf]WL[30]OW[5]
|
224
|
+
;B[me]BL[433.462]
|
225
|
+
;W[ne]WL[30]OW[5]
|
226
|
+
;B[mf]BL[430.536]
|
227
|
+
;W[lr]WL[30]OW[5]
|
228
|
+
;B[ls]BL[425.674]
|
229
|
+
;W[ms]WL[30]OW[5]
|
230
|
+
;B[ks]BL[423.798]
|
231
|
+
;W[pm]WL[30]OW[5]
|
232
|
+
;B[om]BL[419.794]
|
233
|
+
;W[qm]WL[30]OW[5]
|
234
|
+
;B[id]BL[406.893]
|
235
|
+
;W[ie]WL[30]OW[5]
|
236
|
+
;B[an]BL[401.121]
|
237
|
+
;W[am]WL[30]OW[5]
|
238
|
+
;B[bn]BL[396.109]
|
239
|
+
;W[ge]WL[30]OW[5]
|
240
|
+
;B[fe]BL[393.014]
|
241
|
+
;W[rn]WL[30]OW[5]
|
242
|
+
;B[sl]BL[390.425]
|
243
|
+
;W[bh]WL[30]OW[5]
|
244
|
+
;B[sd]BL[383.494]
|
245
|
+
;W[kp]WL[30]OW[5]
|
246
|
+
;B[iq]BL[365.597]
|
247
|
+
;W[]WL[30]OW[5]
|
248
|
+
;B[]BL[365.597]TW[ba][bb][ac][oe][if][jf][hg][ig][jg][pg][qg][rg][sg][gh][hh][ih][jh][sh][ai][ei][fi][gi][hi][ii][ji][ri][aj][dj][ej][fj][gj][hj][ij][jj][ak][ck][ek][fk][hk][ik][jk][al][bl][cl][dl][el][fl][qo][qp][nq][oq][qq][rq][sq][nr][or][pr][qr][rr][sr][ns][os][ps][qs][rs][ss]TB[ea][fa][ga][ha][ia][ja][ka][la][ma][na][oa][pa][qa][ra][sa][eb][fb][ib][kb][lb][mb][nb][ob][pb][qb][rb][sb][ec][lc][sc][dd][ce][ee][bf][mg][mh][mi][nj][ao][fo][go][ho][ap][bp][cp][ep][hp][ip][aq][cq][eq][fq][gq][hq][jq][ar][br][er][fr][gr][hr][ir][jr][as][bs][cs][ds][es][fs][gs][hs][is][js]C[stoic [3k\]: thanks
|
249
|
+
bojo [3k\]: thank you
|
250
|
+
])
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require '../lib/kantan-sgf'
|
2
|
+
|
3
|
+
# Load and parse
|
4
|
+
#sgf = KantanSgf::Sgf.new('../data/game-01.sgf')
|
5
|
+
sgf = KantanSgf::Sgf.new('../data/stoic-bojo.sgf')
|
6
|
+
sgf.parse
|
7
|
+
|
8
|
+
# Pull back properties
|
9
|
+
puts sgf.player_black
|
10
|
+
puts sgf.player_white
|
11
|
+
puts sgf.komi
|
12
|
+
puts sgf.result
|
13
|
+
|
14
|
+
# do some magic with the move hash
|
15
|
+
#for move in sgf.move_list
|
16
|
+
# puts "%s: (%i, %i)" % [move[:color], move[:x], move[:y]]
|
17
|
+
#end
|
@@ -0,0 +1,427 @@
|
|
1
|
+
module SgfGrammar
|
2
|
+
include Treetop::Runtime
|
3
|
+
|
4
|
+
def root
|
5
|
+
@root || :node
|
6
|
+
end
|
7
|
+
|
8
|
+
module Node0
|
9
|
+
def sp
|
10
|
+
elements[1]
|
11
|
+
end
|
12
|
+
|
13
|
+
def sp
|
14
|
+
elements[3]
|
15
|
+
end
|
16
|
+
|
17
|
+
def sp
|
18
|
+
elements[5]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module Node1
|
23
|
+
def value
|
24
|
+
get([elements[2]])
|
25
|
+
end
|
26
|
+
|
27
|
+
def get(e)
|
28
|
+
a = []
|
29
|
+
if !e.nil?
|
30
|
+
e.each do |el|
|
31
|
+
if el.respond_to?(:value)
|
32
|
+
a << el.value
|
33
|
+
else
|
34
|
+
a += get(el.elements) if !el.nil?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
a
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def _nt_node
|
43
|
+
start_index = index
|
44
|
+
if node_cache[:node].has_key?(index)
|
45
|
+
cached = node_cache[:node][index]
|
46
|
+
@index = cached.interval.end if cached
|
47
|
+
return cached
|
48
|
+
end
|
49
|
+
|
50
|
+
i0, s0 = index, []
|
51
|
+
if input.index('(', index) == index
|
52
|
+
r1 = (SyntaxNode).new(input, index...(index + 1))
|
53
|
+
@index += 1
|
54
|
+
else
|
55
|
+
terminal_parse_failure('(')
|
56
|
+
r1 = nil
|
57
|
+
end
|
58
|
+
s0 << r1
|
59
|
+
if r1
|
60
|
+
r2 = _nt_sp
|
61
|
+
s0 << r2
|
62
|
+
if r2
|
63
|
+
s3, i3 = [], index
|
64
|
+
loop do
|
65
|
+
r4 = _nt_chunk
|
66
|
+
if r4
|
67
|
+
s3 << r4
|
68
|
+
else
|
69
|
+
break
|
70
|
+
end
|
71
|
+
end
|
72
|
+
r3 = SyntaxNode.new(input, i3...index, s3)
|
73
|
+
s0 << r3
|
74
|
+
if r3
|
75
|
+
r5 = _nt_sp
|
76
|
+
s0 << r5
|
77
|
+
if r5
|
78
|
+
if input.index(')', index) == index
|
79
|
+
r6 = (SyntaxNode).new(input, index...(index + 1))
|
80
|
+
@index += 1
|
81
|
+
else
|
82
|
+
terminal_parse_failure(')')
|
83
|
+
r6 = nil
|
84
|
+
end
|
85
|
+
s0 << r6
|
86
|
+
if r6
|
87
|
+
r7 = _nt_sp
|
88
|
+
s0 << r7
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
if s0.last
|
95
|
+
r0 = (SyntaxNode).new(input, i0...index, s0)
|
96
|
+
r0.extend(Node0)
|
97
|
+
r0.extend(Node1)
|
98
|
+
else
|
99
|
+
self.index = i0
|
100
|
+
r0 = nil
|
101
|
+
end
|
102
|
+
|
103
|
+
node_cache[:node][start_index] = r0
|
104
|
+
|
105
|
+
return r0
|
106
|
+
end
|
107
|
+
|
108
|
+
module Chunk0
|
109
|
+
def sp
|
110
|
+
elements[1]
|
111
|
+
end
|
112
|
+
|
113
|
+
def sp
|
114
|
+
elements[3]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
module Chunk1
|
119
|
+
def value
|
120
|
+
get([elements[2]])
|
121
|
+
end
|
122
|
+
|
123
|
+
def get(e)
|
124
|
+
a = []
|
125
|
+
if !e.nil?
|
126
|
+
e.each do |el|
|
127
|
+
if el.respond_to?(:value)
|
128
|
+
a << el.value
|
129
|
+
else
|
130
|
+
a += get(el.elements) if !el.nil?
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
a
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def _nt_chunk
|
139
|
+
start_index = index
|
140
|
+
if node_cache[:chunk].has_key?(index)
|
141
|
+
cached = node_cache[:chunk][index]
|
142
|
+
@index = cached.interval.end if cached
|
143
|
+
return cached
|
144
|
+
end
|
145
|
+
|
146
|
+
i0, s0 = index, []
|
147
|
+
if input.index(';', index) == index
|
148
|
+
r1 = (SyntaxNode).new(input, index...(index + 1))
|
149
|
+
@index += 1
|
150
|
+
else
|
151
|
+
terminal_parse_failure(';')
|
152
|
+
r1 = nil
|
153
|
+
end
|
154
|
+
s0 << r1
|
155
|
+
if r1
|
156
|
+
r2 = _nt_sp
|
157
|
+
s0 << r2
|
158
|
+
if r2
|
159
|
+
s3, i3 = [], index
|
160
|
+
loop do
|
161
|
+
r4 = _nt_property_set
|
162
|
+
if r4
|
163
|
+
s3 << r4
|
164
|
+
else
|
165
|
+
break
|
166
|
+
end
|
167
|
+
end
|
168
|
+
r3 = SyntaxNode.new(input, i3...index, s3)
|
169
|
+
s0 << r3
|
170
|
+
if r3
|
171
|
+
r5 = _nt_sp
|
172
|
+
s0 << r5
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
if s0.last
|
177
|
+
r0 = (SyntaxNode).new(input, i0...index, s0)
|
178
|
+
r0.extend(Chunk0)
|
179
|
+
r0.extend(Chunk1)
|
180
|
+
else
|
181
|
+
self.index = i0
|
182
|
+
r0 = nil
|
183
|
+
end
|
184
|
+
|
185
|
+
node_cache[:chunk][start_index] = r0
|
186
|
+
|
187
|
+
return r0
|
188
|
+
end
|
189
|
+
|
190
|
+
module PropertySet0
|
191
|
+
def property
|
192
|
+
elements[0]
|
193
|
+
end
|
194
|
+
|
195
|
+
def sp
|
196
|
+
elements[2]
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
module PropertySet1
|
201
|
+
def value
|
202
|
+
{
|
203
|
+
:property => elements[0].text_value,
|
204
|
+
:data => elements[1].text_value[1..-2]
|
205
|
+
}
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def _nt_property_set
|
210
|
+
start_index = index
|
211
|
+
if node_cache[:property_set].has_key?(index)
|
212
|
+
cached = node_cache[:property_set][index]
|
213
|
+
@index = cached.interval.end if cached
|
214
|
+
return cached
|
215
|
+
end
|
216
|
+
|
217
|
+
i0, s0 = index, []
|
218
|
+
r1 = _nt_property
|
219
|
+
s0 << r1
|
220
|
+
if r1
|
221
|
+
s2, i2 = [], index
|
222
|
+
loop do
|
223
|
+
r3 = _nt_property_bracket
|
224
|
+
if r3
|
225
|
+
s2 << r3
|
226
|
+
else
|
227
|
+
break
|
228
|
+
end
|
229
|
+
end
|
230
|
+
r2 = SyntaxNode.new(input, i2...index, s2)
|
231
|
+
s0 << r2
|
232
|
+
if r2
|
233
|
+
r4 = _nt_sp
|
234
|
+
s0 << r4
|
235
|
+
end
|
236
|
+
end
|
237
|
+
if s0.last
|
238
|
+
r0 = (SyntaxNode).new(input, i0...index, s0)
|
239
|
+
r0.extend(PropertySet0)
|
240
|
+
r0.extend(PropertySet1)
|
241
|
+
else
|
242
|
+
self.index = i0
|
243
|
+
r0 = nil
|
244
|
+
end
|
245
|
+
|
246
|
+
node_cache[:property_set][start_index] = r0
|
247
|
+
|
248
|
+
return r0
|
249
|
+
end
|
250
|
+
|
251
|
+
module PropertyBracket0
|
252
|
+
end
|
253
|
+
|
254
|
+
def _nt_property_bracket
|
255
|
+
start_index = index
|
256
|
+
if node_cache[:property_bracket].has_key?(index)
|
257
|
+
cached = node_cache[:property_bracket][index]
|
258
|
+
@index = cached.interval.end if cached
|
259
|
+
return cached
|
260
|
+
end
|
261
|
+
|
262
|
+
i0, s0 = index, []
|
263
|
+
if input.index('[', index) == index
|
264
|
+
r1 = (SyntaxNode).new(input, index...(index + 1))
|
265
|
+
@index += 1
|
266
|
+
else
|
267
|
+
terminal_parse_failure('[')
|
268
|
+
r1 = nil
|
269
|
+
end
|
270
|
+
s0 << r1
|
271
|
+
if r1
|
272
|
+
s2, i2 = [], index
|
273
|
+
loop do
|
274
|
+
i3 = index
|
275
|
+
if input.index(Regexp.new('[^\\[\\]]'), index) == index
|
276
|
+
r4 = (SyntaxNode).new(input, index...(index + 1))
|
277
|
+
@index += 1
|
278
|
+
else
|
279
|
+
r4 = nil
|
280
|
+
end
|
281
|
+
if r4
|
282
|
+
r3 = r4
|
283
|
+
else
|
284
|
+
r5 = _nt_property_bracket
|
285
|
+
if r5
|
286
|
+
r3 = r5
|
287
|
+
else
|
288
|
+
self.index = i3
|
289
|
+
r3 = nil
|
290
|
+
end
|
291
|
+
end
|
292
|
+
if r3
|
293
|
+
s2 << r3
|
294
|
+
else
|
295
|
+
break
|
296
|
+
end
|
297
|
+
end
|
298
|
+
r2 = SyntaxNode.new(input, i2...index, s2)
|
299
|
+
s0 << r2
|
300
|
+
if r2
|
301
|
+
if input.index(']', index) == index
|
302
|
+
r6 = (SyntaxNode).new(input, index...(index + 1))
|
303
|
+
@index += 1
|
304
|
+
else
|
305
|
+
terminal_parse_failure(']')
|
306
|
+
r6 = nil
|
307
|
+
end
|
308
|
+
s0 << r6
|
309
|
+
end
|
310
|
+
end
|
311
|
+
if s0.last
|
312
|
+
r0 = (SyntaxNode).new(input, i0...index, s0)
|
313
|
+
r0.extend(PropertyBracket0)
|
314
|
+
else
|
315
|
+
self.index = i0
|
316
|
+
r0 = nil
|
317
|
+
end
|
318
|
+
|
319
|
+
node_cache[:property_bracket][start_index] = r0
|
320
|
+
|
321
|
+
return r0
|
322
|
+
end
|
323
|
+
|
324
|
+
module Property0
|
325
|
+
def sp
|
326
|
+
elements[1]
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def _nt_property
|
331
|
+
start_index = index
|
332
|
+
if node_cache[:property].has_key?(index)
|
333
|
+
cached = node_cache[:property][index]
|
334
|
+
@index = cached.interval.end if cached
|
335
|
+
return cached
|
336
|
+
end
|
337
|
+
|
338
|
+
i0 = index
|
339
|
+
s1, i1 = [], index
|
340
|
+
loop do
|
341
|
+
if input.index(Regexp.new('[A-Z]'), index) == index
|
342
|
+
r2 = (SyntaxNode).new(input, index...(index + 1))
|
343
|
+
@index += 1
|
344
|
+
else
|
345
|
+
r2 = nil
|
346
|
+
end
|
347
|
+
if r2
|
348
|
+
s1 << r2
|
349
|
+
else
|
350
|
+
break
|
351
|
+
end
|
352
|
+
end
|
353
|
+
if s1.empty?
|
354
|
+
self.index = i1
|
355
|
+
r1 = nil
|
356
|
+
else
|
357
|
+
r1 = SyntaxNode.new(input, i1...index, s1)
|
358
|
+
end
|
359
|
+
if r1
|
360
|
+
r0 = r1
|
361
|
+
else
|
362
|
+
i3, s3 = index, []
|
363
|
+
if input.index(Regexp.new('[A-Z]'), index) == index
|
364
|
+
r4 = (SyntaxNode).new(input, index...(index + 1))
|
365
|
+
@index += 1
|
366
|
+
else
|
367
|
+
r4 = nil
|
368
|
+
end
|
369
|
+
s3 << r4
|
370
|
+
if r4
|
371
|
+
r5 = _nt_sp
|
372
|
+
s3 << r5
|
373
|
+
end
|
374
|
+
if s3.last
|
375
|
+
r3 = (SyntaxNode).new(input, i3...index, s3)
|
376
|
+
r3.extend(Property0)
|
377
|
+
else
|
378
|
+
self.index = i3
|
379
|
+
r3 = nil
|
380
|
+
end
|
381
|
+
if r3
|
382
|
+
r0 = r3
|
383
|
+
else
|
384
|
+
self.index = i0
|
385
|
+
r0 = nil
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
node_cache[:property][start_index] = r0
|
390
|
+
|
391
|
+
return r0
|
392
|
+
end
|
393
|
+
|
394
|
+
def _nt_sp
|
395
|
+
start_index = index
|
396
|
+
if node_cache[:sp].has_key?(index)
|
397
|
+
cached = node_cache[:sp][index]
|
398
|
+
@index = cached.interval.end if cached
|
399
|
+
return cached
|
400
|
+
end
|
401
|
+
|
402
|
+
s0, i0 = [], index
|
403
|
+
loop do
|
404
|
+
if input.index(Regexp.new('[\\r\\n\\t ]'), index) == index
|
405
|
+
r1 = (SyntaxNode).new(input, index...(index + 1))
|
406
|
+
@index += 1
|
407
|
+
else
|
408
|
+
r1 = nil
|
409
|
+
end
|
410
|
+
if r1
|
411
|
+
s0 << r1
|
412
|
+
else
|
413
|
+
break
|
414
|
+
end
|
415
|
+
end
|
416
|
+
r0 = SyntaxNode.new(input, i0...index, s0)
|
417
|
+
|
418
|
+
node_cache[:sp][start_index] = r0
|
419
|
+
|
420
|
+
return r0
|
421
|
+
end
|
422
|
+
|
423
|
+
end
|
424
|
+
|
425
|
+
class SgfGrammarParser < Treetop::Runtime::CompiledParser
|
426
|
+
include SgfGrammar
|
427
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
grammar SgfGrammar
|
2
|
+
rule node
|
3
|
+
'(' sp chunk* sp ')' sp {
|
4
|
+
def value
|
5
|
+
get([elements[2]])
|
6
|
+
end
|
7
|
+
|
8
|
+
def get(e)
|
9
|
+
a = []
|
10
|
+
if !e.nil?
|
11
|
+
e.each do |el|
|
12
|
+
if el.respond_to?(:value)
|
13
|
+
a << el.value
|
14
|
+
else
|
15
|
+
a += get(el.elements) if !el.nil?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
a
|
20
|
+
end
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
rule chunk
|
25
|
+
';' sp property_set* sp {
|
26
|
+
def value
|
27
|
+
get([elements[2]])
|
28
|
+
end
|
29
|
+
|
30
|
+
def get(e)
|
31
|
+
a = []
|
32
|
+
if !e.nil?
|
33
|
+
e.each do |el|
|
34
|
+
if el.respond_to?(:value)
|
35
|
+
a << el.value
|
36
|
+
else
|
37
|
+
a += get(el.elements) if !el.nil?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
a
|
42
|
+
end
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
rule property_set
|
47
|
+
property property_bracket* sp {
|
48
|
+
def value
|
49
|
+
{
|
50
|
+
:property => elements[0].text_value,
|
51
|
+
:data => elements[1].text_value[1..-2]
|
52
|
+
}
|
53
|
+
end
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
rule property_bracket
|
58
|
+
'[' ( [^\[\]] / property_bracket )* ']'
|
59
|
+
end
|
60
|
+
|
61
|
+
rule property
|
62
|
+
[A-Z]+ / [A-Z] sp
|
63
|
+
end
|
64
|
+
|
65
|
+
rule sp
|
66
|
+
[\r\n\t ]*
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
dir = File.dirname(__FILE__)
|
2
|
+
require 'rubygems'
|
3
|
+
require 'treetop'
|
4
|
+
|
5
|
+
# Grammar
|
6
|
+
require "#{dir}/../grammar/sgf-grammar"
|
7
|
+
|
8
|
+
module KantanSgf
|
9
|
+
class Sgf
|
10
|
+
|
11
|
+
attr_accessor :move_list, :properties
|
12
|
+
|
13
|
+
def initialize(file)
|
14
|
+
@file = file
|
15
|
+
@data = nil
|
16
|
+
@properties = {}
|
17
|
+
|
18
|
+
@symbol_table = {}
|
19
|
+
a = 'a'
|
20
|
+
for i in 0..18
|
21
|
+
@symbol_table.store(a, i)
|
22
|
+
a = a.succ
|
23
|
+
end
|
24
|
+
|
25
|
+
@move_list = []
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse
|
29
|
+
f = File.open(@file)
|
30
|
+
@data = f.read
|
31
|
+
f.close
|
32
|
+
|
33
|
+
@sgf = SgfGrammarParser.new
|
34
|
+
results = @sgf.parse(@data)
|
35
|
+
raise "Parsing failed due to [#{@sgf.failure_reason}]." if results.nil?
|
36
|
+
|
37
|
+
# Pull the data out of the Treetop grammar parser
|
38
|
+
data = results.value
|
39
|
+
|
40
|
+
# Header info
|
41
|
+
header = data.shift
|
42
|
+
header.each do |chunk|
|
43
|
+
@properties.store(chunk[:property], chunk[:data])
|
44
|
+
end
|
45
|
+
# Footer info
|
46
|
+
footer = data.pop
|
47
|
+
footer.each do |chunk|
|
48
|
+
@properties.store(chunk[:property], chunk[:data])
|
49
|
+
end
|
50
|
+
# Moves
|
51
|
+
data.each do |chunk|
|
52
|
+
move = {}
|
53
|
+
chunk.each do |info|
|
54
|
+
case info[:property]
|
55
|
+
when 'B', 'W'
|
56
|
+
move[:color] = info[:property]
|
57
|
+
if !info[:data].empty?
|
58
|
+
move[:x] = @symbol_table[info[:data][0].chr]
|
59
|
+
move[:y] = @symbol_table[info[:data][1].chr]
|
60
|
+
end
|
61
|
+
when 'BL', 'WL'
|
62
|
+
move[:time] = info[:data]
|
63
|
+
when 'OB', 'OW'
|
64
|
+
move[:ot_stones] = info[:data]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
@move_list << move
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def player_black
|
72
|
+
return @properties["PB"]
|
73
|
+
end
|
74
|
+
|
75
|
+
def player_white
|
76
|
+
return @properties["PW"]
|
77
|
+
end
|
78
|
+
|
79
|
+
def rank_black
|
80
|
+
return @properties["BR"]
|
81
|
+
end
|
82
|
+
|
83
|
+
def rank_white
|
84
|
+
return @properties["WR"]
|
85
|
+
end
|
86
|
+
|
87
|
+
def board_size
|
88
|
+
return @properties.include?("SZ") ? @properties["SZ"].to_i : 19
|
89
|
+
end
|
90
|
+
|
91
|
+
def komi
|
92
|
+
return @properties["KM"]
|
93
|
+
end
|
94
|
+
|
95
|
+
def result
|
96
|
+
return @properties["RE"]
|
97
|
+
end
|
98
|
+
|
99
|
+
def handicap
|
100
|
+
return @properties["HA"]
|
101
|
+
end
|
102
|
+
|
103
|
+
def player_time
|
104
|
+
return @properties["TM"]
|
105
|
+
end
|
106
|
+
|
107
|
+
def game_date
|
108
|
+
return @properties["DT"]
|
109
|
+
end
|
110
|
+
|
111
|
+
def game_event
|
112
|
+
return @properties["EV"]
|
113
|
+
end
|
114
|
+
|
115
|
+
def game_round
|
116
|
+
return @properties["RO"]
|
117
|
+
end
|
118
|
+
|
119
|
+
def game_place
|
120
|
+
return @properties["PC"]
|
121
|
+
end
|
122
|
+
|
123
|
+
def game_rules
|
124
|
+
return @properties["RU"]
|
125
|
+
end
|
126
|
+
|
127
|
+
def game_name
|
128
|
+
return @properties["GN"]
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
data/lib/kantan-sgf.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
dir = File.dirname(__FILE__)
|
2
|
+
require 'rubygems'
|
3
|
+
require 'treetop'
|
4
|
+
require "test/unit"
|
5
|
+
|
6
|
+
# Grammar
|
7
|
+
Treetop.load "#{dir}/../lib/grammar/sgf-grammar.tt"
|
8
|
+
|
9
|
+
class SgfGrammarTest < Test::Unit::TestCase
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@sgf = SgfGrammarParser.new
|
13
|
+
f = File.open("../data/game-01.sgf")
|
14
|
+
@data = f.read
|
15
|
+
f.close
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_parse
|
19
|
+
result = @sgf.parse(@data)
|
20
|
+
#puts @sgf.failure_reason
|
21
|
+
#puts result.value.inspect
|
22
|
+
assert result
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kantan-sgf
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian bojo Jones
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-03-02 00:00:00 +09:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: a Simple Game Format (SGF) parser
|
17
|
+
email: mojobojo@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README
|
24
|
+
- LICENSE
|
25
|
+
files:
|
26
|
+
- data/game-01.sgf
|
27
|
+
- data/stoic-bojo.sgf
|
28
|
+
- examples/kantan_example.rb
|
29
|
+
- lib/grammar/sgf-grammar.rb
|
30
|
+
- lib/grammar/sgf-grammar.tt
|
31
|
+
- lib/kantan-sgf/sgf.rb
|
32
|
+
- lib/kantan-sgf/version.rb
|
33
|
+
- lib/kantan-sgf.rb
|
34
|
+
- LICENSE
|
35
|
+
- Rakefile
|
36
|
+
- README
|
37
|
+
- test/sgf-grammar_test.rb
|
38
|
+
has_rdoc: false
|
39
|
+
homepage: http://github.com/boj/kantan-sgf
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: "0"
|
56
|
+
version:
|
57
|
+
requirements: []
|
58
|
+
|
59
|
+
rubyforge_project:
|
60
|
+
rubygems_version: 1.0.1
|
61
|
+
signing_key:
|
62
|
+
specification_version: 2
|
63
|
+
summary: a Simple Game Format (SGF) parser
|
64
|
+
test_files: []
|
65
|
+
|