File Coverage

blib/lib/Games/Goban.pm
Criterion Covered Total %
statement 129 152 84.8
branch 40 54 74.0
condition 22 30 73.3
subroutine 24 31 77.4
pod 13 13 100.0
total 228 280 81.4


line stmt bran cond sub pod time code
1 5     5   140651 use strict;
  5         11  
  5         174  
2 5     5   28 use warnings;
  5         8  
  5         265  
3             package Games::Goban;
4             {
5             $Games::Goban::VERSION = '1.102';
6             }
7             # ABSTRACT: Board for playing go, renju, othello, etc.
8              
9 5     5   112 use 5.006;
  5         14  
  5         253  
10 5     5   23 use Carp;
  5         9  
  5         12574  
11              
12             my $ORIGIN = ord('a');
13             my $piececlass = 'Games::Goban::Piece';
14              
15             our %types = (
16             go => 1,
17             othello => 2,
18             renju => 4,
19             gomoku => 4,
20             );
21              
22             our %defaults = (
23             game => 'go',
24             size => 19,
25             white => 'Miss White',
26             black => 'Mr. Black',
27             skip_i => 0,
28             referee => sub { 1 }
29             );
30              
31              
32             sub new {
33 9     9 1 1049 my $class = shift;
34 9         81 my %opts = (%defaults, @_);
35              
36 9 50 33     131 unless (($opts{size} !~ /\D/) and ($opts{size} > 4) and ($opts{size} <= 26)) {
      33        
37 0         0 croak "Illegal size $opts{size} (must be integer > 4)";
38             }
39              
40 9         29 $opts{game} = lc $opts{game};
41 9 50       44 croak "Unknown game $opts{game}" unless exists $types{ $opts{game} };
42              
43 9         115 my $board = bless {
44             move => 1,
45             moves => [],
46             turn => 'b',
47             game => $opts{game},
48             size => $opts{size},
49             black => $opts{black},
50             white => $opts{white},
51             skip_i => $opts{skip_i},
52             referee => $opts{referee},
53             callbacks => {},
54             magiccookie => "a0000",
55             }, $class;
56              
57 9         36 for (0 .. ($opts{size} - 1)) {
58 161         166 push @{ $board->{board} }, [ (undef) x $opts{size} ];
  161         533  
59             }
60 9         38 $board->{hoshi} = $board->_calc_hoshi;
61              
62 9         41 return $board;
63             }
64              
65              
66             sub move {
67 10     10 1 632 my ($self, $move) = @_;
68              
69 10         27 my ($x, $y) = $self->_pos2grid($move, $self->skip_i);
70              
71 10         27 $self->_check_pos($move);
72 9         24 my $stat = $self->{referee}->($self, $move);
73              
74 9 50       19 return $stat if !$stat;
75 9         64 $self->{board}[$x][$y] = bless {
76             colour => $self->{turn},
77             move => $self->{move},
78             xy => [ $x, $y ],
79             board => $self
80             },
81             "Games::Goban::Piece";
82 9         11 push @{ $self->{moves} },
  9         45  
83             {
84             player => $self->{turn},
85             piece => $self->{board}[$x][$y]
86             };
87 9         15 $self->{move}++;
88 9 100       26 $self->{turn} = $self->{turn} eq "b" ? "w" : "b";
89              
90 9         12 while (my ($key, $cb) = each %{ $self->{callbacks} }) { $cb->($key, $self) }
  11         37  
  2         6  
91              
92 9         21 return 1;
93             }
94              
95              
96             sub pass {
97 2     2 1 9 my $self = shift;
98              
99 2         3 push @{ $self->{moves} },
  2         8  
100             {
101             player => $self->{turn},
102             piece => undef
103             };
104 2         3 $self->{move}++;
105 2 100       9 $self->{turn} = $self->{turn} eq "b" ? "w" : "b";
106             }
107              
108              
109             sub get {
110 447     447 1 1518 my ($self, $pos) = @_;
111 447         650 my ($x, $y) = $self->_pos2grid($pos, $self->skip_i);
112 447         1289 $self->_check_grid($x, $y);
113              
114 447         1177 return $self->{board}[$x][$y];
115             }
116              
117              
118 996     996 1 25515 sub size { $_[0]->{size} }
119              
120              
121             sub hoshi {
122 441     441 1 415 my $self = shift;
123              
124 441         408 map { $self->_grid2pos(@$_, $self->skip_i) } @{ $self->{hoshi} };
  3645         5629  
  441         887  
125             }
126              
127              
128             sub is_hoshi {
129 437     437 1 410 my $board = shift;
130 437         408 my $point = shift;
131 437 100       613 return 1 if grep { /^$point$/ } $board->hoshi;
  3613         12661  
132             }
133              
134              
135             sub as_sgf {
136 2     2 1 6 my $self = shift;
137 2         2 my $sgf;
138              
139 2         14 $sgf
140             .= "(;GM[$types{$self->{game}}]FF[4]AP[Games::Goban]SZ[$self->{size}]PB[$self->{black}]PW[$self->{white}]\n";
141 2         4 foreach (@{ $self->{moves} }) {
  2         6  
142 6         16 $sgf .= q{;}
143             . uc($_->{player}) . q<[>
144 8 100       33 . ($_->{piece} ? $self->_grid2pos(@{ $_->{piece}->_xy }, 0) : q{}) . q<]>;
145             }
146 2         4 $sgf .= ")\n";
147              
148 2         5 return $sgf;
149             }
150              
151              
152             sub as_text {
153 2     2 1 7 my $board = shift;
154 2         3 my %opts = @_;
155 2         6 my @hoshi = $board->hoshi;
156 2         4 my $text;
157 2         4 for (my $y = $board->size - 1; $y >= 0; $y--) { ## no critic For
158 28 50       62 $text .= substr($board->_grid2pos(0, $y, $board->skip_i), 1, 1) . ': '
159             if $opts{coords};
160 28         46 for my $x (0 .. ($board->size - 1)) {
161 442         896 my $pos = $board->_grid2pos($x, $y, $board->skip_i);
162 442         1507 my $p = $board->get($pos);
163 442 100 100     932 if ( $p
      66        
      100        
164             and $p->move == $board->{move} - 1
165             and $text
166             and substr($text, -1, 1) ne "\n")
167             {
168 1         3 chop $text;
169 1         1 $text .= "(";
170             }
171             $text .= (
172 442 100       1069 $p
    100          
    100          
173             ? ($p->color eq "b" ? "X" : "O")
174             : ($board->is_hoshi($pos) ? q{+} : q{.})
175             ) . q{ };
176 442 100 100     1345 if ($p and $p->move == $board->{move} - 1) { chop $text; $text .= ")"; }
  2         4  
  2         5  
177             }
178 28         83 $text .= "\n";
179             }
180 2 50       10 if ($opts{coords}) {
181 0         0 $text .= q{ } x 3;
182 0         0 for (0 .. ($board->size - 1)) {
183 0         0 $text .= substr($board->_grid2pos($_, 0, $board->skip_i), 0, 1) . q{ };
184             }
185 0         0 $text .= "\n";
186             }
187 2         28 return $text;
188             }
189              
190              
191             sub register {
192 1     1 1 12 my ($board, $cb) = @_;
193 1         3 my $key = ++$board->{magiccookie};
194 1         3 $board->{callbacks}{$key} = $cb;
195 1         2 $board->{notes}->{$key} = {};
196 1         4 return $key;
197             }
198              
199              
200             sub notes {
201 4     4 1 13 my ($board, $key) = @_;
202 4         45 return $board->{notes}->{$key};
203             }
204              
205              
206             sub hash {
207 0     0 1 0 my $board = shift;
208 0         0 my $hash = chr(0) x 91;
209 0         0 my $bit = 0;
210             $board->_iterboard(
211             sub {
212 0     0   0 my $piece = shift;
213 0 0       0 vec($hash, $bit, 2) = $piece->color eq "b" ? 1 : 2 if $piece;
    0          
214 0         0 $bit += 3;
215             }
216 0         0 );
217 0         0 return $hash;
218             }
219              
220              
221 4555     4555 1 9413 sub skip_i { return (shift)->{skip_i} }
222              
223             # This method accepts a position string and checks whether it is a valid
224             # position on the given board. If it is, 1 is returned. Otherwise, it carps
225             # that the position is not on the board. It does this by calling _check_grid,
226             # also below.
227              
228             sub _check_pos {
229 10     10   37 my $self = shift;
230 10         11 my $pos = shift;
231              
232 10         19 my ($x, $y) = $self->_pos2grid($pos, $self->skip_i);
233              
234 10         23 return $self->_check_grid($x, $y);
235             }
236              
237             sub _check_grid {
238 458     458   1100 my $self = shift;
239 458         431 my ($x, $y) = @_;
240              
241 458 100 66     644 return 1
242             if (($x < $self->size) and ($y < $self->size));
243              
244 1         3 croak "position '"
245             . $self->_grid2pos($x, $y, $self->skip_i)
246             . "' not on board";
247             }
248              
249             # This method returns a list of the hoshi points that should be found on the
250             # board, given its size.
251              
252             sub _calc_hoshi {
253 9     9   14 my $self = shift;
254 9         30 my $size = $self->size;
255 9         24 my $half = ($size - 1) / 2;
256              
257 9         16 my @hoshi = ();
258              
259 9 50       31 if ($size % 2) { push @hoshi, [ $half, $half ]; } # middle center
  9         21  
260              
261 9 0       30 my $margin = ($size > 11 ? 4 : ($size > 6 ? 3 : ($size > 4 ? 2 : undef)));
    50          
    100          
262              
263 9 50       23 return \@hoshi unless $margin;
264              
265 9         58 push @hoshi, (
266             [ $margin - 1, $margin - 1 ], # top left
267             [ $size - $margin, $margin - 1 ], # top right
268             [ $margin - 1, $size - $margin ], # bottom left
269             [ $size - $margin, $size - $margin ] # bottom right
270             );
271              
272 9 100 66     52 if (($size % 2) && ($size > 9)) {
273 8         43 push @hoshi, (
274             [ $half, $margin - 1 ], # top center
275             [ $margin - 1, $half ], # middle left
276             [ $size - $margin, $half ], # middle right
277             [ $half, $size - $margin ] # bottom center
278             );
279             }
280              
281 9         32 return \@hoshi;
282             }
283              
284             # This subroutine passes every findable square on the board to the supplied
285             # subroutine reference.
286              
287             sub _iterboard {
288 0     0   0 my ($self, $sub) = @_;
289 0         0 for my $x ('a' .. chr($self->size + ord("a") - 1)) {
290 0         0 for my $y ('a' .. chr($self->size + ord("a") - 1)) {
291 0         0 $sub->($self->get("$x$y"));
292             }
293             }
294              
295             }
296              
297             # This method accepts an (x,y) position, starting with (0,0) and returns the
298             # 'xy' text representing it.
299             # The third parameter, if true, indicates that 'i' should be skipped.
300              
301             sub _grid2pos {
302 8901     8901   445332 my $self = shift;
303 8901         11526 my ($x, $y, $skip_i) = @_;
304              
305 8901 100       18395 if ($skip_i) {
306 11         15 for ($x, $y) {
307 22 100       51 $_++ if ($_ >= 8);
308             }
309             }
310              
311 8901         27771 return chr($ORIGIN + $x) . chr($ORIGIN + $y);
312             }
313              
314             # This method accepts an 'xy' position string and returns the (x,y) indexes
315             # where that position falls in the board.
316             # The second parameter, if true, indicates that 'i' should be skipped.
317              
318             sub _pos2grid {
319 3676     3676   8596 my $self = shift;
320 3676         4791 my ($pos, $skip_i) = @_;
321              
322 3676         13807 my ($xc, $yc) = (lc($pos) =~ /^([a-z])([a-z])$/);
323 3676         4658 my ($x, $y);
324              
325 3676         4899 $x = ord($xc) - $ORIGIN;
326 3676 100 66     7826 $x-- if ($skip_i and ($x > 8));
327              
328 3676         4199 $y = ord($yc) - $ORIGIN;
329 3676 100 100     8004 $y-- if ($skip_i and ($y > 8));
330              
331 3676         11474 return ($x, $y);
332             }
333              
334             package Games::Goban::Piece;
335             {
336             $Games::Goban::Piece::VERSION = '1.102';
337             }
338              
339              
340              
341 5     5   22 sub color { $_[0]->{colour} }
342 0     0   0 sub colour { $_[0]->{colour} }
343              
344              
345 0     0   0 sub notes { $_[0]->{notes}->{ $_[1] } }
346              
347              
348             sub position {
349 0     0   0 my $piece = shift;
350              
351             ## no critic Private
352 0         0 $piece->board->_grid2pos(@{ $piece->_xy }, $piece->board->skip_i);
  0         0  
353             }
354              
355 6     6   20 sub _xy { $_[0]->{xy} }
356              
357              
358 10     10   62 sub move { $_[0]->{move} }
359              
360              
361 0     0     sub board { $_[0]->{board} }
362              
363             1;
364              
365             __END__