File Coverage

blib/lib/Games/Go/SGF2misc.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             package Games::Go::SGF2misc;
2              
3 6     6   33300 use strict;
  6         15  
  6         205  
4 6     6   31 no warnings;
  6         12  
  6         224  
5              
6 6     6   29 use Carp;
  6         13  
  6         320  
7 6     6   5107 use Parse::Lex;
  6         140406  
  6         176  
8 6     6   7067 use Data::Dumper;
  6         60641  
  6         451  
9 6     6   6163 use Compress::Zlib;
  6         462951  
  6         1758  
10 6     6   13146 use CGI qw(escapeHTML);
  6         91647  
  6         43  
11              
12             our $VERSION = 0.9782;
13              
14             1;
15              
16             # main calls
17             # new {{{
18             sub new {
19             my $this = shift;
20             $this = bless {}, $this;
21              
22             if( $ENV{DEBUG} > 0 ) {
23 6     6   6974 use Number::Format;
  6         91777  
  6         395  
24 6     6   10061 use Devel::Size qw(total_size);
  0            
  0            
25             use Time::HiRes qw(time);
26              
27             $this->{frm} = new Number::Format;
28             }
29              
30             return $this;
31             }
32             # }}}
33             # parse {{{
34             sub parse {
35             my $this = shift;
36             my $file = shift;
37              
38             if( -f $file ) {
39             local $/; # Enable local "slurp" ... ie, by unsetting $/ for this local scope, it will not end lines on \n
40             open SGFIN, $file or die "couldn't open $file: $!";
41              
42             our $FILENAME = $file;
43              
44             return $this->parse_internal(\*SGFIN);
45             }
46              
47             $this->{error} = "Parse Error reading $file: unknown";
48             return 0;
49             }
50             # }}}
51             # parse_string {{{
52             sub parse_string {
53             my $this = shift;
54             my $string = shift;
55              
56             our $FILENAME = "STRING";
57              
58             return $this->parse_internal($string);
59             }
60             # }}}
61             # parse_internal {{{
62             sub parse_internal {
63             my $this = shift;
64             my $file = shift;
65              
66             our $FILENAME;
67              
68             for my $k (keys %$this) {
69             delete $this->{$k} unless {Time=>1, frm=>1}->{$k};
70             }
71             $global::lex_error = undef;
72              
73             $this->_time("parse");
74              
75             my @rules = (
76             VALUE => '(?:\[\]|\[(?s:.*?[^\x5c])\])',
77              
78             BCOL => '\(', # begin collection
79             ECOL => '\)', # end collection
80             PID => '(?:CoPyright|[A-Z]+)', # property identifier (CoPyright is the spurious IGS tag, assholes)
81             NODE => ';', # new node
82             WSPACE => '[\s\r\n]',
83              
84             qw(ERROR .*), sub {
85             $global::lex_error = "Parse Error reading $FILENAME: $_[1]\n";
86             }
87             );
88              
89             Parse::Lex->trace if $ENV{DEBUG} > 30;
90              
91             my $lex = Parse::Lex->new(@rules); $^W = 0; no warnings;
92             $lex->from($file);
93              
94             $this->{parse} = { p => undef, n => [], c=>[] }; # p => parent, n => nodes, c => child Collections
95              
96             my $ref = $this->{parse}; # our current position
97              
98             # parse rules:
99             my $nos = -1; # the current node (array position). -1 when we're not in a node
100             my $pid = 0; # 0 unless we just got a pid; otherwise, node array position
101              
102             TOKEN: while (1) {
103             my $token = $lex->next;
104              
105             if (not $lex->eoi) {
106             my $C = $token->name;
107             my $V = $token->text;
108              
109             if( $C eq "ERROR" or defined $global::lex_error ) {
110             $global::lex_error = "Parse Error reading $FILENAME: unknown"; # TODO: this $file should be ... the name of it instead
111             $this->{error} = $global::lex_error;
112             return 0;
113             }
114              
115             if( $C eq "BCOL" ) {
116             push @{ $ref->{c} }, { p=>$ref, n=>[], c=>[] };
117             $ref = $ref->{c}[$#{ $ref->{c} }];
118             $nos = -1;
119              
120             } elsif( $C eq "ECOL" ) {
121             $ref = $ref->{p};
122             $nos = -1;
123              
124             } elsif( $C eq "NODE" ) {
125             push @{ $ref->{n} }, [];
126             $nos = $#{ $ref->{n} };
127              
128             }
129            
130             # this get's it's own if block for the $pid
131             if( $C eq "PID" ) {
132             if( $nos == -1 ) {
133             $this->{error} = "Parse Error reading $FILENAME: property identifier ($V) in strange place";
134             return 0;
135             }
136             push @{ $ref->{n}[$nos] }, {P=>$V};
137             $pid = $#{ $ref->{n}[$nos] };
138              
139             } elsif( $C eq "VALUE" ) {
140             $V =~ s/^\[//ms; $V =~ s/\]$//ms;
141             $V =~ s/\\(.)/$1/msg;
142              
143             if( $nos == -1 or $pid == -1 ) {
144             $this->{error} = "Parse Error reading $FILENAME: property value ($V) in strange place";
145             return 0;
146             }
147             if( defined $ref->{n}[$nos][$pid]{V} ) {
148             push @{ $ref->{n}[$nos] }, {P=>$ref->{n}[$nos][$pid]{P}};
149             $pid = $#{ $ref->{n}[$nos] };
150             }
151              
152             $ref->{n}[$nos][$pid]{V} = $V;
153              
154             } elsif( $C eq "WSPACE" ) {
155             # don't set pid to -1 here (2006-5-12)
156              
157             } else {
158             $pid = -1;
159             }
160              
161             } else {
162             last TOKEN;
163             }
164             }
165              
166             $this->_time("parse");
167              
168             print STDERR "SGF Parsed! Calling internal _parse() routine\n" if $ENV{DEBUG} > 0;
169             print STDERR "\$this size (before _parse)= ", $this->{frm}->format_bytes(total_size( $this )), "\n" if $ENV{DEBUG} > 0;
170              
171             $this->_time("_parse");
172              
173             my $r = $this->_parse(0, $this->{parse});
174              
175             $this->_time("_parse");
176              
177             print STDERR "\$this size (after _parse)= ", $this->{frm}->format_bytes(total_size( $this )), "\n" if $ENV{DEBUG} > 0;
178              
179             print STDERR "rebuilding {refdb} (for ref2id/id2ref)\n" if $ENV{DEBUG} > 0;
180              
181             $this->_time("_nodelist");
182              
183             $this->{nodelist} = { map {$this->_ref2id($_) => $this->_nodelist([], $_)} @{$this->{gametree}} };
184              
185             $this->_time("_nodelist");
186              
187             print STDERR "\$this size (after _nodelist())= ", $this->{frm}->format_bytes(total_size( $this )), "\n" if $ENV{DEBUG} > 0;
188              
189             $this->_time("nuke(gametree and parse)");
190             my @to_nuke;
191            
192             push @to_nuke, (@{$this->{gametree}}) if ref($this->{gametree}) eq "ARRAY";
193             push @to_nuke, $this->{parse} if ref($this->{parse}) eq "HASH";
194              
195             while( @to_nuke ) {
196             my $ref = shift @to_nuke;
197             for my $k (qw(p c kids parent)) {
198             if( my $v = $ref->{$k} ) {
199             if( ref($v) eq "ARRAY" ) {
200             push @to_nuke, @$v;
201             }
202              
203             delete $ref->{$k};
204             }
205             }
206             }
207             $this->_time("nuke(gametree and parse)");
208              
209             $this->_show_timings if $ENV{DEBUG} > 0;
210              
211             return $r;
212             }
213             # }}}
214             # freeze {{{
215             sub freeze {
216             my $this = shift;
217              
218             local $Data::Dumper::Indent = 0;
219             local $Data::Dumper::Purity = 1;
220              
221             my $fm = {};
222             for my $k (qw(nodelist refdb)) {
223             $fm->{$k} = $this->{$k};
224             }
225              
226             $this->_time("freeze Dumper");
227             my $buf = Dumper( $fm );
228             $this->_time("freeze Dumper");
229              
230             return Compress::Zlib::memGzip( $buf );
231             }
232             # }}}
233             # thaw {{{
234             sub thaw {
235             my $this = shift;
236             my $frz = shift;
237             my ($VAR1);
238              
239             if( ref($frz) eq "GLOB" ) {
240             $this->_time("gzreads");
241             my $gz = gzopen($frz, "r");
242              
243             $frz = "";
244              
245             my $x;
246             while( my $r = $gz->gzread($x, 32768) ) {
247             $frz .= $x;
248             }
249             $gz->gzclose;
250              
251             $this->_time("gzreads");
252              
253             $this->_time("eval");
254             eval $frz;
255             $this->_time("eval");
256             } else {
257             $this->_time("memgunzip/eval");
258             eval Compress::Zlib::memGunzip( $frz );
259             $this->_time("memgunzip/eval");
260             if( $@ ) {
261             $this->{error} = $@;
262             return 0;
263             }
264             }
265              
266             $this->_time("assign refs");
267             for my $k (keys %$VAR1) {
268             $this->{$k} = $VAR1->{$k};
269             }
270             $this->_time("assign refs");
271              
272             $this->_show_timings if $ENV{DEBUG} > 0;
273              
274             return 1;
275             }
276             # }}}
277             # errstr {{{
278             sub errstr {
279             my $this = shift;
280              
281             $this->{error} =~ s/[\r\n\s]$//msg;
282              
283             return $this->{error};
284             }
285             # }}}
286              
287             # tools -- can croak()!
288             # sgfco2numco {{{
289             sub sgfco2numco {
290             my $this = shift;
291             my $gref = shift;
292             my $co = shift;
293              
294             my ($sz, $ff);
295             if( ref($gref) eq "HASH" and ref($gref->{game_properties}) eq "HASH" ) {
296             $sz = $gref->{game_properties}{SZ};
297             $ff = $gref->{game_properties}{FF};
298              
299             unless( $sz and $ff ) {
300             croak "Error: sgfco2numco needs FF and SZ properties to function, sorry.\n";
301             }
302             } else {
303             croak "Syntax Error: You must pass a game reference to sgfco2numco because it needs the FF and SZ properties.\n";
304             }
305              
306             if( $co =~ m/\w{2}\:\w{2}/ ) {
307             croak "Parsed Stupidly: SGF2misc.pm doesn't handle compressed co-ordinates ($co) yet... *sigh*\n";
308             }
309              
310             my $inty = sub {
311             my $x = -1;
312              
313             $x = int(hex(unpack("H*", $_[0]))) - 97 if $_[0] =~ m/[a-z]/;
314             $x = int(hex(unpack("H*", $_[0]))) - 65 if $_[0] =~ m/[A-Z]/;
315              
316             die "unexpected error reading column identifier" unless $x > -1;
317              
318             return $x;
319             };
320              
321             if( not $co or ($co eq "tt" and ($ff == 3 or $sz<=19)) ) {
322             return (wantarray ? (qw(PASS PASS)) : [qw(PASS PASS)]);
323             }
324              
325             if( $co =~ m/^([a-zA-Z])([a-zA-Z])$/ ) {
326             my ($row, $col) = ($1, $2);
327              
328             return (wantarray ? ($inty->($col), $inty->($row)) : [ $inty->($col), $inty->($row) ]);
329             }
330              
331             croak "Parse Error: co-ordinate not understood ($co)\n";
332             }
333             # }}}
334              
335             # outputers
336             # parse_hash {{{
337             sub parse_hash {
338             my $this = shift;
339              
340             return $this->{parse};
341             }
342             # }}}
343             # nodelist {{{
344             sub nodelist {
345             my $this = shift;
346              
347             return $this->{nodelist};
348             }
349             # }}}
350             # is_node {{{
351             sub is_node {
352             my $this = shift;
353             my $node = shift;
354              
355             return ($this->{refdb}{$node} ? 1:0);
356             }
357             # }}}
358             # as_perl {{{
359             sub as_perl {
360             my $this = shift;
361             my $node = shift;
362             my $soft = shift;
363              
364             if( $node ) {
365             if( my $ref = $this->{refdb}{$node} ) {
366             return $ref;
367             }
368             }
369              
370             $this->{error} = "no such node: $node";
371             return 0 if $soft;
372              
373             croak $this->{error};
374             }
375             # }}}
376             # as_text {{{
377             sub as_text {
378             my $this = shift;
379             my $node = shift;
380              
381             $node = $this->as_perl( $node, 1 ) or croak $this->errstr;
382              
383             my $board = $node->{board};
384              
385             my $x = "";
386             for my $i (0..$#{ $board }) {
387             for my $j (0..$#{ $board->[$i] }) {
388             $x .= " " . { ' '=>'.', 'W'=>'O', 'B'=>'X' }->{$board->[$i][$j]};
389             }
390             $x .= "\n";
391             }
392              
393             return $x;
394             }
395             # }}}
396             # _mark_alg {{{
397             sub _mark_alg {
398             my $this = shift;
399             my ($mark, $img) = @_;
400              
401             return "bt.gif" if $mark eq "TR" and $img eq "b.gif";
402             return "wt.gif" if $mark eq "TR" and $img eq "w.gif";
403             return "bc.gif" if $mark eq "CR" and $img eq "b.gif";
404             return "wc.gif" if $mark eq "CR" and $img eq "w.gif";
405             return "bq.gif" if $mark eq "SQ" and $img eq "b.gif";
406             return "wq.gif" if $mark eq "SQ" and $img eq "w.gif";
407              
408             if( ($mark = int($mark)) > 0 and $mark <= 100 ) {
409             return "b$mark.gif" if $img =~ "b[tcq]?.gif";
410             return "w$mark.gif"
411             }
412              
413             return $img;
414             }
415             # }}}
416             # _crazy_moku_alg {{{
417             sub _crazy_moku_alg {
418             my $this = shift;
419             my ($i, $j, $size) = @_;
420              
421             our $cma_size;
422             our $hoshi;
423              
424             if( $size != $cma_size or not $hoshi ) {
425             $hoshi = {};
426             if( $size == 19 ) {
427             $hoshi = { "3 3" => 1, "3 15" => 1, "15 3" => 1, "15 15" => 1,
428             "9 3" => 1, "9 15" => 1, "3 9" => 1, "15 9" => 1, "9 9" => 1, };
429             } elsif( $size == 13 ) {
430             $hoshi = { "3 3" => 1, "9 9" => 1, "3 9" => 1, "9 3" => 1,
431             "6 3" => 1, "3 6" => 1, "9 6" => 1, "6 9" => 1, "6 6" => 1, };
432             } elsif( $size == 9 ) {
433             $hoshi = { "2 2" => 1, "2 6" => 1, "6 6" => 1, "6 2" => 1, "4 4" => 1, };
434             }
435             }
436              
437             return "ulc.gif" if $i == 0 and $j == 0;
438             return "urc.gif" if $i == 0 and $j == $size;
439             return "llc.gif" if $i == $size and $j == 0;
440             return "lrc.gif" if $i == $size and $j == $size;
441             return "ts.gif" if $i == 0 and $j != 0 and $j != $size;
442             return "bs.gif" if $i == $size and $j != 0 and $j != $size;
443             return "ls.gif" if $j == 0 and $i != 0 and $i != $size;
444             return "rs.gif" if $j == $size and $i != 0 and $i != $size;
445              
446             return "h.gif" if $hoshi->{"$i $j"};
447             return "p.gif",
448             }
449             # }}}
450             # as_html {{{
451             sub as_html {
452             my $this = shift;
453             my $node = shift;
454             my $dir = shift;
455             $dir = "./img" unless $dir;
456             my $id = shift;
457             my $onode = $node;
458              
459             $node = $this->as_perl( $node, 1 ) or croak $this->errstr;
460              
461             # use Data::Dumper; $Data::Dumper::Indent = 0;
462             # warn Dumper( $node );
463              
464             my $gref = $this->as_perl(1);
465             my $game_info = $gref->{game_properties};
466            
467             =cut
468             game_properties' => {'FF' => 4,'PB' => 'Orien Vandenbergh
469             (nichus)','GM' => 1,'KM' => '6.5','SZ' => 19,'PC' => 'Dragon Go Server: http://www.dragongoserver.net','RE' => 'W+29.5','RU' =>
470             'Japanese','BR' => '13 kyu','GN' => 'jettero-nichus-20041229.sgf','GC' => 'Game ID: 85389','DT' => '2004-10-29,2004-12-29','PW' =>
471             'Jettero Heller (jettero)','WR' => '13 kyu','OT' => '30 days + 1 day/10 periods Japanese byoyomi'}
472             =cut
473              
474             my $board = $node->{board};
475             my $size = @{$board->[0]}; # inaccurate?
476             $size--;
477              
478             my %marks = ();
479             for my $m (@{ $node->{marks} }) {
480             $marks{"$m->[1] $m->[2]"} = ($m->[0] eq "LB" ? $m->[4] : $m->[0]);
481             }
482              
483             my @letters = qw(A B C D E F G H J K L M N O P Q R S T);
484             my $arow = "
" . join("", map("$_", @letters[0..$size])) . "";
485              
486             my $x = "$arow\n";
W.Caps: $node->{captures}->{W}\  \ B.Caps: $node->{captures}->{B}";

W($tw t + $cw c + $km k), B($tb t + $cb c): $f";
487              
488             for my $i (0..$#{ $board }) {
489             $x .= "
". (($size+1)-$i);
490             for my $j (0 .. $#{ $board->[$i] }) {
491             my $iid = "";
492             $iid = " id='$id.$i.$j'" if $id;
493              
494             my $c = {
495             'B' => "b.gif",
496             'W' => "w.gif",
497             }->{$board->[$i][$j]};
498              
499             $c = "wc.gif" if $c eq "w.gif" and $node->{moves}[0][1] == $i and $node->{moves}[0][2] == $j;
500             $c = "bc.gif" if $c eq "b.gif" and $node->{moves}[0][1] == $i and $node->{moves}[0][2] == $j;
501              
502             $c = $this->_crazy_moku_alg($i, $j, $size) unless $c;
503             $c = $this->_mark_alg($marks{"$i $j"}, $c);
504              
505             $c = "$dir/$c";
506             $x .= "";
507             }
508             $x .= "". (($size+1)-$i) . "\n";
509             }
510              
511             my $cpid = "";
512             $cpid = " id='$id.state'" if $id;
513              
514             my $p = "
515              
516             if( $node->{other} ) {
517             my $TB = $node->{other}->{TB};
518             my $TW = $node->{other}->{TW};
519              
520             if ($TB and $TW) {
521             my $cb = $node->{captures}->{B};
522             my $cw = $node->{captures}->{W};
523             my $km = $game_info->{KM};
524              
525             my ($tb, $tw) = (0, 0);
526             for my $r (@$TB) {
527             my @a = $this->sgfco2numco($gref, $r);
528              
529             $tb ++;
530             $cb ++ if $board->[$a[0]][$a[1]] eq "W";
531             }
532              
533             for my $r (@$TW) {
534             my @a = $this->sgfco2numco($gref, $r);
535              
536             $tw ++;
537             $cw ++ if $board->[$a[0]][$a[1]] eq "B";
538             }
539              
540             my $f = ($tw + $cw + $km) - ($tb + $cb);
541             $f = ($f<0 ? "B+".abs($f) : "W+$f");
542              
543             $p = "
544             }
545             }
546              
547             my $cmid = "";
548             $cmid = " id='$id.comment'" if $id;
549              
550             my $comments = "";
551             $comments .= escapeHTML($_) for @{$node->{comments}};
552             $comments =~ s/[\r\n]//sg;
553              
554             return "$x$arow$p
$comments";
555             }
556             # }}}
557             # as_js {{{
558             sub as_js {
559             my $this = shift;
560             my $node = shift;
561              
562             $node = $this->as_perl( $node, 1 ) or croak $this->errstr;
563              
564             my $gref = $this->as_perl(1);
565             my $game_info = $gref->{game_properties};
566              
567             my $board = $node->{board};
568             my $size = @{$board->[0]}; # inaccurate?
569             $size--;
570              
571             my %marks = ();
572             for my $m (@{ $node->{marks} }) {
573             $marks{"$m->[1] $m->[2]"} = ($m->[0] eq "LB" ? $m->[4] : $m->[0]);
574             }
575              
576             my @board = ();
577             for my $i (0..$#{ $board }) {
578             my $row = [];
579             for my $j (0 .. $#{ $board->[$i] }) {
580             my $c = {
581             'B' => "b.gif",
582             'W' => "w.gif",
583             }->{$board->[$i][$j]};
584              
585             $c = "wc.gif" if $c eq "w.gif" and $node->{moves}[0][1] == $i and $node->{moves}[0][2] == $j;
586             $c = "bc.gif" if $c eq "b.gif" and $node->{moves}[0][1] == $i and $node->{moves}[0][2] == $j;
587              
588             $c = $this->_crazy_moku_alg($i, $j, $size) unless $c;
589             $c = $this->_mark_alg($marks{"$i $j"}, $c);
590              
591             push @$row, $c;
592             }
593              
594             push @board, $row;
595             }
596              
597             local $Data::Dumper::Indent = 0;
598             my $board = Dumper(\@board);
599             $board =~ s/^\$VAR1\s*=\s*//s;
600             $board =~ s/\s*\;\s*$//s;
601             $board =~ s/\.gif//sg;
602              
603             my $p = "
W.Caps: $node->{captures}->{W}\  \ B.Caps: $node->{captures}->{B}";
604              
605             if( $node->{other} ) {
606             my $TB = $node->{other}->{TB};
607             my $TW = $node->{other}->{TW};
608              
609             if ($TB and $TW) {
610             my $cb = $node->{captures}->{B};
611             my $cw = $node->{captures}->{W};
612             my $km = $game_info->{KM};
613              
614             my ($tb, $tw) = (0, 0);
615             for my $r (@$TB) {
616             my @a = $this->sgfco2numco($gref, $r);
617              
618             $tb ++;
619             $cb ++ if $board->[$a[0]][$a[1]] eq "W";
620             }
621              
622             for my $r (@$TW) {
623             my @a = $this->sgfco2numco($gref, $r);
624              
625             $tw ++;
626             $cw ++ if $board->[$a[0]][$a[1]] eq "B";
627             }
628              
629             my $f = ($tw + $cw + $km) - ($tb + $cb);
630             $f = ($f<0 ? "B+".abs($f) : "W+$f");
631              
632             $p = "
W($tw t + $cw c + $km k), B($tb t + $cb c): $f";
633             }
634             }
635              
636             my $comments = "";
637             $comments .= escapeHTML($_) for @{$node->{comments}};
638             $comments =~ s/[\r\n]//sg;
639              
640             $p =~ s/"/\\"/sg;
641             $comments =~ s/"/\\"/sg;
642              
643             return "{ board: $board, status: \"$p\", comment: \"$comments\" }";
644             }
645             # }}}
646             # as_image {{{
647             sub as_image {
648             my $this = shift;
649             my $node = shift; my $nm = $node;
650             my $argu = shift;
651             my %opts = (imagesize=>256, antialias=>0);
652              
653             $node = $this->as_perl( $node, 1 ) or croak $this->errstr;
654              
655             my $board = $node->{board};
656             my $size = @{$board->[0]}; # inaccurate?
657              
658             if( ref($argu) ne "HASH" ) {
659             croak
660             "as_image() takes a hashref argument... e.g., {imagesize=>256, etc=>1} or nothing at all.";
661             }
662              
663             my $package = $argu->{'use'} || 'Games::Go::SGF2misc::GD';
664             if ($package =~ /svg/i) {
665             $opts{'imagesize'} = '256px';
666             }
667              
668             @opts{keys %$argu} = (values %$argu);
669             $opts{boardsize} = $size;
670             $opts{filename} = "$nm.png" unless $opts{filename};
671              
672             my $image;
673             eval qq( use $package; \$image = $package->new(%opts); );
674              
675             $image->drawGoban();
676              
677             # draw moves
678             for my $i (0..$#{ $board }) {
679             for my $j (0..$#{ $board->[$i] }) {
680             if( $board->[$i][$j] =~ m/([WB])/ ) {
681             if( $ENV{DEBUG} > 0 ) {
682             print STDERR "placeStone($1, [$i, $j])\n";
683             }
684              
685             # SGFs are $y, $x, the matrix is $x, $y ...
686             $image->placeStone(lc($1), [reverse( $i, $j )]);
687             }
688             }
689             }
690              
691             my $marks = 0;
692             # draw marks
693             for my $m (@{ $node->{marks} }) {
694             $image->addCircle($m->[3]) if $m->[0] eq "CR";
695             $image->addSquare($m->[3]) if $m->[0] eq "SQ";
696             $image->addTriangle($m->[3]) if $m->[0] eq "TR";
697              
698             $image->addLetter($m->[3], 'X', "./times.ttf") if $m->[0] eq "MA";
699             $image->addLetter($m->[3], $m->[4], "./times.ttf") if $m->[0] eq "LB";
700             $marks++;
701             }
702              
703             if ($argu->{'automark'}) {
704             unless ($marks) {
705             my $moves = $node->{moves};
706             foreach my $m (@$moves) {
707             $image->addCircle($m->[3]) unless $m->[3];
708             }
709             }
710             }
711              
712             if ($package =~ /svg/i) {
713             if( $opts{filename} =~ m/.png$/ ) {
714             $image->export($opts{'filename'});
715             } else {
716             $image->save($opts{filename});
717             }
718             } else {
719             if( $opts{filename} =~ m/^\-\.(\w+)$/ ) {
720             return $image->dump($1);
721             }
722              
723             $image->save($opts{filename});
724             }
725             }
726             # }}}
727             # as_freezerbag {{{
728             sub as_freezerbag {
729             my $this = shift;
730             my $file = shift or croak "You must name your freezerbag.";
731             my $code = shift;
732             $code = "# your code here\n" unless $code;
733             my $perl = shift;
734              
735             if( not $perl ) {
736             for my $try (qw{ /usr/bin/perl /usr/local/bin/perl }) {
737             $perl = $try if -x $try;
738             }
739             croak "couldn't find perl" unless -x $perl;
740             }
741            
742             open OUTMF, ">$file" or croak "Couldn't open freezerbag ($file) for output: $!";
743             print OUTMF "#!$perl\n# vi:fdm=marker fdl=0:\n\nuse strict;\nno warnings;\nuse Games::Go::SGF2misc;\n\n";
744             print OUTMF "my \$sgf = new Games::Go::SGF2misc;\n";
745             print OUTMF " \$sgf->thaw(\\*DATA);\n\n$code\n\n# freezer DATA {\{\{\n__DATA__\n";
746              
747             $this->_time("print freeze");
748             print OUTMF $this->freeze;
749             $this->_time("print freeze");
750              
751             close OUTMF;
752              
753             $this->_show_timings if $ENV{DEBUG} > 0;
754             }
755             # }}}
756              
757             # internals
758             # _show_timings {{{
759             sub _show_timings {
760             my $this = shift;
761              
762             my @times = ();
763             for my $k (keys %{ $this->{Time} }) {
764             my $x = $this->{Time}{$k}{diffs}; next unless ref($x) eq "ARRAY";
765             my $n = int @$x;
766             my $sum = 0;
767             $sum += $_ for @$x;
768              
769             push @times, [ $k, $sum, $n, ($sum/$n) ];
770             }
771              
772             for my $x (sort {$b->[1] <=> $a->[1]} @times) {
773             printf('%-35s: sum=%3.4fs cardinality=%5d avg=%3.2fs%s', @$x, "\n");
774             }
775              
776             delete $this->{Time};
777             }
778             # }}}
779             # _time {{{
780             sub _time {
781             return unless $ENV{DEBUG} > 0;
782              
783             my $this = shift;
784             my $tag = shift;
785              
786             if( $ENV{DEBUG} == 1.2 ) {
787             my @a;
788              
789             for (sort keys %{ $this->{Time} }) {
790             push @a, $_ if $this->{Time}{$_}{start};
791             }
792              
793             print STDERR "clocks: @a\n";
794             }
795              
796             if( defined $this->{Time}{$tag}{start} ) {
797             push @{ $this->{Time}{$tag}{diffs} }, (time - $this->{Time}{$tag}{start});
798             delete $this->{Time}{$tag}{start};
799             } else {
800             $this->{Time}{$tag}{start} = time;
801             }
802             }
803             # }}}
804             # _nodelist {{{
805             sub _nodelist {
806             my $this = shift;
807             my $list = shift;
808             my $cur = shift;
809              
810             # $this->{nodelist} = { map {$this->_ref2id($_) => $this->_nodelist([], $_)} @{$this->{gametree}} };
811              
812             for my $kid (@{ $cur->{kids} }) {
813             my $id = $this->_ref2id( $kid );
814              
815             die "problem parsing node id" unless $id =~ m/(\d+)\.(\d+)\-(.+)/;
816              
817             my ($g, $v, $m) = ($1, $2, $3);
818              
819             if( $v > @{ $list } ) {
820             my $x = [];
821             push @$list, $x;
822             for (1..$m) {
823             push @$x, undef;
824             }
825             }
826              
827             push @{ $list->[$v-1] }, $id;
828              
829             $this->_nodelist($list, $kid);
830             }
831              
832             return $list;
833             }
834             # }}}
835             # _parse (aka, the internal parse) {{{
836             sub _parse {
837             my $this = shift;
838             my $level = shift;
839             my $pref = shift;
840             my $gref = shift;
841             my $parent = shift;
842              
843             if( $ENV{DEBUG} > 1 ) {
844             print STDERR "\t_parse($level)";
845             print STDERR " ... variation = $gref->{variations} " if ref($gref) and defined $gref->{variations};
846             print STDERR "\n";
847             }
848              
849             my $gm_pr_reg = qr{^(?:GM|SZ|CA|AP|RU|KM|HA|FF|PW|PB|RE|TM|OT|BR|WR|DT|PC|AN|BT|CP|EV|GN|GC|ON|RO|SO|US)$};
850              
851             if( $level == 0 ) {
852             # The file level... $gref is most certainly undefined...
853             # We're also starting the gametree from scratch here
854              
855             $this->{gametree} = [];
856              
857             if( int(@{ $this->{parse}{n} }) ) {
858             $this->{error} = "Parse Error: nodes found at top level... very strange.";
859             return 0;
860             }
861              
862             for my $c (@{ $this->{parse}{c} }) {
863             $this->_parse($level+1, $c, undef) or return 0;
864             }
865              
866             return 1;
867              
868             } elsif( $level == 1 ) {
869             # Every collection should be a new game
870             # At this $level, all we do is make a game and look for game properties.
871             # Then we re _parse() at our current position
872              
873             $gref = { variations=>1, kids=>[] }; push @{ $this->{gametree} }, $gref;
874             $gref->{gnum} = int @{ $this->{gametree} };
875              
876             my $pnode = $pref->{n}[0];
877             for my $p (@$pnode) {
878             if( $p->{P} =~ m/$gm_pr_reg/ ) {
879             $gref->{game_properties}{$p->{P}} = $p->{V};
880             }
881             if( $p->{P} eq "CoPyright" ) {
882             $gref->{game_properties}{FF} = 4;
883             }
884             }
885              
886             unless( $gref->{game_properties}{GM} == 1 ) {
887             $this->{error} = "Parse Error: Need GM[1] property in the first node of the game... not found.";
888             return 0;
889             }
890              
891             unless( $gref->{game_properties}{FF} == 3 or $gref->{game_properties}{FF} == 4 ) {
892             unless( $ENV{ALLOW_STRANGE_FFs} ) {
893             $this->{error} = "Parse Error: Need FF[3] or FF[4] property in the first node of the game... not found.";
894             return 0;
895             }
896             }
897              
898             if( $gref->{game_properties}{SZ} < 3 ) {
899             $this->{error} = "Parse Error: SZ must be set and be greater than 2 (SZ was $gref->{game_properties}{SZ})";
900             return 0;
901             }
902              
903             if( $gref->{game_properties}{FF} == 3 and $gref->{game_properties}{SZ} > 19 ) {
904             $this->{error} = "Parse Error: In FF[3] a move of B[tt] is a pass and therefore, SZ must be less than 20 " .
905             "(SZ was $gref->{game_properties}{SZ}).";
906             return 0;
907             }
908              
909             if( $gref->{game_properties}{FF} == 4 and $gref->{game_properties}{SZ} > 52 ) {
910             $this->{error} = "Parse Error: In FF[4] the size of the board must be no greater than 52" .
911             "(SZ was $gref->{game_properties}{SZ})";
912             return 0;
913             }
914              
915             $this->_parse($level+1, $pref, $gref) or return 0;
916              
917             return 1;
918              
919             } elsif( defined $gref ) {
920             # OK, now we're getting into some serious parsing.
921              
922             my $gnode; # this has the effect of forking the variations off the last node in the collection.
923             # is that correct?
924              
925             for my $i (0..$#{ $pref->{n} }) {
926             my $pnode = $pref->{n}[$i];
927              
928             $parent = ($gnode ? $gnode : $parent ? $parent : $gref);
929              
930             $gnode = { variation=>$gref->{variations}, kids=>[] };
931             push @{ $parent->{kids} }, $gnode;
932              
933             $gnode->{board} = $this->_copy_board_matrix( $parent->{board} ) if $parent->{board};
934             $gnode->{board} = $this->_new_board_matrix( $gref ) unless $gnode->{board};
935              
936             $gnode->{captures} = { B=>0, W=>0 };
937             if( ref($parent) and ref(my $pc = $parent->{captures}) ) {
938             $gnode->{captures}{B} += $pc->{B};
939             $gnode->{captures}{W} += $pc->{W};
940             }
941              
942             for my $p (@$pnode) {
943             if( $p->{P} =~ m/^([BW])$/) {
944             my $c = $1;
945             my @c = $this->sgfco2numco($gref, $p->{V});
946              
947             print STDERR "\t\tmove: $c($p->{V}) == [@c]\n" if $ENV{DEBUG} >= 4;
948              
949             push @{ $gnode->{moves} }, [ $c, @c, $p->{V} ];
950              
951             unless( $c[0] eq "PASS" ) {
952             # fix up board
953             $gnode->{board}[$c[0]][$c[1]] = $c;
954              
955             # check for captures
956             $this->_check_for_captures($gref->{game_properties}{SZ}, $gnode, @c );
957             }
958              
959             } elsif( $p->{P} =~ m/^A([WBE])$/ ) {
960             my $c = $1;
961             my @c = $this->sgfco2numco($gref, $p->{V});
962              
963             push @{ $gnode->{edits} }, [ $c, @c, $p->{V} ];
964              
965             # fix up board
966             # do NOT check for captures
967             if( $c eq "E" ) {
968             $gnode->{board}[$c[0]][$c[1]] = ' ';
969             } else {
970             $gnode->{board}[$c[0]][$c[1]] = $c;
971             }
972             } elsif( $p->{P} =~ m/^C$/ ) {
973             push @{ $gnode->{comments} }, $p->{V};
974              
975             } elsif( $p->{P} =~ m/^(?:CR|TR|SQ)$/ ) {
976             my @c = $this->sgfco2numco($gref, $p->{V});
977              
978             push @{ $gnode->{marks} }, [ $p->{P}, @c, $p->{V} ];
979              
980             # It's tempting to put the marks ON THE BOARD Do not do
981             # this. They'd need to get handled in _copy, and also,
982             # whosoever get's the $board out of the $gnode, can
983             # also get the $marks!
984              
985             } elsif( $p->{P} =~ m/^(?:LB)$/ and $p->{V} =~ m/^(..)\:(.+)$/ ) {
986             push @{ $gnode->{marks} }, [ "LB", $this->sgfco2numco($gref, $1), $1, $2 ];
987              
988             } elsif( not $p->{P} =~ m/$gm_pr_reg/ ) {
989             push @{ $gnode->{other}{$p->{P}} }, $p->{V};
990             }
991             }
992              
993             $gnode->{gnum} = $parent->{gnum};
994             $gnode->{move_no} =
995             (ref($gnode->{moves}) ? int(@{ $gnode->{moves} }) : 0)
996             + (ref($parent) and defined $parent->{move_no} ? $parent->{move_no} : 0);
997             }
998              
999             my $j = @{ $pref->{c} };
1000             if( $j > 1 ) {
1001             # pretend we're in the node with move #12
1002            
1003             # The first fork is still this variation, and contains move #13
1004             $this->_parse($level+1, $pref->{c}[0], $gref, $gnode) or return 0;
1005              
1006             # Every other fork is an alternate move #13
1007             for my $i (1..$#{ $pref->{c} }) {
1008             $gref->{variations}++;
1009             $this->_parse($level+1, $pref->{c}[$i], $gref, $gnode) or return 0;
1010             }
1011             } elsif( $j == 1 ) {
1012             $this->{error} = "Parse Error: the author didn't think this condition could come up ... ";
1013             return 0;
1014             }
1015              
1016             return 1;
1017             }
1018              
1019             $this->{error} = "Parse Error: unknown parse depth ($level) or broken reference(s) ($pref, $gref)... error unknown";
1020             return 0;
1021             }
1022             # }}}
1023             # _ref2id {{{
1024             sub _ref2id {
1025             my $this = shift;
1026             my $ref = shift;
1027              
1028             croak "invalid ref given to _ref2id()" unless ref($ref) eq "HASH";
1029              
1030             unless( defined $this->{refdb2}{$ref} ) {
1031             my $id;
1032             my $c = 2;
1033             if( defined($ref->{variation}) and defined($ref->{move_no}) ) {
1034             $id = "$ref->{gnum}." .
1035             $ref->{variation} . "-" . ($ref->{move_no} ? $ref->{move_no} : "root");
1036             my $cur = $id;
1037             while( defined $this->{refdb}{$cur} ) {
1038             $cur = $id . "-" . $c++;
1039             }
1040             $id = $cur;
1041             } else {
1042             $id = ++$this->{games};
1043             }
1044              
1045             print STDERR "$ref 2 id: $id\n" if $ENV{DEBUG} >= 10;
1046              
1047             $this->{refdb2}{$ref} = $id;
1048              
1049             for my $k (qw(comments board marks moves other captures game_properties variations)) {
1050             $this->{refdb}{$id}{$k} = $ref->{$k} if defined $ref->{$k};
1051             }
1052              
1053             for my $k (qw(gnum kids)) {
1054             delete $this->{refdb}{$id}{$k};
1055             }
1056              
1057             if( $ENV{DEBUG} > 20 ) {
1058             print STDERR "\$this\->\{refdb\}\{\$ref($ref)\} = $this->{refdb2}{$ref} ",
1059             "/ \$this\-\>\{refdb\}\{\$id($id)\} = $this->{refdb}{$id}\n";
1060             }
1061             }
1062              
1063             return $this->{refdb2}{$ref};
1064             }
1065             # }}}
1066             # _new_board_matrix {{{
1067             sub _new_board_matrix {
1068             my $this = shift;
1069             my $gref = shift;
1070              
1071             $this->_time("_new_board_matrix");
1072              
1073             my $board = [];
1074              
1075             my $size = $gref->{game_properties}{SZ};
1076             croak "Syntax Error: You must pass a game reference to sgfco2numco because it needs the FF and SZ properties.\n" unless $size;
1077              
1078             for my $i (1..$size) {
1079             my $row = [];
1080             for my $j (1..$size) {
1081             push @$row, ' ';
1082             }
1083             push @$board, $row;
1084             }
1085              
1086             $this->_time("_new_board_matrix");
1087              
1088             return $board;
1089             }
1090             # }}}
1091             # _copy_board_matrix {{{
1092             sub _copy_board_matrix {
1093             my $this = shift;
1094             my $tocp = shift;
1095              
1096             $this->_time("_copy_board_matrix");
1097              
1098             my $board = [];
1099              
1100             my $double_check = int @$tocp;
1101             for (@$tocp) {
1102             my @a = @{ $_ };
1103             push @$board, \@a;
1104              
1105             die "Problem copying board (" . (int @a) . " vs $double_check)!" unless int @a == $double_check;
1106             }
1107              
1108             $this->_time("_copy_board_matrix");
1109              
1110             return $board;
1111             }
1112             # }}}
1113              
1114             # _check_for_captures {{{
1115             sub _check_for_captures {
1116             my ($this, $SZ, $node, @p) = @_;
1117             my $board = $node->{board};
1118             my $caps = $node->{captures};
1119              
1120             $this->_time("_check_for_captures");
1121              
1122             my $tc = $board->[$p[0]][$p[1]];
1123              
1124             croak "crazy unexpected error: checking for caps, and current pos doesn't have a stone. Two times double odd, and fatal"
1125             unless $tc =~ m/^[WB]$/;
1126              
1127             my $oc = ($tc eq "W" ? "B" : "W");
1128              
1129             # 1. Find groups for all adjacent stones.
1130              
1131             $this->_time("for(_find_group)");
1132              
1133             my %checked = ();
1134             my @groups = ();
1135             for my $p ( [$p[0]-1, $p[1]+0], [$p[0]+1, $p[1]+0], [$p[0]+0, $p[1]-1], [$p[0]+0, $p[1]+1] ) {
1136             my @g = $this->_find_group( \%checked, $SZ, $oc, $board, @$p );
1137              
1138             push @groups, [ @g ] if @g;
1139             }
1140              
1141             $this->_time("for(_find_group)");
1142             $this->_time("for(\@groups), _count_liberties");
1143              
1144             if( @groups ) {
1145             # 2. Any groups without liberties are toast!
1146             print STDERR "_check_for_captures() found ", int(@groups), " neighboring groups:" if $ENV{DEBUG} > 3 and int(@groups);
1147              
1148             for my $group (@groups) {
1149             my $l = $this->_count_liberties( $SZ, $board, @$group );
1150              
1151             print STDERR " liberties($l)" if $ENV{DEBUG}>3;
1152             if( $l < 1 ) {
1153             print STDERR "-killed! " if $ENV{DEBUG}>3;
1154             for my $p (@$group) {
1155             $caps->{$tc}++;
1156             $board->[$p->[0]][$p->[1]] = ' ';
1157             }
1158             }
1159             }
1160              
1161             print STDERR "\n" if $ENV{DEBUG} > 3;
1162             }
1163              
1164             $this->_time("for(\@groups), _count_liberties");
1165             $this->_time("_find_group/_count_liberties of me");
1166              
1167             # 3. Check my own liberties, I may be toast
1168             %checked = ();
1169             my @me_group = $this->_find_group( \%checked, $SZ, $tc, $board, @p );
1170             my $me_lifec = $this->_count_liberties( $SZ, $board, @me_group );
1171             print STDERR "_check_for_captures() me_group ", int(@me_group), " stones: " if $ENV{DEBUG} > 3;
1172             print STDERR " me liberties($me_lifec)" if $ENV{DEBUG}>3;
1173             if( $me_lifec < 1 ) {
1174             print STDERR "-killed! " if $ENV{DEBUG}>3;
1175             for my $p (@me_group) {
1176             $caps->{$oc}++;
1177             $board->[$p->[0]][$p->[1]] = ' ';
1178             }
1179             }
1180             print STDERR "\n" if $ENV{DEBUG}>3;
1181              
1182             $this->_time("_find_group/_count_liberties of me");
1183             $this->_time("_check_for_captures");
1184             }
1185             # }}}
1186             # _count_liberties {{{
1187             sub _count_liberties {
1188             my ($this, $SZ, $board, @group) = @_;
1189              
1190             $this->_time("_count_liberties");
1191              
1192             my %checked = ();
1193             my $count = 0;
1194              
1195             for my $g (@group) {
1196             for my $p ( [$g->[0]-1, $g->[1]+0], [$g->[0]+1, $g->[1]+0], [$g->[0]+0, $g->[1]-1], [$g->[0]+0, $g->[1]+1] ) {
1197             if( not $checked{"@$p"} ) {
1198             $checked{"@$p"} = 1;
1199             unless( ($p->[0] < 0 or $p->[0] > ($SZ-1)) or ($p->[1] < 0 or $p->[1] > ($SZ-1)) ) {
1200             if( $board->[$p->[0]][$p->[1]] eq ' ' ) {
1201             $count++;
1202             }
1203             }
1204             }
1205             }
1206             }
1207              
1208             $this->_time("_count_liberties");
1209              
1210             return $count;
1211             }
1212             # }}}
1213             # _find_group {{{
1214             sub _find_group {
1215             my ($this, $checked, $SZ, $oc, $board, @p) = @_;
1216              
1217             $this->_time("_find_group");
1218              
1219             print STDERR "\t_find_group(@p)" if $ENV{DEBUG}>12;
1220             my @g;
1221              
1222             if( not $checked->{"@p"} ) {
1223             $checked->{"@p"} = 1;
1224             print STDERR "." if $ENV{DEBUG}>12;
1225             unless( ($p[0] < 0 or $p[0] > ($SZ-1)) or ($p[1] < 0 or $p[1] > ($SZ-1)) ) {
1226             print STDERR ".." if $ENV{DEBUG}>12;
1227             if( $board->[$p[0]][$p[1]] eq $oc ) {
1228             print STDERR " !" if $ENV{DEBUG}>12;
1229             push @g, [ @p ];
1230             for my $p ( [$p[0]-1, $p[1]+0], [$p[0]+1, $p[1]+0], [$p[0]+0, $p[1]-1], [$p[0]+0, $p[1]+1] ) {
1231             push @g, $this->_find_group( $checked, $SZ, $oc, $board, @$p );
1232             }
1233             }
1234             }
1235             }
1236             print STDERR "\n" if $ENV{DEBUG}>12;
1237              
1238             $this->_time("_find_group");
1239              
1240             return @g;
1241             }
1242             # }}}
1243