SgfParser 2.0.0 → 3.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.
- 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
@@ -0,0 +1,40 @@
|
|
1
|
+
class SGF::IdentityToken
|
2
|
+
def still_inside? char, token_so_far, sgf_stream
|
3
|
+
char != "["
|
4
|
+
end
|
5
|
+
|
6
|
+
def transform token
|
7
|
+
token.gsub "\n", ""
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class SGF::CommentToken
|
12
|
+
def still_inside? char, token_so_far, sgf_stream
|
13
|
+
char != "]" || (char == "]" && token_so_far[-1..-1] == "\\")
|
14
|
+
end
|
15
|
+
|
16
|
+
def transform token
|
17
|
+
token.gsub "\\]", "]"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class SGF::MultiPropertyToken
|
22
|
+
def still_inside? char, token_so_far, sgf_stream
|
23
|
+
return true if char != "]"
|
24
|
+
sgf_stream.peek_skipping_whitespace == "["
|
25
|
+
end
|
26
|
+
|
27
|
+
def transform token
|
28
|
+
token.gsub("][", ",").split(",")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class SGF::GenericPropertyToken
|
33
|
+
def still_inside? char, token_so_far, sgf_stream
|
34
|
+
char != "]"
|
35
|
+
end
|
36
|
+
|
37
|
+
def transform token
|
38
|
+
token
|
39
|
+
end
|
40
|
+
end
|
data/lib/sgf/properties.rb
CHANGED
@@ -1,90 +1,87 @@
|
|
1
1
|
module SGF
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
"
|
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
|
-
"white_team"=>"WT"}
|
2
|
+
# http://www.red-bean.com/sgf/proplist.html
|
3
|
+
class Gametree
|
4
|
+
PROPERTIES = {
|
5
|
+
annotator: "AN",
|
6
|
+
black_octisquares: "BO", # Octi
|
7
|
+
black_rank: "BR",
|
8
|
+
black_team: "BT",
|
9
|
+
copyright: "CP",
|
10
|
+
date: "DT",
|
11
|
+
event: "EV",
|
12
|
+
game_content: "GC",
|
13
|
+
handicap: "HA", # Go
|
14
|
+
initial_position: "IP", # Lines of Action
|
15
|
+
invert_y_axis: "IY", # Lines of Action
|
16
|
+
komi: "KM", # Go
|
17
|
+
match_information: "MI", # Backgammon
|
18
|
+
name: "GN",
|
19
|
+
prongs: "NP", # Octi
|
20
|
+
reserve: "NR", # Octi
|
21
|
+
superprongs: "NS", # Octi
|
22
|
+
opening: "ON",
|
23
|
+
overtime: "OT",
|
24
|
+
black_player: "PB",
|
25
|
+
place: "PC",
|
26
|
+
puzzle: "PZ",
|
27
|
+
white_player: "PW",
|
28
|
+
result: "RE",
|
29
|
+
round: "RO",
|
30
|
+
rules: "RU",
|
31
|
+
setup_type: "SU", # Lines of Action
|
32
|
+
source: "SO",
|
33
|
+
time: "TM",
|
34
|
+
data_entry: "US",
|
35
|
+
white_octisquares: "WO", # Octi
|
36
|
+
white_rank: "WR",
|
37
|
+
white_team: "WT"
|
38
|
+
}
|
40
39
|
end
|
41
40
|
|
42
41
|
class Node
|
43
42
|
PROPERTIES = {
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
"
|
57
|
-
"
|
58
|
-
"
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
"
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
43
|
+
black_move: "B",
|
44
|
+
black_time_left: "BL",
|
45
|
+
bad_move: "BM",
|
46
|
+
doubtful: "DO",
|
47
|
+
interesting: "IT",
|
48
|
+
ko: "KO",
|
49
|
+
set_move_number: "MN",
|
50
|
+
otstones_black: "OB",
|
51
|
+
otstones_white: "OW",
|
52
|
+
tesuji: "TE",
|
53
|
+
white_move: "W",
|
54
|
+
white_time_left: "WL",
|
55
|
+
add_black: "AB",
|
56
|
+
add_empty: "AE",
|
57
|
+
add_white: "AW",
|
58
|
+
player: "PL",
|
59
|
+
arrow: "AR",
|
60
|
+
comment: "C",
|
61
|
+
circle: "CR",
|
62
|
+
dim_points: "DD",
|
63
|
+
even_position: "DM", # Yep. No idea how that makes sense.
|
64
|
+
figure: "FG",
|
65
|
+
good_for_black: "GB",
|
66
|
+
good_for_white: "GW",
|
67
|
+
hotspot: "HO",
|
68
|
+
label: "LB",
|
69
|
+
line: "LN",
|
70
|
+
mark: "MA",
|
71
|
+
node_name: "N",
|
72
|
+
print_move_node: "PM", # Am I going to have to code this?
|
73
|
+
selected: "SL",
|
74
|
+
square: "SQ",
|
75
|
+
triangle: "TR",
|
76
|
+
unclear_position: "UC",
|
77
|
+
value: "V",
|
78
|
+
view: "VW",
|
79
|
+
application: "AP",
|
80
|
+
charset: "CA",
|
81
|
+
file_format: "FF",
|
82
|
+
game: "GM",
|
83
|
+
style: "ST",
|
84
|
+
size: "SZ"
|
85
|
+
}
|
87
86
|
end
|
88
|
-
|
89
87
|
end
|
90
|
-
|
data/lib/sgf/stream.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
class SGF::Stream
|
4
|
+
attr_reader :stream
|
5
|
+
|
6
|
+
def initialize sgf, error_checker
|
7
|
+
sgf = sgf.read if sgf.instance_of?(File)
|
8
|
+
sgf = File.read(sgf) if File.exist?(sgf)
|
9
|
+
error_checker.check_for_errors_before_parsing sgf
|
10
|
+
@stream = StringIO.new clean(sgf), 'r'
|
11
|
+
end
|
12
|
+
|
13
|
+
def eof?
|
14
|
+
stream.eof?
|
15
|
+
end
|
16
|
+
|
17
|
+
def next_character
|
18
|
+
!stream.eof? && stream.sysread(1)
|
19
|
+
end
|
20
|
+
|
21
|
+
def read_token format
|
22
|
+
property = ""
|
23
|
+
while char = next_character and format.still_inside? char, property, self
|
24
|
+
property << char
|
25
|
+
end
|
26
|
+
format.transform property
|
27
|
+
end
|
28
|
+
|
29
|
+
def peek_skipping_whitespace
|
30
|
+
while char = next_character
|
31
|
+
next if char[/\s/]
|
32
|
+
break
|
33
|
+
end
|
34
|
+
rewind if char
|
35
|
+
char
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def rewind
|
41
|
+
stream.pos -= 1
|
42
|
+
end
|
43
|
+
|
44
|
+
def clean sgf
|
45
|
+
sgf.gsub("\\\\n\\\\r", '')
|
46
|
+
.gsub("\\\\r\\\\n", '')
|
47
|
+
.gsub("\\\\r", '')
|
48
|
+
.gsub("\\\\n", '')
|
49
|
+
end
|
50
|
+
end
|
data/lib/sgf/version.rb
CHANGED
data/lib/sgf/writer.rb
CHANGED
@@ -1,54 +1,52 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
class SGF::Writer
|
2
|
+
# Takes a node and a filename as arguments
|
3
|
+
def save(root_node, filename)
|
4
|
+
#TODO - accept any I/O object?
|
5
|
+
stringify_tree_from root_node
|
6
|
+
File.open(filename, 'w') { |f| f << @sgf }
|
7
|
+
end
|
3
8
|
|
4
9
|
def stringify_tree_from root_node
|
5
10
|
@indentation = 0
|
6
11
|
@sgf = ""
|
7
12
|
write_new_branch_from root_node
|
13
|
+
@sgf
|
8
14
|
end
|
9
15
|
|
10
|
-
|
11
|
-
def save(root_node, filename)
|
12
|
-
stringify_tree_from root_node
|
16
|
+
private
|
13
17
|
|
14
|
-
|
18
|
+
def write_new_branch_from node
|
19
|
+
node.each_child do |child_node|
|
20
|
+
open_branch
|
21
|
+
write_tree_from child_node
|
22
|
+
close_branch
|
15
23
|
end
|
24
|
+
end
|
16
25
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
decide_what_comes_after node
|
22
|
-
end
|
26
|
+
def write_tree_from node
|
27
|
+
@sgf << "\n" << node.to_s(@indentation)
|
28
|
+
decide_what_comes_after node
|
29
|
+
end
|
23
30
|
|
24
|
-
|
25
|
-
|
31
|
+
def decide_what_comes_after node
|
32
|
+
if node.children.size == 1
|
26
33
|
then write_tree_from node.children[0]
|
27
|
-
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def write_new_branch_from node
|
32
|
-
node.each_child do |child_node|
|
33
|
-
open_branch
|
34
|
-
write_tree_from child_node
|
35
|
-
close_branch
|
36
|
-
end
|
34
|
+
else write_new_branch_from node
|
37
35
|
end
|
36
|
+
end
|
38
37
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
38
|
+
def open_branch
|
39
|
+
@sgf << "\n" << whitespace << "("
|
40
|
+
@indentation += 2
|
41
|
+
end
|
43
42
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
43
|
+
def close_branch
|
44
|
+
@indentation -= 2
|
45
|
+
@indentation = (@indentation < 0) ? 0 : @indentation
|
46
|
+
@sgf << "\n" << whitespace << ")"
|
47
|
+
end
|
49
48
|
|
50
|
-
|
51
|
-
|
52
|
-
end
|
49
|
+
def whitespace
|
50
|
+
" " * @indentation
|
53
51
|
end
|
54
|
-
end
|
52
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require_relative '../lib/sgf'
|
3
|
+
|
4
|
+
RSpec.describe 'End To End' do
|
5
|
+
let(:new_file) { full_path_to_file('./simple_changed.sgf', starting_point: __FILE__) }
|
6
|
+
|
7
|
+
after do
|
8
|
+
File.delete(new_file) if File.exist?(new_file)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should modify an object and save the changes' do
|
12
|
+
collection = SGF.parse(full_path_to_file('./data/simple.sgf', starting_point: __FILE__))
|
13
|
+
game = collection.gametrees.first
|
14
|
+
game.current_node[:PB] = 'kokolegorille'
|
15
|
+
|
16
|
+
expect(game.current_node[:PB]).to eq 'kokolegorille'
|
17
|
+
collection.save(new_file)
|
18
|
+
expect(game.current_node[:PB]).to eq 'kokolegorille'
|
19
|
+
expect(SGF.parse(new_file).gametrees.first.current_node[:PB]).to eq 'kokolegorille'
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'throws an error if asked to open a non-existing file'do
|
23
|
+
expect do
|
24
|
+
SGF.parse('some_file.sgf')
|
25
|
+
end.to raise_error(SGF::FileDoesNotExistError)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe SGF::Collection do
|
4
|
+
|
5
|
+
let(:collection) { get_collection_from 'spec/data/ff4_ex.sgf' }
|
6
|
+
subject { collection }
|
7
|
+
|
8
|
+
it "has two games" do
|
9
|
+
expect(subject.gametrees.size).to eq 2
|
10
|
+
end
|
11
|
+
|
12
|
+
context 'on the gametree level' do
|
13
|
+
subject { collection.root }
|
14
|
+
it "should have two children" do
|
15
|
+
expect(subject.children.size).to eq 2
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'in the first gametree' do
|
19
|
+
subject { collection.root.children[0] }
|
20
|
+
it "should have five children" do
|
21
|
+
expect(subject.children.size).to eq 5
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "inspect" do
|
27
|
+
subject { collection.inspect }
|
28
|
+
it { is_expected.to match(/SGF::Collection/) }
|
29
|
+
it { is_expected.to match(/#{collection.object_id}/) }
|
30
|
+
it { is_expected.to match(/2 Games/) }
|
31
|
+
it { is_expected.to match(/62 Nodes/) }
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should use preorder traversal for each" do
|
35
|
+
collection = get_collection_from 'spec/data/example1.sgf'
|
36
|
+
array = []
|
37
|
+
collection.each { |node| array << node }
|
38
|
+
expect(array[0].c).to eq "root"
|
39
|
+
expect(array[1].c).to eq "a"
|
40
|
+
expect(array[2].c).to eq "b"
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should properly compare two collections" do
|
44
|
+
new_collection = get_collection_from 'spec/data/ff4_ex.sgf'
|
45
|
+
expect(collection).to eq new_collection
|
46
|
+
end
|
47
|
+
|
48
|
+
context "gametrees" do
|
49
|
+
it "always returns the same objects for the same gametrees" do
|
50
|
+
expect(subject.gametrees).to eq subject.gametrees
|
51
|
+
end
|
52
|
+
|
53
|
+
it "knows if you've added a new gametree" do
|
54
|
+
expect do
|
55
|
+
subject << SGF::Gametree.new(SGF::Node.new)
|
56
|
+
end.to change { subject.gametrees.count }.by(1)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
it "barfs if you try to add a non-gametree object" do
|
61
|
+
expect do
|
62
|
+
subject << Object.new
|
63
|
+
end.to raise_error(ArgumentError)
|
64
|
+
end
|
65
|
+
end
|