File Coverage

blib/lib/Games/Goban.pm
Criterion Covered Total %
statement 128 151 84.7
branch 40 54 74.0
condition 22 30 73.3
subroutine 24 31 77.4
pod 13 13 100.0
total 227 279 81.3


line stmt bran cond sub pod time code
1 5     5   341047 use strict;
  5         58  
  5         143  
2 5     5   27 use warnings;
  5         9  
  5         247  
3             package Games::Goban 1.103;
4             # ABSTRACT: Board for playing go, renju, othello, etc.
5              
6 5     5   104 use 5.006;
  5         16  
7 5     5   30 use Carp;
  5         9  
  5         12084  
8              
9             my $ORIGIN = ord('a');
10             my $piececlass = 'Games::Goban::Piece';
11              
12             our %types = (
13             go => 1,
14             othello => 2,
15             renju => 4,
16             gomoku => 4,
17             );
18              
19             our %defaults = (
20             game => 'go',
21             size => 19,
22             white => 'Miss White',
23             black => 'Mr. Black',
24             skip_i => 0,
25             referee => sub { 1 }
26             );
27              
28             #pod =head1 SYNOPSIS
29             #pod
30             #pod use Games::Goban;
31             #pod my $board = new Games::Goban (
32             #pod size => 19,
33             #pod game => "go",
34             #pod white => "Seigen, Go",
35             #pod black => "Minoru, Kitani",
36             #pod referee => \&Games::Goban::Rules::Go,
37             #pod );
38             #pod
39             #pod $board->move("pd"); $board->move("dd");
40             #pod print $board->as_sgf;
41             #pod
42             #pod =head1 DESCRIPTION
43             #pod
44             #pod This is a generic module for handling goban-based board games.
45             #pod Theoretically, it can be used to handle many of the other games which
46             #pod can use Smart Game Format (SGF) but I want to keep it reasonably
47             #pod restricted in order to keep it simple.
48             #pod
49             #pod =head1 METHODS
50             #pod
51             #pod =head2 new(%options);
52             #pod
53             #pod Creates and initializes a new goban. The options and their legal
54             #pod values (* marks defaults):
55             #pod
56             #pod size Any integer between 5 and 26, default: 19
57             #pod game *go, othello, renju, gomoku
58             #pod white Any text, default: "Miss White"
59             #pod black Any text, default: "Mr Black"
60             #pod skip_i Truth value; whether 'i' should be skipped; false by default
61             #pod referee Any subroutine, default: sub {1} # (All moves are valid)
62             #pod
63             #pod The referee subroutine takes a board object and a piece object, and
64             #pod determines whether or not the move is legal. It also reports if the
65             #pod game is won.
66             #pod
67             #pod =cut
68              
69             sub new {
70 9     9 1 1300 my $class = shift;
71 9         78 my %opts = (%defaults, @_);
72              
73 9 50 33     122 unless (($opts{size} !~ /\D/) and ($opts{size} > 4) and ($opts{size} <= 26)) {
      33        
74 0         0 croak "Illegal size $opts{size} (must be integer > 4)";
75             }
76              
77 9         35 $opts{game} = lc $opts{game};
78 9 50       40 croak "Unknown game $opts{game}" unless exists $types{ $opts{game} };
79              
80             my $board = bless {
81             move => 1,
82             moves => [],
83             turn => 'b',
84             game => $opts{game},
85             size => $opts{size},
86             black => $opts{black},
87             white => $opts{white},
88             skip_i => $opts{skip_i},
89             referee => $opts{referee},
90 9         88 callbacks => {},
91             magiccookie => "a0000",
92             }, $class;
93              
94 9         44 for (0 .. ($opts{size} - 1)) {
95 161         202 push @{ $board->{board} }, [ (undef) x $opts{size} ];
  161         425  
96             }
97 9         37 $board->{hoshi} = $board->_calc_hoshi;
98              
99 9         42 return $board;
100             }
101              
102             #pod =head2 move
103             #pod
104             #pod $ok = $board->move($position)
105             #pod
106             #pod Takes a move, creates a Games::Goban::Piece object, and attempts to
107             #pod place it on the board, subject to the constraints of the I.
108             #pod If this is not successful, it returns C<0> and sets C<$@> to be an error
109             #pod message explaining why the move could not be made. If successful,
110             #pod updates the board, updates the move number and the turn, and returns
111             #pod true.
112             #pod
113             #pod =cut
114              
115             sub move {
116 10     10 1 696 my ($self, $move) = @_;
117              
118 10         26 my ($x, $y) = $self->_pos2grid($move, $self->skip_i);
119              
120 10         37 $self->_check_pos($move);
121 9         25 my $stat = $self->{referee}->($self, $move);
122              
123 9 50       22 return $stat if !$stat;
124             $self->{board}[$x][$y] = bless {
125             colour => $self->{turn},
126             move => $self->{move},
127 9         49 xy => [ $x, $y ],
128             board => $self
129             },
130             "Games::Goban::Piece";
131 9         32 push @{ $self->{moves} },
132             {
133             player => $self->{turn},
134 9         16 piece => $self->{board}[$x][$y]
135             };
136 9         17 $self->{move}++;
137 9 100       26 $self->{turn} = $self->{turn} eq "b" ? "w" : "b";
138              
139 9         14 while (my ($key, $cb) = each %{ $self->{callbacks} }) { $cb->($key, $self) }
  2         5  
  11         36  
140              
141 9         20 return 1;
142             }
143              
144             #pod =head2 pass
145             #pod
146             #pod This method causes the current player to pass. At present, nothing happens for
147             #pod two subsequent passes.
148             #pod
149             #pod =cut
150              
151             sub pass {
152 2     2 1 7 my $self = shift;
153              
154 2         6 push @{ $self->{moves} },
155             {
156             player => $self->{turn},
157 2         3 piece => undef
158             };
159 2         4 $self->{move}++;
160 2 100       5 $self->{turn} = $self->{turn} eq "b" ? "w" : "b";
161             }
162              
163             #pod =head2 get
164             #pod
165             #pod $move = $board->get($position)
166             #pod
167             #pod Gets the C object at the given location, if there
168             #pod is one. Locations are specified as per SGF - a 19x19 board starts from
169             #pod C in the top left corner, with C in the bottom right. (If the skip_i
170             #pod option was set while creating the board, C is the bottom right and there
171             #pod are no C positions. This allows for traditional notation.)
172             #pod
173             #pod =cut
174              
175             sub get {
176 447     447 1 1238 my ($self, $pos) = @_;
177 447         732 my ($x, $y) = $self->_pos2grid($pos, $self->skip_i);
178 447         955 $self->_check_grid($x, $y);
179              
180 447         869 return $self->{board}[$x][$y];
181             }
182              
183             #pod =head2 size
184             #pod
185             #pod $size = $board->size
186             #pod
187             #pod Returns the size of the goban.
188             #pod
189             #pod =cut
190              
191 996     996 1 19382 sub size { $_[0]->{size} }
192              
193             #pod =head2 hoshi
194             #pod
195             #pod @hoshi_points = $board->hoshi
196             #pod
197             #pod Returns a list of hoshi points.
198             #pod
199             #pod =cut
200              
201             sub hoshi {
202 441     441 1 521 my $self = shift;
203              
204 441         519 map { $self->_grid2pos(@$_, $self->skip_i) } @{ $self->{hoshi} };
  3645         6200  
  441         772  
205             }
206              
207             #pod =head2 is_hoshi
208             #pod
209             #pod $star = $board->is_hoshi('dp')
210             #pod
211             #pod Returns true if the named position is a hoshi (star) point.
212             #pod
213             #pod =cut
214              
215             sub is_hoshi {
216 437     437 1 567 my $board = shift;
217 437         553 my $point = shift;
218 437 100       615 return 1 if grep { /^$point$/ } $board->hoshi;
  3613         11507  
219             }
220              
221             #pod =head2 as_sgf
222             #pod
223             #pod $sgf = $board->as_sgf;
224             #pod
225             #pod Returns a representation of the board as an SGF (Smart Game Format) file.
226             #pod
227             #pod =cut
228              
229             sub as_sgf {
230 2     2 1 8 my $self = shift;
231 2         4 my $sgf;
232              
233 2         13 $sgf
234             .= "(;GM[$types{$self->{game}}]FF[4]AP[Games::Goban]SZ[$self->{size}]PB[$self->{black}]PW[$self->{white}]\n";
235 2         5 foreach (@{ $self->{moves} }) {
  2         7  
236             $sgf .= q{;}
237             . uc($_->{player}) . q<[>
238 8 100       30 . ($_->{piece} ? $self->_grid2pos(@{ $_->{piece}->_xy }, 0) : q{}) . q<]>;
  6         14  
239             }
240 2         4 $sgf .= ")\n";
241              
242 2         6 return $sgf;
243             }
244              
245             #pod =head2 as_text
246             #pod
247             #pod print $board->as_text(coords => 1)
248             #pod
249             #pod Returns a printable text picture of the board, similar to that printed
250             #pod by C. Black pieces are represented by C, white pieces by C,
251             #pod and the latest move is enclosed in parentheses. I points are in their
252             #pod normal position for Go, and printed as an C<+>. Coordinates are not printed by
253             #pod default, but can be enabled as suggested in the synopsis.
254             #pod
255             #pod =cut
256              
257             sub as_text {
258 2     2 1 6 my $board = shift;
259 2         5 my %opts = @_;
260 2         6 my @hoshi = $board->hoshi;
261 2         4 my $text;
262 2         4 for (my $y = $board->size - 1; $y >= 0; $y--) { ## no critic For
263             $text .= substr($board->_grid2pos(0, $y, $board->skip_i), 1, 1) . ': '
264 28 50       55 if $opts{coords};
265 28         52 for my $x (0 .. ($board->size - 1)) {
266 442         815 my $pos = $board->_grid2pos($x, $y, $board->skip_i);
267 442         807 my $p = $board->get($pos);
268 442 100 100     797 if ( $p
      66        
      100        
269             and $p->move == $board->{move} - 1
270             and $text
271             and substr($text, -1, 1) ne "\n")
272             {
273 1         4 chop $text;
274 1         2 $text .= "(";
275             }
276             $text .= (
277 442 100       803 $p
    100          
    100          
278             ? ($p->color eq "b" ? "X" : "O")
279             : ($board->is_hoshi($pos) ? q{+} : q{.})
280             ) . q{ };
281 442 100 100     1267 if ($p and $p->move == $board->{move} - 1) { chop $text; $text .= ")"; }
  2         6  
  2         6  
282             }
283 28         67 $text .= "\n";
284             }
285 2 50       7 if ($opts{coords}) {
286 0         0 $text .= q{ } x 3;
287 0         0 for (0 .. ($board->size - 1)) {
288 0         0 $text .= substr($board->_grid2pos($_, 0, $board->skip_i), 0, 1) . q{ };
289             }
290 0         0 $text .= "\n";
291             }
292 2         20 return $text;
293             }
294              
295             #pod =head2 register
296             #pod
297             #pod my $key = $board->register(\&callback);
298             #pod
299             #pod Register a callback to be called after every move is made. This is useful for
300             #pod analysis programs which wish to maintain statistics on the board state. The
301             #pod C returned from this can be fed to...
302             #pod
303             #pod =cut
304              
305             sub register {
306 1     1 1 10 my ($board, $cb) = @_;
307 1         4 my $key = ++$board->{magiccookie};
308 1         2 $board->{callbacks}{$key} = $cb;
309 1         3 $board->{notes}->{$key} = {};
310 1         3 return $key;
311             }
312              
313             #pod =head2 notes
314             #pod
315             #pod $board->notes($key)->{score} += 5;
316             #pod
317             #pod C returns a hash reference which can be used by a callback to
318             #pod store local state about the board.
319             #pod
320             #pod =cut
321              
322             sub notes {
323 4     4 1 16 my ($board, $key) = @_;
324 4         16 return $board->{notes}->{$key};
325             }
326              
327             #pod =head2 hash
328             #pod
329             #pod $hash = $board->hash
330             #pod
331             #pod Provides a unique hash of the board position. If the phrase "positional
332             #pod superko" means anything to you, you want to use this method. If not,
333             #pod move along, nothing to see here.
334             #pod
335             #pod =cut
336              
337             sub hash {
338 0     0 1 0 my $board = shift;
339 0         0 my $hash = chr(0) x 91;
340 0         0 my $bit = 0;
341             $board->_iterboard(
342             sub {
343 0     0   0 my $piece = shift;
344 0 0       0 vec($hash, $bit, 2) = $piece->color eq "b" ? 1 : 2 if $piece;
    0          
345 0         0 $bit += 3;
346             }
347 0         0 );
348 0         0 return $hash;
349             }
350              
351             #pod =head2 skip_i
352             #pod
353             #pod This method returns true if the 'skip_i' argument to the constructor was true
354             #pod and the 'i' coordinant should be skipped. (Note that 'i' is never skipped when
355             #pod producing SGF output.)
356             #pod
357             #pod =cut
358              
359 4555     4555 1 7234 sub skip_i { return (shift)->{skip_i} }
360              
361             # This method accepts a position string and checks whether it is a valid
362             # position on the given board. If it is, 1 is returned. Otherwise, it carps
363             # that the position is not on the board. It does this by calling _check_grid,
364             # also below.
365              
366             sub _check_pos {
367 10     10   16 my $self = shift;
368 10         14 my $pos = shift;
369              
370 10         24 my ($x, $y) = $self->_pos2grid($pos, $self->skip_i);
371              
372 10         26 return $self->_check_grid($x, $y);
373             }
374              
375             sub _check_grid {
376 458     458   1005 my $self = shift;
377 458         678 my ($x, $y) = @_;
378              
379 458 100 66     658 return 1
380             if (($x < $self->size) and ($y < $self->size));
381              
382 1         4 croak "position '"
383             . $self->_grid2pos($x, $y, $self->skip_i)
384             . "' not on board";
385             }
386              
387             # This method returns a list of the hoshi points that should be found on the
388             # board, given its size.
389              
390             sub _calc_hoshi {
391 9     9   19 my $self = shift;
392 9         25 my $size = $self->size;
393 9         30 my $half = ($size - 1) / 2;
394              
395 9         20 my @hoshi = ();
396              
397 9 50       32 if ($size % 2) { push @hoshi, [ $half, $half ]; } # middle center
  9         26  
398              
399 9 0       33 my $margin = ($size > 11 ? 4 : ($size > 6 ? 3 : ($size > 4 ? 2 : undef)));
    50          
    100          
400              
401 9 50       56 return \@hoshi unless $margin;
402              
403 9         46 push @hoshi, (
404             [ $margin - 1, $margin - 1 ], # top left
405             [ $size - $margin, $margin - 1 ], # top right
406             [ $margin - 1, $size - $margin ], # bottom left
407             [ $size - $margin, $size - $margin ] # bottom right
408             );
409              
410 9 100 66     119 if (($size % 2) && ($size > 9)) {
411 8         51 push @hoshi, (
412             [ $half, $margin - 1 ], # top center
413             [ $margin - 1, $half ], # middle left
414             [ $size - $margin, $half ], # middle right
415             [ $half, $size - $margin ] # bottom center
416             );
417             }
418              
419 9         31 return \@hoshi;
420             }
421              
422             # This subroutine passes every findable square on the board to the supplied
423             # subroutine reference.
424              
425             sub _iterboard {
426 0     0   0 my ($self, $sub) = @_;
427 0         0 for my $x ('a' .. chr($self->size + ord("a") - 1)) {
428 0         0 for my $y ('a' .. chr($self->size + ord("a") - 1)) {
429 0         0 $sub->($self->get("$x$y"));
430             }
431             }
432              
433             }
434              
435             # This method accepts an (x,y) position, starting with (0,0) and returns the
436             # 'xy' text representing it.
437             # The third parameter, if true, indicates that 'i' should be skipped.
438              
439             sub _grid2pos {
440 8901     8901   344233 my $self = shift;
441 8901         13230 my ($x, $y, $skip_i) = @_;
442              
443 8901 100       14541 if ($skip_i) {
444 11         17 for ($x, $y) {
445 22 100       47 $_++ if ($_ >= 8);
446             }
447             }
448              
449 8901         23776 return chr($ORIGIN + $x) . chr($ORIGIN + $y);
450             }
451              
452             # This method accepts an 'xy' position string and returns the (x,y) indexes
453             # where that position falls in the board.
454             # The second parameter, if true, indicates that 'i' should be skipped.
455              
456             sub _pos2grid {
457 3676     3676   8653 my $self = shift;
458 3676         5845 my ($pos, $skip_i) = @_;
459              
460 3676         13706 my ($xc, $yc) = (lc($pos) =~ /^([a-z])([a-z])$/);
461 3676         5798 my ($x, $y);
462              
463 3676         4947 $x = ord($xc) - $ORIGIN;
464 3676 100 66     6776 $x-- if ($skip_i and ($x > 8));
465              
466 3676         4585 $y = ord($yc) - $ORIGIN;
467 3676 100 100     6134 $y-- if ($skip_i and ($y > 8));
468              
469 3676         8661 return ($x, $y);
470             }
471              
472             package Games::Goban::Piece 1.103;
473              
474             #pod =head1 C methods
475             #pod
476             #pod Here are the methods which can be called on a C
477             #pod object, representing a piece on the board.
478             #pod
479             #pod =cut
480              
481             #pod =head1 color
482             #pod
483             #pod Returns "b" for a black piece and "w" for a white. C is also
484             #pod provided for Anglophones.
485             #pod
486             #pod =cut
487              
488 5     5   14 sub color { $_[0]->{colour} }
489 0     0   0 sub colour { $_[0]->{colour} }
490              
491             #pod =head1 notes
492             #pod
493             #pod Similar to the C method on the board class, this provides a
494             #pod private area for callbacks to scribble on.
495             #pod
496             #pod =cut
497              
498 0     0   0 sub notes { $_[0]->{notes}->{ $_[1] } }
499              
500             #pod =head1 position
501             #pod
502             #pod Returns the position of this piece, as a two-character string.
503             #pod Incidentally, try to avoid taking references to C objects, since
504             #pod this stops them being destroyed in a timely fashion. Use a C
505             #pod and C if you can get away with it, or take a weak reference if
506             #pod you're worried about the piece going away or being replaced by another
507             #pod one in that position.
508             #pod
509             #pod =cut
510              
511             sub position {
512 0     0   0 my $piece = shift;
513              
514             ## no critic Private
515 0         0 $piece->board->_grid2pos(@{ $piece->_xy }, $piece->board->skip_i);
  0         0  
516             }
517              
518 6     6   15 sub _xy { $_[0]->{xy} }
519              
520             #pod =head1 move
521             #pod
522             #pod Returns the move number on which this piece was played.
523             #pod
524             #pod =cut
525              
526 10     10   49 sub move { $_[0]->{move} }
527              
528             #pod =head1 board
529             #pod
530             #pod Returns the board object whence this piece came.
531             #pod
532             #pod =cut
533              
534 0     0     sub board { $_[0]->{board} }
535              
536             1;
537              
538             #pod =head1 TODO
539             #pod
540             #pod =over
541             #pod
542             #pod =item *
543             #pod
544             #pod use Games::Goban::Board for game board
545             #pod
546             #pod =item *
547             #pod
548             #pod add C<<$board->pass>>
549             #pod
550             #pod =item *
551             #pod
552             #pod possibly enable C<<$board->move('')>> to pass
553             #pod
554             #pod =item *
555             #pod
556             #pod produce example referee
557             #pod
558             #pod =item *
559             #pod
560             #pod produce sample method for removing captured stones
561             #pod
562             #pod =back
563             #pod
564             #pod =head1 SEE ALSO
565             #pod
566             #pod Smart Game Format: http://www.red-bean.com/sgf/
567             #pod
568             #pod C
569             #pod
570             #pod The US Go Association: http://www.usgo.org/
571             #pod
572              
573             __END__