File Coverage

blib/lib/Games/Go/SGF/Grove.pm
Criterion Covered Total %
statement 38 178 21.3
branch 15 70 21.4
condition 11 27 40.7
subroutine 9 36 25.0
pod 4 4 100.0
total 77 315 24.4


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Games::Go::SGF::Grove - SGF the Perl way
4              
5             =head1 SYNOPSIS
6              
7             use Games::Go::SGF::Grove;
8              
9             $game = load_sgf $path;
10             save_sgf $path, $game;
11              
12             $game = decode_sgf $sgf_data;
13             $sgf_data = encode_sgf $game;
14              
15             =head1 DESCRIPTION
16              
17             This module loads and saves Go SGF files. Unlike other modules, it doesn't
18             build a very fancy data structure with lot's of java-like accessors
19             but instead returns a simple Perl data structure you can inspect with
20             Data::Dumper and modify easily. The structure follows the SGF file format
21             very closely.
22              
23             The SGF format is documented here: L.
24              
25             All the functions below use a common data format and throw exceptions on
26             any errors.
27              
28             =over 4
29              
30             =cut
31              
32             package Games::Go::SGF::Grove;
33              
34 1     1   848 use strict;
  1         2  
  1         35  
35 1     1   4 no warnings;
  1         1  
  1         36  
36              
37 1     1   5 use Carp;
  1         5  
  1         85  
38              
39 1     1   5 use base Exporter::;
  1         1  
  1         347  
