#! /usr/bin/perl -w # perl code to turn an SGF file into diagrams # Copyright (C) 1997-2005 Reid Augustin reid@hellosix.com # 1000 San Mateo Dr. # Menlo Park, CA 94025 USA # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111, USA # todo: =head1 NAME sgf2dg - convert Smart Go Format (SGF) files to diagrams similar to those seen in Go books and magazines. =head1 SYNOPSIS sgf2dg [ option ... ] file[.sgf|.mgt] =head1 DESCRIPTION B takes a Smart Go Format (SGF) file I or I.sgf or I.mgt and produces a diagram file I.suffix where suffix is determined by the B (see below). The default B is Dg2TeX which converts the Diagram to TeX source code (sgf2dg is a superset replacement for the sgf2tex script and package). If you have the GOOE fonts (provided in the same package as sgf2dg) correctly installed on your system you will be able to tex I.tex to produce a .dvi file. You can of course embed all or parts of I.tex into other TeX documents. =cut use strict; require 5.001; use IO::File; use Games::Go::Diagram; # the go diagram module BEGIN { our $VERSION = sprintf "4.%03d", '$Revision: 221 $' =~ /(\d+)/; } use constant a_MINUS_1 => ord('a') - 1; # base of SGF coordinates is 'a' my $version = sprintf "version 4.%03d", '$Revision: 221 $' =~ /(\d+)/; my $myName = $0; # full pathname of this file $myName =~ s".*/""; # delete any preceding path $version = "(sgf2dg) $version" if($myName ne 'sgf2dg'); my $commandLine = "$0 " . join(' ', @ARGV); my $help = <next; # each variation in this group starts from this parent local $removedCount = $removedCount; local $moveNum = $parentDiagram[0]->var_on_move; local @lastMove2; local @lastMove1; printIndent("Parsing Variation on move $moveNum\n"); # link back to parent which is 2 levels up: my $parent = $parentDiagram[0]->parent; # add to parent's variation list. Note: variation # will be on next move (unless stones are removed) push(@{$parent->user->{variations}{$moveNum+1}}, $diagram); $diagram->var_on_move($moveNum + 1); $diagram->parent($parent), $diagram->user({id => $diagramId++}); if (($option{varNumbersFlag} eq 'relative') or # each variation starts at 1 ($option{varNumbersFlag} eq 'correlative' and # each root variation starts at 1 ($variationDepth <= 1))) { $moveNum = 0; } SGF_ProcessVariation($fileID); $variationDepth--; $chr = ')'; # done with variation # and now continue with the main line } elsif (not $option{ignoreVariations}) { $nestCount++; # create a new diagram from starting from the # current position. this new diagram is really a # place holder since we have to spawn another level # of diagrams when we start parsing the variations unshift (@parentDiagram, $diagram->next); # put a fresh diagram on the parent stack $parentDiagram[0]->parent($diagram); # point back to parent $parentDiagram[0]->var_on_move($moveNum); # and remember move number # where variation was spawned $parentDiagram[0]->user({id => $diagramId++}); printIndent("Creating Variation(s) at move $moveNum\n"); } } elsif ($chr eq ')') { # end of a variation $nestCount--; if ($prevChr eq ')') { shift(@parentDiagram); # done with this group of variations } if ($nestCount <= 0) { printVerbose('SGF_ProcessVariation done: '); return; } } else { # a node ungetC($fileID, $chr) if ($chr ne ';'); # hmm, missing ';'? SGF_ProcessNode($fileID); $diagram->node if (defined($diagram)); while (@nodePlays) { my $coords = shift @nodePlays; CheckForDeadGroups(SGF2Coords($coords)); # check for captures } } if ((@{$option{breakList}} > 0) and ($diagram->last_number >= $option{breakList}[0])) { printVerbose('BreakList: '); my $m = shift(@{$option{breakList}}); @lastMove2 = @lastMove1; # use last 1 for repeatLast finishDiagram(undef, "BreakList at move $m"); } if (($diagram->last_number - $diagram->first_number + 1) >= $option{movesPerDiagram} + $option{repeatLast}) { printVerbose('movesPerDiagram: '); @lastMove2 = @lastMove1; # use last 1 for repeatLast finishDiagram(undef, "movesPerDiagram"); } if ($option{doubleDigits} and ($option{newNumbers} ? ($diagram->last_number - $diagram->first_number >= 100) : (($diagram->first_number != $diagram->last_number) and ($diagram->last_number % 100 == 0)))) { printVerbose('doubleDigits: '); @lastMove2 = @lastMove1; # use last 1 for repeatLast finishDiagram(undef, "doubleDigits"); } $prevChr = $chr; } } sub SGF_ProcessNode { my ($fileID) = shift; my ($propVal, @propList, $prop, $p); local $currentLetter = 'a'; # start each node with a new set of letters # sadly, the properties can be in any order. for example, you might have # a mark before the stone that is to be marked. so we'll accumulate # all the properties and put them into some sane order. my %props; for(;;) { my $shortPropID = SGF_GetShortPropID(SGF_GetPropID($fileID)); last if ($shortPropID eq ''); while((defined (my $propVal = SGF_GetPropVal($fileID)))) { push(@{$props{$shortPropID}}, $propVal); } } foreach my $p (sort { &PropOrder } keys(%props)) { SGF_ProcessProperty($p, $props{$p}); } } # specify order in which to process properties: my %propOrder = ( N => 0, # first because it always flushes AE => 15, # might flush AB => 20, # might flush AW => 20, # might flush B => 25, # changes $moveNum W => 25, # changes $moveNum # marks, letter, comments, etc all after ); sub PropOrder { my $aa = $propOrder{$a} || 100; my $bb = $propOrder{$b} || 100; return ($aa <=> $bb); } # called every time stones are added or played sub SGF_ProcessProperty { my ($shortPropID, $propVals) = @_; foreach my $propVal (@{$propVals}) { # Move properties if (($shortPropID eq 'W') or # W[pt] Play white move ($shortPropID eq 'B')) { # B[pt] Play black move $moveNum++; if (($propVal eq '') or # new style pass (($option{boardSizeX} <= 19) and ($option{boardSizeY} <= 19) and ($propVal eq 'tt'))) { # old (non-scalable) style pass unless($option{ignorePass}) { printVerbose("Pass($shortPropID), move=$moveNum\n"); $diagram->property($moveNum, $shortPropID, 'pass'); } } else { printVerbose("Playing $propVal, color=$shortPropID, move=$moveNum\n"); my $int = $diagram->get($propVal); $diagram->put($propVal, $shortPropID, $moveNum); @lastMove2 = @lastMove1; @lastMove1 = ($propVal, $shortPropID, $moveNum); push(@nodePlays, $propVal); } } elsif ($shortPropID eq 'KO') { # KO force illegal move # ignore printVerbose("$shortPropID (force move), move=$moveNum\n"); $diagram->property($moveNum, $shortPropID, '') } elsif ($shortPropID eq 'MN') { # MN[num] set move number printVerbose("Set move number to $propVal at move $moveNum\n"); $moveNum = $propVal - 1; # TODO does this work at all? # Setup properties } elsif (($shortPropID eq 'AW') or # AW[pt] AddWhite ($shortPropID eq 'AB')) { # AB[pt] AddBlack finishDiagram(undef, "AddB/W"); # need a new diagram my $color = substr($shortPropID, 1, 1); printVerbose("Add $color stone to $propVal at move $moveNum\n"); foreach(composed_pt($propVal)) { $diagram->put($_, $color); } } elsif ($shortPropID eq 'AE') { # AE[pt] AddEmpty finishDiagram(undef, "AddEmpty"); printVerbose("Empty (remove) $propVal at move $moveNum\n"); unless ($diagram->last_number) { # if no stones played yet # keep track of number of stones removed at the # start of the diagram so we can figure out which # move in the parent diagram spawned the variation $removedCount++; $diagram->user->{removedCount} = $removedCount; } foreach(composed_pt($propVal)) { $diagram->remove($_); } } elsif ($shortPropID eq 'PL') { #PL[W|B] set Player # ignore printVerbose("$shortPropID $propVal (set player), move=$moveNum\n"); $diagram->property($moveNum, $shortPropID, $propVal) # Node annotation properties } elsif ($shortPropID eq 'C') { # C[text] Comment unless ($option{ignoreComments}) { if ($propVal =~ m/\S/m) { # if nothing but whitespace, delete printVerbose("Add comment at move $moveNum:\n$propVal\n"); $diagram->property($moveNum, $shortPropID, text($propVal)); } } } elsif (($shortPropID eq 'DM') or # DM[dbl] Even position ($shortPropID eq 'GB') or # GB[dbl] Good for black ($shortPropID eq 'GW') or # GW[dbl] Good for white ($shortPropID eq 'HO') or # HO[dbl] Hotspot ($shortPropID eq 'UC')) { # UC[dbl] Unclear # ignore ? printVerbose("$shortPropID $propVal (position evaluation), move=$moveNum\n"); $diagram->property($moveNum, $shortPropID, $propVal) } elsif ($shortPropID eq 'N') { # N[stxt] Name (node name) if(defined($propVal) and ($propVal ne '')) { # no name? return finishDiagram(undef, "Named node"); # node names should be at the start of the diagram if ($diagram->last_number) { @lastMove2 = @lastMove1; # use last 1 for repeatLast } $propVal = simple_text($propVal); printVerbose("Node name to $propVal at move $moveNum\n"); $diagram->property($moveNum, $shortPropID, $propVal); } } elsif ($shortPropID eq 'V ') { # V[real] Value (estimated game score) # ignore ? printVerbose("$shortPropID $propVal (value estimate), move=$moveNum\n"); $diagram->property($moveNum, $shortPropID, $propVal) # Move annotation properties } elsif (($shortPropID eq 'BM') or # BM[dbl] bad move ($shortPropID eq 'DO') or # DO doubtful move ($shortPropID eq 'IT') or # IT interesting move ($shortPropID eq 'TE')) { # TE[dbl] tesuji (good move) # ignore? printVerbose("$shortPropID $propVal (move evaluation), move=$moveNum\n"); $diagram->property($moveNum, $shortPropID, $propVal) # Markup properties } elsif ($shortPropID eq 'AR') { # AR[c_pt] Arrow printVerbose("Arrow: $propVal move $moveNum\n"); $diagram->property($moveNum, $shortPropID, $propVal) } elsif ($shortPropID eq 'DD') { # DD[elst] Dim points: DD[] clears any previous dimming printVerbose("Dim $propVal move $moveNum\n"); $diagram->property($moveNum, $shortPropID, $propVal) } elsif ($shortPropID eq 'LB') { # LB[pt:stxt] Label point with text unless ($option{ignoreMarks}) { my ($item, $coord, $label); foreach $item (split(/\s+/, $propVal)) { ($coord, $label) = ($item =~ m/(.*):(.*)/); if (length($coord) != 2) { # didn't work? hmmm, let's try the other way around ($label, $coord) = ($label, $coord); } $label = simple_text($label); printVerbose("Label \"$label\" at $coord move $moveNum\n"); $diagram->label($coord, $label); } } } elsif ($shortPropID eq 'LN') { # LN[c_pt] Line printVerbose("Line: $propVal move $moveNum\n"); $diagram->property($moveNum, $shortPropID, $propVal) } elsif ( ($shortPropID eq 'CR') or # CR[pt] circle ($shortPropID eq 'M') or # M[pt] old style mark ($shortPropID eq 'MA') or # MA[pt] mark (X) ($shortPropID eq 'SQ') or # SQ[pt] square ($shortPropID eq 'TR')) { # TR[pt] triangle $shortPropID = 'TR' if (($shortPropID eq 'M') or # old style marks were assumed to be triangles ($option{converter} eq 'SL') or # Sensei's Library only understands one kind of mark ($option{converter} eq 'ASCII')); # ASCII converter only understands one kind of mark unless ($option{ignoreMarks}) { printVerbose("$shortPropID mark at $propVal move $moveNum\n"); foreach(composed_pt($propVal)) { $diagram->mark($_, $shortPropID); } } } elsif ($shortPropID eq 'SL') { # SL[pt] Select points (markup unknown) printVerbose("Select points $propVal move $moveNum\n"); $diagram->property($moveNum, $shortPropID, $propVal) # Root properties } elsif (($shortPropID eq 'AP') or # AP[stxt:stxt] application name:version ($shortPropID eq 'CA') or # CA[stxt] charset ($shortPropID eq 'FF') or # FF[1-4] FileFormat ($shortPropID eq 'GM') or # GM[1-16] Game ($shortPropID eq 'ST')) { # ST[0-3] how to show variations (style?) # ignore printVerbose("$shortPropID $propVal (root property), move=$moveNum\n"); $diagram->property(0, $shortPropID, $propVal) } elsif ($shortPropID eq 'SZ') { # SZ[num[:num] board size [cols[:rows]] if ($propVal =~ m/(\d+):(\d+)/) { $option{boardSizeX} = $1; $option{boardSizeY} = $2; } else { $option{boardSizeX} = $propVal; $option{boardSizeY} = $propVal; } # Game info properties } elsif (($shortPropID eq 'AN') or # AN[stxt] annotater (name) ($shortPropID eq 'BR') or # BR[stxt] Black rank ($shortPropID eq 'WR') or # WR[stxt] White rank ($shortPropID eq 'BT') or # BT[stxt] black team ($shortPropID eq 'WT') or # WT[stxt] white team ($shortPropID eq 'CP') or # CP[stxt] copyright ($shortPropID eq 'DT') or # DT[stxt] Date ($shortPropID eq 'EV') or # EV[stxt] Event ($shortPropID eq 'ON') or # ON[stxt] opening information ($shortPropID eq 'OT') or # OT[stxt] overtime description (byo-yomi) ($shortPropID eq 'PC') or # PC[stxt] place game was played ($shortPropID eq 'PB') or # PB[stxt] Player Black ($shortPropID eq 'PW') or # PW[stxt] Player White ($shortPropID eq 'RE') or # RE[stxt] result ($shortPropID eq 'RO') or # RO[stxt] round ($shortPropID eq 'RU') or # RU[stxt] rules ($shortPropID eq 'SO') or # SO[stxt] source ($shortPropID eq 'US')) { # US[stxt] user/program who entered the game $propVal = simple_text($propVal); printVerbose("${shortPropID} $propVal (game info), move=$moveNum\n"); $diagram->property(0, $shortPropID, $propVal); } elsif (($shortPropID eq 'GC') or # GC[text] game comment ($shortPropID eq 'TM')) { # TM[real] time limits $propVal = simple_text($propVal); printVerbose("${shortPropID} $propVal (game info), move=$moveNum\n"); $diagram->property(0, $shortPropID, $propVal); # Timing properties } elsif (($shortPropID eq 'BL') or # BL[real] BlackLeft (time) ($shortPropID eq 'WL') or # WL[real] WhiteLeft (time) ($shortPropID eq 'OB') or # OB[num] Black moves left (after this move) ($shortPropID eq 'OW')) { # OW[num] White moves left # ignore printVerbose("${shortPropID} $propVal (time/byo-yomi info), move=$moveNum\n"); $diagram->property($moveNum, $shortPropID, $propVal); # Go-specific properties } elsif ($shortPropID eq 'HA' and # HA[num] handicap, but should not place stones, notation only $propVal !~ m/\D/ and # digits only $propVal >= 2) { # at least two printVerbose("Handicap $propVal at move $moveNum\n"); if ($option{placeHandi}) { foreach(@{hoshi($propVal)}) { printVerbose("Place handicap on $_\n"); $diagram->put($_, 'B'); } } $diagram->property(0, $shortPropID, $propVal); # a game property } elsif ($shortPropID eq 'KM') { # KM[real] komi printVerbose("Komi $propVal at move $moveNum\n"); $diagram->property(0, $shortPropID, $propVal); # a game property } elsif (($shortPropID eq 'TB') or # TB[el_pt] black territory ($shortPropID eq 'TW')) { # TW[el_pt] white territory my $color = substr($shortPropID, 1, 1); printVerbose("Territory for $color at $propVal move $moveNum\n"); $diagram->territory($shortPropID, $propVal); #Misc. properties } elsif ($shortPropID eq 'L') { # L[pt] next letter at pt (deprecated) unless ($option{ignoreLetters}) { $diagram->label($propVal, $currentLetter); printVerbose("Letter \"$currentLetter\" at $propVal move $moveNum\n"); $currentLetter++; } } elsif ($shortPropID eq 'FG') { # FG[pt:stext]] Figure - see spec unless ($option{ignoreComments}) { printVerbose("${shortPropID} $propVal, move=$moveNum\n"); $diagram->property($moveNum, 'C', $propVal); # treat like a comment } } elsif ($shortPropID eq 'PM') { # PM[num] Print mode: 0=>no numbers, 1=>normal 2=>modulo 100 (see spec for details) # ignore printVerbose("${shortPropID} $propVal, move=$moveNum\n"); $diagram->property($moveNum, $shortPropID, $propVal); } elsif ($shortPropID eq 'VW') { # VW[pt:pt] View parts of board - show only listed points, VW[] clears any previous views printVerbose("View $propVal\n"); if ($propVal ne '') { foreach (composed_pt($propVal)) { $diagram->view($_); } finishDiagram(undef, "VieW"); # views need to end the diagram } else { $diagram->view($_); # clears a previous view } } elsif (($shortPropID eq 'BS') or # BS[stext] BlackSpecies (deprecated) ($shortPropID eq 'WS')) { # WS[stext] WhiteSpecies (deprecated) printVerbose("Species (${shortPropID}) $propVal\n"); $diagram->property(0, $shortPropID, $propVal); } else { printVerbose("Unknown property: ${shortPropID} $propVal, move=$moveNum\n"); $diagram->property($moveNum, $shortPropID, $propVal); # shrug } } } # a property is an ID followed by value(s) # the propertyID consists of text sub SGF_GetPropID { my ($fileID) = @_; my ($chr, $pos); my ($propID) = ''; $chr = skipToToken($fileID); while ($chr ne '') { if ($chr =~ m/\w/) { $propID .= $chr; } else { ungetC($fileID, $chr); last; } $chr = getC($fileID); } return($propID); } # a property is an ID followed by value(s) # proertyVals consists of stuff inside brackets [] sub SGF_GetPropVal { my ($fileID) = @_; my ($chr, $pos); my ($propVal) = ''; $chr = skipToToken($fileID); if ($chr ne '[') { ungetC($fileID, $chr); return undef; # no more property values } for(;;) { $chr = getC($fileID); if ($chr eq '\\') { $propVal .= '\\' . getC($fileID); } elsif ($chr eq ']') { return($propVal); } elsif (($chr eq '') and eof($fileID)) { print(STDERR "Unterminated property: $propVal\n"); return($propVal); } else { $propVal .= $chr; } } } sub SGF_GetShortPropID { my ($propID) = @_; return('') unless (defined($propID)); $propID =~ s/[a-z]//g; # just get rid of all lower case letters return($propID); } sub SGF2Coords { my ($coords) = @_; my ($x, $y) = unpack('C2', $coords); return($x - a_MINUS_1, $y - a_MINUS_1); } sub Coords2SGF { my ($x, $y) = @_; $x = chr($x + a_MINUS_1); $y = chr($y + a_MINUS_1); return("$x$y"); } sub text { my ($text) = @_; $text =~ s/\n\r/\n/gm; # all newline/returns to newlines $text =~ s/\r\n/\n/gm; # all return/newlines to newlines $text =~ s/\r/\n/gm; # all stand-alone returns to newlines $text =~ s/\\\n//gm; # remove backslashed newlines $text =~ s/(\s)/($1 eq "\n") ? "\n" : ' '/egm; # convert all non-newline whitespace into space $text =~ s/\\(.)/$1/gm; # other backslashed chars are taken literally return $text; } sub simple_text { my ($text) = @_; $text = text($text); $text =~ s/\n/ /gm; # all remaining newlines turn into spaces return $text; } # composed points look like pt:pt (or perhaps just pt or even ''). pt:pt means # all the points in the rectangle from upper left to lower right. sub composed_pt { my ($text) = @_; if (my ($p1, $p2) = ($text =~ m/^(.+):(.+)$/)) { my ($x1, $y1) = split('', $p1); my ($x2, $y2) = split('', $p2); ($x2, $x1) = ($x1, $x2) if ($x1 gt $x2); ($y2, $y1) = ($y1, $y2) if ($y1 gt $y2); my @r; foreach my $y ($y1 .. $y2) { foreach my $x ($x1 .. $x2) { push (@r, "$x$y"); } } return @r; } return $text; } # returns a ref to list of SGF coords for hoshi (or handicap) points sub hoshi { my ($num) = @_; if ($option{boardSizeX} != $option{boardSizeY}) { return; } my %hoshiTable = (21 => [4, 11, 18], 19 => [4, 10, 16], 17 => [4, 9, 14], 15 => [4, 8, 12], 13 => [4, 7, 10], 11 => [4, 5, 8 ], 9 => [3, 5, 7 ], 7 => [2, 4, 6 ], 5 => [2, 3, 4 ], ); unless(defined($num)) { # 11x11 and smaller get 5 hoshi points $num = ($option{boardSizeX} > 11) ? 9 : 5; } my ($a, $b, $c); if (exists($hoshiTable{$option{boardSizeX}})) { ($a, $b, $c) = @{$hoshiTable{$option{boardSizeX}}}; } else { print(STDERR "I don't know about hoshi/handicaps for boardSize $option{boardSizeX}\n"); return; } my @hoshi; push(@hoshi, Coords2SGF($a,$c), Coords2SGF($c,$a)) if ($num >= 2); push(@hoshi, Coords2SGF($c,$c)) if ($num >= 3); push(@hoshi, Coords2SGF($a,$a)) if ($num >= 4); push(@hoshi, Coords2SGF($b,$b)) if (($num == 5) or ($num == 7) or ($num == 9)); push(@hoshi, Coords2SGF($a,$b), Coords2SGF($c,$b)) if ($num >= 6); push(@hoshi, Coords2SGF($b,$a), Coords2SGF($b,$c)) if ($num >= 8); if (($num > 9) or ($num < 2)) { print(STDERR "Handicap is $num - I can only handle 2 through 9.\n"); } return \@hoshi; } sub CheckForDeadGroups { my ($x, $y) = @_; my $color = $diagram->game_stone(Coords2SGF($x, $y)); return unless(defined($color)); my $otherColor = ($color eq 'black') ? 'white' : 'black'; CheckIfDead($x + 1, $y, $otherColor); # first check the four neighboring stones of the other color CheckIfDead($x - 1, $y, $otherColor); CheckIfDead($x, $y + 1, $otherColor); CheckIfDead($x, $y - 1, $otherColor); CheckIfDead($x, $y, $color); # and finally we need to check the stone just placed } sub CheckIfDead { my ($x, $y, $color) = @_; my $stone = $diagram->game_stone(Coords2SGF($x, $y)); return unless(defined($stone) and # no stone/group here to check ($stone eq $color)); # color doesn't match unless (HasLibs($x, $y, $color, {}, 0)) { RemoveGroup($x, $y, $color); # no liberties? - it's dead! } } sub HasLibs { my ($x, $y, $color, $been_here, $depth) = @_; if ($depth > 1000) { die("Oops, recursion > 1000 while checking for liberties at move $moveNum coords ($x,$y)\n" . "This isn't supposed to be possible! Aborting...\n"); } if (($x < 1) or ($x > ($option{boardSizeX})) or ($y < 1) or ($y > ($option{boardSizeY}))) { return(0); # oops! off the board. } return 0 if (exists($been_here->{"$x,$y"})); # we've been here before $been_here->{"$x,$y"} = 1; # mark that we've been here my $thisStone = $diagram->game_stone(Coords2SGF($x, $y)); return 1 unless(defined($thisStone)); # empty, the group has liberties return(0) if ($thisStone ne $color); # this is an opponents stone - no liberties here! # this is a connected stone of the same color $depth++; if (HasLibs($x + 1, $y, $color, $been_here, $depth) or HasLibs($x - 1, $y, $color, $been_here, $depth) or HasLibs($x, $y + 1, $color, $been_here, $depth) or HasLibs($x, $y - 1, $color, $been_here, $depth)) { return(1); # yes! we're alive! } return(0); # uh-oh! no liberties yet... } sub RemoveGroup { my ($x, $y, $color) = @_; my $thisStone = $diagram->game_stone(Coords2SGF($x, $y)); if (defined($thisStone) and ($thisStone eq $color)) { $diagram->capture(Coords2SGF($x, $y)); RemoveGroup($x + 1, $y, $color); # remove any connected stones of the same color RemoveGroup($x - 1, $y, $color); RemoveGroup($x, $y + 1, $color); RemoveGroup($x, $y - 1, $color); } } sub finishDiagram { my ($d, $cause) = @_; $d = $diagram unless(defined($d)); return unless ($d->actions_done); # no new actions pending? just use current diagram if (exists($d->user->{mainId})) { printVerbose("Finish Diagram ", $d->user->{mainId}, " at move $moveNum due to $cause\n"); } else { printVerbose("Finish Variation at move $moveNum due to $cause\n"); } my $prevDiagram = $d; $diagram = $d->next; # start a fresh diagram $diagram->user({id => $diagramId++}); # init user hash if (exists($prevDiagram->user->{mainId})) { my $mainId = $prevDiagram->user->{mainId} + 1; $diagram->user->{mainId} = $mainId; printIndent("Parsing Diagram $mainId at move $moveNum\n"); } else { printIndent("Parsing Variation continuation at move $moveNum\n"); } $prevDiagram->user->{next} = $diagram; # link from previous to new diagram if ($option{repeatLast} and defined($lastMove2[0])) { $diagram->renumber($lastMove2[0], $lastMove2[1], undef, $lastMove2[2]); } } sub CompareVariation { my @aa = split(/\./, $a); my @bb = split(/\./, $b); my ($ii, $max, $return); #print "CompareVariation($a, $b)\n"; if (@aa > @bb) { $max = @aa; } else { $max = @bb; } for ($ii = 0; $ii < $max; $ii++) { $aa[$ii] = -1 unless(defined($aa[$ii])); $bb[$ii] = -1 unless(defined($bb[$ii])); $return = ($aa[$ii] <=> $bb[$ii]); #print("ii=$ii, aa=$aa[$ii], bb=$bb[$ii], returns $return\n"); last unless ($return == 0); } return($return); } sub nameDiagram { my ($diagram, $sequence, $level) = @_; my ($name, $type, $num); if ($level) { if ($sequence) { $name = "Variation $variationNum.$sequence"; } else { $variationNum++; $name = "Variation $variationNum"; } } else { $name = "Diagram $diagramNum"; $diagramNum++; } if (0) { my $id = $diagram->user->{id} || '?'; # helpful for debugging $diagram->name("$id: $name"); } else { $diagram->name($name); } if ($level) { if ($sequence) { $diagram->name(" (continued)") } else { my $rc = $diagram->user->{removedCount} || 0; # adjust "variation on move" for removed stone count $diagram->var_on_move($diagram->var_on_move - $rc); } } return ($diagramNum - 1, $name); } sub convertDiagram { my ($dg2, $diagram, $level) = @_; my $sequence = 0; while (defined($diagram)) { my ($dNum, $dName) = nameDiagram($diagram, $sequence++, $level); if(($dNum >= $option{firstDiagram}) and ($dNum <= $option{lastDiagram})) { auto_bounds($dg2, $diagram); # crop (auto-bounds) courtesy of Marcel # Gruenauer and Dg2SL $diagram->hoshi(@{hoshi()}); # add hoshi points to board $diagram->offset($diagram->first_number - 1) if ($option{newNumbers} and $diagram->first_number); printIndent("Converting $dName\n"); $dg2->convertDiagram($diagram); my $vars = $diagram->user->{variations}; if (defined($vars) and not $option{ignoreVariations}) { foreach my $moveNum (sort(keys(%{$vars}))) { foreach my $varD (@{$vars->{$moveNum}}) { convertDiagram($dg2, $varD, $level + 1); } } } } $diagram = $diagram->user->{next}; } } # # OK, now we've got everything defined, we can start executing stuff # # set the defaults # $option{topLine} = $option{leftLine} = 1; $option{bottomLine} = $option{rightLine} = 19; $option{varNumbersFlag} = 'relative'; $option{doubleDigits} = 0; $option{repeatLast} = 0; $option{newNumbers} = 0; $option{firstDiagram} = 1; $option{lastDiagram} = 10000; $option{boardSizeX} = 19; $option{boardSizeY} = 19; $option{coords_style} = 'normal'; # standard coordinate style $option{breakList} = []; $option{coords} = 0; $option{coordStyle} = 'normal'; $option{floatControl} = 'rx'; $option{verbose} = 0; $option{placeHandi} = 0; $option{converter} = 'TeX'; if ($myName =~ m/sgf2(.*)/) { if (($1 ne 'tex') and ($1 ne 'diagram') and ($1 ne 'dg')) { $option{converter} = $1; } } =head1 OPTIONS =over 4 =item B<-h > | B<-help> Print a help message and quit. =item B<-i> | B<-in> | .sgf | .mgt Specifies the input filename. (STDIN or none for standard input.) This option is not needed in ordinary use. =item B<-o> | B<-out> Specifies the output file. ('STDOUT' for standard output.) If the input file is , .sgf or .mgt, then .I is the default (see the B option). This option is not needed in ordinary usage. =item B<-t> | B<-top> Specifies the top line to print. Default is 1. =item B<-b> | B<-bottom> Specifies the bottom line to print. Default is 19. =item B<-l> | B<-left> Specifies the leftmost line to print. Default is 1. =item B<-r> | B<-right> Specifies the rightmost line to print. Default is 19. =item B<-break> | B<-breakList> 'break list' is a list of moves, separated by comma, with no spaces. These are breakpoints: each will be the last move in one diagram. =item B<-m> | B<-movesPerDiagram> 'moves per diagram' is a positive integer, specifying the maximal number of moves per diagram. Default is 50 unless B<-break> or B<-breakList> is set, in which case the default is set to a very large number (10,000). The two options B<-breakList> and B<-movesPerDiagram> may be used together. =item B<-n> | B<-newNumbers> Begin each diagram with the number 1. The actual move numbers are still used in the label. B<-newNumbers> and B<-doubleDigits> are alternative schemes for avoiding three-digit numbers in the diagrams. They should probably not be used together. =item B<-d> | B<-doubleDigits> If the first move of a diagram exceeds 100, the move number is reduced modulo 100. The actual move numbers are still used in the label. B<-newNumbers> and B<-doubleDigits> are alternative schemes for avoiding three-digit numbers in the diagrams. They should probably not be used together. =item B<-rl> | B<-repeatLast> The last move in each diagram is the first move in the next. This emulates a common style for annotating Go games. =item B<-ic> | B<-ignoreComments> Comments embedded in the SGF with the C property are ignored. =item B<-il> | B<-ignoreLetters> Letters embedded in the SGF with the L or LB property are ignored. =item B<-im> | B<-ignoreMarks> Marks embedded in the SGF with the M or MA property are ignored. =item B<-ip> | B<-ignorePass> Passes are ignored. In sgf, a pass is a move at the fictitious point tt. Without this option, sgf2dg indicates passes in the diagram comments. =item B<-ia> | B<-ignore all> Ignore SGF letters, marks, variations and passes. =item B<-firstDiagram> Specifies the first diagram to print. Default is 1. =item B<-lastDiagram> Specifies the last diagram to print. Default is to print all diagrams until the end. =item B<-coords> Adds coordinates to right and bottom edges. =item B<-verbose> Print diagnostic messages as the conversion proceeds. Most SGF properties produce some kind of message. =item B<-converter> | B<-convert> Selects different output converter plugins. Converters available with the current distribution package are: =over 4 =item L TeX source (default) =item L MetaPost embedded in TeX =item L simple ASCII diagrams =item L Portable Document Format (PDF) =item L PostScript =item L Perl/Tk NoteBook/Canvas =item L PostScript via Dg2Tk (Dg2Ps is prefered) =item L Sensei's Library (by Marcel Gruenauer) =back Bs are quite easy to write - should take just a few hours if you are already conversant with the conversion target. If you would like to create a B plugin module, the easiest way is probably to grab a copy of Dg2Ps.pm (for example) and modify it. Once it's working, please be sure to send us a copy so we can add it to the distribution. Converters are always prepended with 'Games::Go::Dg2', so to select the ASCII converter instead of the default TeX converter, use: -converter ASCII Converter names are case sensitive. The default output filename suffix is determined by the converter: the converter name is lower-cased to become the suffix, so the ASCII converter produces .ascii from .sgf. You can also select different Bs by changing the name of the sgf2dg script (or better, make symbolic links, or copies if your system can't handle links). The B name is extracted from the name with this regular expression: m/sgf2(.*)/ Anything after 'sgf2' is assumed to be the name of a B module. For example, let's create a link to the script: $ cd /usr/local/bin $ ln -s sgf2dg sgf2Xyz Executing: $ sgf2Xyz foo.sgf [ options ] attempts to use Games::Go::Dg2Xyz as the B. The B name extracted from the script name is case sensitive. Note that three extracted names are treated specially: =over 4 =item tex =item diagram =item dg =back These three names (when extracted from the script name) always attempt to use Games::Go::Dg2TeX as the B. =back =head1 CONVERTER OPTIONS Converters may be added dynamically as plugins, so this list only includes converter plugin modules that are included with the Sgf2Dg distribution. Converter options are prepended with the converter name so that option xyz for converter Games::Go::Dg2Abc is written on the command line as: $ sgf2dg ... -Abc-xyz ... Converter options that take arguments must be quoted so that the shell passes the option and any arguments as a single ARGV. For example, if the xyz option for converter Dg2Abc takes 'foo' and 'bar' as additional arguments, the command line would be: $ sgf2dg ... "-Abc-xyz foo bar" ... or a more realistic example of changing the background color: $ sgf2dg genan-shuwa -converter Tk "-Tk-bg #d2f1b4bc8c8b" Since Sgf2Dg is a super-set replacement for the Sgf2TeX package, TeX holds the default position for converters. Because of this historically priviledged position, the Dg2TeX options below do not need to be prepended with 'TeX-'. All of the following options apply to the Dg2TeX converter. Other plugins available at the time of release are Dg2Mp, Dg2ASCII, Dg2PDF, Dg2Ps, Dg2Tk and Dg2TkPs. Dg2ASCII and Dg2TkPs take no additional options. Dg2Tk doesn't explicitly accept options, but it attempts to pass unrecognized options to the Tk::Canvas widgets at creation time (which is why the example above works). For more information about converter-specific options, please refer to the perldoc or manual pages: $ perldoc Games::Go::Dg2PDF or $ man Games::Go::Dg2Ps =head2 Dg2TeX options =over 4 =item B<-simple> (Dg2TeX) This generates very simple TeX which may not look so good on the page, but is convenient if you intend to edit the TeX. =item B<-mag number> (Dg2TeX) Changes the default \\magnification from 1000 to number. =item B<-twoColumn> (Dg2TeX) This generates a two-column format using smaller fonts. =item B<-bigFonts> (Dg2TeX) Use fonts magnified 1.2 times. =item B<-texComments> (Dg2TeX) If this option is NOT used then the characters {, } and \ found in comments are replaced by [, ] and /, since TeX roman fonts do not have these characters. If this option is used, these substitutions are not made, so you can embed TeX source (like {\bf change fonts}) directly inside the comments. =item B<-floatControl 'control_string'> (Dg2TeX) Dg2TeX can float the diagram to the left or the right of the accompanying text. B<-floatControl> allows you to specify the position of each diagram. B is a sequence of letters which specify where each diagram should be positioned. The letters may be: =over 4 =item 'l': left =item 'r': right =item 'a': alternate =item other: random =back B letters are consumed, one per diagram, until there is only one letter left. The final letter is used for all remaining diagrams. The default B is 'rx' which places the first diagram on the right (text on the left) and all remaining diagrams are placed randomly. =back =cut # # parse the command line arguments: # my ($inHandle, $outHandle, $inFileName, $outFileName); my ($arg, @unknownOpt); while (scalar(@ARGV)) { $arg = shift(@ARGV); if (($arg eq '-d') or ($arg eq '-doubleDigits')) { $option{doubleDigits} = 1 } elsif (($arg eq '-i') or ($arg eq '-in')) { $inFileName = shift(@ARGV); } elsif (($arg eq '-o') or ($arg eq '-out')) { $outFileName = shift(@ARGV); } elsif (($arg eq '-m') or ($arg eq '-movesPerDiagram')) { $option{movesPerDiagram} = shift(@ARGV); } elsif (($arg eq '-n') or ($arg eq '-newNumbers')) { $option{newNumbers} = 1; } elsif (($arg eq '-av') or ($arg eq '-absoluteVarNums')) { $option{varNumbersFlag} = 'absolute'; } elsif (($arg eq '-rv') or ($arg eq '-relativeVarNums')) { $option{varNumbersFlag} = 'relative'; } elsif (($arg eq '-cv') or ($arg eq '-correlativeVarNums')) { $option{varNumbersFlag} = 'correlative'; } elsif (($arg eq '-im') or ($arg eq '-ignoreMarks')) { $option{ignoreMarks} = 1; } elsif (($arg eq '-ic') or ($arg eq '-ignoreComments')) { $option{ignoreComments} = 1; } elsif (($arg eq '-il') or ($arg eq '-ignoreLetters')) { $option{ignoreLetters} = 1; } elsif (($arg eq '-ip') or ($arg eq '-ignorePass')) { $option{ignorePass} = 1; } elsif (($arg eq '-iv') or ($arg eq '-ignoreVariations')) { $option{ignoreVariations} = 1; } elsif (($arg eq '-ia') or ($arg eq '-ignoreAll')) { $option{ignoreVariations} = 1; $option{ignoreComments} = 1; $option{ignoreLetters} = 1; $option{ignoreMarks} = 1; $option{ignorePass} = 1; } elsif (($arg eq '-rl') or ($arg eq '-repeatLast')) { $option{repeatLast} = 1; } elsif (($arg eq '-break') or ($arg eq '-breakList')) { @{$option{breakList}} = sort {$::a <=> $::b} (split(/,/, shift(@ARGV))); } elsif (($arg eq '-t') or ($arg eq '-top')) { $option{topLine} = shift(@ARGV); } elsif (($arg eq '-b') or ($arg eq '-bottom')) { $option{bottomLine} = shift(@ARGV); } elsif (($arg eq '-l') or ($arg eq '-left')) { $option{leftLine} = shift(@ARGV); } elsif (($arg eq '-r') or ($arg eq '-right')) { $option{rightLine} = shift(@ARGV); } elsif ($arg eq '-crop') { $option{crop} = 1; } elsif ($arg eq "-placeHandi") { $option{placeHandi} = 1; } elsif ($arg eq "-coords") { $option{coords} = 1; } elsif (($arg eq '-cs') or ($arg eq "-coordStyle")) { $option{coordStyle} = lc(shift(@ARGV)); my %legal = (normal => 1, sgf => 1, '++' => 1, '+-' => 1, '-+' => 1, '--' => 1); unless (exists($legal{$option{coordStyle}})) { die "illegal coordStyle: $option{coordStyle}, must be: normal, sgf, ++, +-, -+, or --\n"; } } elsif ($arg eq '-firstDiagram') { $option{firstDiagram} = shift(@ARGV); } elsif ($arg eq '-lastDiagram') { $option{lastDiagram} = shift(@ARGV); } elsif (($arg eq '-h') or ($arg eq '-help')) { print($help); exit(0); } elsif (($arg eq '-v')or($arg eq '-version')) { print("$myName $version\n"); exit(0); } elsif ($arg eq '-verbose') { $option{verbose} = 1; } elsif (($arg eq '-converter') or ($arg eq '-convert')) { $option{converter} = shift(@ARGV); $option{converter} =~ s/.*Games::Go::Dg2//; } elsif ($arg eq '-texComments') { $option{converterOption}{texComments} = 1; } elsif ($arg eq '-bigFonts') { $option{converterOption}{bigFonts} = 1; } elsif ($arg eq '-simple') { $option{converterOption}{simple} = 1; } elsif ($arg eq '-mag') { $option{converterOption}{mag} = shift(@ARGV); } elsif ($arg eq '-twoColumn') { $option{converterOption}{twoColumn} = 1; } elsif ($arg eq "-floatControl") { unless (@ARGV) { die("Please specify a control_string for the floatControl option\n") } $option{converterOption}{floatControl} = lc(shift(@ARGV)); } elsif (substr($arg, 0, 1) eq '-') { push(@unknownOpt, $arg); # worry about it later... } else { $inFileName = $arg; } } foreach (@unknownOpt) { if (m/^-$option{converter}-(.*)/) { my $cnvOpt = $1; if ($cnvOpt =~ m/(\S+)\s+(.*)/) { $option{converterOption}{$1} = $2; } else { $option{converterOption}{$cnvOpt} = 1; } } else { print("\nUnknown option: $_\n"); print($help); exit(1); } } if ($option{converter} eq 'SL') { # set some options especially for Sensei's Library if(exists($option{movesPerDiagram})) { if ($option{movesPerDiagram} > 10) { print "Warning: Sensei's Library won't accept movesPerDiagram greater than 10\n"; print "I'll continue, but the output may not be valid\n"; } } else { $option{movesPerDiagram} = 10; } $option{newNumbers} = 1; # turn on newNumbers } unless(exists($option{movesPerDiagram})) { $option{movesPerDiagram} = scalar(@{$option{breakList}}) ? 10000 : 50 } # open the input file handle if (not defined($inFileName) or ($inFileName eq '-')) { $inFileName = 'STDIN'; $inHandle = \*STDIN; } else { if (!-e $inFileName) { if (-e "$inFileName.sgf") { $inFileName = "$inFileName.sgf"; } elsif (-e "${inFileName}sgf") { $inFileName = "${inFileName}sgf"; } else { if (-e "$inFileName.mgt") { $inFileName = "$inFileName.mgt"; } else { die("Can't find $inFileName, $inFileName.sgf or $inFileName.mgt\n"); } } } $inHandle = IO::File->new("<$inFileName") or die("Can't open $inFileName for reading: $!\n"); } # convert the input filename into an output filename unless (defined ($outFileName)) { if ($inFileName eq 'STDIN') { $outFileName = 'STDOUT'; } else { $outFileName = $inFileName; unless ($outFileName =~ s/.sgf$//i) { $outFileName =~ s/.mgt$//i; } } $outFileName =~ s/.*\///; # output into current directory } # create the root diagram object $rootDiagram = $diagram = Games::Go::Diagram->new(callback => \&finishDiagram, boardSizeX => $option{boardSizeX}, boardSizeY => $option{boardSizeY}, coord_style => $option{coordStyle}); $diagram->user({first => 'first', # init user hash mainId => 1, id => $diagramId++}); # id isn't really used for anything, # but it helps during debug # create the converter object (early so we get errors right away) my $fullConvName = "Games::Go::Dg2$option{converter}"; eval "require $fullConvName;"; die "Couldn't require $fullConvName: $@" if $@; my $converter; eval "\$converter = $fullConvName->new( doubleDigits => \$option{doubleDigits}, coords => \$option{coords}, \%{\$option{converterOption}});"; die "Couldn't create new $fullConvName converter: $@" if $@; # parse the SGF into the diagrams printIndent("Parsing Diagram 1 at move $moveNum\n"); SGF_ReadFile($inHandle); close($inHandle); # don't need this anymore $converter->configure( boardSizeX => $option{boardSizeX}, boardSizeY => $option{boardSizeY}, %{$option{converterOption}} ); # handle the output file if (($outFileName eq 'STDOUT') or ($outFileName eq '-')) { $converter->configure(file => \*STDOUT); $converter->configure(filename => 'STDOUT'); } else { my $outSuffix = lc $option{converter}; unless ($outFileName =~ m/.$outSuffix$/i) { $outFileName .= ".$outSuffix"; # tack on the converter extension } $converter->configure(file => ">$outFileName"); } # add an attribution comment $converter->comment( "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% This file was created by $myName $version with the following command line: $commandLine $myName was created by Reid Augustin. The go fonts, TeX macros and TeX programming were designed by Daniel Bump. More information about the $myName package can be found at: http://match.stanford.edu/bump/go.html %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% "); # now print the diagrams convertDiagram($converter, $rootDiagram, 0); $converter->close; exit(0); # The auto_bounds method is borrowed (stolen?) from Marcel Gruenauer's # Dg2SL converter for Sensei's Library (with slight modifications). # We also use this for the VW (view) control property to make sure # our coordinates don't extend past the VieW boundaries. # # set topLine, bottomLine, etc. based on the extent of the current diagram sub auto_bounds { my ($dg2, $diagram) = @_; my $vw = exists($diagram->property()->{0}{VW}); if (not $vw and not $option{crop}) { $dg2->configure(leftLine => $option{leftLine}, rightLine => $option{rightLine}, topLine => $option{topLine}, bottomLine => $option{bottomLine}); return; } my $left = $option{boardSizeX}; my $top = $option{boardSizeY}; my $right = my $bottom = 0; # Visit each intersection in this diagram to determine the bounds foreach my $y (1 .. $option{boardSizeY}) { foreach my $x (1 .. $option{boardSizeX}) { my $int = $diagram->get($dg2->diaCoords($x, $y)); next unless ($vw ? exists ($int->{VW}) : (exists ($int->{black}) or exists ($int->{white}))); $left = $x if ($x < $left); $right = $x if ($x > $right); $top = $y if ($y < $top); $bottom = $y if ($y > $bottom); } } # Now we have the boundaries of the visible stones. # Note: VieW has priority over crop option unless ($vw) { # if ($option{crop}) { # Leave two empty lines on each side. $left -= 2; $right += 2; $top -= 2; $bottom += 2; # don't leave out just border lines $left = 1 if $left <= 2; $right = $dg2->{boardSizeX} if $right >= $dg2->{boardSizeX} - 1; $top = 1 if $top <= 2; $bottom = $dg2->{boardSizeY} if $bottom >= $dg2->{boardSizeY} - 1; # don't cut off one line away from the border $left = $option{leftLine} unless $left > 2; $right = $option{rightLine} unless $right < $dg2->{boardSizeX} - 1; $top = $option{topLine} unless $top > 2; $bottom = $option{bottomLine} unless $bottom < $dg2->{boardSizeY} - 1; } $dg2->configure(leftLine => $left, rightLine => $right, topLine => $top, bottomLine => $bottom); } __END__ =head1 SEE ALSO =over 0 =item o sgfsplit(1) - splits a .sgf file into its component variations =back =head1 AUTHOR sgf2dg was written by Reid Augustin, Ereid@hellosix.comE The GOOE fonts and TeX macros were designed by Daniel Bump (bump@math.stanford.edu). Daniel hosts the GOOE and sgf2dg home page at: L =head1 COPYRIGHT AND LICENSE Copyright (C) 1997-2005 by Reid Augustin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111, USA =cut