├── .gitignore
├── .jshintrc
├── .travis.yml
├── LICENSE
├── README.md
├── example
├── example.js
├── output
│ ├── example.sgf
│ └── simple-example.sgf
└── sgf
│ ├── example.sgf
│ └── simple_example.sgf
├── index.js
├── package-lock.json
├── package.json
└── test
├── test-load.js
└── test-methods.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Dependency directory
23 | # Deployed apps should consider commenting this line out:
24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
25 | node_modules
26 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "bitwise": true,
3 | "camelcase": true,
4 | "curly": true,
5 | "eqeqeq": true,
6 | "forin": true,
7 | "immed": true,
8 | "indent": true,
9 | "latedef": true,
10 | "newcap": true,
11 | "noarg": true,
12 | "noempty": true,
13 | "nonew": true,
14 | "plusplus": true,
15 | "quotmark": "single",
16 | "undef": true,
17 | "unused": true,
18 | "strict": true,
19 | "trailing": true,
20 | "maxparams": 4,
21 | "maxdepth": false,
22 | "maxstatements": false,
23 | "maxcomplexity": false,
24 | "maxlen": 120,
25 | "asi": false,
26 | "boss": false,
27 | "debug": false,
28 | "eqnull": true,
29 | "es5": false,
30 | "esnext": false,
31 | "evil": false,
32 | "expr": false,
33 | "funcscope": false,
34 | "globalstrict": false,
35 | "iterator": false,
36 | "lastsemic": false,
37 | "laxbreak": false,
38 | "laxcomma": false,
39 | "loopfunc": false,
40 | "multistr": false,
41 | "proto": false,
42 | "scripturl": false,
43 | "smarttabs": false,
44 | "shadow": false,
45 | "sub": false,
46 | "supernew": false,
47 | "validthis": false,
48 | "nomen": true,
49 | "onevar": false,
50 | "passfail": false,
51 | "white": true,
52 | "browser": false,
53 | "node": true
54 | }
55 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '0.10'
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Nate Eagle
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 all
13 | 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 THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # smartgame [](https://travis-ci.org/neagle/smartgame)
2 |
3 | A node library for parsing [SGF format game records](http://www.red-bean.com/sgf/index.html) into JavaScript and back again. Then use [smartgamer](https://www.npmjs.com/package/smartgamer) to navigate and manipulate those game records.
4 |
5 | Installation
6 | ============
7 |
8 | For most projects, you'll just want to install smartgame locally and add it to your project's dependencies in `package.json`:
9 |
10 | ```
11 | $ npm install --save smartgame
12 | ```
13 |
14 | If you want (for whatever reason) to use smartgame anywhere, you can install it globally.
15 |
16 | ```
17 | $ npm install -g smartgame
18 | ```
19 |
20 | Usage
21 | =====
22 |
23 | var sgf = require('smartgame');
24 | var fs = require('fs');
25 |
26 | // Grab an SGF file from somewhere
27 | var example = fs.readFileSync('sgf/example.sgf', { encoding: 'utf8' });
28 |
29 | var collection = sgf.parse(example);
30 |
31 | // ... use the collection object however you want!
32 |
33 | // ... when collection has been modified and you want to save it as an .sgf file
34 | var collectionSGF = sgf.generate(collection);
35 | fs.writeFileSync('new-example.sgf', collectionSGF, { encoding: 'utf8' });
36 |
37 | Example JS Game Record
38 | ======================
39 |
40 | Let's take a very simple SGF file as an example:
41 |
42 | (;GM[1]FF[4]CA[UTF-8]SZ[19];B[pd];W[dp];B[pp](;W[dd])(;W[dc];B[ce];W[ed](;B[ch];W[jc])(;B[ci])))
43 |
44 | The parse function would turn this into a JS Object that looked like this:
45 |
46 | {
47 | gameTrees: [
48 | {
49 | parent: ,
50 | nodes: [
51 | { GM: '1', FF: '4', CA: 'UTF-8', SZ: '19' },
52 | { B: 'pd' },
53 | { W: 'dp' },
54 | { B: 'pp' }
55 | ],
56 | sequences: [
57 | {
58 | parent: ,
59 | nodes: [
60 | { W: 'dd' }
61 | ]
62 | },
63 | {
64 | parent: ,
65 | nodes: [
66 | { W: 'dc' },
67 | { B: 'ce' },
68 | { W: 'ed' }
69 | ],
70 | sequences: [
71 | {
72 | parent: ,
73 | nodes: [
74 | { B: 'ch' },
75 | { W: 'jc' }
76 | ]
77 | },
78 | {
79 | parent: ,
80 | nodes: [
81 | { B: 'ci' }
82 | ]
83 | }
84 | ]
85 | }
86 | ]
87 | }
88 | ]
89 | }
90 |
91 | You'll still have to [read up a little bit on the way SGFs work](http://www.red-bean.com/sgf/index.html), but the structure is a simple and straightforward representation of the SGF in JS form.
92 |
93 | Want an easy way to navigate and manipulate that game? Check out [smartgamer](https://www.npmjs.com/package/smartgamer).
94 |
95 | License
96 | =======
97 |
98 | MIT
99 |
100 | History
101 | =======
102 |
103 | This parser began life as part of another project, but I thought it was useful enough to become its own module. Breaking it and the other components of that project out into individual modules has helped me improve the separation of concerns and make the project more approachable. It's the first NPM package I've written that's not a plugin for something else (ie, a Grunt plugin or Yeoman generator) and any criticisms or suggestions are welcome.
104 |
--------------------------------------------------------------------------------
/example/example.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A script with some example uses of smartgame.
3 | * It may or may not be hijacked on occasion to debug new issues.
4 | * I'll never tell.
5 | */
6 |
7 | var sgf = require('..');
8 | var fs = require('fs');
9 | var util = require('util');
10 |
11 | // Grab example SGF files
12 | var simpleExample = fs.readFileSync('sgf/simple_example.sgf', { encoding: 'utf8' });
13 | var example = fs.readFileSync('sgf/example.sgf', { encoding: 'utf8' });
14 |
15 | // Parse them into JS Game Records
16 | var parsedSimpleExample = sgf.parse(simpleExample);
17 | var parsedExample = sgf.parse(example);
18 |
19 | // Show our JS Game Records
20 | console.log('A simple example:', util.inspect(parsedSimpleExample, false, null));
21 | console.log('An official example:', util.inspect(parsedExample, false, null));
22 |
23 | // Turn JS Game Records into SGF files
24 | //var simpleExampleSGF = sgf.generate(parsedSimpleExample);
25 | //fs.writeFileSync('output/simple-example.sgf', simpleExampleSGF, { encoding: 'utf8' });
26 |
27 | //var exampleSGF = sgf.generate(parsedExample);
28 | //fs.writeFileSync('output/example.sgf', exampleSGF, { encoding: 'utf8' });
29 |
--------------------------------------------------------------------------------
/example/output/example.sgf:
--------------------------------------------------------------------------------
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]GW[1]C[Marked as "Good for White"];B[pp]GB[2]C[Marked as "Very good for Black"];W[dc]GW[2]C[Marked as "Very good for White"];B[pj]DM[1]C[Marked as "Even position"];W[ci]UC[1]C[Marked as "Unclear position"];B[jd]TE[1]C[Marked as "Tesuji" or "Good move"];W[jp]BM[2]C[Marked as "Very bad move"];B[gd]DO[]C[Marked as "Doubtful move"];W[de]IT[]C[Marked as "Interesting move"];B[jj];C[White "Pass" move]W[];C[Black "Pass" move]B[tt])(;AB[dd][de][df][dg][do:gq]AW[jd][je][jf][jg][kn:lq][pn:pq]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.];AE[ep][fp][kn][lo][lq][pn:pq]C[AddEmpty
4 |
5 | Black stones & stones of left white group are erased in FF3 way.
6 |
7 | White stones at bottom right were erased using compressed point list.];AB[pd]AW[pp]PL[B]C[Added two stones.
8 |
9 | Node marked with "Black to play".];PL[W]C[Node marked with "White to play"])(;AB[dd][de][df][dg][dh][di][dj][nj][ni][nh][nf][ne][nd][ij][ii][ih][hq][gq][fq][eq][dr][ds][dq][dp][cp][bp][ap][iq][ir][is][bo][bn][an][ms][mr]AW[pd][pe][pf][pg][ph][pi][pj][fd][fe][ff][fh][fi][fj][kh][ki][kj][os][or][oq][op][pp][qp][rp][sp][ro][rn][sn][nq][mq][lq][kq][kr][ks][fs][gs][gr][er]N[Markup]C[Position set up without compressed point lists.];TR[dd][de][df][ed][ee][ef][fd:ff]MA[dh][di][dj][ej][ei][eh][fh:fj]CR[nd][ne][nf][od][oe][of][pd:pf]SQ[nh][ni][nj][oh][oi][oj][ph:pj]SL[ih][ii][ij][jj][ji][jh][kh:kj]TW[pq:ss][so][lr:ns]TB[aq:cs][er:hs][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)];LB[dc:1][fc:2][nc:3][pc:4][dj:a][fj:b][nj:c][pj:d][gs:ABCDEFGH][gr:ABCDEFG][gq:ABCDEF][gp:ABCDE][go:ABCD][gn:ABC][gm:AB][mm:12][mn:123][mo:1234][mp:12345][mq:123456][mr:1234567][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.];DD[kq:os][dq:hs]AR[aa:sc][sa:ac][aa:sa][aa:ac][cd:cj][gd:md][fh:ij][kj:nh]LN[pj:pd][nf:ff][ih:fj][kh:nj]C[Arrows, lines and dimmed points.])(;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 "\\n": >o\
30 | k<
31 | linebreak 2 "\\n\\r": >o\
32 |
k<
33 | linebreak 3 "\\r\\n": >o\
34 | k<
35 | linebreak 4 "\\r": >o\
k<](;W[dd]N[W d16]C[Variation C is better.](;B[pp]N[B q4])(;B[dp]N[B d4])(;B[pq]N[B q3])(;B[oq]N[B p3]))(;W[dp]N[W d4])(;W[pp]N[W q4])(;W[cc]N[W c17])(;W[cq]N[W c3])(;W[qq]N[W r3]))(;B[qr]N[Time limits, captures & move numbers]BL[120.0]C[Black time left: 120 sec];W[rr]WL[300]C[White time left: 300 sec];B[rq]BL[105.6]OB[10]C[Black time left: 105.6 sec
36 | Black stones left (in this byo-yomi period): 10];W[qq]WL[200]OW[2]C[White time left: 200 sec
37 | White stones left: 2];B[sr]BL[87.00]OB[9]C[Black time left: 87 sec
38 | Black stones left: 9];W[qs]WL[13.20]OW[1]C[White time left: 13.2 sec
39 | White stones left: 1];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];B[qs]C[Suicide move
41 | (all B stones get captured)]))(;FF[4]AP[Primiview:3.1]GM[1]SZ[19]C[Gametree 2: game-info
42 |
43 | Game-info properties are usually stored in the root node.
44 | If games are merged into a single game-tree, they are stored in the node\
45 | where the game first becomes distinguishable from all other games in\
46 | the tree.];B[pd](;PW[W. Hite]WR[6d]RO[2]RE[W+3.5]PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[dp]C[Game-info:
47 | Black: B. Lack, 5d
48 | White: W. Hite, 6d
49 | Place: London
50 | Event: Go Congress
51 | Round: 2
52 | Result: White wins by 3.5])(;PW[T. Suji]WR[7d]RO[1]RE[W+Resign]PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[cp]C[Game-info:
53 | Black: B. Lack, 5d
54 | White: T. Suji, 7d
55 | Place: London
56 | Event: Go Congress
57 | Round: 1
58 | Result: White wins by resignation])(;W[ep];B[pp](;PW[S. Abaki]WR[1d]RO[3]RE[B+63.5]PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[ed]C[Game-info:
59 | Black: B. Lack, 5d
60 | White: S. Abaki, 1d
61 | Place: London
62 | Event: Go Congress
63 | Round: 3
64 | Result: Balck wins by 63.5])(;PW[A. Tari]WR[12k]KM[-59.5]RO[4]RE[B+R]PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[cd]C[Game-info:
65 | Black: B. Lack, 5d
66 | White: A. Tari, 12k
67 | Place: London
68 | Event: Go Congress
69 | Round: 4
70 | Komi: -59.5 points
71 | Result: Black wins by resignation])))
--------------------------------------------------------------------------------
/example/output/simple-example.sgf:
--------------------------------------------------------------------------------
1 | (;GM[1]FF[4]CA[UTF-8]SZ[19];B[pd];W[dp];B[pp](;W[dd])(;W[dc];B[ce];W[ed](;B[ch];W[jc])(;B[ci])))
--------------------------------------------------------------------------------
/example/sgf/example.sgf:
--------------------------------------------------------------------------------
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 |
k<
83 | linebreak 3 "\\r\\n": >o\
84 | k<
85 | linebreak 4 "\\r": >o\
k<]
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 | ))
166 |
--------------------------------------------------------------------------------
/example/sgf/simple_example.sgf:
--------------------------------------------------------------------------------
1 | (
2 | ;GM[1]FF[4]CA[UTF-8]SZ[19]
3 | ;B[pd]
4 | ;W[dp]
5 | ;B[pp]
6 | (
7 | ;W[dd]
8 | )
9 | (
10 | ;W[dc]
11 | ;B[ce]
12 | ;W[ed]
13 | (
14 | ;B[ch]
15 | ;W[jc]
16 | )
17 | (
18 | ;B[ci]
19 | )
20 | )
21 | )
22 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Convert SGF files to a JS object
3 | * @param {string} sgf A valid SGF file.
4 | * @see http://www.red-bean.com/sgf/sgf4.html
5 | * @return {object} The SGF file represented as a JS object
6 | */
7 | exports.parse = function (sgf) {
8 | 'use strict';
9 |
10 | var parse;
11 | var parser;
12 | var collection = {};
13 |
14 | // tracks the current sequence
15 | var sequence;
16 |
17 | // tracks the current node
18 | var node;
19 |
20 | // tracks the last PropIdent
21 | var lastPropIdent;
22 |
23 | // A map of functions to parse the different components of an SGF file
24 | parser = {
25 |
26 | beginSequence: function (sgf) {
27 | var key = 'sequences';
28 |
29 | // Top-level sequences are gameTrees
30 | if (!sequence) {
31 | sequence = collection;
32 | key = 'gameTrees';
33 | }
34 |
35 | if (sequence.gameTrees) {
36 | key = 'gameTrees';
37 | }
38 |
39 | var newSequence = {
40 | parent: sequence
41 | };
42 |
43 | sequence[key] = sequence[key] || [];
44 | sequence[key].push(newSequence);
45 | sequence = newSequence;
46 |
47 | return sgf.substring(1);
48 | },
49 |
50 | endSequence: function (sgf) {
51 | if (sequence.parent) {
52 | sequence = sequence.parent;
53 | } else {
54 | sequence = null;
55 | }
56 | return sgf.substring(1);
57 | },
58 |
59 | node: function (sgf) {
60 | node = {};
61 | sequence.nodes = sequence.nodes || [];
62 | sequence.nodes.push(node);
63 | return sgf.substring(1);
64 | },
65 |
66 | property: function (sgf) {
67 | var propValue;
68 |
69 | // Search for the first unescaped ]
70 | var firstPropEnd = sgf.match(/([^\\\]]|\\(.|\n|\r))*\]/);
71 |
72 | if (!firstPropEnd.length) {
73 | throw new Error('malformed sgf');
74 | }
75 |
76 | firstPropEnd = firstPropEnd[0].length;
77 |
78 | var property = sgf.substring(0, firstPropEnd);
79 | var propValueBegin = property.indexOf('[');
80 | var propIdent = property.substring(0, propValueBegin);
81 |
82 | // Point lists don't declare a PropIdent for each PropValue
83 | // Instead, they should use the last declared property
84 | // See: http://www.red-bean.com/sgf/sgf4.html#move/pos
85 | if (!propIdent) {
86 | propIdent = lastPropIdent;
87 |
88 | // If this is the first property in a list of multiple
89 | // properties, we need to wrap the PropValue in an array
90 | if (!Array.isArray(node[propIdent])) {
91 | node[propIdent] = [node[propIdent]];
92 | }
93 | }
94 |
95 | lastPropIdent = propIdent;
96 |
97 | propValue = property.substring(propValueBegin + 1, property.length - 1);
98 |
99 | // We have no problem parsing PropIdents of any length, but the spec
100 | // says they should be no longer than two characters.
101 | //
102 | // http://www.red-bean.com/sgf/sgf4.html#2.2
103 | if (propIdent.length > 2) {
104 | // TODO: What's the best way to issue a warning?
105 | console.warn(
106 | 'SGF PropIdents should be no longer than two characters:', propIdent
107 | );
108 | }
109 |
110 | if (Array.isArray(node[propIdent])) {
111 | node[propIdent].push(propValue);
112 | } else {
113 | node[propIdent] = propValue;
114 | }
115 |
116 | return sgf.substring(firstPropEnd);
117 | },
118 |
119 | // Whitespace, tabs, or anything else we don't recognize
120 | unrecognized: function (sgf) {
121 |
122 | // March ahead to the next character
123 | return sgf.substring(1);
124 | }
125 | };
126 |
127 | // Processes an SGF file character by character
128 | parse = function (sgf) {
129 | while (sgf) {
130 | var initial = sgf.substring(0, 1);
131 | var type;
132 |
133 | // Use the initial (the first character in the remaining sgf file) to
134 | // decide which parser function to use
135 | if (initial === '(') {
136 | type = 'beginSequence';
137 | } else if (initial === ')') {
138 | type = 'endSequence';
139 | } else if (initial === ';') {
140 | type = 'node';
141 | } else if (initial.search(/[A-Z\[]/) !== -1) {
142 | type = 'property';
143 | } else {
144 | type = 'unrecognized';
145 | }
146 |
147 | sgf = parser[type](sgf);
148 | }
149 |
150 | return collection;
151 | };
152 |
153 | // Begin parsing the SGF file
154 | return parse(sgf);
155 | };
156 |
157 | /**
158 | * Generate an SGF file from a SmartGame Record JavaScript Object
159 | * @param {object} record A record object.
160 | * @return {string} The record as a string suitable for saving as an SGF file
161 | */
162 | exports.generate = function (record) {
163 | 'use strict';
164 |
165 | function stringifySequences(sequences) {
166 | var contents = '';
167 |
168 | sequences.forEach(function (sequence) {
169 | contents += '(';
170 |
171 | // Parse all nodes in this sequence
172 | if (sequence.nodes) {
173 | sequence.nodes.forEach(function (node) {
174 | var nodeString = ';';
175 | for (var property in node) {
176 | if (node.hasOwnProperty(property)) {
177 | var prop = node[property];
178 | if (Array.isArray(prop)) {
179 | prop = prop.join('][');
180 | }
181 | nodeString += property + '[' + prop + ']';
182 | }
183 | }
184 | contents += nodeString;
185 | });
186 | }
187 |
188 | // Call the function we're in recursively for any child sequences
189 | if (sequence.sequences) {
190 | contents += stringifySequences(sequence.sequences);
191 | }
192 |
193 | contents += ')';
194 | });
195 |
196 | return contents;
197 | }
198 |
199 | return stringifySequences(record.gameTrees);
200 | };
201 |
202 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "smartgame",
3 | "version": "0.1.4",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "balanced-match": {
8 | "version": "1.0.0",
9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
10 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
11 | "dev": true
12 | },
13 | "brace-expansion": {
14 | "version": "1.1.8",
15 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
16 | "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
17 | "dev": true,
18 | "requires": {
19 | "balanced-match": "1.0.0",
20 | "concat-map": "0.0.1"
21 | }
22 | },
23 | "browser-stdout": {
24 | "version": "1.3.0",
25 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz",
26 | "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=",
27 | "dev": true
28 | },
29 | "commander": {
30 | "version": "2.9.0",
31 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
32 | "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=",
33 | "dev": true,
34 | "requires": {
35 | "graceful-readlink": "1.0.1"
36 | }
37 | },
38 | "concat-map": {
39 | "version": "0.0.1",
40 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
41 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
42 | "dev": true
43 | },
44 | "debug": {
45 | "version": "2.6.8",
46 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz",
47 | "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=",
48 | "dev": true,
49 | "requires": {
50 | "ms": "2.0.0"
51 | }
52 | },
53 | "diff": {
54 | "version": "3.2.0",
55 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz",
56 | "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=",
57 | "dev": true
58 | },
59 | "escape-string-regexp": {
60 | "version": "1.0.5",
61 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
62 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
63 | "dev": true
64 | },
65 | "fs.realpath": {
66 | "version": "1.0.0",
67 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
68 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
69 | "dev": true
70 | },
71 | "glob": {
72 | "version": "7.1.1",
73 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz",
74 | "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=",
75 | "dev": true,
76 | "requires": {
77 | "fs.realpath": "1.0.0",
78 | "inflight": "1.0.6",
79 | "inherits": "2.0.3",
80 | "minimatch": "3.0.4",
81 | "once": "1.4.0",
82 | "path-is-absolute": "1.0.1"
83 | }
84 | },
85 | "graceful-readlink": {
86 | "version": "1.0.1",
87 | "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
88 | "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=",
89 | "dev": true
90 | },
91 | "growl": {
92 | "version": "1.9.2",
93 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz",
94 | "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=",
95 | "dev": true
96 | },
97 | "has-flag": {
98 | "version": "1.0.0",
99 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
100 | "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
101 | "dev": true
102 | },
103 | "he": {
104 | "version": "1.1.1",
105 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
106 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
107 | "dev": true
108 | },
109 | "inflight": {
110 | "version": "1.0.6",
111 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
112 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
113 | "dev": true,
114 | "requires": {
115 | "once": "1.4.0",
116 | "wrappy": "1.0.2"
117 | }
118 | },
119 | "inherits": {
120 | "version": "2.0.3",
121 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
122 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
123 | "dev": true
124 | },
125 | "json3": {
126 | "version": "3.3.2",
127 | "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
128 | "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=",
129 | "dev": true
130 | },
131 | "lodash._baseassign": {
132 | "version": "3.2.0",
133 | "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz",
134 | "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=",
135 | "dev": true,
136 | "requires": {
137 | "lodash._basecopy": "3.0.1",
138 | "lodash.keys": "3.1.2"
139 | }
140 | },
141 | "lodash._basecopy": {
142 | "version": "3.0.1",
143 | "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
144 | "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=",
145 | "dev": true
146 | },
147 | "lodash._basecreate": {
148 | "version": "3.0.3",
149 | "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz",
150 | "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=",
151 | "dev": true
152 | },
153 | "lodash._getnative": {
154 | "version": "3.9.1",
155 | "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
156 | "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=",
157 | "dev": true
158 | },
159 | "lodash._isiterateecall": {
160 | "version": "3.0.9",
161 | "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz",
162 | "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=",
163 | "dev": true
164 | },
165 | "lodash.create": {
166 | "version": "3.1.1",
167 | "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz",
168 | "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=",
169 | "dev": true,
170 | "requires": {
171 | "lodash._baseassign": "3.2.0",
172 | "lodash._basecreate": "3.0.3",
173 | "lodash._isiterateecall": "3.0.9"
174 | }
175 | },
176 | "lodash.isarguments": {
177 | "version": "3.1.0",
178 | "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
179 | "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=",
180 | "dev": true
181 | },
182 | "lodash.isarray": {
183 | "version": "3.0.4",
184 | "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
185 | "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=",
186 | "dev": true
187 | },
188 | "lodash.keys": {
189 | "version": "3.1.2",
190 | "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
191 | "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
192 | "dev": true,
193 | "requires": {
194 | "lodash._getnative": "3.9.1",
195 | "lodash.isarguments": "3.1.0",
196 | "lodash.isarray": "3.0.4"
197 | }
198 | },
199 | "minimatch": {
200 | "version": "3.0.4",
201 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
202 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
203 | "dev": true,
204 | "requires": {
205 | "brace-expansion": "1.1.8"
206 | }
207 | },
208 | "minimist": {
209 | "version": "0.0.8",
210 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
211 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
212 | "dev": true
213 | },
214 | "mkdirp": {
215 | "version": "0.5.1",
216 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
217 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
218 | "dev": true,
219 | "requires": {
220 | "minimist": "0.0.8"
221 | }
222 | },
223 | "mocha": {
224 | "version": "3.5.3",
225 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz",
226 | "integrity": "sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==",
227 | "dev": true,
228 | "requires": {
229 | "browser-stdout": "1.3.0",
230 | "commander": "2.9.0",
231 | "debug": "2.6.8",
232 | "diff": "3.2.0",
233 | "escape-string-regexp": "1.0.5",
234 | "glob": "7.1.1",
235 | "growl": "1.9.2",
236 | "he": "1.1.1",
237 | "json3": "3.3.2",
238 | "lodash.create": "3.1.1",
239 | "mkdirp": "0.5.1",
240 | "supports-color": "3.1.2"
241 | }
242 | },
243 | "ms": {
244 | "version": "2.0.0",
245 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
246 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
247 | "dev": true
248 | },
249 | "once": {
250 | "version": "1.4.0",
251 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
252 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
253 | "dev": true,
254 | "requires": {
255 | "wrappy": "1.0.2"
256 | }
257 | },
258 | "path-is-absolute": {
259 | "version": "1.0.1",
260 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
261 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
262 | "dev": true
263 | },
264 | "supports-color": {
265 | "version": "3.1.2",
266 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz",
267 | "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=",
268 | "dev": true,
269 | "requires": {
270 | "has-flag": "1.0.0"
271 | }
272 | },
273 | "util": {
274 | "version": "0.10.3",
275 | "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
276 | "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
277 | "dev": true,
278 | "requires": {
279 | "inherits": "2.0.1"
280 | },
281 | "dependencies": {
282 | "inherits": {
283 | "version": "2.0.1",
284 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
285 | "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
286 | "dev": true
287 | }
288 | }
289 | },
290 | "wrappy": {
291 | "version": "1.0.2",
292 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
293 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
294 | "dev": true
295 | }
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "smartgame",
3 | "version": "0.1.5",
4 | "description": "Parse SGF (Smart Game Format) files into JavaScript and back again.",
5 | "license": "MIT",
6 | "main": "index.js",
7 | "repository": {
8 | "type": "git",
9 | "url": "git://github.com/neagle/smartgame.git"
10 | },
11 | "engines": {
12 | "node": ">=0.10.0"
13 | },
14 | "scripts": {
15 | "test": "mocha"
16 | },
17 | "keywords": [
18 | "smartgame",
19 | "sgf",
20 | "go",
21 | "weiqi",
22 | "baduk"
23 | ],
24 | "author": {
25 | "name": "neagle",
26 | "email": "nate@nateeagle.com",
27 | "url": "https://github.com/neagle"
28 | },
29 | "bugs": {
30 | "url": "https://github.com/neagle/smartgame/issues"
31 | },
32 | "homepage": "https://github.com/neagle/smartgame",
33 | "devDependencies": {
34 | "mocha": "*",
35 | "util": "^0.10.3"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/test/test-load.js:
--------------------------------------------------------------------------------
1 | /*global describe, beforeEach, it*/
2 | 'use strict';
3 | var assert = require('assert');
4 |
5 | describe('smartgame parser', function () {
6 | it('can be imported without blowing up', function () {
7 | var sgf = require('..');
8 | assert(sgf !== undefined);
9 | });
10 | });
11 |
12 | describe('parse', function () {
13 | it('provides the parse method', function () {
14 | var sgf = require('..');
15 | assert(typeof sgf.parse === 'function');
16 | });
17 | });
18 |
19 | describe('generate', function () {
20 | it('provides the generate method', function () {
21 | var sgf = require('..');
22 | assert(typeof sgf.generate === 'function');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/test/test-methods.js:
--------------------------------------------------------------------------------
1 | /*global describe, beforeEach, it*/
2 | 'use strict';
3 | var assert = require('assert');
4 |
5 | // Check if the output of a parsed and generated SGF file matches what went
6 | // into it
7 | describe('parse & generate', function () {
8 | it('generate sgf files after parsing that match the sgf files they started with', function () {
9 | var sgf = require('..'),
10 | fs = require('fs');
11 |
12 | /**
13 | * Strip whitespace outside of node values in an SGF file
14 | */
15 | function stripWhitespace(sgf) {
16 | var nodes = [];
17 | var placeholder = '##NODE##';
18 |
19 | function stripNodes(sgf) {
20 | var node = sgf.match(/\[[^\]]*\]/);
21 |
22 | if (node) {
23 | nodes.push(node[0]);
24 | sgf = sgf.replace(/\[[^\]]*\]/, placeholder);
25 | return stripNodes(sgf);
26 | } else {
27 | return sgf;
28 | }
29 | }
30 |
31 | function putNodesBack(sgf) {
32 | if (sgf.match(placeholder) && nodes.length) {
33 | sgf = sgf.replace(placeholder, nodes.shift());
34 | return putNodesBack(sgf);
35 | } else {
36 | return sgf;
37 | }
38 | }
39 |
40 | // Strip out escaped square brackets
41 | sgf = sgf.replace(/\\\]/g, '##ESCAPEDBRACKET##');
42 |
43 | sgf = stripNodes(sgf);
44 |
45 | // Strip all whitespace
46 | sgf = sgf.replace(/[\n\r\s]/g, '');
47 |
48 | sgf = putNodesBack(sgf);
49 |
50 | // Put escaped square brackets back in
51 | sgf = sgf.replace('##ESCAPEDBRACKET##', '\\]');
52 |
53 | return sgf;
54 | }
55 |
56 | var files = fs.readdirSync('example/sgf');
57 | files.forEach(function (file) {
58 | var sgfFile = fs.readFileSync('example/sgf/' + file, { encoding: 'utf-8' });
59 | assert(stripWhitespace(sgfFile) === sgf.generate(sgf.parse(sgfFile)));
60 | });
61 | });
62 | });
63 |
64 | describe('parse', function () {
65 | it('creates a collection object that has gameTrees', function () {
66 | var sgf = require('..'),
67 | fs = require('fs');
68 |
69 |
70 | var files = fs.readdirSync('example/sgf');
71 | files.forEach(function (file) {
72 | var sgfFile = fs.readFileSync('example/sgf/' + file, { encoding: 'utf-8' });
73 | assert(sgf.parse(sgfFile).gameTrees.length);
74 | });
75 | });
76 | });
77 |
--------------------------------------------------------------------------------