SgfParser 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.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
|