File Coverage

blib/lib/Games/Go/Referee.pm
Criterion Covered Total %
statement 284 574 49.4
branch 89 238 37.3
condition 33 70 47.1
subroutine 40 89 44.9
pod 12 72 16.6
total 458 1043 43.9


line stmt bran cond sub pod time code
1             package Games::Go::Referee;
2              
3 1     1   73084 use strict;
  1         3  
  1         46  
4 1     1   5 use warnings;
  1         2  
  1         33  
5 1     1   1345 use Games::Go::SGF;
  1         105755  
  1         70  
6 1     1   1039 use Games::Go::Referee::Node;
  1         3  
  1         35  
7 1     1   6 use English qw(-no_match_vars); # Avoids regex performance penalty
  1         1  
  1         10  
8 1     1   536 use Carp;
  1         2  
  1         9670  
9             our $VERSION = 0.10;
10              
11             sub new {
12 1     1 0 17 my $this = shift;
13 1   33     7 my $class = ref($this) || $this;
14 1         3 my $self = {};
15 1         14 $self->{_const} = { # defaults
16             size => 18, # default board size
17             selfcapture => 0, # is self capture OK?
18             ssk => 0, # situational super ko
19             passes => 2, # number of consecutive passes required to finish play
20             hfree => 0, # are handicap stones freely placed?
21             handicap => 0, # the handicap number
22             exitonerror => 0, # exit on (Go) error if set, or continue if not set
23             alternation => 1, # flag alternation errors as errors? yes/on
24             passcount => 1, # flag passcount errors as errors? yes/on
25             pointformat => 'sgf' # can be sgf or gmp
26             };
27 1         4 $self->{_node} = {}; # contains a Referee::Node object
28 1         3 $self->{_boardstr} = {};
29 1         3 $self->{_nodecount} = 0;
30 1         3 $self->{_movecount} = 0;
31 1         3 $self->{_passcount} = 0;
32 1         3 $self->{_colour} = 'None';
33 1         4 $self->{_cellfarm} = {}; # eg key = 0,12 value = 'o','x', or '.'
34 1         4 $self->{_errors} = []; # eg [3][12] where 3 is an error code, 12 the node it happened
35 1         3 $self->{_prisonersB} = 0;
36 1         2 $self->{_prisonersW} = 0;
37 1         4 $self->{_sgf} = {}; # refererence to sgf file
38 1         3 $self->{_coderef} = undef;
39 1         3 $self->{_cellfarm}{','} = ''; # pass is empty
40 1         3 $self->{_debug} = 0;
41 1         8 $self->{_logfile} = './refereelog.txt';
42 1         4 bless $self, $class;
43 1         12 $self->{_node}{0} = makenode($self, $self->{_colour});
44 1         3 return $self;
45             }
46              
47             sub sgffile{
48 2     2 1 168674 my ($self, $sgf_file, $p1, $p2) = @_;
49 2         5 my $sgf;
50 2 100       9 if (ref($sgf_file) eq 'Games::Go::SGF') {
51 1         3 $sgf = $sgf_file;
52             } else {
53 1         11 $sgf = new Games::Go::SGF($sgf_file, $p1, $p2);
54 1 50       143902 defined $sgf or croak "Bad Go sgf";
55             }
56 2         12 restart($self);
57 2         22 size($self, $sgf->SZ);
58 2         26 initrules($self, $sgf->RU);
59 2         17 $self->{_sgf} = $sgf;
60 2 50       26 $self->{_const}{handicap} = $sgf->HA if $sgf->HA;
61 2         27 my $clicker = 0;
62 2         2 my $movecount = 0;
63              
64 2         18 while (my $node = $sgf->move($clicker++)) {
65 114         717 $movecount = donode($self, $node, $movecount);
66             }
67 2         18 return Games::Go::SGF::getsgf($sgf);
68             }
69              
70             sub donode {
71 114     114 0 180 my ($self, $node, $movecount) = @_;
72 114 100       354 if (ref($node) eq 'Games::Go::SGF::Node'){
73 112 50 66     340 if (ismove($node) or issetup($node)){
74 112         246 processtags($self, $node);
75 112         191 $movecount++;
76             }
77             } else {
78 2 50       10 if (ref($node) eq 'Games::Go::SGF::Variation'){
79 0         0 dovar($self, $node, $movecount);
80             }
81             }
82 114         1016 return $movecount
83             }
84              
85             sub dovar {
86 0     0 0 0 my ($self, $startpoint, $base) = @_;
87 0         0 my $v = 0;
88 0         0 my @vars = $startpoint->variations;
89              
90 0         0 while (defined $vars[$v]){
91 0         0 my $basenumber = $base;
92 0 0       0 restore($self, $base) unless $v == 0;
93              
94 0         0 for (@{$vars[$v++]}){
  0         0  
95 0         0 $basenumber = donode($self, $_, $basenumber);
96             }
97              
98             }
99              
100             }
101              
102             sub _iterboard (&$) {
103 122     122   172 my ($sub, $size) = @_;
104 122         218 for my $y (0..$size){
105 2318         3704 for my $x (0..$size){
106 44042         73148 $sub->($x, $y);
107             }
108             }
109             }
110              
111             sub size {
112 2     2 1 39 my ($self, $size) = @_;
113 2         2 my $adjust = 1;
114 2   50     9 $size ||= 19;
115 2         9 $self->{_const}{size} = _numbersetting($self, $size, 'size', $adjust);
116 2         8 clearboard($self);
117 2         6 return $self->{_const}{size}
118             }
119              
120 0     0 1 0 sub ruleset { &initrules }
121              
122             sub debug {
123 0     0 0 0 my $self = shift;
124 0         0 my $debug = shift;
125 0 0 0     0 $self->{_debug} = $debug if defined $debug and $debug =~ /0|1/;
126 0         0 return $self->{_debug}
127             }
128              
129             sub logfile {
130 0     0 0 0 my $self = shift;
131 0         0 my $logfile = shift;
132 0 0       0 $self->{_logfile} = $logfile if defined $logfile;
133 0         0 return $self->{_logfile}
134             }
135              
136             sub ssk {
137 0     0 1 0 my $self = shift;
138 0         0 $self->{_const}{ssk} = _rulesetting($self, 'ssk', @_);
139 0         0 return $self->{_const}{ssk}
140             }
141              
142             sub alternation {
143 0     0 0 0 my $self = shift;
144 0         0 $self->{_const}{alternation} = _rulesetting($self, 'alternation', @_);
145 0         0 return $self->{_const}{alternation}
146             }
147              
148             sub selfcapture {
149 0     0 1 0 my $self = shift;
150 0         0 $self->{_const}{selfcapture} = _rulesetting($self, 'selfcapture', @_);
151 0         0 return $self->{_const}{selfcapture}
152             }
153              
154             sub exitonerror {
155 0     0 0 0 my $self = shift;
156 0         0 $self->{_const}{exitonerror} = _rulesetting($self, 'exitonerror', @_);
157 0         0 return $self->{_const}{exitonerror}
158             }
159            
160             sub passes {
161 0     0 1 0 my $self = shift;
162 0         0 $self->{_const}{passes} = _numbersetting($self, @_, 'passes', 0);
163 0         0 return $self->{_const}{passes}
164             }
165              
166             sub pointformat {
167 1     1 0 1117 my $self = shift;
168 1 50       6 if (@_) {
169 1         2 my $format = shift ;
170 1 50 33     7 if ($format eq 'sgf' or $format eq 'gtp') {
171 1         4 $self->{_const}{pointformat} = $format;
172             } else {
173 0 0       0 croak 'Illegal value ', $format if defined $format;
174             }
175             }
176 1         6 return $self->{_const}{pointformat}
177             }
178              
179             sub _numbersetting {
180 2     2   5 my ($self, $value, $rule, $adjust) = @_;
181 2 50 33     24 if ($value =~ /\d+/o and $value > 0) {
182 2         9 $self->{_const}{$rule} = $value - $adjust;
183             } else {
184 0         0 croak 'Illegal value ', $value
185             }
186 2         8 return $self->{_const}{$rule}
187             }
188              
189             sub _rulesetting {
190 0     0   0 my $self = shift;
191 0         0 my $rule = shift;
192              
193 0 0       0 if (@_) {
194 0         0 my $switch = shift;
195 0         0 for ($switch) {
196 0 0       0 if ($switch eq 'on') {
197 0         0 $self->{_const}{$rule} = 1;
198 0         0 last;
199             }
200 0 0       0 if ($switch eq 'off') {
201 0         0 $self->{_const}{$rule} = 0;
202 0         0 last;
203             }
204 0         0 croak 'Unknown setting';
205             }
206             }
207 0         0 return $self->{_const}{$rule}
208             }
209              
210             sub play {
211 0     0 1 0 my ($self, $colour, $ab) = @_;
212 0 0       0 croak 'Illegal move format' unless checkmove($self, $ab);
213 0 0 0     0 if (($colour eq 'B') or ($colour eq 'W')) {
214 0         0 $self->{_errors} = [];
215 0         0 $self->{_node}{++$self->{_nodecount}} = makenode($self, $colour, $ab);
216 0         0 move($self, $colour, $ab);
217             } else {
218 0         0 croak 'Colour not recognised';
219             }
220 0         0 return errorcode($self);
221             }
222              
223             sub setup {
224 0     0 1 0 my ($self, $type, $ablist) = @_;
225 0         0 for ($type) {
226 0 0       0 if (',AB,AW,AE,' =~ /,($_),/) {
227 0         0 $self->{_errors} = [];
228 0         0 $self->{_node}{++$self->{_nodecount}} = makenode($self, 'None');
229 0         0 for (split (',', $ablist)){ changecell($self, $1, $_) }
  0         0  
230 0         0 last;
231             }
232 0         0 croak 'Setup type not recognised';
233             }
234 0         0 return errorcode($self);
235             }
236              
237             sub handicap {
238 0     0 1 0 my ($self, $number) = @_;
239 0 0       0 if ($number =~ /[2-9]/o){
240 0 0       0 if ($self->{_const}{hfree}){
241 0         0 $self->{_const}{handicap} = $number;
242             } else {
243 0 0       0 if ($self->{_const}{size} == 18){
244 0         0 my @hpoints = ('dp','pd','pp','dd','jj','dj','pj','jd','jp');
245 0 0       0 splice @hpoints, 4, 1 if $number % 2 == 0;
246 0         0 splice @hpoints, $number;
247 0         0 setup($self, 'AB', join ',', @hpoints);
248             }
249             }
250             } else {
251 0         0 croak 'Handicap not allowed';
252             }
253 0         0 return errorcode($self);
254             }
255              
256             # return true if a co-ordinate pair is a legal move
257              
258             sub islegal {
259 0     0 0 0 my ($self, $colour, $point) = @_;
260 0         0 my $res = play($self, $colour, $point);
261 0 0       0 myprint ($self, $colour, $point, 'has legality:', $res) if $self->{_debug};
262 0         0 restore($self, -1);
263 0 0       0 return $res?0:1
264             }
265              
266             # return a list of the co-ordinates of all legal moves
267              
268             sub legal {
269 0     0 1 0 my ($self, $colour) = @_;
270 0         0 my @legallist;
271              
272             _iterboard {
273 0     0   0 my ($x, $y) = @_;
274 0 0       0 if ($self->{_cellfarm}{$x.','.$y} eq '.') {
275 0         0 my $point = insertpoints($self, $x, $y);
276 0 0       0 push @legallist, $point unless play($self, $colour, $point);
277 0         0 restore($self, -1);
278             }
279 0         0 } $self->{_const}{size};
280              
281 0         0 return @legallist;
282             }
283              
284             # return a list of the co-ordinates of all illegal moves
285              
286             sub illegal {
287 0     0 0 0 my ($self, $colour) = @_;
288 0         0 my @illegallist;
289              
290             _iterboard {
291 0     0   0 my ($x, $y) = @_;
292 0 0       0 if ($self->{_cellfarm}{$x.','.$y} eq '.') {
293 0         0 my $point = insertpoints($self, $x, $y);
294 0 0       0 push @illegallist, $point if play($self, $colour, $point);
295 0         0 restore($self, -1);
296             }
297 0         0 } $self->{_const}{size};
298              
299 0         0 return @illegallist;
300             }
301              
302             # return true if $colour (ie 'B' or 'W') has a legal move, otherwise return false
303              
304             sub haslegal {
305 0     0 1 0 my ($self, $colour) = @_;
306 0         0 my $exit = 0;
307 0         0 my $size = $self->{_const}{size};
308 0         0 for my $y (0..$size){
309 0         0 for my $x (0..$size){
310 0 0       0 if ($self->{_cellfarm}{$x.','.$y} eq '.') {
311 0 0       0 $exit = 1 unless play($self, $colour, insertpoints($self, $x, $y));
312 0         0 restore($self, -1);
313 0 0       0 return 1 if $exit;
314             }
315             }
316             }
317 0         0 return 0;
318             }
319              
320             # return a ':' seperated list of the co-ordinates of any captured stones
321              
322             sub captures {
323 0     0 0 0 my ($self, $id) = @_;
324 0   0     0 $id ||= $self->{_nodecount};
325 0         0 my $s = '';
326 0         0 my $capsref = $self->{_node}{$id}->captures;
327 0 0       0 if ($capsref) {
328 0         0 my @delstones = @{$capsref};
  0         0  
329 0         0 my $seperator = ':';
330 0         0 for my $i (0..$#delstones) {
331 0 0       0 $seperator = '' if $i == $#delstones;
332 0         0 $s .= insertpoints($self, ($delstones[$i][0]), ($delstones[$i][1])).$seperator;
333             }
334             }
335 0         0 return $s
336             }
337              
338             # restore the game to that at move $howmany
339             # if $howmany is negative, go back that number of moves.
340              
341             sub restore{
342 0     0 0 0 my ($self, $howmany) = @_;
343 0 0       0 croak 'Cannot restore to ', $howmany if (abs($howmany) > $self->{_nodecount});
344 0 0       0 $howmany += $self->{_nodecount} if ($howmany < 0);
345 0         0 boardrestore($self, $howmany);
346 0         0 deletenodes($self, $howmany);
347 0         0 $self->{_nodecount} = $howmany;
348 0         0 my $node = $self->{_node}{$howmany};
349 0         0 $self->{_movecount} = $node->movecount;
350 0         0 $self->{_colour} = $node->colour;
351 0         0 $self->{_passcount} = $node->passcount;
352             return
353 0         0 }
354              
355             # return the board as a string
356              
357             sub showboard{
358 0     0 0 0 my $self = shift;
359 0         0 my $h;
360 0         0 my $size = $self->{_const}{size};
361             _iterboard {
362 0     0   0 my ($x, $y) = @_;
363 0         0 $h .= $self->{_cellfarm}{$x.','.$y};
364 0 0       0 $h .= "\n" if $x == $size;
365 0         0 } $size;
366 0         0 $h .= "\n";
367 0         0 return $h;
368             }
369              
370             # return a section of the board as a string
371              
372             sub getboardsection{
373 0     0 0 0 my ($self, $ox, $oy, $size) = @_;
374 0         0 my $h;
375             _iterboard {
376 0     0   0 my ($x, $y) = @_;
377 0         0 my $xnew = $x + $ox;
378 0         0 my $ynew = $y + $oy;
379 0   0     0 $h .= $self->{_cellfarm}{$xnew.','.$ynew} || '-';
380 0         0 } $size;
381 0         0 return $h;
382             }
383              
384             # get contents of a point
385              
386             sub point{
387 0     0 0 0 my ($self, $ab, $y) = @_;
388 0 0       0 ($ab, $y) = extractpoints($self, $ab) unless defined($y);
389 0         0 return $self->{_cellfarm}{$ab.','.$y};
390             }
391              
392             # get contents of a point at a particular move
393              
394             sub nodepoint{
395 0     0 0 0 my ($self, $id, $x, $y) = @_;
396 0         0 my $positionref = $self->{_node}{$id}->board;
397 0         0 return substr($$positionref, ($y * ($self->{_const}{size} + 1)) + $x, 1)
398             }
399              
400             # get the co-ordinates of move number '$counter'
401              
402             sub getmove {
403 0     0 0 0 my ($self, $counter) = @_;
404 0         0 my $node = $self->{_node}{$counter};
405 0 0       0 return $node->colour, $node->point if defined $node;
406             }
407              
408             #restore the board position to that of move number $id
409              
410             sub boardrestore{
411 0     0 0 0 my ($self, $id) = @_;
412 0 0       0 myprint ($self, 'Restoring to', $id) if $self->{_debug};
413 0         0 my $positionref = $self->{_node}{$id}->board;
414 0         0 my $size = $self->{_const}{size};
415             _iterboard {
416 0     0   0 my ($x, $y) = @_;
417 0         0 $self->{_cellfarm}{$x.','.$y} = substr($$positionref, ($y*($size+1))+ $x, 1);
418 0         0 } $size;
419             }
420              
421             sub deletenodes {
422 0     0 0 0 my ($self, $upperB) = @_;
423 0         0 for (keys %{$self->{_node}}) {
  0         0  
424 0 0       0 if ($_ > $upperB) {
425 0         0 my $board = $self->{_node}{$_}->board;
426 0 0       0 delete $self->{_boardstr}{$$board} if defined $board;
427 0         0 delete $self->{_node}{$_};
428             }
429             }
430             }
431              
432             #save the board position as a reference to a string
433              
434             sub store{
435 120     120 0 191 my $self = shift;
436 120         168 my $h = '';
437             _iterboard {
438 43320     43320   70305 my ($x, $y) = @_;
439 43320 50       112108 die 'Undefined Value'."$!\n" unless defined $self->{_cellfarm}{$x.','.$y};
440 43320         89878 $h .= $self->{_cellfarm}{$x.','.$y};
441 120         798 } $self->{_const}{size};
442 120         950 return \$h;
443             }
444              
445             # Change the value of a cell
446              
447             sub put_cell{
448 148     148 0 248 my ($self, $where, $what) = @_;
449 148 50 66     696 if ($what ne '.' and $self->{_cellfarm}{$where} ne '.'){
450 0         0 return 1
451             } else {
452 148         283 $self->{_cellfarm}{$where} = $what;
453 148         426 return 0
454             }
455             }
456              
457             sub delete_group{
458 32     32 0 63 my ($self, @mygroup) = @_;
459 32         72 for (0..$#mygroup) {
460 36         148 put_cell($self, $mygroup[$_][0].','.$mygroup[$_][1], '.');
461             }
462             }
463              
464             # return a list of the points solidly connected to x,y
465              
466             sub block{
467 0     0 0 0 my ($self, $x, $y, $c, $group) = @_;
468 0 0       0 unless (offboard($self->{_const}{size}, $x, $y)) {
469 0         0 my $key = "$x,$y";
470 0 0       0 if ($self->{_cellfarm}{$key} eq $c) {
471 0         0 $group->{$key} = undef; # create a hash key
472 0         0 my @directions = ([1,0],[0,1],[-1,0],[0,-1]);
473              
474 0         0 for (0..3) {
475 0         0 my $xx = $directions[$_][0] + $x;
476 0         0 my $yy = $directions[$_][1] + $y;
477 0 0       0 unless (exists($group->{"$xx,$yy"})) {
478 0         0 $group = block($self, $xx, $yy, $c, $group);
479             }
480             }
481              
482             }
483             }
484 0         0 return $group;
485             }
486              
487             sub libertycheck{
488 1568     1568 0 2585 my ($self, $x, $y, $c, $haslibs, $group) = @_;
489 1568 100 100     4745 unless ($haslibs or offboard($self->{_const}{size}, $x, $y)) {
490 984         1619 my $key = "$x,$y";
491 984         1775 my $cellcontents = $self->{_cellfarm}{$key};
492 984 100       1692 if ($cellcontents eq $c) {
493 268         630 $group->{$key} = undef;
494 268         1006 my @directions = ([1,0],[0,1],[-1,0],[0,-1]);
495              
496 268         501 for (0..3) {
497 1072         1682 my $xx = $directions[$_][0] + $x;
498 1072         1315 my $yy = $directions[$_][1] + $y;
499 1072 100       2577 unless (exists($group->{"$xx,$yy"})) {
500 1036         1843 ($haslibs, $group) = libertycheck($self, $xx, $yy, $c, $haslibs, $group);
501             }
502             }
503              
504             } else {
505 716         1145 $haslibs = $cellcontents eq '.';
506             }
507             }
508 1568         5106 return $haslibs, $group;
509             }
510              
511             sub checkforcaptures{
512 196     196 0 303 my ($self, $x, $y, $colour, $type) = @_;
513 196         223 my $capturedSomething = 0;
514 196 100       865 my @directions = ($type eq 'self') ? ([0,0]) : ([1,0],[0,1],[-1,0],[0,-1]);
515 196         310 my @deletedstones;
516              
517 196         451 for (0..$#directions) {
518 532         861 my $xdir = $directions[$_][0]+$x;
519 532         670 my $ydir = $directions[$_][1]+$y;
520 532         1158 my ($haslibs, $points) = libertycheck($self, $xdir, $ydir, $colour, 0, {});
521 532 100 100     636 if (keys(%{$points}) and not $haslibs) {
  532         2643  
522 32         84 my $pointsref = getpoints($points);
523 32         48 delete_group($self, @{$pointsref});
  32         95  
524 32         52 push @deletedstones, @{$pointsref};
  32         57  
525 32         102 $capturedSomething = 1;
526             }
527             }
528              
529 196         734 return $capturedSomething, \@deletedstones
530             }
531              
532             # main move handler and error detector
533              
534             sub processmove{
535 110     110 0 185 my ($self, $colour, $ab) = @_;
536 110         177 my $id = $self->{_nodecount};
537 110 100       273 my $c = ($colour eq 'W')?'o':'x';
538 110         213 my $noderef = \$self->{_node}{$id};
539 110         197 my $move = $self->{_movecount};
540 110 50       295 if (defined $self->{_coderef}) {
541 0         0 my $rank = $colour.'R';
542 0 0       0 myprint ($self, 'learning from move', $id) if $self->{_debug};
543 0         0 $self->{_coderef}->learn($colour, $ab, $self, $move, $self->{_sgf}->$rank);
544             }
545 110 100 66     361 if ($colour eq $self->{_colour} and $self->{_const}{alternation}){
546 4 50 33     23 unless ($id <= $self->{_const}{handicap} and $self->{_const}{hfree}) {
547 4         16 adderror($self, 7, $move);
548 4 50       27 return if $self->{_const}{exitonerror}
549             }
550             }
551 110         157 $self->{_colour} = $colour;
552 110         215 my $size = $self->{_const}{size};
553 110 100       248 if (ispass($self, $ab)) {
554 6         30 $$noderef->passcount(++$self->{_passcount});
555 6         18 $$noderef->board(store($self));
556             } else {
557 104 100 66     368 if ($self->{_passcount} >= $self->{_const}{passes} and $self->{_const}{passcount}) {
558 2         9 adderror($self, 8, $move);
559 2 50       10 return if $self->{_const}{exitonerror};
560             }
561 104         149 $self->{_passcount} = 0;
562 104         368 $$noderef->passcount(0);
563 104         272 my ($x, $y) = extractpoints($self, $ab);
564 104 50       247 if (offboard($size, $x, $y)) {
565 0         0 adderror($self, 1, $move);
566 0 0       0 return if $self->{_const}{exitonerror};
567             } else {
568 104 50       371 if (put_cell($self, "$x,$y", $c)) {
569 0         0 adderror($self, 2, $move);
570 0 0       0 return if $self->{_const}{exitonerror};
571             }
572 104         262 my ($captured, $delstonesref, $error) = checkbothcaptures($self, $x, $y, $c, 1);
573 104         193 my $ctype = '_prisoners'.$colour;
574 104         226 $self->{$ctype} += @$delstonesref;
575 104 100       219 if ($error) {
576 4         13 adderror($self, 5, $move);
577 4 50       13 return if $self->{_const}{exitonerror};
578             }
579 104 100       315 $$noderef->captures($delstonesref) if $captured;
580 104         199 my $board = store($self);
581 104 100       572 if (exists $self->{_boardstr}{$$board}) {
582 6 50       24 if ($self->{_const}{ssk}) {
583 0         0 adderror($self, 6, $move);
584 0 0       0 return if $self->{_const}{exitonerror};
585             } else {
586 6         21 adderror($self, 6, $move);
587 6 50       25 return if $self->{_const}{exitonerror};
588             }
589             } else {
590 98         587 $self->{_boardstr}{$$board} = $colour;
591             }
592 104         501 $$noderef->board($board); # store the board in a Node as a string
593 104 50       258 myprint ($self, 'Node id', $id) if $self->{_debug};
594 104 50       323 myprint ($self, showboard($self)) if $self->{_debug};
595             }
596             }
597 110         483 return 1
598             }
599              
600             # change a value in cellfarm
601             # used when AB, AW, and AE tags found
602              
603             sub changecell{
604 8     8 0 15 my ($self, $colour, $point) = @_;
605 8         10 my $c;
606 8         16 SWITCH:for ($colour) {
607 8 100       22 if ($_ eq 'AW') {$c = 'o'; last}
  6         9  
  6         12  
608 2 50       8 if ($_ eq 'AB') {$c = 'x'; last}
  2         3  
  2         5  
609 0         0 $c = '.';
610             }
611 8         14 my $id = $self->{_nodecount};
612 8         24 my ($x, $y) = extractpoints($self, $point);
613 8         15 my $size = $self->{_const}{size};
614 8 50       25 if (offboard($size, $x, $y)) {
615 0         0 adderror($self, 9, $id);
616             } else {
617 8 50       31 adderror($self, 4, $id) if (put_cell($self, "$x,$y", $c));
618 8 50       119 unless ($c eq '.'){
619 8         22 my ($capturedSomething, undef) = checkbothcaptures($self, $x, $y, $c, 0);
620 8 100       22 if ($capturedSomething) {
621 2         8 adderror($self, 5, $id);
622 2 50       9 return if $self->{_const}{exitonerror};
623             }
624             }
625 8         26 $self->{_node}{$id}->board(store($self));
626             }
627             }
628              
629             sub checkbothcaptures {
630 112     112 0 192 my ($self, $x, $y, $c, $movetype) = @_;
631 112         136 my $myerror = 0;
632 112 100       345 my $reversec = ($c eq 'o')?'x':'o'; # reverse colours
633 112         258 my ($capturedsomething, $delstonesref) = checkforcaptures($self, $x, $y, $reversec, 'opponents');
634 112 100       265 unless ($capturedsomething){
635 84         172 ($capturedsomething, $delstonesref) = checkforcaptures($self, $x, $y, $c, 'self');
636 84 100 66     251 $myerror = 1 if ($capturedsomething and not $self->{_const}{selfcapture});
637             }
638 112         272 return $capturedsomething, $delstonesref, $myerror;
639             }
640              
641             sub move {
642 110     110 0 1023 my $self = shift;
643 110         184 $self->{_movecount}++;
644 110         226 return processmove($self, @_);
645             }
646              
647             sub processtags {
648 112     112 0 185 my ($self, $sgfnode) = @_;
649 112         413 $self->{_node}{++$self->{_nodecount}} = makenode($self, $sgfnode->colour, $sgfnode->move);
650              
651 112         376 for (split (',',$sgfnode->tags)){
652 246 100 100     1480 if (($_ eq 'B') or ($_ eq 'W')) {
653 110 50       307 return unless move($self, $sgfnode->colour, $sgfnode->move);
654 110         447 next;
655             }
656 136 100       783 if (',AB,AW,AE,' =~ /,($_),/) {
657 4         13 my $tag = $1;
658 4         43 for (split (',', $sgfnode->$tag)) {
659 8 50       68 if ( $_ =~ /(..):(..)/) {
660 0         0 my $arrayref = generaterectangle($self, $1, $2);
661 0         0 for (@$arrayref) {changecell($self, $tag, $_)};
  0         0  
662             } else {
663 8         23 changecell($self, $tag, $_);
664             }
665             }
666 4         18 next;
667             }
668             }
669              
670 112         303 return 1
671             }
672              
673             sub generaterectangle {
674 0     0 0 0 my ($self, $topleft, $bottomright) = @_;
675 0         0 my @pointlist;
676 0         0 my ($tx, $ty) = extractpoints($self, $topleft);
677 0         0 my ($bx, $by) = extractpoints($self, $bottomright);
678 0         0 for my $x ($tx..$bx) {
679 0         0 for my $y ($ty..$by) {
680 0         0 push @pointlist, insertpoints($self, $x, $y);
681             }
682             }
683 0         0 return \@pointlist;
684             }
685              
686             # list all the stones of a particular colour
687              
688             sub liststones {
689 0     0 0 0 my ($self, $colour) = @_;
690 0 0       0 my $stone = ($colour eq 'B') ? 'x' : 'o';
691 0         0 my %hash;
692             _iterboard {
693 0     0   0 my ($x, $y) = @_;
694 0 0       0 if ($self->{_cellfarm}{$x.','.$y} eq $stone) {
695 0         0 $hash{$x.','.$y} = undef;
696             }
697 0         0 } $self->{_const}{size};
698 0         0 return \%hash
699             }
700              
701             # list all the live stones of a particular colour
702             # (as the set of all blocks adjacent to their opponent's illegal moves)
703              
704             sub listalive {
705 0     0 0 0 my ($self, $colour) = @_;
706              
707             # turn off alternation and passcount errors temporarily
708 0         0 $self->{_const}{passcount} = 0;
709 0         0 $self->{_const}{alternation} = 0;
710             # first get the list of illegal moves for the other player
711 0         0 my @illegallist = illegal($self, swapcolour($self, $colour));
712 0         0 my $points = {};
713 0 0       0 my $stone = ($colour eq 'B') ? 'x' : 'o';
714              
715             # now get the blocks attached to those illegal points
716 0         0 for (@illegallist) {
717 0         0 my ($x, $y) = extractpoints($self, $_);
718 0         0 my @directions = ([1,0],[0,1],[-1,0],[0,-1]);
719 0         0 for (0..3) {
720 0         0 my $xdir = $directions[$_][0]+$x;
721 0         0 my $ydir = $directions[$_][1]+$y;
722 0         0 $points = block($self, $xdir, $ydir, $stone, $points);
723             }
724             }
725 0         0 $self->{_const}{passcount} = 1;
726 0         0 $self->{_const}{alternation} = 1;
727 0         0 return $points
728             }
729              
730             # list the dead stones of a particular colour
731             # (as the difference between their alive list
732             # and their total list)
733              
734             sub listdead {
735 0     0 0 0 my ($self, $colour) = @_;
736 0         0 my $allref = liststones($self, $colour);
737 0         0 my $aliveref = listalive($self, $colour);
738 0         0 my @dead = ();
739 0         0 for (keys %$allref) {
740 0 0       0 push @dead, $_ unless exists $aliveref->{$_};
741             }
742 0         0 @dead = map {
743 0         0 /(.*),(.*)/;
744 0         0 insertpoints($self, $1, $2)
745             } @dead;
746             return \@dead
747 0         0 }
748              
749             # list all the dead stones on the board
750             # (as the union of the Black and White
751             # dead stone list)
752              
753             sub listalldead {
754 0     0 0 0 my ($self) = @_;
755 0         0 my $bdead = listdead($self, 'B');
756 0         0 my $wdead = listdead($self, 'W');
757 0         0 my @dead = (@$bdead, @$wdead);
758             return \@dead
759 0         0 }
760              
761             sub ismove {
762 112 100   112 0 278 testnode(shift, ',B,W,') ? return 1 : return 0
763             }
764              
765             sub issetup {
766 2 50   2 0 5 testnode(shift, ',AB,AW,AE,') ? return 1 : return 0
767             }
768              
769             sub testnode{
770 114     114 0 269 my ($sgfnode, $type) = @_;
771 114 50       359 if ($sgfnode->tags){
772 114         865 for (split (',',$sgfnode->tags)){
773 270 100       4161 if ($type =~ /,$_,/) {
774 112         631 return 1;
775             }
776             }
777             }
778 2         23 return 0
779             }
780              
781             sub restart {
782 2     2 0 4 my $self = shift;
783 2         9 $self->{_node} = {};
784 2         235 $self->{_boardstr} = {};
785 2         26 $self->{_nodecount} = 0;
786 2         5 $self->{_movecount} = 0;
787 2         6 $self->{_passcount} = 0;
788 2         4 $self->{_colour} = 'None';
789 2         6 $self->{_cellfarm} = {};
790 2         167 $self->{_errors} = [];
791 2         14 $self->{_prisonersB} = 0;
792 2         4 $self->{_prisonersW} = 0;
793 2         4 $self->{_sgf} = {};
794 2         81 $self->{_node}{0} = makenode($self, $self->{_colour});
795             }
796              
797             sub initrules {
798 2     2 0 37 my $self = shift;
799 2         7 my $rules = uc(shift);
800              
801 2 50       9 $rules = ($rules) ? $rules : 'Japanese';
802 2 50       13 $self->{_const}{selfcapture} = 1 if ($rules =~ /^NZ|^NEW ZEALAND|^ING|^GOE/);
803 2 50       8 $self->{_const}{ssk} = 1 if ($rules =~ /^AGA/);
804 2 50       9 $self->{_const}{passes} = 4 if ($rules =~ /^ING|^GOE/);
805 2 50       15 $self->{_const}{hfree} = 1 if ($rules =~ /^NZ|^NEW ZEALAND|^ING|^GOE|^CHINESE/);
806             }
807              
808             sub makenode {
809 115     115 0 1486 my ($self, $colour, $point) = @_;
810 115         674 return new Games::Go::Referee::Node($self->{_movecount}+1, $self->{_passcount}, $colour, $point);
811             }
812              
813             sub errors {
814 2     2 1 2355 my ($self) = @_;
815 2         30 my $errorhash = {
816             1 => 'Not a board co-ordinate at move ',
817             2 => 'Point already occupied at move ',
818             3 => 'Illegal setup at node ',
819             4 => 'Point already occupied at node ',
820             5 => 'Illegal self-capture at move ',
821             6 => 'Board repetition at move ',
822             7 => 'Alternation error at move ',
823             8 => 'Play over at move ',
824             9 => 'Not a board co-ordinate at node ',
825             10 => 'Board repetition at node ',
826             };
827 2         6 my @array = @{$self->{_errors}};
  2         11  
828 2         4 my @return;
829 2         8 for (0..$#array){
830 18         31 my $ecode = $self->{_errors}[$_][0];
831 18         61 push @return, join '', $errorhash->{$ecode}, $self->{_errors}[$_][1], "\n";
832             }
833             return @return
834 2         21 }
835              
836             sub errorcode {
837 0     0 0 0 my $self = shift;
838 0         0 my @array = @{$self->{_errors}};
  0         0  
839 0         0 my $ecode = undef;
840 0         0 for (0..$#array){
841 0         0 $ecode = $self->{_errors}[$_][0];
842 0         0 last;
843             }
844 0 0       0 return defined($ecode)? $ecode: 0;
845             }
846              
847             sub adderror {
848 18     18 0 34 my ($self, $ecode, $place) = @_;
849 18         30 push @{$self->{_errors}}, [$ecode, $place];
  18         78  
850             }
851              
852             # empty board
853              
854             sub clearboard{
855 2     2 0 3 my $self = shift;
856 2         6 $self->{_cellfarm} = {};
857             _iterboard {
858 722     722   855 my ($x, $y) = @_;
859 722         2077 $self->{_cellfarm}{$x.','.$y} = '.';
860 2         16 } $self->{_const}{size};
861 2         18 $self->{_node}{0}->board(store($self));
862             return
863 2         4 }
864              
865             sub checkmove { # check move is OK according to format
866 0     0 0 0 my ($self, $string) = @_;
867 0 0       0 myprint ($self, 'Checking move', $string) if $self->{_debug};
868 0 0       0 return 1 if ispass($self, $string);
869 0 0       0 if ($self->{_const}{pointformat} eq 'sgf') {
870 0         0 return issgf($string)
871             } else {
872 0         0 return isgmp($string)
873             }
874             }
875              
876             sub ispass {
877 222     222 0 314 my ($self, $move) = @_;
878 222 50       553 if ($self->{_const}{pointformat} eq 'sgf') {
879 222 100       449 return 1 if not defined $move;
880 218 100 33     1216 if (($move eq '') or ($move eq 'tt' and $self->{_const}{size} < 19)) {
      66        
881 2         7 return 1
882             }
883             } else {
884 0 0       0 if ('pass' eq lc $move) {
885 0         0 return 1
886             }
887             }
888             }
889              
890             sub issgf { # assuming not a pass
891 0     0 0 0 shift =~ /^[a-z]{2}$/i;
892             }
893              
894             sub isgmp { # assuming not a pass
895 0 0 0 0 0 0 shift =~ /^[a-z]([1-9]\d?)$/i and 1 <= $1 and $1 <= 25;
896             }
897              
898             sub getpoints { # extract points from a hash key eg '10,1'
899 32     32 0 46 my $pointsref = shift;
900 32         38 my @points;
901 32         45 for (keys(%{$pointsref})) {
  32         89  
902 36         203 /(.*),(.*)/;
903 36         208 push @points, [$1,$2];
904             }
905             return \@points
906 32         90 }
907              
908             sub extractpoints { # convert points from an sgf or gmp string to a pair of numbers
909 112     112 0 228 my ($self, $string) = @_;
910 112         190 my $pass = ispass($self, $string);
911 112 50       237 return '','' if $pass;
912 112 50       303 if ($self->{_const}{pointformat} eq 'sgf') {
913 112         240 return fromsgf($string, $pass)
914             } else {
915 0         0 return fromgtp($self, $string)
916             }
917             }
918              
919             sub insertpoints { # convert a pair of numbers to an sgf or gmp string
920 0     0 0 0 my ($self, $x, $y) = @_;
921 0 0       0 if ($self->{_const}{pointformat} eq 'sgf') {
922 0         0 return tosgf($x, $y)
923             } else {
924 0         0 return togtp($self, $x, $y)
925             }
926             }
927              
928             sub fromsgf {
929 112     112 0 157 my ($string) = @_;
930 112         229 my $x = index(aZ(), substr($string,0,1));
931 112         206 my $y = index(aZ(), substr($string,1,1));
932 112         283 return $x,$y;
933             }
934              
935             sub fromgtp {
936 0     0 0 0 my ($self, $string) = @_;
937 0         0 my $a = index aZnoi(), lc substr $string, 0, 1;
938 0         0 my $y = substr $string, 1;
939 0         0 return $a, $self->{_const}{size} - $y + 1;
940             }
941              
942             sub togtp {
943 0     0 0 0 my ($self, $x, $y) = @_;
944 0 0 0     0 return 'pass' if $x eq '' and $y eq '';
945 0         0 join '', uc(substr(aZnoi(), $x, 1)), $self->{_const}{size} - $y + 1
946             }
947              
948             sub tosgf {
949 0 0 0 0 0 0 return '' if $_[0] eq '' and $_[1] eq '';
950 0         0 join '', substr(aZ(), $_[0], 1), substr(aZ(), $_[1], 1)
951             }
952              
953             sub offboard {
954 1226 100 100 1226 0 19963 0 > $_[1] or $_[1] > $_[0] or 0 > $_[2] or $_[2] > $_[0];
      100        
955             }
956              
957             sub swapcolour {
958 0 0   0 0 0 return ($_[1] eq 'B') ? 'W' : 'B'
959             }
960              
961 224     224 0 568 sub aZ { 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' }
962              
963             sub aZnoi {
964 0     0 0   my $str = aZ();
965 0           $str =~ s/i//;
966 0           return $str
967             }
968              
969             sub myprint {
970 0     0 0   my $self = shift;
971 0           my @messages = @_;
972 0 0         if (exists $messages[0]) {
973 0 0         open(LOG, ">>", $self->{_logfile}) or die 'Can\'t open'.$self->{_logfile}."\n";
974 0           print LOG (join ' ', @messages, "\n");
975 0           close(LOG);
976             }
977             }
978              
979             1;
980              
981             =head1 NAME
982              
983             Games::Go::Referee - Check the moves of a game of Go for rule violations.
984              
985             =head1 SYNOPSIS
986              
987             Analyse a file:
988              
989             use Games::Go::Referee;
990             my $referee = new Games::Go::Referee();
991             $referee->sgffile('file.sgf');
992             print $referee->errors;
993              
994             or
995              
996             Analyse move by move:
997              
998             use Games::Go::Referee;
999             my $referee = new Games::Go::Referee();
1000             $referee->size(19);
1001             $referee->ruleset('AGA');
1002             $referee->play('B','ab');
1003             $referee->restore(-1) if $referee->errors;
1004              
1005              
1006             =head1 DESCRIPTION
1007              
1008             Check a game of Go for rules violations, against a specific rule set.
1009              
1010             =head2 General use
1011              
1012             Games::Go::Referee can be used in two ways; to analyse an sgf file, or to check plays
1013             move by move.
1014              
1015             If checking a file, the file will be completely read, and any errors found can be displayed
1016             later using the errors method. Any illegal plays found are 'allowed' (ie play is assumed to
1017             continue as if they were legal). The rule set to be used will be read from the RU sgf
1018             property in the file, alternatively various rules can be set manually.
1019              
1020             If checking move by move, it may be necessary to specify the size and rule set to be
1021             used before starting.
1022              
1023             There are basically two rules that can be set: self-capture allowed/disallowed and
1024             situational superko (ssk) on/off. If ssk is off, positional superko is assumed.
1025              
1026             The following errors are reported:
1027              
1028             Not a board co-ordinate
1029             Point already occupied
1030             Illegal setup (if the setup caused a capture to occur)
1031             Illegal self-capture
1032             Board repetition
1033             Alternation error (two Black moves in a row for example)
1034             Play over (play continues when the game is over)
1035              
1036             =head1 METHODS
1037              
1038             =head2 ruleset
1039              
1040             The ruleset method sets the rule set to be used. If a file is being checked,
1041             the value of the sgf property RU will be used. If that is not found, Japanese rules
1042             are assumed.
1043              
1044             $referee->ruleset('AGA');
1045              
1046             =head2 size
1047              
1048             The size method sets the size of the board to be used. If a file is being checked,
1049             the value of the sgf property SZ will be used. If that is not found, the board is
1050             assumed to be 19 x 19.
1051              
1052             $referee->size(19);
1053              
1054              
1055             =head2 ssk
1056              
1057             The ssk method sets or unsets whether the situational superko rule is being used.
1058             ssk can be turned on only by using this method, or by specifying 'AGA' via the
1059             ruleset method.
1060              
1061             $referee->ssk('on');
1062             $referee->ssk('off');
1063              
1064             =head2 selfcapture
1065              
1066             The selfcapture method sets or unsets whether self-capture (aka suicide) is
1067             allowed or not. selfcapture can be turned on only by using this method, or by
1068             specifying New Zealand or Ing via the rulset method.
1069              
1070             $referee->selfcapture('on');
1071             $referee->selfcapture('off');
1072              
1073             =head2 passes
1074              
1075             The passes method sets the number of consecutive passes required to end the game.
1076             The default value is 2. If the Ing ruleset is being used, this value becomes 4.
1077              
1078             $referee->passes(3);
1079              
1080             =head2 setup
1081              
1082             For move by move analysis, the following two methods are availale.
1083              
1084             The setup method is used to place preliminary stones on the board.
1085              
1086             Setup types (the first argument) are 'AB', 'AW' and 'AE'. Each use of setup can
1087             only use one of these types.
1088              
1089             Setup points (the second argument) are a list of sgf style board co-ordinates.
1090              
1091             $referee->setup('AW','ii,jj,gh');
1092             $referee->setup('AB','aa,bb');
1093              
1094             If the setup creates group with no liberties, an error is reported. The method
1095             returns true if an error was found, otherwise false.
1096              
1097             =head2 handicap
1098              
1099             The handicap method takes as its argument a number from 2 to 9
1100              
1101             $referee->handicap(3);
1102              
1103             This method can be used as a convenient way of placing handicap stones, provided
1104             the board size is 19, and the rules indicate that handicap placement is fixed
1105             (ie neither Ing, AGA nor Chinese).
1106              
1107             If handicap placement is fixed, but the board size is not 19, use the setup method.
1108              
1109             If handicap placement is not fixed, the handicap method should still be used as then
1110             the appropriate number of black consecutive plays will be allowed.
1111              
1112             =head2 play
1113              
1114             Play a move.
1115              
1116             Play types (the first argument) are 'B' or 'W'. Each use of play can
1117             only use one of these types.
1118              
1119             The point played (the second argument) is a single sgf style co-ordinate (or '' for a pass.)
1120              
1121             $referee->play('B','pd');
1122              
1123             The method returns true if an error was found, otherwise false.
1124              
1125             =head2 haslegal
1126              
1127             $referee->haslegal($colour); # $colour must be 'B' or 'W'
1128              
1129             Returns true if $colour (ie 'B' or 'W') has a legal move, otherwise returns false.
1130             Usage example -
1131              
1132             while ($referee->haslegal($colour)){
1133             my $point = getmove();
1134             $referee->play($colour, $point);
1135             if ($referee->errors) {
1136             $referee->restore(-1);
1137             } else {
1138             $colour = ($colour eq 'B') ? 'W' : 'B';
1139             }
1140             }
1141              
1142             =head2 legal
1143              
1144             my @points = $referee->legal($colour); # $colour must be 'B' or 'W'
1145              
1146             Returns an array of a player's legal move co-ordinates.
1147              
1148             Usage example -
1149              
1150             my @legalpoints = $referee->legal($colour);
1151             while ($#legalpoints >= 0){
1152             # play a random legal move
1153             $referee->play($colour, @points[int(rand($#legalpoints))]);
1154             $colour = ($colour eq 'B') ? 'W' : 'B';
1155             @legalpoints = $referee->legal($colour);
1156             }
1157              
1158             =head2 errors
1159              
1160             print $referee->errors;
1161              
1162             Lists any errors occurring either in the file analysed, or as a result of the previous
1163             move/setup.
1164              
1165             =head2 sgffile
1166              
1167             $referee->sgffile('file.sgf');
1168              
1169             or
1170              
1171             my $sgf = new Games::Go::SGF('file.sgf');
1172             $referee->sgffile($sgf);
1173              
1174             Specify an sgf file to be analysed.
1175              
1176             =head1 TODO
1177              
1178             Score?
1179              
1180             =head1 BUGS/CAVEATS
1181              
1182             The move number of a reported error is one too large if it occurs in a variation.
1183             Putting setup stones within a file (not just the first node) can cause problems. For example,
1184             after some stones have been added like this, who is next to play? This needs to be known for
1185             situational superko. Currently no look-ahead is done to see who, in fact, played next.
1186              
1187             Natural Superko - if I understood the difference between this and SSK, I might put it in.
1188              
1189             Ko-pass moves, game resumption ... my head hurts.
1190              
1191             =head1 AUTHOR (version 0.01)
1192              
1193             DG
1194              
1195             =cut