40              
41             our $VERSION = '1.01';
42             our @EXPORT = qw(load_sgf save_sgf encode_sgf decode_sgf);
43              
44             =item $game = load_sgf $path
45              
46             Tries to read the file given by C<$path> and parses it as an SGF file,
47             returning the parsed data structure.
48              
49             =item save_sgf $path, $game
50              
51             Saves the SGF data to the specified file.
52              
53             =item $game = decode_sgf $sgf_data
54              
55             Tries to parse the given string into a Pelr data structure and returns it.
56              
57             =item $sgf_data = encode_sgf $game
58              
59             Takes a Perl data structure and serialises it into an SGF file. Anything
60             stored in the structure that isn't understood by this module will be
61             silently ignored.
62              
63             =cut
64              
65             sub decode_sgf($) {
66 0     0 1 0 my ($sgf_data) = @_;
67              
68 0         0 Games::Go::SGF::Grove::Parser::->new->decode_sgf ($sgf_data)
69             }
70              
71             sub encode_sgf($) {
72 0     0 1 0 my ($game) = @_;
73              
74 0         0 Games::Go::SGF::Grove::Parser::->new->encode_sgf ($game)
75             }
76              
77             sub load_sgf($) {
78 0     0 1 0 my ($path) = @_;
79              
80 0 0       0 open my $fh, "<:perlio", $path
81             or Carp::croak "$path: $!";
82              
83 0         0 local $/;
84 0         0 decode_sgf <$fh>
85             }
86              
87             sub save_sgf($$) {
88 0     0 1 0 my ($path, $game) = @_;
89              
90 0 0       0 open my $fh, ">:perlio", $path
91             or Carp::croak "$path: $!";
92              
93 0         0 print $fh encode_sgf $game;
94             }
95              
96             =back
97              
98             =head2 The Game Data structure
99              
100             The SGF game is represented by a linked Perl data structure consisting of
101             unblessed hashes and arrays.
102              
103             SGF files are a forest of trees, called a collection (i.e. you can have
104             multiple games stored in a file). The C and C
105             functions returns this collection as a reference to an array containing
106             the individual game trees (usually there is only one, though).
107              
108             Each individual tree is again an array of nodes representing the main line
109             of play.
110              
111             Each node is simply a hash reference. Each SGF property is stored with the
112             (uppercase) property name as the key, and a property-dependent value for
113             the contents (e.g., a black move is stored as C<< B => [3, 5] >>.
114              
115             If there are any variations/branches/alternate lines of play, then these
116             are stored in the array reference in the C key (those again
117             are game trees, so array references themselves).
118              
119             This module reserves all uppercase key names for SGF properties, the key
120             C and all keys starting with an underscore (C<_xxx>) as it's
121             own. Users of this module may store additional attributes that don't
122             conflict with these names in any node.
123              
124             Unknown properties will be stored as scalars with the (binary) property
125             contents. Text nodes will always be decoded into Unicode text and encoded
126             into whatever the CA property of the root node says (default: C).
127              
128             When saving, all uppercase keys will be saved, lowercase keys will be
129             ignored.
130              
131             For the actual encoding of other types, best decode some example game that
132             contains them and use Data::Dumper. Here is such an example:
133              
134             [ # list of game-trees, only one here
135             [ # the main node sequence
136             { # the root node, contains some variations
137             DI => '7k',
138             AP => undef,
139             CO => '5',
140             DP => '40',
141             GE => 'tesuji',
142             AW => [
143             [ 2, 16 ], [ 3, 15 ], [ 15, 9 ], [ 14, 13 ], ...
144             ],
145             C => 'White just played a ladder block at h12.',
146             variations => [ # list of variations, only one
147             [ # sequence of variation moves
148             { B => [ 7, 5 ] }, # a black move
149             { W => [ 12, 12 ] }, # a white move
150             ... and so on
151             ]
152             ],
153             }
154             ]
155             }
156              
157             =cut
158              
159             package Games::Go::SGF::Grove::Parser;
160              
161 1     1   4 no warnings;
  1         2  
  1         26  
162 1     1   4 use strict 'vars';
  1         1  
  1         23  
163              
164 1     1   229290 use Encode ();
  1         76353  
  1         32  
165 1     1   13 use Carp qw(croak);
  1         2  
  1         3869  
166              
167             my $ws = qr{[\x00-\x20]*}s;
168             my $property; # property => propertyinfo
169              
170             sub new {
171 0     0   0 my $class = shift;
172 0         0 bless { @_ }, $class;
173             }
174              
175             sub error {
176 0     0   0 my ($self, $error) = @_;
177              
178 0         0 my $pos = pos $self->{sgf};
179              
180 0         0 my $tail = substr $self->{sgf}, $pos, 32;
181 0         0 $tail =~ s/[\x00-\x1f]+/ /g;
182              
183 0         0 croak "$error (at octet $pos, '$tail')";
184             }
185              
186             sub decode_sgf {
187 0     0   0 my ($self, $sgf) = @_;
188              
189             # correct lines
190 0 0       0 if ($sgf =~ /[^\015\012]\015/) {
191 0         0 $sgf =~ s/\015\012?/\n/g;
192             } else {
193 0         0 $sgf =~ s/\012\015?/\n/g;
194             }
195              
196 0         0 $self->{sgf} = $sgf;
197              
198 0         0 $self->{FF} = 1;
199 0         0 $self->{CA} = 'WINDOWS-1252'; # too many files are
200 0         0 $self->{GM} = 1;
201              
202 0         0 my @trees;
203              
204 0         0 eval {
205 0         0 while ($self->{sgf} =~ /\G$ws(?=\()/sgoc) {
206 0         0 push @trees, $self->decode_GameTree;
207             }
208             };
209              
210 0 0       0 croak $@ if $@;
211              
212             \@trees
213 0         0 }
214              
215             sub decode_GameTree {
216 0     0   0 my ($self) = @_;
217              
218 0 0       0 $self->{sgf} =~ /\G$ws\(/sgoc
219             or $self->error ("GameTree does not start with '('");
220              
221 0         0 my $nodes = $self->decode_Sequence;
222              
223 0         0 while ($self->{sgf} =~ /\G$ws(?=\()/sgoc) {
224 0         0 push @{$nodes->[-1]{variations}}, $self->decode_GameTree;
  0         0  
225             }
226 0 0       0 $self->{sgf} =~ /\G$ws\)/sgoc
227             or $self->error ("GameTree does not end with ')'");
228              
229 0         0 $nodes
230             }
231              
232             sub postprocess {
233 0     0   0 my $self = shift;
234              
235 0         0 for (@_) {
236 0 0       0 if ("ARRAY" eq ref) {
    0          
237 0         0 $self->postprocess (@$_);
238             } elsif ("HASH" eq ref) {
239 0 0       0 if (exists $_->{_text}) {
240 0         0 my $value = $_->{_text};
241 0         0 $value =~ s/\\\n/ /g;
242 0         0 $value =~ s/\\(.)/$1/g;
243 0   0     0 $_ = eval { Encode::decode $self->{CA}, $value } || $value;
244             } else {
245 0         0 $self->postprocess (values %$_);
246             }
247             }
248             }
249             }
250              
251             sub decode_Sequence {
252 0     0   0 my ($self) = @_;
253              
254 0         0 my (@nodes, $node, $name, $value, $prop, @val);
255              
256 0         0 while ($self->{sgf} =~ /\G$ws;/goc) {
257 0         0 push @nodes, $node = {};
258             # Node
259 0         0 while ($self->{sgf} =~ /\G$ws([A-Za-z]+)/goc) {
260             # Property
261 0         0 $name = $1;
262 0         0 $name =~ y/a-z//d; # believe me, they exist
263 0         0 $prop = $property->{$name};
264              
265 0         0 while ($self->{sgf} =~
266             /
267             \G $ws
268             \[
269             (
270             (?:
271             [^\\\]]+ # any sequence without \ or ]
272             | \\. # any quoted char
273             | \] # hack to allow ] followed by somehting that doesn't look like SGF
274             (?! \s* (?: [A-Z]+\[ | [\[;()] ) )
275             )*
276             )
277             \]
278             /sgocx
279             ) {
280             # PropValue
281 0         0 $value = $1;
282 0 0       0 if ($prop) {
283 0         0 @val = $prop->{in}->($self, $value, $prop);
284              
285 0 0       0 if ($prop->{is_list}) {
286 0         0 push @{$node->{$name}}, @val
  0         0  
287             } else {
288 0         0 $node->{$name} = $val[0];
289              
290 0 0       0 $self->{CA} = $val[0] if $name eq "CA";
291             }
292             } else {
293             #warn "unknown property '$name', will be saved unchanged.";#d#
294 0         0 push @{$node->{$name}}, $value;
  0         0  
295             }
296             }
297             }
298              
299             # postprocess nodes, currently only to decode text and simpletext
300 0         0 $self->postprocess ($node);
301             }
302              
303             \@nodes
304 0         0 }
305              
306             sub encode_sgf($) {
307 0     0   0 my ($self, $game) = @_;
308              
309 0         0 $self->{sgf} = "";
310              
311 0         0 $self->{FF} = 4;
312 0         0 $self->{CA} = 'UTF-8';
313 0         0 $self->{GM} = 1;
314 0         0 $self->{AP} = ["Games::Go::SGF::Grove", $VERSION];
315              
316 0         0 $self->encode_GameTree ($_, 1) for @$game;
317              
318 0         0 $self->{sgf}
319             }
320              
321             sub encode_GameTree {
322 0     0   0 my ($self, $sequence, $is_root) = @_;
323              
324 0 0       0 if ($is_root) {
325 0         0 my $root = $sequence->[0];
326              
327 0   0     0 $root->{CA} ||= $self->{CA};
328 0   0     0 $root->{FF} ||= $self->{FF};
329 0   0     0 $root->{GM} ||= $self->{GM};
330 0   0     0 $root->{AP} ||= $self->{AP};
331              
332 0         0 $self->{CA} = $root->{CA};
333             }
334              
335 0         0 $self->{sgf} .= "(";
336 0         0 $self->encode_Sequence ($sequence);
337 0         0 $self->{sgf} .= ")";
338             }
339              
340             sub encode_Sequence {
341 0     0   0 my ($self, $sequence) = @_;
342              
343 0         0 my ($value, $prop);
344              
345 0         0 for my $node (@$sequence) {
346 0         0 $self->{sgf} .= ";";
347              
348 0         0 for my $name (sort keys %$node) {
349 0 0       0 next unless $name eq uc $name;
350              
351 0         0 $value = $node->{$name};
352              
353 0         0 $self->{sgf} .= "$name\[";
354              
355 0 0       0 if ($prop = $property->{$name}) {
356 0 0       0 if ($prop->{is_list}) {
357 0         0 $self->{sgf} .= join "][", map $prop->{out}->($self, $_), @$value;
358             } else {
359 0         0 $self->{sgf} .= $prop->{out}->($self, $value);
360             }
361             } else {
362 0 0       0 $self->{sgf} .=
363             ref $value
364             ? join "][", @$value
365             : $value;
366             }
367              
368 0         0 $self->{sgf} .= "]";
369             }
370              
371 0         0 $self->encode_GameTree ($_) for @{ $node->{variations} };
  0         0  
372             }
373             }
374              
375             #############################################################################
376              
377             =head2 Property Type Structure
378              
379             A property type is a hash like this:
380              
381             {
382             name => "SQ",
383             group => {
384             name => "Markup properties",
385             restrictions => "CR, MA, SL, SQ and TR points must be unique, ...",
386             },
387             related => "TR, CR, LB, SL, AR, MA, LN",
388             function => "Marks the given points with a square.\nPoints must be unique.",
389             propertytype => "-",
390             propvalue => "list of point"
391             is_list => 1,
392             }
393              
394             =cut
395              
396              
397             {
398             my ($group, $name, $value, $prop);
399              
400             my (%char2coord, %coord2char);
401              
402             {
403             my @coord = ("a" .. "z", "A" .. "Z");
404              
405             for (0.. $#coord) {
406             $char2coord{ $coord[$_] } = $_;
407             $coord2char{ $_ } = $coord[$_];
408             }
409             }
410              
411             sub _parsetype($);
412             sub _parsetype {
413 126     126   280 for (shift) {
414 126 100 100     1314 if (s/e?list of //) {
    100 100        
    100 100        
    100 66        
    100          
    100          
    100          
    50          
415 25         41 $prop->{is_list} = 1;
416 25         57 return _parsetype $_;
417              
418             } elsif (s/composed (\S+)\s+(?:':'\s+)?(\S+)//) {
419 7         11 $prop->{composed} = 1;
420 7         19 my ($i, $o) = ($1, $2);
421 7         27 my ($i1, $o1, $i2, $o2) = (_parsetype $i, _parsetype $o);
422             return (
423             sub {
424 0 0   0   0 if ($_[1] =~ /^((?:[^\\:]+|\\.)*)(?::(.*))?$/s) {
425             # or $_[0]->error ("'Compose' ($i:$o) expected, got '$_[1]'");
426 0         0 my ($l, $r) = ($1, $2);
427              
428             [
429 0 0       0 $i1->($_[0], $l),
430             defined $r ? $i2->($_[0], $r) : undef,
431             ]
432             }
433             },
434             sub {
435 0     0   0 $o1->($_[0], $_[1][0])
436             . ":"
437             . $o2->($_[0], $_[1][1])
438             },
439 7         68 );
440              
441             } elsif (s/double//) {
442             return (
443             sub {
444 0 0   0   0 $_[1] =~ /^[12]$/
445             or $_[0]->error ("'Double' (1|2) expected, got '$_[1]'");
446 0         0 $_[1]
447             },
448             sub {
449 0     0   0 $_[1]
450             },
451 7         52 );
452             } elsif (s/color//) {
453             return (
454             sub {
455             # too many broken programs write this wrong
456 0 0   0   0 return "B" if $_[1] eq "1";
457 0 0       0 return "W" if $_[1] eq "2";
458              
459 0 0       0 $_[1] =~ /^[BW]$/i
460             or $_[0]->error ("'Color' (B|W) expected, got '$_[1]'");
461 0         0 lc $_[1]
462             },
463             sub {
464 0     0   0 uc $_[1]
465             },
466 1         11 );
467             } elsif (s/none//) {
468             return (
469             sub {
470 0 0   0   0 $_[1] =~ /^$/i
471             or $_[0]->error ("'None' expected, got '$_[1]'");
472             undef
473 0         0 },
474             sub {
475 0     0   0 "",
476             },
477 3         30 );
478             } elsif (s/point// || s/move// || s/stone//) {
479             return (
480             sub {
481 0 0   0   0 if ($_[2]->{is_list}) {
482 0 0       0 if ($_[1] =~ /^([^:]+):(.*)$/) {
483 0         0 my ($ul, $dr) = ($1, $2);
484 0         0 my ($x1, $y1) = map $char2coord{$_}, split //, $ul;
485 0         0 my ($x2, $y2) = map $char2coord{$_}, split //, $dr;
486 0         0 my @stones;
487 0         0 for (my $d = $x1; $d <= $x2; $d++) {
488 0         0 for (my $i = $y1; $i <= $y2; $i++) {
489 0         0 push @stones, [$d, $i];
490             }
491             }
492 0         0 return @stones;
493             }
494             }
495 0 0       0 $_[1] =~ /^(.)(.)$/
496             ? [ $char2coord{$1}, $char2coord{$2} ]
497             : []
498             },
499             sub {
500 0     0   0 $coord2char{$_[1][0]} . $coord2char{$_[1][1]}
501             },
502 30         278 );
503             } elsif (s/real// || s/number//) {
504             return (
505             sub {
506 0     0   0 $_[1]
507             },
508             sub {
509 0     0   0 $_[1]
510             },
511 18         126 );
512             } elsif (s/text// || s/simpletext//i) {
513             return (
514             sub {
515 0     0     { _text => $_[1] }
516             },
517             sub {
518 0     0     my $str = Encode::encode $_[0]{CA}, $_[1];
519 0           $str =~ s/([\:\]\\])/\\$1/g;
520 0           $str
521             },
522 35         416 );
523             } else {
524 0           die "FATAL: garbled DATA section, unknown type '$_'";
525             }
526             }
527             }
528              
529             while () {
530             if (/^(\S+):\t(.*)/) {
531             if ($name eq "Restrictions") {
532             $group->{restrictions} = $value;
533             } elsif ($name eq "Property") {
534             $property->{$value} =
535             $prop = {
536             name => $value,
537             group => $group,
538             };
539             } elsif ($name ne "") {
540             $prop->{lc $name} = $value;
541             if ($name eq "Propvalue") {
542             ($prop->{in}, $prop->{out}) = _parsetype $value;
543             }
544             }
545             $name = $1;
546             $value = $2;
547             } elsif (/^\t\t(.*)/) {
548             $value .= "\n$1";
549             } elsif (/(\S.*)/) {
550             $group = {
551             name => $1,
552             };
553             } elsif (/^$/) {
554             # nop
555             } else {
556             die "FATAL: DATA section garbled\n";
557             }
558             }
559             }
560              
561             1;
562              
563             =head1 AUTHOR
564              
565             Marc Lehmann
566             Robin Redeker
567              
568             =cut
569              
570             # now node descriptions follow
571              
572             __DATA__