SgfParser 2.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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