File Coverage

blib/lib/Games/Go/SimpleBoard.pm
Criterion Covered Total %
statement 12 101 11.8
branch 0 78 0.0
condition 0 74 0.0
subroutine 4 9 44.4
pod 3 5 60.0
total 19 267 7.1


line stmt bran cond sub pod time code
1             package Games::Go::SimpleBoard;
2              
3             =head1 NAME
4              
5             Games::Go::SimpleBoard - represent a simple go board
6              
7             =head1 SYNOPSIS
8              
9             use Games::Go::SimpleBoard;
10              
11             =head1 DESCRIPTION
12              
13             Please supply a description )
14              
15             =head2 EXPORTED CONSTANTS
16              
17             Marker types for each board position (ORed together):
18              
19             MARK_B # normal black stone
20             MARK_W # normal whit stone
21             MARK_GRAYED # in conjunction with MARK_[BW], grays the stone
22              
23             MARK_SMALL_B # small stone, used for scoring or marking
24             MARK_SMALL_W # small stone, used for scoring or marking
25             MARK_SMALL_GRAYED # in conjunction with MARK_SMALL_[BW], grays the stone
26              
27             MARK_TRIANGLE # triangle mark
28             MARK_SQUARE # square mark
29             MARK_CIRCLE # circle mark
30             MARK_CROSS # cross mark
31              
32             MARK_LABEL # a text label
33             MARK_HOSHI # this is a hoshi point (not used much)
34             MARK_MOVE # this is a regular move
35             MARK_KO # this is a ko position
36             MARK_REDRAW # ignored, can be used for your own purposes
37              
38             COLOUR_WHITE # guaranteed to be 0
39             COLOUR_BLACK # guaranteed to be 1
40              
41             MOVE_HANDICAP # used as "x-coordinate" for handicap moves
42             MOVE_PASS # can be used as "x-coordinate" for pass moves
43              
44             =head2 METHODS
45              
46             =over 4
47              
48             =cut
49              
50 1     1   1637 no warnings;
  1         2  
  1         54  
51 1     1   6 use strict;
  1         2  
  1         121  
52              
53 1     1   15 use Carp ();
  1         2  
  1         19  
54              
55 1     1   14 use base Exporter::;
  1         2  
  1         2785  
