sgf_parser 0.1.0

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