SgfParser 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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