56              
57             our $VERSION = '1.01';
58              
59             our @EXPORT = qw(
60             MARK_TRIANGLE MARK_SQUARE MARK_CIRCLE MARK_SMALL_B MARK_SMALL_W MARK_B
61             MARK_W MARK_GRAYED MARK_SMALL_GRAYED MARK_MOVE MARK_LABEL MARK_HOSHI MARK_KO MARK_CROSS
62             MARK_REDRAW
63             COLOUR_BLACK COLOUR_WHITE
64             MOVE_HANDICAP MOVE_PASS
65             );
66              
67             # marker types for each board position (ORed together)
68              
69             sub MARK_TRIANGLE (){ 0x0001 }
70             sub MARK_SQUARE (){ 0x0002 }
71             sub MARK_CIRCLE (){ 0x0004 }
72             sub MARK_CROSS (){ 0x0008 }
73              
74             sub MARK_SMALL_B (){ 0x0010 } # small stone, used for scoring or marking
75             sub MARK_SMALL_W (){ 0x0020 } # small stone, used for scoring or marking
76             sub MARK_SMALL_GRAYED (){ 0x0040 }
77              
78             sub MARK_B (){ 0x0080 } # normal black stone
79             sub MARK_W (){ 0x0100 } # normal whit stone
80             sub MARK_GRAYED (){ 0x0200 } # in conjunction with MARK_[BW], grays the stone
81              
82             sub MARK_LABEL (){ 0x0400 }
83             sub MARK_HOSHI (){ 0x0800 } # this is a hoshi point (not used much)
84             sub MARK_MOVE (){ 0x1000 } # this is a regular move
85             sub MARK_KO (){ 0x2000 } # this is a ko position
86             sub MARK_REDRAW (){ 0x8000 }
87              
88             sub COLOUR_WHITE (){ 0 }
89             sub COLOUR_BLACK (){ 1 }
90              
91 0     0 0   sub MOVE_PASS (){ undef }
92             sub MOVE_HANDICAP (){ -2 }
93              
94             =item my $board = new $size
95              
96             Creates a new empty board of the given size.
97              
98             C<< $board->{size} >> stores the board size.
99              
100             C<< $board->{max} >> stores the maximum board coordinate (size-1).
101              
102             C<< $board->{captures}[COLOUR_xxx] >> stores the number of captured stones for
103             the given colour.
104              
105             C<< $board->{board} >> stores a two-dimensional array with board contents.
106              
107             =cut
108              
109             sub new {
110 0     0 1   my $class = shift;
111 0           my $size = shift;
112              
113 0 0         unless ($size > 0) {
114 0           Carp::croak ("no board size given!");
115             }
116              
117             bless {
118 0           max => $size - 1,
119             size => $size,
120             board => [map [(0) x $size], 1 .. $size],
121             captures => [0, 0], # captures
122             #timer => [],
123             #score => [],
124             @_,
125             }, $class
126             }
127              
128             # inefficient and primitive, I hear you say?
129             # well... you are right :)
130             # use an extremely dumb floodfill algorithm to get rid of captured stones
131             sub capture {
132 0     0 0   my ($self, $mark, $x, $y) = @_;
133              
134 0           my %seen;
135             my @found;
136 0           my @nodes = ([$x,$y]);
137 0           my $board = $self->{board};
138              
139 0           my $max = $self->{max};
140              
141 0           while (@nodes) {
142 0           my ($x, $y) = @{pop @nodes};
  0            
143              
144 0 0         unless ($seen{$x,$y}++) {
145 0 0         if ($board->[$x][$y] & $mark) {
    0          
146 0           push @found, [$x, $y];
147              
148 0 0 0       push @nodes, [$x-1, $y] unless $seen{$x-1, $y} || $x <= 0;
149 0 0 0       push @nodes, [$x+1, $y] unless $seen{$x+1, $y} || $x >= $max;
150 0 0 0       push @nodes, [$x, $y-1] unless $seen{$x, $y-1} || $y <= 0;
151 0 0 0       push @nodes, [$x, $y+1] unless $seen{$x, $y+1} || $y >= $max;
152             } elsif (!($board->[$x][$y] & (MARK_B | MARK_W))) {
153 0           return;
154             }
155             }
156             }
157              
158             @found
159 0           }
160              
161             =item $hint = $board->update ([update-structures...])
162              
163             Each update-structure itself is also an array-ref:
164              
165             [$x, $y, $clr, $set, $label, $hint] # update or move
166             [MOVE_HANDICAP, $handicap] # black move, setup handicap
167             [MOVE_PASS] # pass
168             [] # also pass (deprecated!)
169              
170             It changes the board or executes a move, by first clearing the bits
171             specified in C<$clr>, then setting bits specified in C<$set>.
172              
173             If C<$set> includes C, the label text must be given in
174             C<$label>.
175              
176             If C<$set> contains C then surrounded stones will be removed
177             from the board and (simple) Kos are detected and marked with square
178             symbols and C, after removing other marking symbols. The
179             markings are also removed with the next next update structure that uses
180             C, so this flag is suited well for marking, well, moves. Note
181             that you can make invalid "moves" (such as suicide) and C will
182             try to cope with it. You can use C to avoid making illegal
183             moves.
184              
185             For handicap "moves", currently only board sizes 9, 13 and 19 are
186             supported and only handicap values from 2 to 9. The placement follows the
187             IGS rules, if you want other placements, you have to set it up yourself.
188              
189             This function modifies the C<$hint> member of the specified structure
190             to speed up repeated board generation and updates with the same update
191             structures.
192              
193             If the hint member is a reference the scalar pointed to by the reference
194             is updated instead.
195              
196             If all this hint member thing is confusing, just ignore it and specify
197             it as C or leave it out of the array entirely. Do make sure that
198             you keep your update structures around as long as previous updates don't
199             change, however, as regenerating a full board position from hinted
200             update structures is I faster then recreating it from fresh update
201             structures.
202              
203             Example, make two silly moves:
204              
205             $board->update ([[0, 18, -1, MARK_B | MARK_MOVE],
206             [0, 17, -1, MARK_W | MARK_MOVE]]);
207              
208             =cut
209              
210             our %HANDICAP_COORD = (
211             9 => [2, 4, 6],
212             13 => [3, 6, 9],
213             19 => [3, 9, 15],
214             );
215             our %HANDICAP_XY = (
216             2 => [qw(0,2 2,0 )],
217             3 => [qw(0,2 2,0 0,0 )],
218             4 => [qw(0,2 2,0 0,0 2,2 )],
219             5 => [qw(0,2 2,0 0,0 2,2 1,1)],
220             6 => [qw(0,2 2,0 0,0 2,2 0,1 2,1 )],
221             7 => [qw(0,2 2,0 0,0 2,2 0,1 2,1 1,1)],
222             8 => [qw(0,2 2,0 0,0 2,2 0,1 2,1 1,0 1,2 )],
223             9 => [qw(0,2 2,0 0,0 2,2 0,1 2,1 1,0 1,2 1,1)],
224             );
225              
226             our $mark_symbols = MARK_CIRCLE | MARK_SQUARE | MARK_TRIANGLE | MARK_CROSS | MARK_KO;
227              
228             sub update {
229 0     0 1   my ($self, $path) = @_;
230              
231 0           my $board = $self->{board};
232              
233 0           for (@$path) {
234 0           my ($x, $y, $clr, $set, $label) = @$_;
235              
236 0 0         if (!defined $x) {
    0          
237 0 0         $$_ &= ~$mark_symbols for @{ delete $self->{unmark} || [] };
  0            
238             # pass
239              
240             } elsif ($x == MOVE_HANDICAP) {
241 0 0         $$_ &= ~$mark_symbols for @{ delete $self->{unmark} || [] };
  0            
242              
243             # $y = #handicap stones
244 0 0         my $c = $HANDICAP_COORD{$self->{size}}
245             or Carp::croak "$self->{size}: illegal board size for handicap";
246 0 0         my $h = $HANDICAP_XY{$y}
247             or Carp::croak "$y: illegal number of handicap stones";
248              
249 0           for (@$h) {
250 0           my ($x, $y) = map $c->[$_], split /,/;
251 0           $board->[$x][$y] = MARK_B | MARK_MOVE;
252             }
253              
254             } else {
255 0           my $space = \$board->[$x][$y];
256              
257 0           $$space = $$space & ~$clr | $set;
258              
259 0 0         $self->{label}[$x][$y] = $label if $set & MARK_LABEL;
260              
261 0 0         if ($set & MARK_MOVE) {
262 0 0         $$_ &= ~$mark_symbols for @{ $self->{unmark} || [] };
  0            
263 0           @{ $self->{unmark} } = $space;
  0            
264              
265             # remark the space, in case the move was on the same spot as the
266             # old mark
267 0           $$space |= $set;
268              
269 0 0 0       unless (${ $_->[5] ||= \my $hint }) {
  0            
270 0 0         my ($own, $opp) =
271             $set & MARK_B
272             ? (MARK_B, MARK_W)
273             : (MARK_W, MARK_B);
274              
275 0           my (@capture, @suicide);
276              
277 0 0 0       push @capture, $self->capture ($opp, $x-1, $y) if $x > 0 && $board->[$x-1][$y] & $opp;
278 0 0 0       push @capture, $self->capture ($opp, $x+1, $y) if $x < $self->{max} && $board->[$x+1][$y] & $opp;
279 0 0 0       push @capture, $self->capture ($opp, $x, $y-1) if $y > 0 && $board->[$x][$y-1] & $opp;
280 0 0 0       push @capture, $self->capture ($opp, $x, $y+1) if $y < $self->{max} && $board->[$x][$y+1] & $opp;
281              
282             # keep only unique coordinates
283 0           @capture = do { my %seen; grep !$seen{"$_->[0],$_->[1]"}++, @capture };
  0            
  0            
284              
285             # remove captured stones
286 0 0         $self->{captures}[$own == MARK_B ? COLOUR_BLACK : COLOUR_WHITE] += @capture;
287             $self->{board}[$_->[0]][$_->[1]] = 0
288 0           for @capture;
289              
290 0           push @suicide, $self->capture ($own, $x, $y);
291              
292 0   0       ${ $_->[5] } ||= !(@capture || @suicide);
  0   0        
293              
294 0 0 0       if (@suicide) {
    0          
295             $self->{board}[$_->[0]][$_->[1]] = 0
296 0           for @suicide;
297             # count suicides as other sides stones
298 0 0         $self->{captures}[$opp == MARK_B ? COLOUR_BLACK : COLOUR_WHITE] += @suicide;
299            
300             } elsif (!@suicide && @capture == 1) {
301             # possible ko. now check liberties on placed stone
302              
303 0           my $libs;
304              
305 0 0 0       $libs++ if $x > 0 && !($board->[$x-1][$y] & $opp);
306 0 0 0       $libs++ if $x < $self->{max} && !($board->[$x+1][$y] & $opp);
307 0 0 0       $libs++ if $y > 0 && !($board->[$x][$y-1] & $opp);
308 0 0 0       $libs++ if $y < $self->{max} && !($board->[$x][$y+1] & $opp);
309            
310 0 0         if ($libs == 1) {
311 0           $$space = $$space & ~$mark_symbols | MARK_KO;
312              
313 0           ($x, $y) = @{$capture[0]};
  0            
314 0           $board->[$x][$y] |= MARK_KO;
315              
316 0           push @{ $self->{unmark} }, \$board->[$x][$y];
  0            
317             }
318             }
319             }
320             }
321             }
322             }
323             }
324              
325             =item $board->is_valid_move ($colour, $x, $y[, $may_suicide])
326              
327             Returns true if the move of the given colour on the given coordinates is
328             valid or not. Kos are taken into account as long as they are marked with
329             C. Suicides are invalid unless C<$may_suicide> is true (e.g. for
330             new zealand rules)
331              
332             =cut
333              
334             sub is_valid_move {
335 0     0 1   my ($self, $colour, $x, $y, $may_suicide) = @_;
336              
337 0           my $board = $self->{board};
338              
339 0 0 0       return if $board->[$x][$y] & (MARK_B | MARK_W | MARK_KO)
340             && !($board->[$x][$y] & MARK_GRAYED);
341              
342 0 0         if ($may_suicide) {
343 0           return 1;
344             } else {
345 0 0         my ($own, $opp) = $colour == COLOUR_BLACK
346             ? (MARK_B, MARK_W)
347             : (MARK_W, MARK_B);
348              
349             # try the move
350 0           local $board->[$x][$y] = $board->[$x][$y] | $own;
351              
352 0 0 0       return 1 if $x > 0 && $board->[$x-1][$y] & $opp && $self->capture ($opp, $x-1, $y, 1);
      0        
353 0 0 0       return 1 if $x < $self->{max} && $board->[$x+1][$y] & $opp && $self->capture ($opp, $x+1, $y, 1);
      0        
354 0 0 0       return 1 if $y > 0 && $board->[$x][$y-1] & $opp && $self->capture ($opp, $x, $y-1, 1);
      0        
355 0 0 0       return 1 if $y < $self->{max} && $board->[$x][$y+1] & $opp && $self->capture ($opp, $x, $y+1, 1);
      0        
356              
357 0           return !$self->capture ($own, $x, $y, 1);
358             }
359             }
360              
361             1;
362              
363             =back
364              
365             =head2 AUTHOR
366              
367             Marc Lehmann
368              
369             =head2 SEE ALSO
370              
371             L.
372              
373             =cut
374