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.
@@ -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
@@ -1,90 +1,87 @@
1
1
  module SGF
2
-
3
- # http://www.red-bean.com/sgf/proplist.html
4
-
5
- class Game
6
-
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"}
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
- "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
- }
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
-
@@ -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
@@ -0,0 +1,16 @@
1
+ class SGF::Variation
2
+ attr_reader :root
3
+ def initialize
4
+ @root = SGF::Node.new
5
+ @size = 1
6
+ end
7
+
8
+ def append node
9
+ @root.add_children node
10
+ @size += 1
11
+ end
12
+
13
+ def size
14
+ @size
15
+ end
16
+ end
@@ -1,3 +1,3 @@
1
1
  module SGF
2
- VERSION = "2.0.0"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -1,54 +1,52 @@
1
- module SGF
2
- class Writer
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
- # Takes a node and a filename as arguments
11
- def save(root_node, filename)
12
- stringify_tree_from root_node
16
+ private
13
17
 
14
- File.open(filename, 'w') { |f| f << @sgf }
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
- private
18
-
19
- def write_tree_from node
20
- @sgf << "\n" << node.to_str(@indentation)
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
- def decide_what_comes_after node
25
- if node.children.size == 1
31
+ def decide_what_comes_after node
32
+ if node.children.size == 1
26
33
  then write_tree_from node.children[0]
27
- else write_new_branch_from node
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
- def open_branch
40
- @sgf << "\n" << whitespace << "("
41
- @indentation += 2
42
- end
38
+ def open_branch
39
+ @sgf << "\n" << whitespace << "("
40
+ @indentation += 2
41
+ end
43
42
 
44
- def close_branch
45
- @indentation -= 2
46
- @indentation = (@indentation < 0) ? 0 : @indentation
47
- @sgf << "\n" << whitespace << ")"
48
- end
43
+ def close_branch
44
+ @indentation -= 2
45
+ @indentation = (@indentation < 0) ? 0 : @indentation
46
+ @sgf << "\n" << whitespace << ")"
47
+ end
49
48
 
50
- def whitespace
51
- " " * @indentation
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