SgfParser 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +26 -0
- data/.rvmrc +1 -1
- data/Gemfile +1 -11
- data/Gemfile.lock +20 -17
- data/README.rdoc +9 -35
- data/Rakefile +105 -44
- data/SgfParser.gemspec +14 -59
- data/bin/sgf_indent.rb +13 -0
- data/examples/simple_iteration_of_games_in_a_directory.rb +11 -0
- data/lib/sgf.rb +4 -2
- data/lib/sgf/error.rb +13 -0
- data/lib/sgf/game.rb +64 -0
- data/lib/sgf/node.rb +58 -11
- data/lib/sgf/parser.rb +121 -74
- data/lib/sgf/properties.rb +81 -88
- data/lib/sgf/tree.rb +39 -53
- data/lib/sgf/version.rb +3 -0
- data/lib/sgf/writer.rb +54 -0
- data/spec/data/example1.sgf +10 -0
- data/spec/game_spec.rb +70 -0
- data/spec/node_spec.rb +28 -2
- data/spec/parser_spec.rb +107 -10
- data/spec/spec_helper.rb +25 -1
- data/spec/tree_spec.rb +25 -18
- data/spec/writer_spec.rb +97 -0
- metadata +43 -28
- data/VERSION +0 -1
- data/lib/sgf/indenter.rb +0 -108
- data/sample_usage/parsing_files.rb +0 -19
- data/spec/data/ff4_ex_saved.sgf +0 -45
- data/spec/data/simple_saved.sgf +0 -7
data/lib/sgf/tree.rb
CHANGED
@@ -1,22 +1,20 @@
|
|
1
1
|
module SGF
|
2
2
|
|
3
|
+
#Tree holds most of the logic, for now. It has all the nodes, can iterate over them, and can even save to a file!
|
4
|
+
#Somehow this feels like it should be split into another class or two...
|
3
5
|
class Tree
|
4
6
|
include Enumerable
|
5
7
|
|
6
|
-
attr_accessor :root
|
8
|
+
attr_accessor :root, :current_node, :errors
|
7
9
|
|
8
|
-
def initialize
|
10
|
+
def initialize
|
9
11
|
@root = Node.new
|
10
|
-
@
|
12
|
+
@current_node = @root
|
13
|
+
@errors = []
|
11
14
|
end
|
12
15
|
|
13
|
-
def each
|
14
|
-
|
15
|
-
# Stop complaining.
|
16
|
-
case order
|
17
|
-
when :preorder
|
18
|
-
preorder @root, &block
|
19
|
-
end
|
16
|
+
def each
|
17
|
+
games.each { |game| game.each { |node| yield node } }
|
20
18
|
end
|
21
19
|
|
22
20
|
# Compares a tree to another tree, node by node.
|
@@ -29,54 +27,43 @@ module SGF
|
|
29
27
|
one == two
|
30
28
|
end
|
31
29
|
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
@savable_sgf = "("
|
37
|
-
@root.children.each { |child| write_node child }
|
38
|
-
@savable_sgf << ")"
|
30
|
+
#Returns an array of the Game objects in this tree.
|
31
|
+
def games
|
32
|
+
populate_game_array
|
33
|
+
end
|
39
34
|
|
40
|
-
|
35
|
+
def to_s
|
36
|
+
out = "#<SGF::Tree:#{self.object_id}, "
|
37
|
+
out << "#{games.count} Games, "
|
38
|
+
out << "#{node_count} Nodes"
|
39
|
+
out << ">"
|
41
40
|
end
|
42
41
|
|
43
|
-
|
42
|
+
alias :inspect :to_s
|
44
43
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
unless node.properties.empty?
|
49
|
-
properties = ""
|
50
|
-
node.properties.each do |identity, property|
|
51
|
-
new_property = property[1...-1]
|
52
|
-
new_property.gsub!("]", "\\]") if identity == "C"
|
53
|
-
properties += "#{identity.to_s}[#{new_property}]"
|
54
|
-
end
|
55
|
-
@savable_sgf << properties
|
56
|
-
end
|
44
|
+
def to_str
|
45
|
+
SGF::Writer.new.stringify_tree_from @root
|
46
|
+
end
|
57
47
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
when 1
|
62
|
-
write_node node.children[0]
|
63
|
-
else
|
64
|
-
node.each_child do |child|
|
65
|
-
@savable_sgf << "("
|
66
|
-
write_node child
|
67
|
-
end
|
68
|
-
end
|
48
|
+
# Saves the Tree as an SGF file. Takes a filename as argument.
|
49
|
+
def save filename
|
50
|
+
SGF::Writer.new.save(@root, filename)
|
69
51
|
end
|
70
52
|
|
71
|
-
|
72
|
-
|
73
|
-
def
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
53
|
+
private
|
54
|
+
|
55
|
+
def node_count
|
56
|
+
count = 0
|
57
|
+
games.each { |game| count += game.node_count }
|
58
|
+
count
|
59
|
+
end
|
60
|
+
|
61
|
+
def populate_game_array
|
62
|
+
games = []
|
63
|
+
@root.children.each do |first_node_of_gametree|
|
64
|
+
games << Game.new(first_node_of_gametree)
|
79
65
|
end
|
66
|
+
games
|
80
67
|
end
|
81
68
|
|
82
69
|
def method_missing method_name, *args
|
@@ -85,7 +72,6 @@ module SGF
|
|
85
72
|
output
|
86
73
|
end
|
87
74
|
|
88
|
-
end
|
89
|
-
|
90
|
-
end
|
75
|
+
end # Tree
|
76
|
+
end # SGF
|
91
77
|
|
data/lib/sgf/version.rb
ADDED
data/lib/sgf/writer.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module SGF
|
2
|
+
class Writer
|
3
|
+
|
4
|
+
def stringify_tree_from root_node
|
5
|
+
@indentation = 0
|
6
|
+
@sgf = ""
|
7
|
+
write_new_branch_from root_node
|
8
|
+
end
|
9
|
+
|
10
|
+
# Takes a node and a filename as arguments
|
11
|
+
def save(root_node, filename)
|
12
|
+
stringify_tree_from root_node
|
13
|
+
|
14
|
+
File.open(filename, 'w') { |f| f << @sgf }
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def write_tree_from node
|
20
|
+
@sgf << "\n" << node.to_str(@indentation)
|
21
|
+
decide_what_comes_after node
|
22
|
+
end
|
23
|
+
|
24
|
+
def decide_what_comes_after node
|
25
|
+
if node.children.size == 1
|
26
|
+
then write_tree_from node.children[0]
|
27
|
+
else write_new_branch_from node
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def write_new_branch_from node
|
32
|
+
node.each_child do |child_node|
|
33
|
+
open_branch
|
34
|
+
write_tree_from child_node
|
35
|
+
close_branch
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def open_branch
|
40
|
+
@sgf << "\n" << whitespace << "("
|
41
|
+
@indentation += 2
|
42
|
+
end
|
43
|
+
|
44
|
+
def close_branch
|
45
|
+
@indentation -= 2
|
46
|
+
@indentation = (@indentation < 0) ? 0 : @indentation
|
47
|
+
@sgf << "\n" << whitespace << ")"
|
48
|
+
end
|
49
|
+
|
50
|
+
def whitespace
|
51
|
+
" " * @indentation
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/spec/game_spec.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "SGF::Game" do
|
4
|
+
|
5
|
+
it "should hold the first node of the game" do
|
6
|
+
game = get_first_game_from 'spec/data/ff4_ex.sgf'
|
7
|
+
game.current_node["FF"].should == "4"
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should throw up if initialized with a non-Node argument" do
|
11
|
+
expect { SGF::Game.new("I am a string") }.to raise_error(ArgumentError)
|
12
|
+
expect { SGF::Game.new(SGF::Node.new) }.to_not raise_error(ArgumentError)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should have the expected game-level information" do
|
16
|
+
game = get_first_game_from 'spec/data/ff4_ex.sgf'
|
17
|
+
game.name.should == "Gametree 1: properties"
|
18
|
+
game.data_entry.should == "Arno Hollosi"
|
19
|
+
expect { game.opening }.to raise_error(SGF::NoIdentityError)
|
20
|
+
expect { game.nonexistent_identity }.to raise_error(NoMethodError)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should give you a minimum of useful information on inspect" do
|
24
|
+
game = get_first_game_from 'spec/data/simple.sgf'
|
25
|
+
inspect = game.inspect
|
26
|
+
inspect.should match /SGF::Game/
|
27
|
+
inspect.should match /#{game.object_id}/
|
28
|
+
end
|
29
|
+
|
30
|
+
context "When talking about nodes" do
|
31
|
+
|
32
|
+
it "should have 'root' as the default current node" do
|
33
|
+
game = get_first_game_from 'spec/data/ff4_ex.sgf'
|
34
|
+
game.current_node.should == game.root
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should have a nice way to go to children[0]" do
|
38
|
+
game = get_first_game_from 'spec/data/ff4_ex.sgf'
|
39
|
+
game.next_node
|
40
|
+
game.current_node.should == game.root.children[0]
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should have a way of setting an arbitrary node to the current node" do
|
44
|
+
game = get_first_game_from 'spec/data/ff4_ex.sgf'
|
45
|
+
game.current_node = game.root.children[3]
|
46
|
+
game.current_node.properties.keys.sort.should == ["B", "C", "N"]
|
47
|
+
game.current_node.children.size.should == 6
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should use preorder traversal for each" do
|
53
|
+
game = get_first_game_from 'spec/data/example1.sgf'
|
54
|
+
array = []
|
55
|
+
game.each { |node| array << node }
|
56
|
+
array[0].c.should == "root"
|
57
|
+
array[1].c.should == "a"
|
58
|
+
array[2].c.should == "b"
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should go through all nodes, even if block returns 'nil' (puts, anyone?)" do
|
62
|
+
root = SGF::Node.new :properties => {"FF" => "4", "PB" => "Me", "PW" => "You"}
|
63
|
+
game = SGF::Game.new root
|
64
|
+
root.add_children SGF::Node.new(:properties => {"B" => "dd"})
|
65
|
+
nodes = []
|
66
|
+
game.each { |node| nodes << node; nil }
|
67
|
+
nodes.size.should == 2
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
data/spec/node_spec.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
2
|
|
3
|
-
describe "
|
3
|
+
describe "SGF::Node" do
|
4
4
|
|
5
5
|
before :each do
|
6
6
|
@node = SGF::Node.new
|
@@ -40,7 +40,7 @@ describe "SgfParser::Node" do
|
|
40
40
|
@node.children.each { |child| child.parent.should == @node }
|
41
41
|
end
|
42
42
|
|
43
|
-
it "should allow
|
43
|
+
it "should allow concatenation of properties" do
|
44
44
|
@node.add_properties "TC" => "Hello,"
|
45
45
|
@node.add_properties "TC" => " world!"
|
46
46
|
@node.properties["TC"].should == "Hello, world!"
|
@@ -61,4 +61,30 @@ describe "SgfParser::Node" do
|
|
61
61
|
@node.re.should == "And that too"
|
62
62
|
end
|
63
63
|
|
64
|
+
it "should implement [] as a shortcut to read properties" do
|
65
|
+
@node.add_properties "PB" => "Dosaku"
|
66
|
+
@node["PB"].should == "Dosaku"
|
67
|
+
@node[:PB].should == "Dosaku"
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should give you a relatively useful inspect" do
|
71
|
+
@node.inspect.should match /#{@node.object_id}/
|
72
|
+
@node.inspect.should match /SGF::Node/
|
73
|
+
|
74
|
+
@node.add_properties({"C" => "Oh hi", "PB" => "Dosaku", "AE" => "[dd][gh]"})
|
75
|
+
@node.inspect.should match /3 Properties/
|
76
|
+
|
77
|
+
@node.add_children SGF::Node.new, SGF::Node.new
|
78
|
+
@node.inspect.should match /2 Children/
|
79
|
+
|
80
|
+
@node.inspect.should match /Has no parent/
|
81
|
+
@node.parent = SGF::Node.new
|
82
|
+
@node.inspect.should match /Has a parent/
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should properly show a string version of the node" do
|
86
|
+
@node.add_properties({"C" => "Oh hi]", "PB" => "Dosaku"})
|
87
|
+
@node.to_str.should == ";C[Oh hi\\]]\nPB[Dosaku]"
|
88
|
+
end
|
89
|
+
|
64
90
|
end
|
data/spec/parser_spec.rb
CHANGED
@@ -1,20 +1,117 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
2
|
|
3
|
-
describe "
|
3
|
+
describe "SGF::Parser" do
|
4
4
|
|
5
|
-
it "should
|
6
|
-
|
7
|
-
|
5
|
+
it "should give an error if the first two character are not (;" do
|
6
|
+
parser = sgf_parser
|
7
|
+
expect { parser.parse ';)' }.to raise_error SGF::MalformedDataError
|
8
8
|
end
|
9
9
|
|
10
|
-
it "should give an error if
|
11
|
-
|
10
|
+
it "should not give an error if it is told to sit down and shut up" do
|
11
|
+
parser = sgf_parser
|
12
|
+
expect { parser.parse ';)', false}.to_not raise_error
|
12
13
|
end
|
13
14
|
|
14
|
-
it "should parse
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
it "should parse a simple node" do
|
16
|
+
parser = sgf_parser
|
17
|
+
tree = parser.parse ";PW[5]", false
|
18
|
+
tree.root.children[0].pw.should == "5"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should parse a node with two properties" do
|
22
|
+
parser = sgf_parser
|
23
|
+
tree = parser.parse ";PB[Aldric]PW[Bob]", false
|
24
|
+
tree.root.children[0].pb.should == "Aldric"
|
25
|
+
tree.root.children[0].pw.should == "Bob"
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should parse two nodes with one property each" do
|
29
|
+
parser = sgf_parser
|
30
|
+
tree = parser.parse ";PB[Aldric];PW[Bob]", false
|
31
|
+
node = tree.root.children[0]
|
32
|
+
node.pb.should == "Aldric"
|
33
|
+
node.children[0].pw.should == "Bob"
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should parse a tree with a single branch" do
|
37
|
+
parser = sgf_parser
|
38
|
+
tree = parser.parse "(;FF[4]PW[Aldric]PB[Bob];B[qq])"
|
39
|
+
node = tree.root.children[0]
|
40
|
+
node.pb.should == "Bob"
|
41
|
+
node.pw.should == "Aldric"
|
42
|
+
node.children[0].b.should == "qq"
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should parse a tree with two branches" do
|
46
|
+
parser = sgf_parser
|
47
|
+
tree = parser.parse "(;FF[4](;C[main])(;C[branch]))"
|
48
|
+
node = tree.root.children[0]
|
49
|
+
node.ff.should == "4"
|
50
|
+
node.children.size.should == 2
|
51
|
+
node.children[0].c.should == "main"
|
52
|
+
node.children[1].c.should == "branch"
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should parse a comment with a ] inside" do
|
56
|
+
parser = sgf_parser
|
57
|
+
tree = parser.parse "(;C[Oh hi\\] there])"
|
58
|
+
tree.root.children[0].c.should == "Oh hi] there"
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should parse a multi-property identity well" do
|
62
|
+
parser = sgf_parser
|
63
|
+
tree = parser.parse "(;FF[4];AW[bd][cc][qr])"
|
64
|
+
tree.root.children[0].children[0].aw.should == ["bd", "cc", "qr"]
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should parse multiple trees" do
|
68
|
+
parser = sgf_parser
|
69
|
+
tree = parser.parse "(;FF[4]PW[Aldric];AW[dd][cc])(;FF[4]PW[Hi];PB[ad])"
|
70
|
+
tree.root.children.size.should == 2
|
71
|
+
tree.root.children[0].children[0].aw.should == ["dd", "cc"]
|
72
|
+
tree.root.children[1].children[0].pb.should == "ad"
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should not put (; into the identity when separated by line breaks" do
|
76
|
+
parser = sgf_parser
|
77
|
+
tree = parser.parse "(;FF[4]\n\n(;B[dd])(;B[da]))"
|
78
|
+
game = tree.root.children[0]
|
79
|
+
game.children.size.should == 2
|
80
|
+
game.children[0].b.should == "dd"
|
81
|
+
game.children[1].b.should == "da"
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should parse the simplified sample SGF" do
|
85
|
+
parser = sgf_parser
|
86
|
+
tree = parser.parse SIMPLIFIED_SAMPLE_SGF
|
87
|
+
root = tree.root
|
88
|
+
root.children.size.should == 2
|
89
|
+
node = root.children[0].children[0]
|
90
|
+
node.ar.should == ["aa:sc", "sa:ac", "aa:sa", "aa:ac", "cd:cj", "gd:md", "fh:ij", "kj:nh"]
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should parse a file if given a file handler as input" do
|
94
|
+
parser = sgf_parser
|
95
|
+
file = File.open 'spec/data/simple.sgf'
|
96
|
+
tree = parser.parse file
|
97
|
+
game = tree.games.first
|
98
|
+
game.white_player.should == "redrose"
|
99
|
+
game.black_player.should == "tartrate"
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should parse a file if given a local path as input" do
|
103
|
+
parser = sgf_parser
|
104
|
+
local_path = 'spec/data/simple.sgf'
|
105
|
+
tree = parser.parse local_path
|
106
|
+
game = tree.games.first
|
107
|
+
game.white_player.should == "redrose"
|
108
|
+
game.black_player.should == "tartrate"
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def sgf_parser
|
114
|
+
SGF::Parser.new
|
18
115
|
end
|
19
116
|
|
20
117
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,6 +1,30 @@
|
|
1
1
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
-
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
|
3
3
|
require 'sgf'
|
4
|
+
require 'fileutils'
|
4
5
|
require 'rspec'
|
5
6
|
require 'rspec/autorun'
|
6
7
|
|
8
|
+
ONE_LINE_SIMPLE_SAMPLE_SGF= "(;FF[4]AP[Primiview:3.1]GM[1]SZ[19](;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]))(;FF[4]AP[Primiview:3.1]GM[1]SZ[19])"
|
9
|
+
|
10
|
+
SIMPLIFIED_SAMPLE_SGF= <<EOF
|
11
|
+
(;FF[4]AP[Primiview:3.1]GM[1]SZ[19]
|
12
|
+
(;DD[kq:os][dq:hs]
|
13
|
+
AR[aa:sc][sa:ac][aa:sa][aa:ac][cd:cj]
|
14
|
+
[gd:md][fh:ij][kj:nh]
|
15
|
+
LN[pj:pd][nf:ff][ih:fj][kh:nj]
|
16
|
+
C[Arrows, lines and dimmed points.])
|
17
|
+
(;B[qd]N[Style & text type])
|
18
|
+
)
|
19
|
+
(;FF[4]AP[Primiview:3.1]GM[1]SZ[19])
|
20
|
+
EOF
|
21
|
+
|
22
|
+
def get_first_game_from file
|
23
|
+
tree = get_tree_from file
|
24
|
+
tree.games.first
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_tree_from file
|
28
|
+
parser = SGF::Parser.new
|
29
|
+
parser.parse File.read(file)
|
30
|
+
end
|