sgf_parser 0.1.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.
Files changed (53) hide show
  1. data/.irbrc +3 -0
  2. data/.rvmrc +4 -0
  3. data/Gemfile +10 -0
  4. data/Gemfile.lock +17 -0
  5. data/LICENSE +20 -0
  6. data/README.textile +27 -0
  7. data/Rakefile +20 -0
  8. data/TODO +0 -0
  9. data/VERSION +1 -0
  10. data/bin/sgf +28 -0
  11. data/bin/stm2dot +18 -0
  12. data/doc/sgf_state_machine.dot +58 -0
  13. data/doc/sgf_state_machine.svg +269 -0
  14. data/lib/sgf.rb +15 -0
  15. data/lib/sgf/binary_file_error.rb +4 -0
  16. data/lib/sgf/debugger.rb +15 -0
  17. data/lib/sgf/default_event_listener.rb +37 -0
  18. data/lib/sgf/model/constants.rb +19 -0
  19. data/lib/sgf/model/event_listener.rb +72 -0
  20. data/lib/sgf/model/game.rb +52 -0
  21. data/lib/sgf/model/label.rb +12 -0
  22. data/lib/sgf/model/node.rb +115 -0
  23. data/lib/sgf/model/property_handler.rb +51 -0
  24. data/lib/sgf/more/state_machine_presenter.rb +43 -0
  25. data/lib/sgf/more/stm_dot_converter.rb +139 -0
  26. data/lib/sgf/parse_error.rb +25 -0
  27. data/lib/sgf/parser.rb +56 -0
  28. data/lib/sgf/renderer.rb +25 -0
  29. data/lib/sgf/sgf_helper.rb +55 -0
  30. data/lib/sgf/sgf_state_machine.rb +174 -0
  31. data/lib/sgf/state_machine.rb +76 -0
  32. data/sgf_parser.gemspec +95 -0
  33. data/spec/fixtures/2009-11-01-1.sgf +24 -0
  34. data/spec/fixtures/2009-11-01-2.sgf +23 -0
  35. data/spec/fixtures/chinese_gb.sgf +9 -0
  36. data/spec/fixtures/chinese_utf.sgf +9 -0
  37. data/spec/fixtures/example.sgf +18 -0
  38. data/spec/fixtures/good.sgf +55 -0
  39. data/spec/fixtures/good1.sgf +167 -0
  40. data/spec/fixtures/kgs.sgf +723 -0
  41. data/spec/fixtures/test.png +0 -0
  42. data/spec/sgf/model/event_listener_spec.rb +97 -0
  43. data/spec/sgf/model/game_spec.rb +29 -0
  44. data/spec/sgf/model/node_spec.rb +84 -0
  45. data/spec/sgf/more/state_machine_presenter_spec.rb +29 -0
  46. data/spec/sgf/parse_error_spec.rb +10 -0
  47. data/spec/sgf/parser_spec.rb +210 -0
  48. data/spec/sgf/sgf_helper_spec.rb +68 -0
  49. data/spec/sgf/sgf_state_machine_spec.rb +166 -0
  50. data/spec/sgf/state_machine_spec.rb +137 -0
  51. data/spec/spec.opts +4 -0
  52. data/spec/spec_helper.rb +47 -0
  53. metadata +150 -0
