SgfParser 0.8.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/.document +5 -0
- data/.gitignore +25 -0
- data/LICENSE +20 -0
- data/README.rdoc +28 -0
- data/Rakefile +59 -0
- data/SgfParser.gemspec +66 -0
- data/VERSION +1 -0
- data/lib/sgf/parser/node.rb +67 -0
- data/lib/sgf/parser/properties.rb +97 -0
- data/lib/sgf/parser/tree.rb +124 -0
- data/lib/sgf/parser/tree_parse.rb +111 -0
- data/lib/sgf/sgfindent.rb +118 -0
- data/lib/sgf_parser.rb +8 -0
- data/sample_sgf/ff4_ex.sgf +165 -0
- data/sample_sgf/ff4_ex_saved.sgf +45 -0
- data/sample_sgf/redrose-tartrate.sgf +1068 -0
- data/sample_usage/parsing_files.rb +19 -0
- data/spec/node_spec.rb +34 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/tree_spec.rb +23 -0
- metadata +87 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Aldric Giacomoni
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
SGF: all formats (but untested with FF < 4)
|
2
|
+
Ruby: >1.8.7 (may work with 1.8.6)
|
3
|
+
|
4
|
+
Example:
|
5
|
+
require 'sgf_parser'
|
6
|
+
tree = SgfParser::Tree.new :filename => File
|
7
|
+
tree = SgfParser::Tree.new :sgf_string => String
|
8
|
+
|
9
|
+
All trees begin with an empty node ( @root) which allows a simple support of multiple gametrees.
|
10
|
+
Most games will just care about, say,
|
11
|
+
tree.root.children[0] which is the first node of the first gametree.
|
12
|
+
|
13
|
+
For any node, one can summon the properties as such:
|
14
|
+
node.properties # => returns a hash of the properties.
|
15
|
+
A single property can be called, like the comments, for instance, like so:
|
16
|
+
node.C # => returns the comments for this node.
|
17
|
+
|
18
|
+
The library currently uses method_missing to painlessly return the data. I must admit that this is both clever coding and laziness on my part.
|
19
|
+
|
20
|
+
The 'SGF Indenter', the purpose of which is to make the actual SGF file more
|
21
|
+
human readable, is working.
|
22
|
+
|
23
|
+
___
|
24
|
+
|
25
|
+
TODO
|
26
|
+
? Create a "Game" class, and if a whole set of () exists, then I have a game?
|
27
|
+
That way maybe I can easily go to multiple games stored in a single SGF file?
|
28
|
+
Mostly syntactic sugar, but may be worth implementing.
|
data/Rakefile
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "SgfParser"
|
8
|
+
gem.summary = %Q{A library for working with SGF files.}
|
9
|
+
gem.description = %Q{SGFParser is a library that parses and saves SGF (Smart Game Format) files.}
|
10
|
+
gem.email = "aldric@trevoke.net"
|
11
|
+
gem.homepage = "http://github.com/Trevoke/SGFParser"
|
12
|
+
gem.authors = ["Aldric Giacomoni"]
|
13
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
end
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'spec/rake/spectask'
|
22
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
23
|
+
spec.libs << 'lib' << 'spec'
|
24
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
25
|
+
end
|
26
|
+
|
27
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
28
|
+
spec.libs << 'lib' << 'spec'
|
29
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
30
|
+
spec.rcov = true
|
31
|
+
end
|
32
|
+
|
33
|
+
task :spec => :check_dependencies
|
34
|
+
|
35
|
+
# In case I ever want to go back to cucumber.
|
36
|
+
#begin
|
37
|
+
# require 'cucumber/rake/task'
|
38
|
+
# Cucumber::Rake::Task.new(:features)
|
39
|
+
#
|
40
|
+
# task :features => :check_dependencies
|
41
|
+
#rescue LoadError
|
42
|
+
# task :features do
|
43
|
+
# abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
|
44
|
+
# end
|
45
|
+
#end
|
46
|
+
|
47
|
+
#task :default => [:spec, :features]
|
48
|
+
|
49
|
+
task :default => [:spec]
|
50
|
+
|
51
|
+
require 'rake/rdoctask'
|
52
|
+
Rake::RDocTask.new do |rdoc|
|
53
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
54
|
+
|
55
|
+
rdoc.rdoc_dir = 'rdoc'
|
56
|
+
rdoc.title = "SgfParser #{version}"
|
57
|
+
rdoc.rdoc_files.include('README*')
|
58
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
59
|
+
end
|
data/SgfParser.gemspec
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{SgfParser}
|
8
|
+
s.version = "0.8.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Aldric Giacomoni"]
|
12
|
+
s.date = %q{2010-01-05}
|
13
|
+
s.description = %q{SGFParser is a library that parses and saves SGF (Smart Game Format) files.}
|
14
|
+
s.email = %q{aldric@trevoke.net}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"LICENSE",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"SgfParser.gemspec",
|
26
|
+
"VERSION",
|
27
|
+
"lib/sgf/parser/node.rb",
|
28
|
+
"lib/sgf/parser/properties.rb",
|
29
|
+
"lib/sgf/parser/tree.rb",
|
30
|
+
"lib/sgf/parser/tree_parse.rb",
|
31
|
+
"lib/sgf/sgfindent.rb",
|
32
|
+
"lib/sgf_parser.rb",
|
33
|
+
"sample_sgf/ff4_ex.sgf",
|
34
|
+
"sample_sgf/ff4_ex_saved.sgf",
|
35
|
+
"sample_sgf/redrose-tartrate.sgf",
|
36
|
+
"sample_usage/parsing_files.rb",
|
37
|
+
"spec/node_spec.rb",
|
38
|
+
"spec/spec.opts",
|
39
|
+
"spec/spec_helper.rb",
|
40
|
+
"spec/tree_spec.rb"
|
41
|
+
]
|
42
|
+
s.homepage = %q{http://github.com/Trevoke/SGFParser}
|
43
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
44
|
+
s.require_paths = ["lib"]
|
45
|
+
s.rubygems_version = %q{1.3.5}
|
46
|
+
s.summary = %q{A library for working with SGF files.}
|
47
|
+
s.test_files = [
|
48
|
+
"spec/node_spec.rb",
|
49
|
+
"spec/spec_helper.rb",
|
50
|
+
"spec/tree_spec.rb"
|
51
|
+
]
|
52
|
+
|
53
|
+
if s.respond_to? :specification_version then
|
54
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
55
|
+
s.specification_version = 3
|
56
|
+
|
57
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
58
|
+
s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
|
59
|
+
else
|
60
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
61
|
+
end
|
62
|
+
else
|
63
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.8.0
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module SgfParser
|
2
|
+
|
3
|
+
# Each part of the SGF Tree is a node. This holds links to the parent node,
|
4
|
+
# to the next node(s) in the tree, and of course, the properties of said node.
|
5
|
+
# Accessors : node.parent, node.children, node.properties
|
6
|
+
class Node
|
7
|
+
|
8
|
+
attr_accessor :parent, :children, :properties
|
9
|
+
|
10
|
+
# Creates a new node. Options which can be passed in are:
|
11
|
+
# :parent => parent_node (nil by default)
|
12
|
+
# : children => [list, of, children] (empty array by default)
|
13
|
+
# :properties => {hash_of => properties} (empty hash by default)
|
14
|
+
def initialize args={}
|
15
|
+
@parent = args[:parent]
|
16
|
+
@children = []
|
17
|
+
add_children args[:children] if !args[:children].nil?
|
18
|
+
@properties = Hash.new
|
19
|
+
@properties.merge! args[:properties] if !args[:properties].nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
# Adds one child or several children. Can be passed in as a comma-separated
|
23
|
+
# list or an array of node children. Will raise an error if one of the
|
24
|
+
# arguments is not of class Node.
|
25
|
+
def add_children *nodes
|
26
|
+
raise "Non-node child given!" if nodes.find { |node| node.class != Node }
|
27
|
+
@children.concat nodes.flatten
|
28
|
+
end
|
29
|
+
|
30
|
+
# Adds one or more properties to the node.
|
31
|
+
# Argument: a hash {'property' => 'value'}
|
32
|
+
def add_properties hash
|
33
|
+
hash.each do |key, value|
|
34
|
+
@properties[key] ||= ""
|
35
|
+
@properties[key].concat value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Iterates over each child of the given node
|
40
|
+
# node.each_child { |child| puts child.properties }
|
41
|
+
def each_child
|
42
|
+
@children.each { |child| yield child }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Compares one node's properties to another node's properties
|
46
|
+
def == other_node
|
47
|
+
@properties == other_node.properties
|
48
|
+
end
|
49
|
+
|
50
|
+
# Making comments easier to access.
|
51
|
+
def comments
|
52
|
+
@properties["C"]
|
53
|
+
end
|
54
|
+
|
55
|
+
alias :comment :comments
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def method_missing method_name, *args
|
60
|
+
output = @properties[method_name.to_s.upcase]
|
61
|
+
super(method_name, args) if output.nil?
|
62
|
+
output
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# A parser for SGF Files. Main usage: SGF::Tree.new :filename => file_name
|
2
|
+
module SgfParser
|
3
|
+
|
4
|
+
# http://www.red-bean.com/sgf/proplist.html
|
5
|
+
|
6
|
+
# Here we define SGF::Properties, so we can figure out what each property
|
7
|
+
# is and does.
|
8
|
+
|
9
|
+
property_string = %Q{AB Add Black setup list of stone
|
10
|
+
AE Add Empty setup list of point
|
11
|
+
AN Annotation game-info simpletext
|
12
|
+
AP Application root composed simpletext ':' simpletext
|
13
|
+
AR Arrow - list of composed point ':' point
|
14
|
+
AS Who adds stones - (LOA) simpletext
|
15
|
+
AW Add White setup list of stone
|
16
|
+
B Black move move
|
17
|
+
BL Black time left move real
|
18
|
+
BM Bad move move double
|
19
|
+
BR Black rank game-info simpletext
|
20
|
+
BT Black team game-info simpletext
|
21
|
+
C Comment - text
|
22
|
+
CA Charset root simpletext
|
23
|
+
CP Copyright game-info simpletext
|
24
|
+
CR Circle - list of point
|
25
|
+
DD Dim points - (inherit) elist of point
|
26
|
+
DM Even position - double
|
27
|
+
DO Doubtful move none
|
28
|
+
DT Date game-info simpletext
|
29
|
+
EV Event game-info simpletext
|
30
|
+
FF Fileformat root number (range: 1-4)
|
31
|
+
FG Figure - none | composed number ":" simpletext
|
32
|
+
GB Good for Black - double
|
33
|
+
GC Game comment game-info text
|
34
|
+
GM Game root number (range: 1-5,7-16)
|
35
|
+
GN Game name game-info simpletext
|
36
|
+
GW Good for White - double
|
37
|
+
HA Handicap game-info (Go) number
|
38
|
+
HO Hotspot - double
|
39
|
+
IP Initial pos. game-info (LOA) simpletext
|
40
|
+
IT Interesting move none
|
41
|
+
IY Invert Y-axis game-info (LOA) simpletext
|
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]
|
91
|
+
end
|
92
|
+
|
93
|
+
# All this work for this minuscule line!
|
94
|
+
PROPERTIES = hash
|
95
|
+
|
96
|
+
end
|
97
|
+
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module SgfParser
|
2
|
+
|
3
|
+
# This is a placeholder for the root of the gametree(s). It's an abstraction,
|
4
|
+
# but it allows an easy way to save, iterate over, and compare other trees.
|
5
|
+
# Accessors: tree.root
|
6
|
+
class Tree
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
attr_accessor :root
|
10
|
+
|
11
|
+
# Create a new tree. Can also be used to load a tree from either a file or
|
12
|
+
# a string. Raises an error if both are provided.
|
13
|
+
# options: \n
|
14
|
+
# :filename => filename \n
|
15
|
+
# !!! OR !!! \n
|
16
|
+
# :sgf_string => string \n
|
17
|
+
def initialize args={}
|
18
|
+
@root = Node.new
|
19
|
+
@sgf = ""
|
20
|
+
raise ArgumentError, "Both file and string provided" if args[:filename] &&
|
21
|
+
args[:sgf_string]
|
22
|
+
if !args[:filename].nil?
|
23
|
+
load_file args[:filename]
|
24
|
+
elsif !args[:sgf_string].nil?
|
25
|
+
load_string args[:sgf_string]
|
26
|
+
end
|
27
|
+
|
28
|
+
end # initialize
|
29
|
+
|
30
|
+
|
31
|
+
# Iterates over the tree, node by node, in preorder fashion.
|
32
|
+
# Does not support other types of iteration, but may in the future.
|
33
|
+
# tree.each { |node| puts "I am node. Hear me #{node.properties} !"}
|
34
|
+
def each order=:preorder, &block
|
35
|
+
case order
|
36
|
+
when :preorder
|
37
|
+
preorder @root, &block
|
38
|
+
end
|
39
|
+
end # each
|
40
|
+
|
41
|
+
# Compares a tree to another tree, node by node.
|
42
|
+
# Nodes must by the same (same properties, parents and children).
|
43
|
+
def == other_tree
|
44
|
+
one = []
|
45
|
+
two = []
|
46
|
+
each { |node| one << node }
|
47
|
+
other_tree.each { |node| two << node }
|
48
|
+
one == two
|
49
|
+
end # ==
|
50
|
+
|
51
|
+
# Saves the tree as an SGF file. raises an error if a filename is not given.
|
52
|
+
# tree.save :filename => file_name
|
53
|
+
def save args={}
|
54
|
+
raise ArgumentError, "No file name provided" if args[:filename].nil?
|
55
|
+
# SGF files are trees stored in pre-order traversal.
|
56
|
+
@sgf_string = "("
|
57
|
+
@root.children.each { |child| write_node child }
|
58
|
+
# write_node @root
|
59
|
+
@sgf_string << ")"
|
60
|
+
|
61
|
+
File.open(args[:filename], 'w') { |f| f << @sgf_string }
|
62
|
+
end #save
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# Adds a stringified node to the variable @sgf_string - for saving purposes.
|
67
|
+
def write_node node=@root
|
68
|
+
@sgf_string << ";"
|
69
|
+
unless node.properties.empty?
|
70
|
+
properties = ""
|
71
|
+
node.properties.each do |k, v|
|
72
|
+
v_escaped = v.gsub("]", "\\]")
|
73
|
+
properties += "#{k.to_s}[#{v_escaped}]"
|
74
|
+
end
|
75
|
+
@sgf_string << "#{properties}"
|
76
|
+
end
|
77
|
+
|
78
|
+
case node.children.size
|
79
|
+
when 0
|
80
|
+
@sgf_string << ")"
|
81
|
+
when 1
|
82
|
+
write_node node.children[0]
|
83
|
+
else
|
84
|
+
node.each_child do |child|
|
85
|
+
@sgf_string << "("
|
86
|
+
write_node child
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Used to load and parse a string if a string was given.
|
92
|
+
def load_string string
|
93
|
+
@sgf = string
|
94
|
+
parse unless @sgf.empty?
|
95
|
+
end # load_string
|
96
|
+
|
97
|
+
# Used to load and parse a file if a file was given.
|
98
|
+
def load_file filename
|
99
|
+
@sgf = ""
|
100
|
+
File.open(filename, 'r') { |f| @sgf = f.read }
|
101
|
+
parse unless @sgf.empty?
|
102
|
+
end # load_file
|
103
|
+
|
104
|
+
# Traverse the tree in preorder fashion, starting with the @root node if
|
105
|
+
# no node is given, and activating the passed block on each.
|
106
|
+
def preorder node=@root, &block
|
107
|
+
# stop processing if the block returns false
|
108
|
+
if yield node then
|
109
|
+
node.each_child do |child|
|
110
|
+
preorder(child, &block)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end # preorder
|
114
|
+
|
115
|
+
def method_missing method_name, *args
|
116
|
+
output = @root.children[0].properties[method_name]
|
117
|
+
super(method_name, args) if output.nil?
|
118
|
+
output
|
119
|
+
end # method_missing
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|