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.
@@ -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 sgf
10
+ def initialize
9
11
  @root = Node.new
10
- @sgf = sgf
12
+ @current_node = @root
13
+ @errors = []
11
14
  end
12
15
 
13
- def each order=:preorder, &block
14
- # I know, I know. SGF is only preorder. Well, it's implemented, ain't it?
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
- # Saves the tree as an SGF file. raises an error if a filename is not given.
33
- # tree.save :filename => file_name
34
- def save args={}
35
- raise ArgumentError, "No file name provided" if args[:filename].nil?
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
- File.open(args[:filename], 'w') { |f| f << @savable_sgf }
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
- private
42
+ alias :inspect :to_s
44
43
 
45
- # Adds a stringified node to the variable @savable_sgf.
46
- def write_node node=@root
47
- @savable_sgf << ";"
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
- case node.children.size
59
- when 0
60
- @savable_sgf << ")"
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
- # Traverse the tree in preorder fashion, starting with the @root node if
72
- # no node is given, and activating the passed block on each.
73
- def preorder node=@root, &block
74
- # stop processing if the block returns false
75
- if yield node then
76
- node.each_child do |child|
77
- preorder(child, &block)
78
- end
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
 
@@ -0,0 +1,3 @@
1
+ module SGF
2
+ VERSION = "2.0.0"
3
+ end
@@ -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
@@ -0,0 +1,10 @@
1
+ (;FF[4]C[root]
2
+ (;C[a];C[b]
3
+ (;C[c])
4
+ (;C[d];C[e])
5
+ )
6
+ (;C[f]
7
+ (;C[g];C[h];C[i])
8
+ (;C[j])
9
+ )
10
+ )
@@ -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
@@ -1,6 +1,6 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
 
3
- describe "SgfParser::Node" do
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 properties to be added to" do
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
@@ -1,20 +1,117 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
 
3
- describe "SgfParser::Tree.parse" do
3
+ describe "SGF::Parser" do
4
4
 
5
- it "should have FF in the first node" do
6
- tree = SGF::Parser.new('spec/data/ff4_ex.sgf').parse
7
- tree.root.children[0].properties.keys.should include("FF")
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 FF is missing from the first node" do
11
- pending "To be coded later"
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 properly the AW property" do
15
- sgf = "dd][de][ef]"
16
- parser = SGF::Parser.new sgf
17
- parser.get_property.should == "[dd][de][ef]"
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
@@ -1,6 +1,30 @@
1
1
  $LOAD_PATH.unshift(File.dirname(__FILE__))
2
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
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