File Coverage

blib/lib/Games/Go/SGF.pm
Criterion Covered Total %
statement 21 203 10.3
branch 0 72 0.0
condition 0 20 0.0
subroutine 7 40 17.5
pod 3 14 21.4
total 31 349 8.8


line stmt bran cond sub pod time code
1             package Games::Go::SGF;
2            
3 1     1   42874 use 5.006;
  1         4  
  1         35  
4 1     1   5 use strict;
  1         2  
  1         27  
5 1     1   4 use warnings;
  1         5  
  1         31  
6 1     1   4 use Carp;
  1         2  
  1         96  
7 1     1   1579 use IO::File;
  1         14713  
  1         137  
8 1     1   990 use English;
  1         3804  
  1         5  
9 1     1   2636 use Parse::RecDescent;
  1         50515  
  1         8  
10            
11             require Exporter;
12            
13             our @ISA = qw(Exporter);
14             our %EXPORT_TAGS = ( 'all' => [ qw() ] );
15             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
16             our @EXPORT = qw();
17             our $VERSION = '0.11';
18             our $AUTOLOAD;
19            
20             {
21             my %nodehash;
22             my %onersfound;
23             my %duplicates;
24            
25             # organise a node's property values and tags
26             # drop leading [, and trailing ]
27            
28             sub store {
29 0     0 0   my ($ident, $values) = @_;
30 0 0         if (exists($nodehash{tags})) {
31 0 0         if ($nodehash{tags} !~ /,$ident,/) {
32 0           $nodehash{tags} .= $ident.',';
33             }
34             } else {
35 0           $nodehash{tags} = ','.$ident.','
36             }
37            
38 0 0         if (exists($nodehash{$ident})){
39 0           $nodehash{$ident} = join (',', $nodehash{$ident}, map (substr($_,1,-1), @{$values}));
  0            
40             } else {
41 0           $nodehash{$ident} = join (',', map (substr($_,1,-1), @{$values}));
  0            
42             }
43             }
44            
45             # detect duplicate tags and mixed nodes
46            
47             sub isDuplicate {
48 0     0 0   my $ident = shift;
49            
50             # bad sgf if more than one of these in a file
51 0           my $oners = ',SZ,GM,ST,FF,CA,AP,RU,SZ,KM,';
52 0 0 0       if (exists($onersfound{$ident}) and $oners =~ /,$ident,/) {
53 0           print 'Duplicated ',$ident, ' property',"\n";
54 0           return 1;
55             }
56 0           $onersfound{$ident} = undef;
57            
58             # bad sgf if any of these are duplicated in a node
59 0           my $singletons = ',B,W,PL,MN,';
60 0 0 0       if (exists($duplicates{$ident}) and $singletons =~ /,$ident,/) {
61 0           print 'Duplicated ',$ident, ' property',"\n";
62 0           return 1;
63             }
64            
65             # bad sgf if both of these are in a node
66 0           my $alones = ',B,W,';
67 0 0 0       if ((grep (exists($duplicates{$_}),('B','W')) ) and $alones =~ /,$ident,/) {
68 0           print $ident, ' property not allowed in this node',"\n";
69 0           return 1;
70             }
71            
72             # flag mixed nodes - if this is B or W, have we already got AB or AW or AE
73 0           my $setup = ',AB,AW,AE,';
74 0 0 0       if ((grep (exists($duplicates{$_}),('B','W')) ) and $setup =~ /,$ident,/) {
75 0           print 'Setup and move in the same node',"\n";
76 0           return 1;
77             }
78            
79             # flag mixed nodes - if this is AB or AW or AE, have we already got B or W
80 0           my $move = ',B,W,';
81 0 0 0       if ((grep (exists($duplicates{$_}),('AB','AW','AE')) ) and $move =~ /,$ident,/) {
82 0           print 'Setup and move in the same node',"\n";
83 0           return 1;
84             }
85            
86 0           $duplicates{$ident} = 0;
87            
88 0           return 0;
89             }
90            
91             # return and clear the tags and values for a node
92            
93             sub unload {
94 0     0 0   my %hash = %nodehash;
95 0           %nodehash = ();
96 0           %duplicates = ();
97 0           return %hash
98             }
99            
100             sub refresh {
101 0     0 0   %onersfound = ();
102 0           %nodehash = ();
103 0           %duplicates = ();
104             }
105            
106             }
107            
108             my $grammar = q{
109             File : GameTree { $return = $item[1]; Games::Go::SGF::refresh }
110             GameTree : '(' Node(s) GameTree(s?) ')' {
111             $return = $item[2];
112             push @{$return} , bless( $item[3], 'Games::Go::SGF::Variation') if (@{$item[3]})
113             }
114             Node : ';' Property(s?) {
115             $return = bless({Games::Go::SGF::unload()}, 'Games::Go::SGF::Node')
116             }
117             Property : ...Validate Tag Value(s) {
118             Games::Go::SGF::store( $item[2], $item[3] );
119             }
120             Validate : (/B\[/|/W\[/) MovePoint
121             |('AB'|'AW'|'AE'|'CR'|'MA'|'SL'|'SQ'|'TR') Point(s)
122             |'PL' Colour
123             |/C\[/ Comment
124             |'AP'|'CA' Value
125             |('SZ'|'FF'|'HA'|'OW'|'OB'|'ST'|'GM') Integer
126             |('BL'|'WL') Real
127             |'LB' Markup(s)
128             |/[A-Z]+/ Value(s)
129             Tag : /[A-Z]+/ { $return = $item[1] }
130             Value : /\[.*?(?
131             Comment : /.*?(?
132             Markup : /\[[a-zA-Z]{2}/ ':' /.*?(?
133             MovePoint : /[a-zA-Z]{2}\][^\[]/ | /\]/
134             Point : /\[[a-zA-Z]{2}\]/
135             Integer : /\[\d+\]/
136             Real :/\[\d+\.\d+\]|\[\d+\]/
137             Colour : /\[[WB]\]/
138             };
139            
140             sub new {
141 0     0 0   my ($class, $file, $grammarflag) = @_;
142 0           my $grammar = _choosegrammar($grammarflag);
143 0 0         my $parser = new Parse::RecDescent $grammar or croak "Bad grammar!\n";
144 0 0         my $fh = IO::File->new($file, '<') or croak $ERRNO;
145 0           my $slurpfile = do { local $/; <$fh> };
  0            
  0            
146 0 0         $fh->close or croak $ERRNO;
147 0           my $a = $parser->File($slurpfile);
148 0 0         defined $a or croak "Bad Go sgf\n";
149 0           bless $a, 'Games::Go::SGF';
150 0           _sew($a);
151 0           return $a;
152             }
153              
154             sub _sew {
155 0     0     my $a = shift;
156 0           $a->[0]->{moves_to_first_variation} = 0;
157 0           for (0..@$a) {
158 0 0         if (ref $a->[$_] eq 'Games::Go::SGF::Variation') {
159 0   0       $a->[0]->{moves_to_first_variation} ||= $_;
160 0           _sew($_) for $a->[$_]->variations;
161             } else {
162 0           $a->[$_]->{next} = $a->[$_+1];
163             }
164             }
165             }
166            
167             sub _choosegrammar {
168 0     0     my $grammarflag = shift;
169 0           my $res;
170 0   0       $grammarflag ||= 'lite';
171 0           for ($grammarflag) {
172 0 0         if ($_ eq 'lite') { $res = $grammar;
  0            
173 0           $res =~ s/\.\.\.Validate//;
174 0           $res =~ s/\[2\], \$item\[3\]/\[1\], \$item\[2\]/;
175 0           $res =~ s/Validate.*Value\(s\)//s;
176 0           $res =~ s/Comment.*eofile/eofile/s;
177 0           last }
178 0 0         if ($_ eq 'full') { $res = $grammar; last }
  0            
  0            
179 0           croak 'Unknown grammar type';
180             }
181 0           return $res
182             }
183            
184             # Game info methods
185            
186             sub date {
187 0     0 0   my ($self, $value) = @_;
188 0 0         _setvalue($self, 'DT', $value) if ($value);
189 0           return $self->[0]->{DT};
190             }
191            
192 0     0 0   sub time { date(@_) }
193            
194             sub white {
195 0     0 0   my ($self, $value) = @_;
196 0 0         _setvalue($self, 'PW', $value) if ($value);
197 0           return $self->[0]->{PW};
198             }
199            
200             sub black {
201 0     0 0   my ($self, $value) = @_;
202 0 0         _setvalue($self, 'PB', $value) if ($value);
203 0           return $self->[0]->{PB};
204             }
205            
206             sub size {
207 0     0 0   my ($self, $value) = @_;
208 0 0         _setvalue($self, 'SZ', $value) if ($value);
209 0           return $self->[0]->{SZ};
210             }
211            
212             sub komi {
213 0     0 0   my ($self, $value) = @_;
214 0 0         _setvalue($self, 'KM', $value) if ($value);
215 0           return $self->[0]->{KM};
216             }
217            
218             sub delete{
219 0     0 1   my ($self, $tag) = @_;
220 0 0         if (exists $self->[0]->{$tag}) {
221 0           delete $self->[0]->{$tag};
222 0           $self->[0]->{tags} =~ s/$tag,?//;
223             }
224             }
225            
226             # change the value of a tag
227             # if a new tag is being created, add it to {tags}
228             sub _setvalue {
229 0     0     my ($self, $tag, $value) = @_;
230 0 0         $self->[0]->{tags} = join(',', $self->[0]->{tags}, $tag) unless (exists $self->[0]->{$tag});
231 0           $self->[0]->{$tag} = $value;
232             }
233            
234 0     0 1   sub move { $_[0]->[$_[1]]; }
235            
236             sub getsgf {
237 0     0 1   my $self = shift;
238 0           my $move_no = 0;
239 0           my $startvar = 1; # used for formatting of output
240 0           my $string = '(';
241            
242 0           while (my $walker = $self->move($move_no++)) {
243 0           $string .= _donode($walker, $startvar);
244 0           $startvar = 0;
245             }
246            
247 0           $string .= ')'."\n";
248 0           return $string
249             }
250            
251             sub _iterate {
252 0     0     my $startpoint = shift;
253 0           my $v = 0;
254 0           my $string;
255 0           my @vars = $startpoint->variations;
256            
257 0           while (defined $vars[$v]){
258 0           $string .= "\n".'(';
259 0           my $startvar = 1;
260 0           for (@{$vars[$v++]}){
  0            
261 0           $string .= _donode($_, $startvar);
262 0           $startvar = 0;
263             }
264 0           $string .= ')';
265             }
266            
267 0           return $string
268             }
269            
270             sub _donode {
271 0     0     my ($node, $startvar) = @_;
272 0           my $string = '';
273 0 0         if (ref($node) eq 'Games::Go::SGF::Node'){
274 0 0         $string .= "\n" unless $startvar;
275 0           $string .= ';';
276 0 0         if ($node->tags) {
277 0           for (split (',', $node->tags)) {
278 0           $string .= $_;
279 0           my $property = $node->$_;
280 0 0         if ($property) {
281 0           for (split (',', $property)) {
282 0           $string .= '['.$_.']';
283             }
284             } else {
285 0           $string .= '[]';
286             }
287             }
288             }
289             } else {
290 0 0         if (ref($node) eq 'Games::Go::SGF::Variation'){
291 0           $string .= _iterate($node);
292             }
293             }
294 0           return $string
295             }
296            
297             sub AUTOLOAD {
298 0     0     my ($self, $value) = @_;
299 0 0         my $type = ref($self) or croak $self.' is not an object';
300 0           my $name = $AUTOLOAD;
301 0           $name =~ s/.*://; # strip fully-qualified portion
302 0 0         _setvalue($self, $name, $value) if ($value);
303 0           return $self->[0]->{$name};
304             }
305            
306             package Games::Go::SGF::Variation;
307             our $AUTOLOAD;
308 0     0     sub mainline { return $_[0]->[0] }
309 0     0     sub variation { return $_[0]->[$_[1]]}
310 0     0     sub variations { return @{$_[0]} }
  0            
311            
312             # This is - as I shouldn't need to tell you - is a dirty hack.
313             # But I like it (Simon)
314             sub AUTOLOAD {
315 0     0     $AUTOLOAD=~ s/Variation/Node/;
316 0           &$AUTOLOAD($_[0]->mainline, @_[1..@_]);
317             }
318 0     0     sub DESTROY { }
319            
320             package Games::Go::SGF::Node;
321             our $AUTOLOAD;
322            
323 0 0   0     sub move { my $node = shift; $node->{B} || $node->{W} }
  0            
324            
325 0     0     sub color { colour(shift) }
326            
327             sub colour {
328 0     0     my $node = shift;
329 0 0         if (exists($node->{B})){'B'}
  0            
330             else {
331 0 0         if (exists($node->{W})){'W'}
  0            
  0            
332             else {'None'}
333             }
334             }
335            
336             sub nodedump {
337 0     0     my $node = shift;
338 0           my $result;
339 0           for (split(',',$node->{tags})) {$result .= join(' ', $_, $node->{$_}, "\n")}
  0            
340 0           return $result
341             }
342            
343             sub tags {
344 0     0     my $node = shift;
345 0           $node->{tags};
346             }
347            
348             sub delete{
349 0     0     my ($node, $tag) = @_;
350 0 0         if (exists $node->{$tag}) {
351 0           delete $node->{$tag};
352 0           $node->{tags} =~ s/$tag,?//;
353             }
354             }
355            
356             sub AUTOLOAD {
357 0     0     my ($node, $value) = @_;
358 0           my $name = $AUTOLOAD;
359 0           $name =~ s/.*://; # strip fully-qualified portion
360 0 0         _nodesetvalue($node, $name, $value) if $value;
361 0           return $node->{$name};
362             }
363            
364             sub _nodesetvalue {
365 0     0     my ($node, $tag, $value) = @_;
366 0 0         if (exists $node->{tags}) {
367 0 0         $node->{tags} = join(',', $node->{tags}, $tag) unless exists $node->{$tag};
368             } else {
369 0           $node->{tags} = $tag;
370             }
371 0           $node->{$tag} = $value;
372             }
373            
374             # Preloaded methods go here.
375            
376             1;
377             __END__