SgfParser 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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/node.rb
CHANGED
@@ -1,51 +1,98 @@
|
|
1
1
|
module SGF
|
2
2
|
|
3
|
+
#Your basic node. It holds information about itself, its parent, and its children.
|
3
4
|
class Node
|
4
5
|
|
5
6
|
attr_accessor :parent, :children, :properties
|
6
7
|
|
7
8
|
# Creates a new node. Arguments which can be passed in are:
|
8
9
|
# :parent => parent_node (nil by default)
|
9
|
-
# :children => [list, of, children] (empty array
|
10
|
-
# :properties => {hash_of => properties} (empty hash
|
10
|
+
# :children => [list, of, children] (empty array if nothing is passed)
|
11
|
+
# :properties => {hash_of => properties} (empty hash if nothing is passed)
|
11
12
|
def initialize args={}
|
12
13
|
@parent = args[:parent]
|
13
14
|
@children = []
|
14
15
|
add_children args[:children] if args[:children]
|
15
16
|
@properties = Hash.new
|
16
|
-
|
17
|
+
add_properties args[:properties] if args[:properties]
|
17
18
|
end
|
18
19
|
|
20
|
+
#Takes an arbitrary number of child nodes, adds them to the list of children, and make this node their parent.
|
19
21
|
def add_children *nodes
|
20
22
|
nodes.flatten!
|
21
23
|
raise "Non-node child given!" if nodes.any? { |node| node.class != Node }
|
22
|
-
nodes.each
|
23
|
-
|
24
|
+
nodes.each do |node|
|
25
|
+
node.parent = self
|
26
|
+
@children << node
|
27
|
+
end
|
24
28
|
end
|
25
29
|
|
30
|
+
#Takes a hash {identity => property} and adds those to the current node.
|
31
|
+
#If a property already exists, it will append to it.
|
26
32
|
def add_properties hash
|
27
|
-
hash.each do |
|
28
|
-
@properties[
|
29
|
-
@properties[
|
33
|
+
hash.each do |identity, property|
|
34
|
+
@properties[identity] ||= property.class.new
|
35
|
+
@properties[identity].concat property
|
30
36
|
end
|
37
|
+
update_human_readable_methods
|
31
38
|
end
|
32
39
|
|
40
|
+
#Iterate through each child. Yields a child node, if one exists.
|
33
41
|
def each_child
|
34
42
|
@children.each { |child| yield child }
|
35
43
|
end
|
36
44
|
|
45
|
+
#Compare to another node.
|
37
46
|
def == other_node
|
38
47
|
@properties == other_node.properties
|
39
48
|
end
|
40
49
|
|
41
|
-
|
42
|
-
|
50
|
+
#Syntactic sugar for node.properties["XX"]
|
51
|
+
def [] identity
|
52
|
+
identity = identity.to_s
|
53
|
+
@properties[identity]
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s
|
57
|
+
out = "#<#{self.class}:#{self.object_id}, "
|
58
|
+
out << (@parent ? "Has a parent, " : "Has no parent, ")
|
59
|
+
out << "#{@children.size} Children, "
|
60
|
+
out << "#{@properties.keys.size} Properties"
|
61
|
+
out << ">"
|
43
62
|
end
|
44
63
|
|
45
|
-
alias :
|
64
|
+
alias :inspect :to_s
|
65
|
+
|
66
|
+
def to_str(indent = 0)
|
67
|
+
properties = []
|
68
|
+
@properties.each do |identity, property|
|
69
|
+
properties << stringify_identity_and_property(identity, property)
|
70
|
+
end
|
71
|
+
whitespace = leading_whitespace(indent)
|
72
|
+
"#{whitespace};#{properties.join("\n#{whitespace}")}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def stringify_identity_and_property(identity, property)
|
76
|
+
new_property = property.instance_of?(Array) ? property.join("][") : property
|
77
|
+
new_property = new_property.gsub("]", "\\]") if identity == "C"
|
78
|
+
"#{identity.to_s}[#{new_property}]"
|
79
|
+
end
|
46
80
|
|
47
81
|
private
|
48
82
|
|
83
|
+
def update_human_readable_methods
|
84
|
+
SGF::Node::PROPERTIES.each do |human_readable_method, sgf_identity|
|
85
|
+
next if defined? human_readable_method.to_sym
|
86
|
+
define_method(human_readable_method.to_sym) do
|
87
|
+
@properties[sgf_identity] ? @properties[sgf_identity] : raise(SGF::NoIdentityError)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def leading_whitespace(indent)
|
93
|
+
"#{" " * indent}"
|
94
|
+
end
|
95
|
+
|
49
96
|
def method_missing method_name, *args
|
50
97
|
property = method_name.to_s.upcase
|
51
98
|
if property[/(.*?)=$/]
|
data/lib/sgf/parser.rb
CHANGED
@@ -1,120 +1,167 @@
|
|
1
1
|
require 'stringio'
|
2
2
|
|
3
3
|
module SGF
|
4
|
+
|
5
|
+
#The parser returns a SGF::Tree representation of the SGF file
|
6
|
+
#parser = SGF::Parser.new
|
7
|
+
#tree = parser.parse sgf_in_string_form
|
4
8
|
class Parser
|
5
9
|
|
6
|
-
|
7
|
-
|
8
|
-
|
10
|
+
NEW_NODE = ";"
|
11
|
+
BRANCHING = ["(", ")"]
|
12
|
+
PROPERTY = ["[", "]"]
|
13
|
+
NODE_DELIMITERS = [NEW_NODE].concat BRANCHING
|
14
|
+
LIST_IDENTITIES = ["AW", "AB", "AE", "AR", "CR", "DD",
|
15
|
+
"LB", "LN", "MA", "SL", "SQ", "TR", "VW",
|
16
|
+
"TB", "TW"]
|
17
|
+
|
18
|
+
# This takes as argument an SGF and returns an SGF::Tree object
|
19
|
+
# It accepts a local path (String), a stringified SGF (String),
|
20
|
+
# or a file handler (File).
|
21
|
+
# The second argument is optional, in case you don't want this to raise errors.
|
22
|
+
# You probably shouldn't use it, but who's gonna stop you?
|
23
|
+
def parse sgf, strict_parsing = true
|
24
|
+
@strict_parsing = strict_parsing
|
25
|
+
@stream = streamably_stringify sgf
|
26
|
+
@tree = Tree.new
|
9
27
|
@root = @tree.root
|
28
|
+
@current_node = @root
|
29
|
+
@branches = []
|
30
|
+
until @stream.eof?
|
31
|
+
case next_character
|
32
|
+
when "(" then open_branch
|
33
|
+
when ";" then
|
34
|
+
create_new_node
|
35
|
+
parse_node_data
|
36
|
+
add_properties_to_current_node
|
37
|
+
when ")" then close_branch
|
38
|
+
else next
|
39
|
+
end
|
40
|
+
end
|
41
|
+
@tree
|
10
42
|
end
|
11
43
|
|
12
|
-
|
13
|
-
|
44
|
+
private
|
45
|
+
|
46
|
+
def streamably_stringify sgf
|
47
|
+
sgf = sgf.read if sgf.instance_of?(File)
|
48
|
+
sgf = File.read(sgf) if File.exist?(sgf)
|
49
|
+
|
50
|
+
check_for_errors_before_parsing sgf if @strict_parsing
|
51
|
+
StringIO.new clean(sgf), 'r'
|
14
52
|
end
|
15
53
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
when ';' then store_node_and_create_new_node
|
22
|
-
when '[' then get_and_store_property
|
23
|
-
else store_character(char)
|
24
|
-
end
|
54
|
+
def check_for_errors_before_parsing string
|
55
|
+
msg = "The first two non-whitespace characters of the string should be (;"
|
56
|
+
unless string[/\A\s*\(\s*;/]
|
57
|
+
msg << " but they were #{string[0..1]} instead."
|
58
|
+
raise(SGF::MalformedDataError, msg)
|
25
59
|
end
|
26
|
-
@tree
|
27
60
|
end
|
28
61
|
|
29
|
-
def
|
30
|
-
|
62
|
+
def clean sgf
|
63
|
+
sgf.gsub! "\\\\n\\\\r", ''
|
64
|
+
sgf.gsub! "\\\\r\\\\n", ''
|
65
|
+
sgf.gsub! "\\\\r", ''
|
66
|
+
sgf.gsub! "\\\\n", ''
|
67
|
+
sgf
|
31
68
|
end
|
32
69
|
|
33
|
-
def
|
34
|
-
@
|
35
|
-
!@stream.eof?
|
70
|
+
def open_branch
|
71
|
+
@branches.unshift @current_node
|
36
72
|
end
|
37
73
|
|
38
|
-
def
|
39
|
-
@
|
40
|
-
|
41
|
-
@sgf.gsub! "\\\\r", ""
|
42
|
-
@sgf.gsub! "\\\\n", ""
|
43
|
-
@sgf
|
44
|
-
end
|
74
|
+
def close_branch
|
75
|
+
@current_node = @branches.shift
|
76
|
+
end
|
45
77
|
|
46
|
-
def
|
47
|
-
|
48
|
-
@
|
78
|
+
def create_new_node
|
79
|
+
node = Node.new
|
80
|
+
@current_node.add_children node
|
81
|
+
@current_node = node
|
49
82
|
end
|
50
83
|
|
51
|
-
def
|
52
|
-
@
|
53
|
-
|
84
|
+
def parse_node_data
|
85
|
+
@node_properties = {}
|
86
|
+
while still_inside_node?
|
87
|
+
parse_identity
|
88
|
+
parse_property
|
89
|
+
@node_properties[@identity] = @property
|
90
|
+
end
|
91
|
+
end
|
54
92
|
|
55
|
-
def
|
56
|
-
@current_node
|
57
|
-
clear_temporary_data
|
93
|
+
def add_properties_to_current_node
|
94
|
+
@current_node.add_properties @node_properties
|
58
95
|
end
|
59
96
|
|
60
|
-
def
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
97
|
+
def still_inside_node?
|
98
|
+
inside_a_node = false
|
99
|
+
while char = next_character
|
100
|
+
next if char[/\s/]
|
101
|
+
inside_a_node = !NODE_DELIMITERS.include?(char)
|
102
|
+
break
|
103
|
+
end
|
104
|
+
@stream.pos -= 1 if char
|
105
|
+
inside_a_node
|
66
106
|
end
|
67
107
|
|
68
|
-
def
|
69
|
-
@content[@identity] ||= ""
|
70
|
-
@content[@identity] << get_property
|
108
|
+
def parse_identity
|
71
109
|
@identity = ""
|
110
|
+
while char = next_character and char != "["
|
111
|
+
@identity << char unless char == "\n"
|
112
|
+
end
|
72
113
|
end
|
73
114
|
|
74
|
-
def
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
end
|
115
|
+
def parse_property
|
116
|
+
@property = ""
|
117
|
+
case @identity.upcase
|
118
|
+
when "C" then parse_comment
|
119
|
+
when *LIST_IDENTITIES then parse_multi_property
|
120
|
+
else parse_generic_property
|
121
|
+
end
|
122
|
+
end
|
83
123
|
|
84
|
-
|
124
|
+
def parse_comment
|
125
|
+
while char = next_character and still_inside_comment? char
|
126
|
+
@property << char
|
85
127
|
end
|
86
|
-
"
|
128
|
+
@property.gsub! "\\]", "]"
|
87
129
|
end
|
88
130
|
|
89
|
-
def
|
90
|
-
|
91
|
-
|
92
|
-
char = next_character if char == "\n"
|
93
|
-
if char == "["
|
94
|
-
multiple_properties = true
|
95
|
-
end
|
96
|
-
@stream.pos -= 1
|
97
|
-
multiple_properties
|
131
|
+
def parse_multi_property
|
132
|
+
while char = next_character and still_inside_multi_property? char
|
133
|
+
@property << char
|
98
134
|
end
|
135
|
+
@property = @property.gsub("][", ",").split(",")
|
99
136
|
end
|
100
137
|
|
101
|
-
def
|
102
|
-
|
138
|
+
def parse_generic_property
|
139
|
+
while char = next_character and char != "]"
|
140
|
+
@property << char
|
141
|
+
end
|
103
142
|
end
|
104
143
|
|
105
|
-
def
|
106
|
-
@
|
107
|
-
@identity = ""
|
144
|
+
def still_inside_comment? char
|
145
|
+
char != "]" || (char == "]" && @property[-1..-1] == "\\")
|
108
146
|
end
|
109
147
|
|
110
|
-
def
|
111
|
-
|
148
|
+
def still_inside_multi_property? char
|
149
|
+
return true if char != "]"
|
150
|
+
inside_multi_property = false
|
151
|
+
while char = next_character
|
152
|
+
next if char[/\s/]
|
153
|
+
inside_multi_property = char == "["
|
154
|
+
break
|
155
|
+
end
|
156
|
+
@stream.pos -= 1 if char
|
157
|
+
inside_multi_property
|
112
158
|
end
|
113
159
|
|
114
|
-
def
|
115
|
-
|
160
|
+
def next_character
|
161
|
+
!@stream.eof? && @stream.sysread(1)
|
116
162
|
end
|
117
163
|
|
118
164
|
end
|
165
|
+
|
119
166
|
end
|
120
167
|
|
data/lib/sgf/properties.rb
CHANGED
@@ -1,97 +1,90 @@
|
|
1
|
-
|
2
|
-
module SgfParser
|
1
|
+
module SGF
|
3
2
|
|
4
3
|
# http://www.red-bean.com/sgf/proplist.html
|
5
4
|
|
6
|
-
|
7
|
-
# is and does.
|
5
|
+
class Game
|
8
6
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
KM Komi game-info (Go) real
|
43
|
-
KO Ko move none
|
44
|
-
LB Label - list of composed point ':' simpletext
|
45
|
-
LN Line - list of composed point ':' point
|
46
|
-
MA Mark - list of point
|
47
|
-
MN set move number move number
|
48
|
-
N Nodename - simpletext
|
49
|
-
OB OtStones Black move number
|
50
|
-
ON Opening game-info simpletext
|
51
|
-
OT Overtime game-info simpletext
|
52
|
-
OW OtStones White move number
|
53
|
-
PB Player Black game-info simpletext
|
54
|
-
PC Place game-info simpletext
|
55
|
-
PL Player to play setup color
|
56
|
-
PM Print move mode - (inherit) number
|
57
|
-
PW Player White game-info simpletext
|
58
|
-
RE Result game-info simpletext
|
59
|
-
RO Round game-info simpletext
|
60
|
-
RU Rules game-info simpletext
|
61
|
-
SE Markup - (LOA) point
|
62
|
-
SL Selected - list of point
|
63
|
-
SO Source game-info simpletext
|
64
|
-
SQ Square - list of point
|
65
|
-
ST Style root number (range: 0-3)
|
66
|
-
SU Setup type game-info (LOA) simpletext
|
67
|
-
SZ Size root (number | composed number ':' number)
|
68
|
-
TB Territory Black - (Go) elist of point
|
69
|
-
TE Tesuji move double
|
70
|
-
TM Timelimit game-info real
|
71
|
-
TR Triangle - list of point
|
72
|
-
TW Territory White - (Go) elist of point
|
73
|
-
UC Unclear pos - double
|
74
|
-
US User game-info simpletext
|
75
|
-
V Value - real
|
76
|
-
VW View - (inherit) elist of point
|
77
|
-
W White move move
|
78
|
-
WL White time left move real
|
79
|
-
WR White rank game-info simpletext
|
80
|
-
WT White team game-info simpletext }
|
81
|
-
|
82
|
-
property_array = property_string.split("\n")
|
83
|
-
hash = {}
|
84
|
-
property_array.each do |set|
|
85
|
-
temp = set.gsub("\t", " ")
|
86
|
-
id = temp[0..3].strip
|
87
|
-
desc = temp[4..19].strip
|
88
|
-
property_type = temp[20..35].strip
|
89
|
-
property_value = temp[37..-1].strip
|
90
|
-
hash[id] = [desc, property_type, property_value]
|
7
|
+
PROPERTIES = {"annotator"=>"AN",
|
8
|
+
"black_octisquares"=>"BO", #Octi
|
9
|
+
"black_rank"=>"BR",
|
10
|
+
"black_team"=>"BT",
|
11
|
+
"copyright"=>"CP",
|
12
|
+
"date"=>"DT",
|
13
|
+
"event"=>"EV",
|
14
|
+
"game_content"=>"GC",
|
15
|
+
"handicap"=>"HA", #Go
|
16
|
+
"initial_position"=>"IP", #Lines of Action
|
17
|
+
"invert_y_axis"=>"IY", #Lines of Action
|
18
|
+
"komi"=>"KM", #Go
|
19
|
+
"match_information"=>"MI", #Backgammon
|
20
|
+
"name"=>"GN",
|
21
|
+
"prongs"=>"NP", #Octi
|
22
|
+
"reserve"=>"NR", #Octi
|
23
|
+
"superprongs"=>"NS", #Octi
|
24
|
+
"opening"=>"ON",
|
25
|
+
"overtime"=>"OT",
|
26
|
+
"black_player"=>"PB",
|
27
|
+
"place"=>"PC",
|
28
|
+
"puzzle"=>"PZ",
|
29
|
+
"white_player"=>"PW",
|
30
|
+
"result"=>"RE",
|
31
|
+
"round"=>"RO",
|
32
|
+
"rules"=>"RU",
|
33
|
+
"setup_type"=>"SU", #Lines of Action
|
34
|
+
"source"=>"SO",
|
35
|
+
"time"=>"TM",
|
36
|
+
"data_entry"=>"US",
|
37
|
+
"white_octisquares"=>"WO", #Octi
|
38
|
+
"white_rank"=>"WR",
|
39
|
+
"white_team"=>"WT"}
|
91
40
|
end
|
92
41
|
|
93
|
-
|
94
|
-
|
42
|
+
class Node
|
43
|
+
PROPERTIES = {
|
44
|
+
"black_move" => "B",
|
45
|
+
"black_time_left" => "BL",
|
46
|
+
"bad_move" => "BM",
|
47
|
+
"doubtful" => "DO",
|
48
|
+
"interesting" => "IT",
|
49
|
+
"ko" => "KO",
|
50
|
+
"set_move_number" => "MN",
|
51
|
+
"otstones_black" => "OB", # What?
|
52
|
+
"otstones_white" => "OW", # Again! What?
|
53
|
+
"tesuji" => "TE",
|
54
|
+
"white_move" => "W",
|
55
|
+
"white_time_left" => "WL",
|
56
|
+
"add_black" => "AB",
|
57
|
+
"add_empty" => "AE",
|
58
|
+
"add_white" => "AW",
|
59
|
+
"player" => "PL",
|
60
|
+
"arrow" => "AR",
|
61
|
+
"comment" => "C",
|
62
|
+
"circle" => "CR",
|
63
|
+
"dim_points" => "DD",
|
64
|
+
"even_position" => "DM", #Yep. No idea how that makes sense.
|
65
|
+
"figure" => "FG",
|
66
|
+
"good_for_black" => "GB",
|
67
|
+
"good_for_white" => "GW",
|
68
|
+
"hotspot" => "HO",
|
69
|
+
"label" => "LB",
|
70
|
+
"line" => "LN",
|
71
|
+
"mark" => "MA",
|
72
|
+
"node_name" => "N",
|
73
|
+
"print_move_node" => "PM", #Am I going to have to code this?
|
74
|
+
"selected" => "SL",
|
75
|
+
"square" => "SQ",
|
76
|
+
"triangle" => "TR",
|
77
|
+
"unclear_position" => "UC",
|
78
|
+
"value" => "V",
|
79
|
+
"view" => "VW",
|
80
|
+
"application" => "AP",
|
81
|
+
"charset" => "CA",
|
82
|
+
"file_format" => "FF",
|
83
|
+
"game" => "GM",
|
84
|
+
"style" => "ST",
|
85
|
+
"size" => "SZ"
|
86
|
+
}
|
87
|
+
end
|
95
88
|
|
96
89
|
end
|
97
90
|
|