Binary file
@@ -0,0 +1,97 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ module SGF
4
+ module Model
5
+ describe EventListener do
6
+ before :each do
7
+ @listener = EventListener.new
8
+ end
9
+
10
+ (DefaultEventListener.instance_methods - DefaultEventListener.superclass.instance_methods).each do |method|
11
+ it "should implement #{method} from SGF::DefaultEventListener" do
12
+ @listener.methods.should include(method)
13
+ end
14
+ end
15
+
16
+ it "should start a new game on start_game" do
17
+ @listener.start_game
18
+ game1 = @listener.game
19
+ @listener.start_game
20
+ game2 = @listener.game
21
+ game1.should_not == game2
22
+ end
23
+
24
+ it "start_variation should create a new node as variation root" do
25
+ @listener.start_game
26
+ @listener.start_node
27
+ node = @listener.node
28
+ @listener.start_variation
29
+ @listener.node.should_not == node
30
+ @listener.node.variation_root?.should be_true
31
+ end
32
+
33
+ it "end_variation should set current node as parent of current variation's root" do
34
+ @listener.start_game
35
+ @listener.start_node
36
+ node = @listener.node
37
+ @listener.start_variation
38
+ @listener.node.should_not == node
39
+ @listener.end_variation
40
+ @listener.node.should == node
41
+ end
42
+
43
+ SGF::Model::GAME_PROPERTY_MAPPINGS.each do |prop_name, attr_name|
44
+ it "call property_name=#{prop_name} and property_value=something should set #{attr_name} on game" do
45
+ mock(@listener.game).send(attr_name, '123')
46
+ @listener.property_name = prop_name
47
+ @listener.property_value = '123'
48
+ end
49
+ end
50
+
51
+ it "should set node as game's root node on first call to start_node" do
52
+ @listener.start_game
53
+ @listener.start_node
54
+ @listener.node.should == @listener.game.root_node
55
+ end
56
+
57
+ it "should create child node on current node when call start_node" do
58
+ @listener.start_game
59
+ @listener.start_node
60
+ node = @listener.node
61
+ @listener.start_node
62
+ @listener.node.should_not == node
63
+ @listener.node.parent.should == node
64
+ node.children.should == [@listener.node]
65
+ end
66
+
67
+ it "property_name='C' and property_value=something should add comment to node" do
68
+ @listener.start_game
69
+ @listener.start_node
70
+ @listener.property_name = "C"
71
+ @listener.property_value = "comment"
72
+ @listener.game.root_node.comment.should == 'comment'
73
+ end
74
+
75
+ it "game misc properties" do
76
+ @listener.start_game
77
+ @listener.start_node
78
+ @listener.property_name = "US"
79
+ @listener.property_value = "A"
80
+ @listener.game.misc_properties["US"].should == "A"
81
+ @listener.property_value = "B"
82
+ @listener.game.misc_properties["US"].should == ["A", "B"]
83
+ end
84
+
85
+ it "node misc properties" do
86
+ @listener.start_game
87
+ @listener.start_node
88
+ @listener.property_name = "TR"
89
+ @listener.property_value = "AB"
90
+ @listener.node.misc_properties["TR"].should == "AB"
91
+ @listener.property_value = "AC"
92
+ @listener.node.misc_properties["TR"].should == ["AB", "AC"]
93
+ end
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,29 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ module SGF
4
+ module Model
5
+ describe Game do
6
+ it "moves should return number of moves" do
7
+ game = Game.new
8
+ n1 = Node.new game.root_node
9
+ n1.sgf_play_black 'AB'
10
+ n2 = Node.new n1
11
+ n2.sgf_play_white 'BC'
12
+
13
+ game.moves.should == 2
14
+ end
15
+
16
+ it "moves should not count non-move nodes" do
17
+ game = Game.new
18
+ n1 = Node.new game.root_node
19
+ n1.sgf_play_black 'AB'
20
+ dummy1 = Node.new(n1)
21
+ n2 = Node.new dummy1
22
+ n2.sgf_play_white 'BC'
23
+ dummy2 = Node.new(n2)
24
+
25
+ game.moves.should == 2
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,84 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ module SGF
4
+ module Model
5
+ describe Node do
6
+ describe "move_no" do
7
+ it "should return parent node's move number + 1 if current node is a move" do
8
+ parent = Node.new
9
+ mock(parent).move_no{ 10 }
10
+ node = Node.new(parent)
11
+ node.sgf_play_black 'AB'
12
+ node.move_no.should == 11
13
+ end
14
+
15
+ it "should return parent node's move number if current node is not a move" do
16
+ parent = Node.new
17
+ mock(parent).move_no{ 10 }
18
+ node = Node.new(parent)
19
+ node.move_no.should == 10
20
+ end
21
+
22
+ it "should return 0 if parent is nil and current node is not a move" do
23
+ node = Node.new
24
+ node.move_no.should == 0
25
+ end
26
+
27
+ it "should return 1 if parent is nil and current node is a move" do
28
+ node = Node.new
29
+ node.sgf_play_black "AB"
30
+ node.move_no.should == 1
31
+ end
32
+ end
33
+
34
+ it "sgf_setup_black should add an entry to black_moves" do
35
+ node = Node.new
36
+ node.sgf_setup_black "AB"
37
+ node.black_moves.first.should == [0, 1]
38
+ end
39
+
40
+ it "sgf_setup_white should add an entry to white_moves" do
41
+ node = Node.new
42
+ node.sgf_setup_white "AB"
43
+ node.white_moves.first.should == [0, 1]
44
+ end
45
+
46
+ it "sgf_play_black should set node type to MOVE and save move information" do
47
+ node = Node.new
48
+ node.sgf_play_black "AB"
49
+ node.node_type.should == Constants::NODE_MOVE
50
+ node.color.should == Constants::BLACK
51
+ node.move.should == [0, 1]
52
+ end
53
+
54
+ it "sgf_play_black should set node type to PASS on empty move coordinate" do
55
+ node = Node.new
56
+ node.sgf_play_black " "
57
+ node.node_type.should == Constants::NODE_PASS
58
+ node.color.should == Constants::BLACK
59
+ end
60
+
61
+ it "sgf_play_white should set node type to MOVE and save move information" do
62
+ node = Node.new
63
+ node.sgf_play_white "AB"
64
+ node.node_type.should == Constants::NODE_MOVE
65
+ node.color.should == Constants::WHITE
66
+ node.move.should == [0, 1]
67
+ end
68
+
69
+ it "sgf_play_white should set node type to PASS on empty move coordinate" do
70
+ node = Node.new
71
+ node.sgf_play_white " "
72
+ node.node_type.should == Constants::NODE_PASS
73
+ node.color.should == Constants::WHITE
74
+ end
75
+
76
+ it "child returns first child" do
77
+ node = Node.new
78
+ first_child = Node.new(node)
79
+ second_child = Node.new(node)
80
+ node.child.should == first_child
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,29 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+ require File.expand_path(File.dirname(__FILE__) + '/../../../lib/sgf/more/state_machine_presenter')
3
+
4
+ describe SGF::More::StateMachinePresenter do
5
+ it "should create nodes for start state and all reachable states" do
6
+ stm = SGF::StateMachine.new(:start)
7
+ stm.transition(:start, /a/, :state_a)
8
+
9
+ presenter = SGF::More::StateMachinePresenter.new
10
+ presenter.process(stm)
11
+
12
+ presenter.nodes.should include("start")
13
+ presenter.nodes.should include("state_a")
14
+ end
15
+
16
+ it "should create edges for all transitions" do
17
+ stm = SGF::StateMachine.new(:start)
18
+ stm.desc (desc1 = "start + /a/ => state_a")
19
+ stm.transition(:start, /a/, :state_a)
20
+ stm.desc (desc2 = "start + /b/ => state_b")
21
+ stm.transition(:start, /b/, :state_b)
22
+
23
+ presenter = SGF::More::StateMachinePresenter.new
24
+ presenter.process(stm)
25
+
26
+ presenter.edges.should include(["start", "state_a", desc1])
27
+ presenter.edges.should include(["start", "state_b", desc2])
28
+ end
29
+ end
@@ -0,0 +1,10 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ module SGF
4
+ describe ParseError do
5
+ it "message should return content up to the bad input" do
6
+ error = ParseError.new("(;AB]", 4)
7
+ error.message.should include("(;AB] <=== SGF parse error occurred here")
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,210 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ module SGF
4
+ describe Parser, 'with DefaultEventListener' do
5
+ before :each do
6
+ @listener = DefaultEventListener.new
7
+ @parser = Parser.new @listener
8
+ end
9
+
10
+ {'nil' => nil, '' => '', ' ' => ' '}.each do |name, value|
11
+ it "should throw error on bad input - '#{name}'" do
12
+ begin
13
+ @parser.parse(value)
14
+ fail("Should raise error")
15
+ rescue => e
16
+ e.class.should == ArgumentError
17
+ end
18
+ end
19
+ end
20
+
21
+ it "should call start_game" do
22
+ mock(@listener).start_game
23
+ @parser.parse("(")
24
+ end
25
+
26
+ it "should call end_game" do
27
+ mock(@listener).end_game
28
+ @parser.parse("(;)")
29
+ end
30
+
31
+ it "should call start_node" do
32
+ mock(@listener).start_node
33
+ @parser.parse("(;")
34
+ end
35
+
36
+ it "should call end_variation" do
37
+ mock(@listener).end_variation
38
+ @parser.parse("(;)")
39
+ end
40
+
41
+ it "should call property_name= with name" do
42
+ mock(@listener).property_name = 'GN'
43
+ @parser.parse("(;GN[)")
44
+ end
45
+
46
+ it "should call property_value= with value" do
47
+ mock(@listener).property_value = 'a game'
48
+ @parser.parse("(;GN[a game])")
49
+ end
50
+
51
+ it "should call property_value= for each value" do
52
+ mock(@listener).property_value = 'DB'
53
+ mock(@listener).property_value = 'KS'
54
+ @parser.parse("(;AB[DB][KS])")
55
+ end
56
+
57
+ it "should call start_variation" do
58
+ mock(@listener).start_variation
59
+ @parser.parse("(;(")
60
+ end
61
+
62
+ it "should call end_variation" do
63
+ mock(@listener).end_variation
64
+ @parser.parse("(;(;)")
65
+ end
66
+ end
67
+
68
+ describe Parser, 'with SGF::Model::EventListener' do
69
+ before :each do
70
+ @listener = SGF::Model::EventListener.new
71
+ @parser = Parser.new @listener
72
+ end
73
+
74
+ it "should parse game in file" do
75
+ @parser.parse_file File.expand_path(File.dirname(__FILE__) + '/../fixtures/good.sgf')
76
+ game = @listener.game
77
+ game.name.should == 'White (W) vs. Black (B)'
78
+ game.rule.should == 'Japanese'
79
+ game.board_size.should == 19
80
+ game.handicap.should == 0
81
+ game.komi.should == 5.5
82
+ game.played_on.should == "1999-07-28"
83
+ game.white_player.should == 'White'
84
+ game.black_player.should == 'Black'
85
+ game.program.should == 'Cgoban 1.9.2'
86
+ game.time_rule.should == '30:00(5x1:00)'
87
+ end
88
+
89
+ it "should raise BinaryFileError on parsing binary file" do
90
+ pending 'check if file is binary is problematic'
91
+ lambda {
92
+ @parser.parse_file File.expand_path(File.dirname(__FILE__) + '/../fixtures/test.png')
93
+ }.should raise_error(SGF::BinaryFileError)
94
+ end
95
+
96
+ it "should parse game without moves" do
97
+ @parser.parse <<-INPUT
98
+ (;GM[1]FF[3]
99
+ GN[White (W) vs. Black (B)];
100
+ )
101
+ INPUT
102
+ game = @listener.game
103
+ game.name.should == 'White (W) vs. Black (B)'
104
+ end
105
+
106
+ it "should raise error on invalid input" do
107
+ lambda { @parser.parse("123") }.should raise_error
108
+ end
109
+
110
+ it "should parse a complete game" do
111
+ @parser.parse <<-INPUT
112
+ (;GM[1]FF[3]
113
+ RU[Japanese]SZ[19]HA[0]KM[5.5]
114
+ PW[White]
115
+ PB[Black]
116
+ GN[White (W) vs. Black (B)]
117
+ DT[1999-07-28]
118
+ RE[W+R]
119
+ SY[Cgoban 1.9.2]TM[30:00(5x1:00)];
120
+ AW[ea][eb][ec][bd][dd][ae][ce][de][cf][ef][cg][dg][eh][ci][di][bj][ej]
121
+ AB[da][db][cc][dc][cd][be][bf][ag][bg][bh][ch][dh]LB[bd:A]PL[2]
122
+ C[guff plays A and adum tenukis to fill a 1-point ko. white to kill.
123
+ ]
124
+ (;W[bc];B[bb]
125
+ (;W[ca];B[cb]
126
+ (;W[ab];B[ba]
127
+ (;W[bi]
128
+ C[RIGHT black can't push (but no such luck in the actual game)
129
+ ])
130
+ )))
131
+ )
132
+ INPUT
133
+ game = @listener.game
134
+ game.name.should == 'White (W) vs. Black (B)'
135
+ game.rule.should == 'Japanese'
136
+ game.board_size.should == 19
137
+ game.handicap.should == 0
138
+ game.komi.should == 5.5
139
+ game.result.should == 'W+R'
140
+ game.played_on.should == "1999-07-28"
141
+ game.white_player.should == 'White'
142
+ game.black_player.should == 'Black'
143
+ game.program.should == 'Cgoban 1.9.2'
144
+ game.time_rule.should == '30:00(5x1:00)'
145
+
146
+ root_node = game.root_node
147
+ root_node.node_type.should == SGF::Model::Constants::NODE_SETUP
148
+
149
+ node2 = root_node.child
150
+ node2.node_type.should == SGF::Model::Constants::NODE_SETUP
151
+ node2.trunk?.should be_true
152
+ node2.comment.should == "guff plays A and adum tenukis to fill a 1-point ko. white to kill."
153
+ node2.black_moves.should include([3, 0])
154
+ node2.black_moves.should include([3, 1])
155
+ node2.white_moves.should include([4, 0])
156
+ node2.white_moves.should include([4, 1])
157
+ node2.labels[0].should == SGF::Model::Label.new([1, 3], "A")
158
+
159
+ var1_root = node2.children[0]
160
+ var1_root.trunk?.should be_false
161
+ var1_root.variation_root?.should be_true
162
+ var1_root.color.should == SGF::Model::Constants::WHITE
163
+ var1_root.move.should == [1, 2]
164
+
165
+ var1_node2 = var1_root.child
166
+ var1_node2.trunk?.should be_false
167
+ var1_node2.variation_root?.should_not be_true
168
+ var1_node2.color.should == SGF::Model::Constants::BLACK
169
+ var1_node2.move.should == [1, 1]
170
+ end
171
+ end
172
+
173
+ describe "class methods" do
174
+ it "parse should take a String and parse it and return the game" do
175
+ game = SGF::Parser.parse <<-INPUT
176
+ (;GM[1]FF[3]
177
+ GN[White (W) vs. Black (B)];
178
+ )
179
+ INPUT
180
+ game.name.should == 'White (W) vs. Black (B)'
181
+ end
182
+
183
+ it "parse should pass debug parameter to event listener" do
184
+ event_listener = SGF::Model::EventListener.new(false)
185
+ mock(SGF::Model::EventListener).new(true) {event_listener}
186
+ SGF::Parser.parse "(;)", true
187
+ end
188
+
189
+ it "parse_file should take a sgf file and parse it and return the game" do
190
+ game = SGF::Parser.parse_file File.expand_path(File.dirname(__FILE__) + '/../fixtures/good.sgf')
191
+ game.name.should == 'White (W) vs. Black (B)'
192
+ end
193
+
194
+ it "parse_file should pass debug parameter to event listener" do
195
+ event_listener = SGF::Model::EventListener.new(false)
196
+ mock(SGF::Model::EventListener).new(true) {event_listener}
197
+ game = SGF::Parser.parse_file(File.expand_path(File.dirname(__FILE__) + '/../fixtures/good.sgf'), true)
198
+ game.name.should == 'White (W) vs. Black (B)'
199
+ end
200
+
201
+ it "should parse game with escaped []" do
202
+ game = SGF::Parser.parse_file(File.expand_path(File.dirname(__FILE__) + '/../fixtures/good1.sgf'))
203
+ end
204
+
205
+ it "is_binary? should return true for binary file" do
206
+ pending 'check if file is binary is problematic'
207
+ SGF::Parser.is_binary?(File.expand_path(File.dirname(__FILE__) + '/../fixtures/test.png')).should be_true
208
+ end
209
+ end
210
+ end