SgfParser 2.0.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.autotest +13 -0
- data/.gitignore +9 -0
- data/.rspec +3 -1
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/CHANGELOG +12 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +42 -16
- data/README.md +97 -0
- data/SgfParser.gemspec +5 -8
- data/bin/sgf_indent.rb +5 -4
- data/examples/simple_iteration_of_games_in_a_directory.rb +3 -3
- data/lib/sgf.rb +18 -9
- data/lib/sgf/collection.rb +75 -0
- data/lib/sgf/collection_assembler.rb +32 -0
- data/lib/sgf/error.rb +1 -2
- data/lib/sgf/error_checkers.rb +15 -0
- data/lib/sgf/gametree.rb +64 -0
- data/lib/sgf/node.rb +139 -83
- data/lib/sgf/parser.rb +57 -157
- data/lib/sgf/parsing_tokens.rb +40 -0
- data/lib/sgf/properties.rb +80 -83
- data/lib/sgf/stream.rb +50 -0
- data/lib/sgf/variation.rb +16 -0
- data/lib/sgf/version.rb +1 -1
- data/lib/sgf/writer.rb +35 -37
- data/spec/acceptance_spec.rb +27 -0
- data/spec/collection_spec.rb +65 -0
- data/spec/gametree_spec.rb +99 -0
- data/spec/node_spec.rb +174 -68
- data/spec/parser_spec.rb +76 -74
- data/spec/spec_helper.rb +90 -9
- data/spec/variation_spec.rb +15 -0
- data/spec/writer_spec.rb +22 -21
- data/wercker.yml +21 -0
- metadata +77 -39
- data/.rvmrc +0 -1
- data/README.rdoc +0 -18
- data/lib/sgf/game.rb +0 -64
- data/lib/sgf/tree.rb +0 -77
- data/spec/game_spec.rb +0 -70
- data/spec/tree_spec.rb +0 -36
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 84fb3f7396b1239b5f0ca9e5df6c46e3990ecae0
|
4
|
+
data.tar.gz: 15e29afd726d23469558d382766447f0fe473b95
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d9780b3835dba470d819e8fbe99623806727b7188dd2c2e1c0745d7e778dadb38fbeeff767bb6996707b05b15cfd4c706a068d3fd3140d8d2478196ec062839c
|
7
|
+
data.tar.gz: 2f13b065d70201190e927d9da7f8100c2828f48abe7044fc555763a50efe0c964626fa655d54ac57dc9b090f05f79c7d5de88756598c9a02875c98801a6b534c
|
data/.autotest
ADDED
data/.gitignore
CHANGED
data/.rspec
CHANGED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.0
|
data/.travis.yml
ADDED
data/CHANGELOG
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
== 3.0.0
|
2
|
+
- Rename Tree to Collection
|
3
|
+
- Rename Game to Gametree
|
4
|
+
- `SGF.parse` takes a filename and creates a collection
|
5
|
+
- Collection has `errors` properties collecting parsing problems
|
6
|
+
- Fix a bug preventing `node[:prop] = new_value` from working
|
7
|
+
- Fix a bug causing a collection to always generate new gametree objects
|
8
|
+
|
9
|
+
== 2.1.0
|
10
|
+
- Adding children and adding parents all works beautifully
|
11
|
+
- Node depth is set properly and updated properly
|
12
|
+
- Node.new takes an optional hash of properties with special keys :children and :parent
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,32 +1,58 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
SgfParser (
|
4
|
+
SgfParser (3.0.0)
|
5
5
|
|
6
6
|
GEM
|
7
|
-
remote:
|
7
|
+
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
byebug (8.2.4)
|
10
|
+
coderay (1.1.1)
|
11
|
+
diff-lcs (1.2.5)
|
12
|
+
docile (1.1.5)
|
13
|
+
json (1.8.3)
|
14
|
+
method_source (0.8.2)
|
15
|
+
pry (0.10.3)
|
16
|
+
coderay (~> 1.1.0)
|
17
|
+
method_source (~> 0.8.1)
|
18
|
+
slop (~> 3.4)
|
19
|
+
pry-byebug (3.3.0)
|
20
|
+
byebug (~> 8.0)
|
21
|
+
pry (~> 0.10)
|
22
|
+
rake (11.1.2)
|
23
|
+
rdoc (4.2.2)
|
14
24
|
json (~> 1.4)
|
15
|
-
rspec (
|
16
|
-
rspec-core (~>
|
17
|
-
rspec-expectations (~>
|
18
|
-
rspec-mocks (~>
|
19
|
-
rspec-core (
|
20
|
-
|
21
|
-
|
22
|
-
|
25
|
+
rspec (3.4.0)
|
26
|
+
rspec-core (~> 3.4.0)
|
27
|
+
rspec-expectations (~> 3.4.0)
|
28
|
+
rspec-mocks (~> 3.4.0)
|
29
|
+
rspec-core (3.4.4)
|
30
|
+
rspec-support (~> 3.4.0)
|
31
|
+
rspec-expectations (3.4.0)
|
32
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
33
|
+
rspec-support (~> 3.4.0)
|
34
|
+
rspec-mocks (3.4.1)
|
35
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
36
|
+
rspec-support (~> 3.4.0)
|
37
|
+
rspec-support (3.4.1)
|
38
|
+
simplecov (0.11.2)
|
39
|
+
docile (~> 1.1.0)
|
40
|
+
json (~> 1.8)
|
41
|
+
simplecov-html (~> 0.10.0)
|
42
|
+
simplecov-html (0.10.0)
|
43
|
+
slop (3.6.0)
|
23
44
|
|
24
45
|
PLATFORMS
|
25
46
|
ruby
|
47
|
+
x64-mingw32
|
26
48
|
|
27
49
|
DEPENDENCIES
|
28
50
|
SgfParser!
|
51
|
+
pry-byebug
|
29
52
|
rake
|
30
|
-
rcov
|
31
53
|
rdoc
|
32
54
|
rspec
|
55
|
+
simplecov
|
56
|
+
|
57
|
+
BUNDLED WITH
|
58
|
+
1.11.2
|
data/README.md
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# SGFParser
|
2
|
+
[![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/Trevoke/SGFParser?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
3
|
+
|
4
|
+
Build status {<img src="https://secure.travis-ci.org/Trevoke/SGFParser.png" />}[http://travis-ci.org/Trevoke/SGFParser]
|
5
|
+
|
6
|
+
# Intro
|
7
|
+
I'm hoping that this is and remains the fastest SGF parser in Ruby. On my desktop, loading the SGF library and parsing Kogo's Joseki dictionary takes a little under six seconds. It's a 3MB file and the average SGF is maybe 10k, so on average it's rather snappy.
|
8
|
+
|
9
|
+
Are you using this gem? Is there functionality you wish it had? Is something hard to do? Does the documentation not make sense, and you know how to make it more helpful? Let me know and I'll make it possible, or easier!
|
10
|
+
|
11
|
+
# Supported versions
|
12
|
+
SGF: FF4 - may support earlier ones as well, but untested.
|
13
|
+
Ruby: >=2.1
|
14
|
+
|
15
|
+
|
16
|
+
# Intro to SGF
|
17
|
+
According to the standard, An SGF file holds a `Collection` of one or more `Gametree` objects. Each of those is made of a tree of `Node` objects.
|
18
|
+
|
19
|
+
In other words: FILE (1 ↔ ∞) Collection (1 ↔ ∞) Gametree (1 ↔ ∞) Node
|
20
|
+
|
21
|
+
## Basics of our data structure
|
22
|
+
|
23
|
+
In this implementation, when you parse a file, you get a `Collection` back. This object has a root `Node` used as the top-level node for all gametrees. The children of that node are the root nodes of the actual games.
|
24
|
+
|
25
|
+
Assuming a common SGF file with a single game, you could get to the game by doing this:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
SGF.parse(file).gametrees.first # => <SGF::Game:70180384181460>
|
29
|
+
```
|
30
|
+
|
31
|
+
## Basics of properties
|
32
|
+
|
33
|
+
Some properties belong on the root node of a game only, such as the identity of the players. For convenience, some human-readable methods are defined on the gametree object itself to reach this information, for instance
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
gametree.black_player # => "tartrate"
|
37
|
+
```
|
38
|
+
|
39
|
+
Calling a property that is not defined in the current tree will result in an error. For instance, a property that does not exist in the game of Go:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
gametree.black_octisquares # => SGF::NoIdentityError
|
43
|
+
```
|
44
|
+
|
45
|
+
## Basics of navigating
|
46
|
+
|
47
|
+
Since a game is a tree (each node can be the source of many variations), a convenience method is defined to help you traverse the main branch one node at a time.
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
gametree.current_node # => starts as root node, e.g. #<SGF::Node:70180384857820, Has a parent, 1 Children, 16 Properties>
|
51
|
+
gametree.next_node # => #<SGF::Node:70180384839420, Has a parent, 1 Children, 4 Properties>
|
52
|
+
gametree.current_node # => #<SGF::Node:70180384839420, Has a parent, 1 Children, 4 Properties>
|
53
|
+
```
|
54
|
+
|
55
|
+
Since it's easy to get lost when you're looking at things one node at a time (or because sometimes you don't want to iterate with an index), we also provide a convenience `depth` method on a given node to tell you how far down the tree you are.
|
56
|
+
|
57
|
+
And since this is Ruby, all of the objects (`Collection`, `Gametree` and `Node`) provide iteration through `each`. Note that in this example, we are using a gametree, and iteration on a gametree starts from the gametree's root, so the depth is 1. Iteration on a collection starts from the collection's root, and that node's depth would be 0. Iteration on any node starts from that node and goes through all its children.
|
58
|
+
|
59
|
+
NOTE: iteration is done as preorder tree traversal. You shouldn't have to care about this, but you might.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
gametree.each do |node|
|
63
|
+
puts "Node at depth #{node.depth} has #{node.properties.count} properties"
|
64
|
+
end
|
65
|
+
=begin
|
66
|
+
Node at depth 1 has 16 properties
|
67
|
+
Node at depth 2 has 4 properties
|
68
|
+
Node at depth 3 has 3 properties
|
69
|
+
Node at depth 4 has 4 properties
|
70
|
+
Node at depth 5 has 3 properties
|
71
|
+
Node at depth 6 has 3 properties
|
72
|
+
Node at depth 7 has 3 properties
|
73
|
+
Node at depth 8 has 4 properties
|
74
|
+
... And so on
|
75
|
+
=end
|
76
|
+
```
|
77
|
+
|
78
|
+
## Basics of saving
|
79
|
+
|
80
|
+
There is `SGF::Writer`, which you can use starting from any node. There is also a convenience method on collection:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
collection.save(filename) # => Shiny new text file
|
84
|
+
SGF::Writer.new.stringify_tree_from(node) # => Shiny string
|
85
|
+
SGF::Writer.new.save(node, filename) # => File with tree starting at node
|
86
|
+
```
|
87
|
+
|
88
|
+
If you need a raw SGF version of your data, you can use `to_s`:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
node.to_s
|
92
|
+
gametree.to_s
|
93
|
+
collection.to_s
|
94
|
+
```
|
95
|
+
|
96
|
+
# SGF Parsing warning (À bon entendeur…)
|
97
|
+
WARNING: An implementation requirement is to make sure any closing bracket ']' inside a comment is escaped: '\\]'. If this is not done, you will be one sad panda! This library will do this for you upon saving, but will most likely die horribly when parsing anything which does not follow this rule.
|
data/SgfParser.gemspec
CHANGED
@@ -10,19 +10,16 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.homepage = %q{http://github.com/Trevoke/SGFParser}
|
11
11
|
s.date = %q{2011-08-01}
|
12
12
|
s.summary = %q{A library that parses and saves SGF (Smart Game Format) files.}
|
13
|
-
s.description = %q{SGF::Parser does standard stream parsing of the SGF file, instead of using an AG or some other auto-generated
|
14
|
-
s.extra_rdoc_files =
|
15
|
-
"LICENSE",
|
16
|
-
"README.rdoc"
|
17
|
-
]
|
13
|
+
s.description = %q{SGF::Parser does standard stream parsing of the SGF file, instead of using an AG or some other auto-generated parser. It is therefore faster to use. It also intends to be very object-oriented and hopefully will also be easier to use.}
|
14
|
+
s.extra_rdoc_files = %w(LICENSE README.md)
|
18
15
|
s.files = `git ls-files`.split("\n")
|
19
16
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
20
17
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
|
-
s.require_paths =
|
18
|
+
s.require_paths = %w(lib)
|
22
19
|
|
23
20
|
s.add_development_dependency 'rspec'
|
24
|
-
s.add_development_dependency '
|
21
|
+
s.add_development_dependency 'simplecov'
|
25
22
|
s.add_development_dependency 'rake'
|
26
23
|
s.add_development_dependency 'rdoc'
|
24
|
+
s.add_development_dependency 'pry-byebug'
|
27
25
|
end
|
28
|
-
|
data/bin/sgf_indent.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
1
2
|
#Thankfully, whitespace doesn't matter in SGF, unless you're inside a comment.
|
2
3
|
#This makes it possible to indent an SGF file and let it be somewhat human-readable.
|
3
4
|
|
4
|
-
|
5
|
+
require_relative '../lib/sgf'
|
5
6
|
|
6
7
|
if ARGV.size != 2
|
7
8
|
puts "Usage:\n sgf_indent source.sgf destination.sgf"
|
8
|
-
exit
|
9
|
+
exit 1
|
9
10
|
end
|
10
11
|
|
11
12
|
parser = SGF::Parser.new
|
12
|
-
|
13
|
-
|
13
|
+
collection = parser.parse ARGV[0]
|
14
|
+
collection.save ARGV[1]
|
@@ -3,9 +3,9 @@ require 'sgf'
|
|
3
3
|
parser = SGF::Parser.new
|
4
4
|
|
5
5
|
Dir['*.sgf'].each do |file|
|
6
|
-
|
7
|
-
|
6
|
+
collection = parser.parse File.read(file)
|
7
|
+
collection.gametrees.each do |game|
|
8
8
|
puts "White player: #{game.white_player} and Black player: #{game.black_player}"
|
9
9
|
end
|
10
|
-
|
10
|
+
collection.save file #Because may as well indent the files while I'm here, right?
|
11
11
|
end
|
data/lib/sgf.rb
CHANGED
@@ -1,10 +1,19 @@
|
|
1
|
-
|
1
|
+
module SGF
|
2
|
+
class FileDoesNotExistError < StandardError ; end
|
3
|
+
def self.parse(filename)
|
4
|
+
SGF::Parser.new.parse File.read(filename)
|
5
|
+
rescue Errno::ENOENT
|
6
|
+
raise FileDoesNotExistError
|
7
|
+
end
|
2
8
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
end
|
10
|
+
|
11
|
+
require_relative 'sgf/error'
|
12
|
+
require_relative 'sgf/version'
|
13
|
+
require_relative 'sgf/properties'
|
14
|
+
require_relative 'sgf/variation'
|
15
|
+
require_relative 'sgf/node'
|
16
|
+
require_relative 'sgf/collection'
|
17
|
+
require_relative 'sgf/parser'
|
18
|
+
require_relative 'sgf/gametree'
|
19
|
+
require_relative 'sgf/writer'
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# Collection holds most of the logic, for now.
|
2
|
+
# It has all the nodes, can iterate over them, and can even save to a file!
|
3
|
+
class SGF::Collection
|
4
|
+
include Enumerable, Observable
|
5
|
+
|
6
|
+
attr_accessor :current_node, :errors, :gametrees
|
7
|
+
attr_reader :root
|
8
|
+
|
9
|
+
def initialize(root = SGF::Node.new)
|
10
|
+
@root = root
|
11
|
+
@current_node = @root
|
12
|
+
@root.add_observer(self)
|
13
|
+
@errors = []
|
14
|
+
@gametrees = @root.children.map do |root_of_tree|
|
15
|
+
SGF::Gametree.new(root_of_tree)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def each
|
20
|
+
gametrees.each do |game|
|
21
|
+
game.each do |node|
|
22
|
+
yield node
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def <<(gametree)
|
28
|
+
unless gametree.instance_of?(SGF::Gametree)
|
29
|
+
raise ArgumentError, "Expected instance of class SGF::Gametree but was instance of #{gametree.class}"
|
30
|
+
end
|
31
|
+
@root.add_children gametree.root
|
32
|
+
end
|
33
|
+
|
34
|
+
# Compares a tree to another tree, node by node.
|
35
|
+
# Nodes must be the same (same properties, parents and children).
|
36
|
+
def == other
|
37
|
+
self.map { |node| node } == other.map { |node| node }
|
38
|
+
end
|
39
|
+
|
40
|
+
def inspect
|
41
|
+
out = "#<SGF::Collection:#{self.object_id}, "
|
42
|
+
out << "#{gametrees.count} Games, "
|
43
|
+
out << "#{node_count} Nodes"
|
44
|
+
out << ">"
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
SGF::Writer.new.stringify_tree_from @root
|
49
|
+
end
|
50
|
+
|
51
|
+
# Saves the Collection as an SGF file. Takes a filename as argument.
|
52
|
+
def save filename
|
53
|
+
SGF::Writer.new.save(@root, filename)
|
54
|
+
end
|
55
|
+
|
56
|
+
def update(message, data)
|
57
|
+
case message
|
58
|
+
when :new_children
|
59
|
+
data.each do |new_gametree_root|
|
60
|
+
@gametrees << SGF::Gametree.new(new_gametree_root)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def node_count
|
68
|
+
gametrees.inject(0) { |sum, game| sum + game.count }
|
69
|
+
end
|
70
|
+
|
71
|
+
def method_missing method_name, *args
|
72
|
+
super(method_name, args) if @root.children.empty? || !@root.children[0].properties.has_key?(method_name)
|
73
|
+
@root.children[0].properties[method_name]
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative 'node'
|
2
|
+
require_relative 'collection'
|
3
|
+
|
4
|
+
class SGF::CollectionAssembler
|
5
|
+
attr_reader :collection
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@collection = SGF::Collection.new
|
9
|
+
@current_node = @collection.root
|
10
|
+
@branches = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def open_branch
|
14
|
+
@branches.unshift @current_node
|
15
|
+
end
|
16
|
+
|
17
|
+
def close_branch
|
18
|
+
@current_node = @branches.shift
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_node_with_properties properties
|
22
|
+
node = SGF::Node.new
|
23
|
+
@current_node.add_children node
|
24
|
+
@current_node = node
|
25
|
+
@current_node.add_properties properties
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_error message
|
29
|
+
collection.errors << message
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
data/lib/sgf/error.rb
CHANGED