package Games::Go::Referee; use strict; use warnings; use Games::Go::SGF; use Games::Go::Referee::Node; use English qw(-no_match_vars); # Avoids regex performance penalty use Carp; our $VERSION = 0.07; sub new { my $this = shift; my $class = ref($this) || $this; my $self = {}; $self->{_const} = { # defaults size => 18, # default board size selfcapture => 0, # is self capture OK? ssk => 0, # situational super ko passes => 2, # number of consecutive passes required to finish play hfree => 0, # are handicap stones freely placed? handicap => 0, # the handicap number exitonerror => 0, # exit on (Go) error if set, or continue if not set alternation => 1, # flag alternation errors as errors? yes/on passcount => 1, # flag passcount errors as errors? yes/on pointformat => 'sgf' # can be sgf or gmp }; $self->{_node} = {}; # contains a Referee::Node object $self->{_boardstr} = {}; $self->{_nodecount} = 0; $self->{_movecount} = 0; $self->{_passcount} = 0; $self->{_colour} = 'None'; $self->{_cellfarm} = {}; # eg key = 0,12 value = 'o','x', or '.' $self->{_errors} = []; # eg [3][12] where 3 is an error code, 12 the node it happened $self->{_prisonersB} = 0; $self->{_prisonersW} = 0; $self->{_sgf} = {}; # refererence to sgf file $self->{_coderef} = undef; bless $self, $class; $self->{_node}{0} = makenode($self, $self->{_colour}); $self->{_cellfarm}{','} = ''; # pass is empty $self->{_debug} = 0; return $self; } sub sgffile{ my ($self, $sgf_file, $grammarflag) = @_; my $sgf = new Games::Go::SGF($sgf_file, $grammarflag); defined $sgf or croak "Bad Go sgf"; restart($self); size($self, $sgf->SZ, 1); initrules($self, $sgf->RU); $self->{_sgf} = $sgf; $self->{_const}{handicap} = $sgf->HA if $sgf->HA; my $clicker = 0; my $movecount = 0; while (my $node = $sgf->move($clicker++)) { $movecount = donode($self, $node, $movecount); } return Games::Go::SGF::getsgf($sgf); } sub donode { my ($self, $node, $movecount) = @_; if (ref($node) eq 'Games::Go::SGF::Node'){ if (ismove($node) or issetup($node)){ processtags($self, $node); $movecount++; } } else { if (ref($node) eq 'Games::Go::SGF::Variation'){ dovar($self, $node, $movecount); } } return $movecount } sub dovar { my ($self, $startpoint, $base) = @_; my $v = 0; my @vars = $startpoint->variations; while (defined $vars[$v]){ my $basenumber = $base; restore($self, $base) unless $v == 0; for (@{$vars[$v++]}){ $basenumber = donode($self, $_, $basenumber); } } } sub _iterboard (&$) { my ($sub, $size) = @_; for my $y (0..$size){ for my $x (0..$size){ $sub->($x, $y); } } } sub size { my ($self, $size, $flag) = @_; my $adjust = 1; if ($size) { $self->{_const}{size} = _numbersetting($self, $size, 'size', $adjust); clearboard($self); } return $self->{_const}{size} } sub gtpclearboard { restore(shift, 0) } sub ruleset { &initrules } sub debug { my $self = shift; my $debug = shift; $self->{_debug} = $debug if defined $debug and $debug =~ /0|1/; return $self->{_debug} } sub ssk { my $self = shift; $self->{_const}{ssk} = _rulesetting($self, 'ssk', @_); return $self->{_const}{ssk} } sub alternation { my $self = shift; $self->{_const}{alternation} = _rulesetting($self, 'alternation', @_); return $self->{_const}{alternation} } sub selfcapture { my $self = shift; $self->{_const}{selfcapture} = _rulesetting($self, 'selfcapture', @_); return $self->{_const}{selfcapture} } sub exitonerror { my $self = shift; $self->{_const}{exitonerror} = _rulesetting($self, 'exitonerror', @_); return $self->{_const}{exitonerror} } sub passes { my $self = shift; $self->{_const}{passes} = _numbersetting($self, @_, 'passes', 0); return $self->{_const}{passes} } sub pointformat { my $self = shift; if (@_) { my $format = shift ; if ($format eq 'sgf' or $format eq 'gmp') { $self->{_const}{pointformat} = $format; } else { croak 'Illegal value ', $format if defined $format; } } return $self->{_const}{pointformat} } sub _numbersetting { my ($self, $value, $rule, $adjust) = @_; if ($value =~ /\d+/o and $value > 0) { $self->{_const}{$rule} = $value - $adjust; } else { croak 'Illegal value ', $value } return $self->{_const}{$rule} } sub _rulesetting { my $self = shift; my $rule = shift; if (@_) { my $switch = shift; for ($switch) { if ($switch eq 'on') { $self->{_const}{$rule} = 1; last; } if ($switch eq 'off') { $self->{_const}{$rule} = 0; last; } croak 'Unknown setting'; } } return $self->{_const}{$rule} } sub play { my ($self, $colour, $ab) = @_; croak 'Illegal move format' unless checkmove($self, $ab); if (($colour eq 'B') or ($colour eq 'W')) { $self->{_errors} = []; $self->{_node}{++$self->{_nodecount}} = makenode($self, $colour, $ab); move($self, $colour, $ab); } else { croak 'Colour not recognised'; } return errorcode($self); } sub setup { my ($self, $type, $ablist) = @_; for ($type) { if (',AB,AW,AE,' =~ /,($_),/) { $self->{_errors} = []; $self->{_node}{++$self->{_nodecount}} = makenode($self, 'None'); for (split (',', $ablist)){ changecell($self, $1, $_) } last; } croak 'Setup type not recognised'; } return errorcode($self); } sub handicap { my ($self, $number) = @_; if ($number =~ /[2-9]/o){ if ($self->{_const}{hfree}){ $self->{_const}{handicap} = $number; } else { if ($self->{_const}{size} == 18){ my @hpoints = ('dp','pd','pp','dd','jj','dj','pj','jd','jp'); splice @hpoints, 4, 1 if $number % 2 == 0; splice @hpoints, $number; setup($self, 'AB', join ',', @hpoints); } } } else { croak 'Handicap not allowed'; } return errorcode($self); } # return true if a co-ordinate pair is a legal move sub islegal { my ($self, $colour, $point) = @_; my $res = play($self, $colour, $point); myprint ($colour, $point, 'has legality:', $res) if $self->{_debug}; restore($self, -1); return $res?0:1 } # return a list of the co-ordinates of all legal moves sub legal { my ($self, $colour) = @_; my @legallist; _iterboard { my ($x, $y) = @_; if ($self->{_cellfarm}{$x.','.$y} eq '.') { my $point = insertpoints($self, $x, $y); push @legallist, $point unless play($self, $colour, $point); restore($self, -1); } } $self->{_const}{size}; return @legallist; } # return a list of the co-ordinates of all illegal moves sub illegal { my ($self, $colour) = @_; my @illegallist; _iterboard { my ($x, $y) = @_; if ($self->{_cellfarm}{$x.','.$y} eq '.') { my $point = insertpoints($self, $x, $y); push @illegallist, $point if play($self, $colour, $point); restore($self, -1); } } $self->{_const}{size}; return @illegallist; } # return true if $colour (ie 'B' or 'W') has a legal move, otherwise return false sub haslegal { my ($self, $colour) = @_; my $exit = 0; my $size = $self->{_const}{size}; for my $y (0..$size){ for my $x (0..$size){ if ($self->{_cellfarm}{$x.','.$y} eq '.') { $exit = 1 unless play($self, $colour, insertpoints($self, $x, $y)); restore($self, -1); return 1 if $exit; } } } return 0; } # return a ':' seperated list of the co-ordinates of any captured stones sub captures { my ($self, $id) = @_; $id ||= $self->{_nodecount}; my $s = ''; my $capsref = $self->{_node}{$id}->captures; if ($capsref) { my @delstones = @{$capsref}; my $seperator = ':'; for my $i (0..$#delstones) { $seperator = '' if $i == $#delstones; $s .= insertpoints($self, ($delstones[$i][0]), ($delstones[$i][1])).$seperator; } } return $s } # restore the game to that at move $howmany # if $howmany is negative, go back that number of moves. sub restore{ my ($self, $howmany) = @_; croak 'Cannot restore to ', $howmany if (abs($howmany) > $self->{_nodecount}); $howmany += $self->{_nodecount} if ($howmany < 0); boardrestore($self, $howmany); deletenodes($self, $howmany); $self->{_nodecount} = $howmany; my $node = $self->{_node}{$howmany}; $self->{_movecount} = $node->movecount; $self->{_colour} = $node->colour; $self->{_passcount} = $node->passcount; return } # return the board as a string sub showboard{ my $self = shift; my $h; my $size = $self->{_const}{size}; _iterboard { my ($x, $y) = @_; $h .= $self->{_cellfarm}{$x.','.$y}; $h .= "\n" if $x == $size; } $size; $h .= "\n"; return $h; } # return a section of the board as a string sub getboardsection{ my ($self, $ox, $oy, $size) = @_; my $h; _iterboard { my ($x, $y) = @_; my $xnew = $x + $ox; my $ynew = $y + $oy; $h .= $self->{_cellfarm}{$xnew.','.$ynew} || '-'; } $size; return $h; } # get contents of a point sub point{ my ($self, $ab, $y) = @_; ($ab, $y) = extractpoints($self, $ab) unless defined($y); return $self->{_cellfarm}{$ab.','.$y}; } # get contents of a point at a particular move sub nodepoint{ my ($self, $id, $x, $y) = @_; my $positionref = $self->{_node}{$id}->board; return substr($$positionref, ($y * ($self->{_const}{size} + 1)) + $x, 1) } # get the co-ordinates of move number '$counter' sub getmove { my ($self, $counter) = @_; my $node = $self->{_node}{$counter}; return $node->colour, $node->point if defined $node; } #restore the board position to that of move number $id sub boardrestore{ my ($self, $id) = @_; myprint('Restoring to', $id) if $self->{_debug}; my $positionref = $self->{_node}{$id}->board; my $size = $self->{_const}{size}; _iterboard { my ($x, $y) = @_; $self->{_cellfarm}{$x.','.$y} = substr($$positionref, ($y*($size+1))+ $x, 1); } $size; } sub deletenodes { my ($self, $upperB) = @_; for (keys %{$self->{_node}}) { if ($_ > $upperB) { my $board = $self->{_node}{$_}->board; delete $self->{_boardstr}{$$board} if defined $board; delete $self->{_node}{$_}; } } } #save the board position as a reference to a string sub store{ my $self = shift; my $h = ''; _iterboard { my ($x, $y) = @_; die 'Undefined Value' unless defined $self->{_cellfarm}{$x.','.$y}; $h .= $self->{_cellfarm}{$x.','.$y}; } $self->{_const}{size}; return \$h; } # Change the value of a cell sub put_cell{ my ($self, $x, $y, $c) = @_; if ($c ne '.' and $self->{_cellfarm}{$x.','.$y} ne '.'){ return 1 } else { $self->{_cellfarm}{$x.','.$y} = $c; return 0 } } sub delete_group{ my ($self, @mygroup) = @_; for (0..$#mygroup) { put_cell($self, $mygroup[$_][0], $mygroup[$_][1], '.'); } } # return a list of the points solidly connected to x,y sub block{ my ($self, $x, $y, $c, $group) = @_; unless (offboard($self->{_const}{size}, $x, $y)) { my $cellcontents = $self->{_cellfarm}{$x.','.$y}; if ($cellcontents eq $c) { $$group{$x.','.$y} = undef; my @directions = ([1,0],[0,1],[-1,0],[0,-1]); for (0..3) { my $xx = $directions[$_][0] + $x; my $yy = $directions[$_][1] + $y; unless (exists($$group{$xx.','.$yy})) { $group = block($self, $xx, $yy, $c, $group); } } } } return $group; } sub libertycheck{ my ($self, $x, $y, $c, $haslibs, $group) = @_; unless ($haslibs or offboard($self->{_const}{size}, $x, $y)) { my $cellcontents = $self->{_cellfarm}{$x.','.$y}; if ($cellcontents eq $c) { $$group{$x.','.$y} = undef; my @directions = ([1,0],[0,1],[-1,0],[0,-1]); for (0..3) { my $xx = $directions[$_][0] + $x; my $yy = $directions[$_][1] + $y; unless (exists($$group{$xx.','.$yy})) { ($haslibs, $group) = libertycheck($self, $xx, $yy, $c, $haslibs, $group); } } } else { $haslibs = $cellcontents eq '.'; } } return $haslibs, $group; } sub checkforcaptures{ my ($self, $x, $y, $colour, $type) = @_; my $capturedSomething = 0; my @directions = ($type eq 'self') ? ([0,0]) : ([1,0],[0,1],[-1,0],[0,-1]); my @deletedstones; for (0..$#directions) { my $xdir = $directions[$_][0]+$x; my $ydir = $directions[$_][1]+$y; my ($haslibs, $points) = libertycheck($self, $xdir, $ydir, $colour, 0, {}); if (keys(%{$points}) and not $haslibs) { my $pointsref = getpoints($points); delete_group($self, @{$pointsref}); push @deletedstones, @{$pointsref}; $capturedSomething = 1; } } return $capturedSomething, \@deletedstones } # main move handler and error detector sub processmove{ my ($self, $colour, $ab) = @_; my $id = $self->{_nodecount}; my $c = ($colour eq 'W')?'o':'x'; my $noderef = \$self->{_node}{$id}; my $move = $self->{_movecount}; if (defined $self->{_coderef}) { my $rank = $colour.'R'; $self->{_coderef}->learn($colour, $ab, $self, $move, $self->{_sgf}->$rank); } if ($colour eq $self->{_colour} and $self->{_const}{alternation}){ unless ($id <= $self->{_const}{handicap} and $self->{_const}{hfree}) { adderror($self, 7, $move); return if $self->{_const}{exitonerror} } } $self->{_colour} = $colour; my $size = $self->{_const}{size}; if (ispass($self, $ab)) { $$noderef->passcount(++$self->{_passcount}); $$noderef->board(store($self)); } else { if ($self->{_passcount} >= $self->{_const}{passes} and $self->{_const}{passcount}) { adderror($self, 8, $move); return if $self->{_const}{exitonerror}; } $self->{_passcount} = 0; $$noderef->passcount(0); my ($x, $y) = extractpoints($self, $ab); if (offboard($size, $x, $y)) { adderror($self, 1, $move); return if $self->{_const}{exitonerror}; } else { if (put_cell($self, $x, $y, $c)) { adderror($self, 2, $move); return if $self->{_const}{exitonerror}; } my ($captured, $delstonesref, $error) = checkbothcaptures($self, $x, $y, $c, 1); my $ctype = '_prisoners'.$colour; $self->{$ctype} += @$delstonesref; if ($error) { adderror($self, 5, $move); return if $self->{_const}{exitonerror}; } $$noderef->captures($delstonesref) if $captured; my $board = store($self); if (exists $self->{_boardstr}{$$board}) { if ($self->{_const}{ssk}) { adderror($self, 6, $move); return if $self->{_const}{exitonerror}; } else { adderror($self, 6, $move); return if $self->{_const}{exitonerror}; } } else { $self->{_boardstr}{$$board} = $colour; } $$noderef->board($board); # store the board in a Node as a string myprint('Node id', $id) if $self->{_debug}; myprint(showboard($self)) if $self->{_debug}; } } return 1 } # change a value in cellfarm # used when AB, AW, and AE tags found sub changecell{ my ($self, $colour, $point) = @_; my $c; SWITCH:for ($colour) { if ($_ eq 'AW') {$c = 'o'; last} if ($_ eq 'AB') {$c = 'x'; last} $c = '.'; } my $id = $self->{_nodecount}; my ($x, $y) = extractpoints($self, $point); my $size = $self->{_const}{size}; if (offboard($size, $x, $y)) { adderror($self, 9, $id); } else { adderror($self, 4, $id) if (put_cell($self, $x, $y, $c)); unless ($c eq '.'){ my ($capturedSomething, undef) = checkbothcaptures($self, $x, $y, $c, 0); if ($capturedSomething) { adderror($self, 5, $id); return if $self->{_const}{exitonerror}; } } $self->{_node}{$id}->board(store($self)); } } sub checkbothcaptures { my ($self, $x, $y, $c, $movetype) = @_; my $myerror = 0; my $reversec = ($c eq 'o')?'x':'o'; # reverse colours my ($capturedsomething, $delstonesref) = checkforcaptures($self, $x, $y, $reversec, 'opponents'); unless ($capturedsomething){ ($capturedsomething, $delstonesref) = checkforcaptures($self, $x, $y, $c, 'self'); $myerror = 1 if ($capturedsomething and not $self->{_const}{selfcapture}); } return $capturedsomething, $delstonesref, $myerror; } sub move { my $self = shift; $self->{_movecount}++; return processmove($self, @_); } sub processtags { my ($self, $sgfnode) = @_; my $colour = ($sgfnode->PL) ? $sgfnode->PL : $sgfnode->colour; $self->{_node}{++$self->{_nodecount}} = makenode($self, $colour, $sgfnode->move); for (split (',',$sgfnode->tags)){ if (($_ eq 'B') or ($_ eq 'W')) { return unless move($self, $sgfnode->colour, $sgfnode->move); next; } if (',AB,AW,AE,' =~ /,($_),/) { my $tag = $1; for (split (',', $sgfnode->$tag)) { if ( $_ =~ /(..):(..)/) { my $arrayref = generaterectangle($self, $1, $2); for (@$arrayref) {changecell($self, $tag, $_)}; } else { changecell($self, $tag, $_); } } next; } } return 1 } sub generaterectangle { my ($self, $topleft, $bottomright) = @_; my @pointlist; my ($tx, $ty) = extractpoints($self, $topleft); my ($bx, $by) = extractpoints($self, $bottomright); for my $x ($tx..$bx) { for my $y ($ty..$by) { push @pointlist, insertpoints($self, $x, $y); } } return \@pointlist; } # list all the stones of a particular colour sub liststones { my ($self, $colour) = @_; my $stone = ($colour eq 'B') ? 'x' : 'o'; my %hash; _iterboard { my ($x, $y) = @_; if ($self->{_cellfarm}{$x.','.$y} eq $stone) { $hash{$x.','.$y} = undef; } } $self->{_const}{size}; return \%hash } # list all the live stones of a particular colour # (as the set of all blocks adjacent to their opponent's illegal moves) sub listalive { my ($self, $colour) = @_; # turn off alternation and passcount errors temporarily $self->{_const}{passcount} = 0; $self->{_const}{alternation} = 0; # first get the list of illegal moves for the other player my @illegallist = illegal($self, swapcolour($self, $colour)); my $points = {}; my $stone = ($colour eq 'B') ? 'x' : 'o'; # now get the blocks attached to those illegal points for (@illegallist) { my ($x, $y) = extractpoints($self, $_); my @directions = ([1,0],[0,1],[-1,0],[0,-1]); for (0..3) { my $xdir = $directions[$_][0]+$x; my $ydir = $directions[$_][1]+$y; $points = block($self, $xdir, $ydir, $stone, $points); } } $self->{_const}{passcount} = 1; $self->{_const}{alternation} = 1; return $points } # list the dead stones of a particular colour # (as the difference between their alive list # and their total list) sub listdead { my ($self, $colour) = @_; my $allref = liststones($self, $colour); my $aliveref = listalive($self, $colour); my @dead = (); for (keys %$allref) { push @dead, $_ unless exists $aliveref->{$_}; } @dead = map { /(.*),(.*)/; insertpoints($self, $1, $2) } @dead; return \@dead } # list all the dead stones on the board # (as the union of the Black and White # dead stone list) sub listalldead { my ($self) = @_; my $bdead = listdead($self, 'B'); my $wdead = listdead($self, 'W'); my @dead = (@$bdead, @$wdead); return \@dead } sub ismove { testnode(shift, ',B,W,') ? return 1 : return 0 } sub issetup { testnode(shift, ',AB,AW,AE,') ? return 1 : return 0 } sub testnode{ my ($sgfnode, $type) = @_; if ($sgfnode->tags){ for (split (',',$sgfnode->tags)){ if ($type =~ /,$_,/) { return 1; } } } return 0 } sub restart { my $self = shift; $self->{_node} = {}; $self->{_boardstr} = {}; $self->{_nodecount} = 0; $self->{_movecount} = 0; $self->{_passcount} = 0; $self->{_colour} = 'None'; $self->{_cellfarm} = {}; $self->{_errors} = []; $self->{_prisonersB} = 0; $self->{_prisonersW} = 0; $self->{_sgf} = {}; $self->{_node}{0} = makenode($self, $self->{_colour}); } sub initrules { my $self = shift; my $rules = uc(shift); $rules = ($rules) ? $rules : 'Japanese'; $self->{_const}{selfcapture} = 1 if ($rules =~ /^NZ|^NEW ZEALAND|^ING|^GOE/); $self->{_const}{ssk} = 1 if ($rules =~ /^AGA/); $self->{_const}{passes} = 4 if ($rules =~ /^ING|^GOE/); $self->{_const}{hfree} = 1 if ($rules =~ /^NZ|^NEW ZEALAND|^ING|^GOE|^CHINESE/); } sub makenode { my ($self, $colour, $point) = @_; return new Games::Go::Referee::Node($self->{_movecount}+1, $self->{_passcount}, $colour, $point); } sub errors { my ($self) = @_; my $errorhash = { 1 => 'Not a board co-ordinate at move ', 2 => 'Point already occupied at move ', 3 => 'Illegal setup at node ', 4 => 'Point already occupied at node ', 5 => 'Illegal self-capture at move ', 6 => 'Board repetition at move ', 7 => 'Alternation error at move ', 8 => 'Play over at move ', 9 => 'Not a board co-ordinate at node ', 10 => 'Board repetition at node ', }; my @array = @{$self->{_errors}}; my @return; for (0..$#array){ my $ecode = $self->{_errors}[$_][0]; push @return, join '', $errorhash->{$ecode}, $self->{_errors}[$_][1], "\n"; } return @return } sub errorcode { my $self = shift; my @array = @{$self->{_errors}}; my $ecode = undef; for (0..$#array){ $ecode = $self->{_errors}[$_][0]; last; } return defined($ecode)? $ecode: 0; } sub adderror { my ($self, $ecode, $place) = @_; push @{$self->{_errors}}, [$ecode, $place]; } # empty board sub clearboard{ my $self = shift; $self->{_cellfarm} = {}; _iterboard { my ($x, $y) = @_; $self->{_cellfarm}{$x.','.$y} = '.'; } $self->{_const}{size}; $self->{_node}{0}->board(store($self)); return } sub checkmove { # check move is OK according to format my ($self, $string) = @_; myprint ('Checking move', $string) if $self->{_debug}; return 1 if ispass($self, $string); if ($self->{_const}{pointformat} eq 'sgf') { return issgf($string) } else { return isgmp($string) } } sub ispass { my ($self, $move) = @_; if ($self->{_const}{pointformat} eq 'sgf') { return 1 if not defined $move; if (($move eq '') or ($move eq 'tt' and $self->{_const}{size} < 19)) { return 1 } } else { if ('pass' eq lc $move) { return 1 } } } sub issgf { # assuming not a pass shift =~ /^[a-z]{2}$/i; } sub isgmp { # assuming not a pass shift =~ /^[a-z]([1-9]\d?)$/i and 1 <= $1 and $1 <= 25; } sub getpoints { # extract points from a hash key eg '10,1' my $pointsref = shift; my @points; for (keys(%{$pointsref})) { /(.*),(.*)/; push @points, [$1,$2]; } return \@points } sub extractpoints { # convert points from an sgf or gmp string to a pair of numbers my ($self, $string) = @_; my $pass = ispass($self, $string); return '','' if $pass; if ($self->{_const}{pointformat} eq 'sgf') { return fromsgf($string, $pass) } else { return fromgmp($self, $string) } } sub insertpoints { # convert a pair of numbers to an sgf or gmp string my ($self, $x, $y) = @_; if ($self->{_const}{pointformat} eq 'sgf') { return tosgf($x, $y) } else { return togmp($self, $x, $y) } } sub fromsgf { my ($string) = @_; my $x = index(aZ(), substr($string,0,1)); my $y = index(aZ(), substr($string,1,1)); return $x,$y; } sub fromgmp { my ($self, $string) = @_; my $a = index aZnoi(), lc substr $string, 0, 1; my $y = substr $string, 1; return $a, $self->{_const}{size} - $y + 1; } sub togmp { my ($self, $x, $y) = @_; return 'pass' if $x eq '' and $y eq ''; join '', uc(substr(aZnoi(), $x, 1)), $self->{_const}{size} - $y + 1 } sub tosgf { return '' if $_[0] eq '' and $_[1] eq ''; join '', substr(aZ(), $_[0], 1), substr(aZ(), $_[1], 1) } sub offboard { 0 > $_[1] or $_[1] > $_[0] or 0 > $_[2] or $_[2] > $_[0]; } sub swapcolour { return ($_[1] eq 'B') ? 'W' : 'B' } sub aZ { 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' } sub aZnoi { my $str = aZ(); $str =~ s/i//; return $str } sub myprint { my @messages = @_; if (exists $messages[0]) { open(LOG, ">>",'./refereelogfile.txt') or die 'Can\'t open'."\n"; print LOG (join ' ', @messages, "\n"); close(LOG); } } 1; =head1 NAME Games::Go::Referee - Check the moves of a game of Go for rule violations. =head1 SYNOPSIS Analyse a file: use Games::Go::Referee; my $referee = new Games::Go::Referee(); $referee->sgffile('file.sgf'); print $referee->errors; or Analyse move by move: use Games::Go::Referee; my $referee = new Games::Go::Referee(); $referee->size(19); $referee->ruleset('AGA'); $referee->play('B','ab'); $referee->restore(-1) if $referee->errors; =head1 DESCRIPTION Check a game of Go for rules violations, against a specific rule set. =head2 General use Games::Go::Referee can be used in two ways; to analyse an sgf file, or to check plays move by move. If checking a file, the file will be completely read, and any errors found can be displayed later using the errors method. Any illegal plays found are 'allowed' (ie play is assumed to continue as if they were legal). The rule set to be used will be read from the RU sgf property in the file, alternatively various rules can be set manually. If checking move by move, it may be necessary to specify the size and rule set to be used before starting. There are basically two rules that can be set: self-capture allowed/disallowed and situational superko (ssk) on/off. If ssk is off, positional superko is assumed. The following errors are reported: Not a board co-ordinate Point already occupied Illegal setup (if the setup caused a capture to occur) Illegal self-capture Board repetition Alternation error (two Black moves in a row for example) Play over (play continues when the game is over) =head1 METHODS =head2 ruleset The ruleset method sets the rule set to be used. If a file is being checked, the value of the sgf property RU will be used. If that is not found, Japanese rules are assumed. $referee->ruleset('AGA'); =head2 size The size method sets the size of the board to be used. If a file is being checked, the value of the sgf property SZ will be used. If that is not found, the board is assumed to be 19 x 19. $referee->size(19); =head2 ssk The ssk method sets or unsets whether the situational superko rule is being used. ssk can be turned on only by using this method, or by specifying 'AGA' via the ruleset method. $referee->ssk('on'); $referee->ssk('off'); =head2 selfcapture The selfcapture method sets or unsets whether self-capture (aka suicide) is allowed or not. selfcapture can be turned on only by using this method, or by specifying New Zealand or Ing via the rulset method. $referee->selfcapture('on'); $referee->selfcapture('off'); =head2 passes The passes method sets the number of consecutive passes required to end the game. The default value is 2. If the Ing ruleset is being used, this value becomes 4. $referee->passes(3); =head2 setup For move by move analysis, the following two methods are availale. The setup method is used to place preliminary stones on the board. Setup types (the first argument) are 'AB', 'AW' and 'AE'. Each use of setup can only use one of these types. Setup points (the second argument) are a list of sgf style board co-ordinates. $referee->setup('AW','ii,jj,gh'); $referee->setup('AB','aa,bb'); If the setup creates group with no liberties, an error is reported. The method returns true if an error was found, otherwise false. =head2 handicap The handicap method takes as its argument a number from 2 to 9 $referee->handicap(3); This method can be used as a convenient way of placing handicap stones, provided the board size is 19, and the rules indicate that handicap placement is fixed (ie neither Ing, AGA nor Chinese). If handicap placement is fixed, but the board size is not 19, use the setup method. If handicap placement is not fixed, the handicap method should still be used as then the appropriate number of black consecutive plays will be allowed. =head2 play Play a move. Play types (the first argument) are 'B' or 'W'. Each use of play can only use one of these types. The point played (the second argument) is a single sgf style co-ordinate (or '' for a pass.) $referee->play('B','pd'); The method returns true if an error was found, otherwise false. =head2 haslegal $referee->haslegal($colour); # $colour must be 'B' or 'W' Returns true if $colour (ie 'B' or 'W') has a legal move, otherwise returns false. Usage example - while ($referee->haslegal($colour)){ my $point = getmove(); $referee->play($colour, $point); if ($referee->errors) { $referee->restore(-1); } else { $colour = ($colour eq 'B') ? 'W' : 'B'; } } =head2 legal my @points = $referee->legal($colour); # $colour must be 'B' or 'W' Returns an array of a player's legal move co-ordinates. Usage example - my @legalpoints = $referee->legal($colour); while ($#legalpoints >= 0){ # play a random legal move $referee->play($colour, @points[int(rand($#legalpoints))]); $colour = ($colour eq 'B') ? 'W' : 'B'; @legalpoints = $referee->legal($colour); } =head2 errors print $referee->errors; Lists any errors occurring either in the file analysed, or as a result of the previous move/setup. =head2 sgffile $referee->sgffile('file.sgf'); Specify an sgf file to be analysed. =head1 TODO Score? =head1 BUGS/CAVEATS Putting setup stones within a file (not just the first node) can cause problems. For example, after some stones have been added like this, who is next to play? This needs to be known for situational superko. Currently no look-ahead is done to see who, in fact, played next. Natural Superko - if I understood the difference between this and SSK, I might put it in. Ko-pass moves, game resumption ... my head hurts. =head1 AUTHOR (version 0.01) Daniel Gilder =head1 THANKS To Dmitry Mayorov, as some functions, notably group_iterator and checkforcapture are adapted from his original javascript. =cut