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.
- 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
|
|